ES6 核心特性解析

May 31, 2019

前言

我最近把原先的 .TOP 域名更换到 .CN 域名,并且用 Gatsby 重建个人博客,由于之前是采用 HTTPS 部署的方式绕过阿里云的域名备案系统,所以不必备案。

更换 CN 域名后,这招不管用了,😭😭 域名必须要备案了,等待幕布邮寄中……

此文用来记录 ES6 的核心特性,以及一些容易被遗落的知识点。

基础篇

Let + Const

ES6 除了固有的函数作用域,还引入了 块级作用域 {}

js
function f() {
{
let x; // ①
{ // ②
const x = "sneaky"; // 包含在块 ② 中,与块 ① 中的 x 分属不同作用域
x = "foo"; // Error,const 定义的变量不可以重新赋值,但如果 const 定义了一个对象,那么对象的属性是可以被修改的
}
x = "bar";= // 属于块 ①,let 定义的变量可以重新赋值
let x = "inner"; // Error, 在块 ① 中已被定义
}
}
js
function f() {
{
let x; // ①
{ // ②
const x = "sneaky"; // 包含在块 ② 中,与块 ① 中的 x 分属不同作用域
x = "foo"; // Error,const 定义的变量不可以重新赋值,但如果 const 定义了一个对象,那么对象的属性是可以被修改的
}
x = "bar";= // 属于块 ①,let 定义的变量可以重新赋值
let x = "inner"; // Error, 在块 ① 中已被定义
}
}

默认、剩余、展开参数(Default + Rest + Spread)

js
function f(x, y = 12) {
// 当形参 y 没有被传递时,或传递 undefined, 那么 y === 12
return x + y;
}
f(3) === 15; // true
js
function f(x, y = 12) {
// 当形参 y 没有被传递时,或传递 undefined, 那么 y === 12
return x + y;
}
f(3) === 15; // true
js
function f(x, ...y) {
// y 被视为一个数组
return x * y.length;
}
f(3, "hello", true) === 6; // true
js
function f(x, ...y) {
// y 被视为一个数组
return x * y.length;
}
f(3, "hello", true) === 6; // true
js
function f(x, y, z) {
return x + y + z;
}
// 将数组装换为参数序列
f(...[1, 2, 3]) === 6; // true
js
function f(x, y, z) {
return x + y + z;
}
// 将数组装换为参数序列
f(...[1, 2, 3]) === 6; // true

解构(Destructuring)

解构赋值只在被解构字段的值和 undefined 严格相等时生效

js
var [a, , b] = [1, 2, 3];
a === 1; // true
b === 3; // true
var {
op: a,
lhs: { op: b },
rhs: c,
} = getASTNode();
var { op, lhs, rhs } = getASTNode();
// 等同于下面的写法
var { op: op, lhs: lhs, rhs: rhs } = getASTNode();
// 参数解构
function g({ name: x }) {
console.log(x);
}
g({ name: 5 });
var [a] = [];
a === undefined; // true
// 参数解构并赋值
var [a = 1] = [];
a === 1; // true
js
var [a, , b] = [1, 2, 3];
a === 1; // true
b === 3; // true
var {
op: a,
lhs: { op: b },
rhs: c,
} = getASTNode();
var { op, lhs, rhs } = getASTNode();
// 等同于下面的写法
var { op: op, lhs: lhs, rhs: rhs } = getASTNode();
// 参数解构
function g({ name: x }) {
console.log(x);
}
g({ name: 5 });
var [a] = [];
a === undefined; // true
// 参数解构并赋值
var [a = 1] = [];
a === 1; // true

箭头函数 & 语法层面上的 This(Arrows and Lexical This)

js
// 除了支持返回语句,还可以将表达式作为返回主体
const foo = () => ({ name: "es6" });
const bar = num => (num++, num ** 2);
foo(); // 返回一个对象 { name: 'es6' }
bar(3) === 16; // true,这里使用了逗号操作符,会返回最后一个表达式的值
js
// 除了支持返回语句,还可以将表达式作为返回主体
const foo = () => ({ name: "es6" });
const bar = num => (num++, num ** 2);
foo(); // 返回一个对象 { name: 'es6' }
bar(3) === 16; // true,这里使用了逗号操作符,会返回最后一个表达式的值

JS 中 this 的指向一直让开发者捉摸不透,初学者很容易误用。

其实,可以总结为一句话: this 永远指向调用它的那个对象

而箭头函数则改写了这一规则,就是 箭头函数共享当前代码上下文的 this

可以理解为以下的阐述:

  • 箭头函数不会创建自己的 this它只会在声明了箭头函数的作用域的上一层作用域去继承 this,如果上一层依旧是箭头函数,则继续向上查找,直至全局作用域,此时 this 指向 global 对象。

这里的作用域存在于函数定义时

因此,在下面的代码中,传递给 setTimeout 的函数内的 this 与 sayHello 函数中的 this 一致:

js
const bob = {
name: "Bob",
sayHello() {
setTimeout(() => {
console.log(`hello, I am ${this.name}`);
}, 1000);
},
};
const hello = bob.sayHello;
bob.sayHello();
// hello, I am Bob
// 作为对象的方法调用,this 指向 bob
hello();
// hello, I am undefined
// 作为普通函数调用,即 window.hello(),this 指向 global
hello.call({ name: "Mike" });
// hello, I am Mike
// call,apply 调用,显式定义 this 对象
js
const bob = {
name: "Bob",
sayHello() {
setTimeout(() => {
console.log(`hello, I am ${this.name}`);
}, 1000);
},
};
const hello = bob.sayHello;
bob.sayHello();
// hello, I am Bob
// 作为对象的方法调用,this 指向 bob
hello();
// hello, I am undefined
// 作为普通函数调用,即 window.hello(),this 指向 global
hello.call({ name: "Mike" });
// hello, I am Mike
// call,apply 调用,显式定义 this 对象

做个小测试,以下代码的执行结果是什么?

js
language = "Python";
const obj = {
language: "Java",
speak() {
language = "Go";
return function() {
return () => {
console.log(`I'm learning ${this.language}`);
};
};
},
};
obj.speak()()();
js
language = "Python";
const obj = {
language: "Java",
speak() {
language = "Go";
return function() {
return () => {
console.log(`I'm learning ${this.language}`);
};
};
},
};
obj.speak()()();

箭头函数还具有以下特点:

  • 由于箭头函数没有自己的 this 指针,通过 callapply 调用,第一个参数会被忽略;
  • 不绑定 Arguments 对象,其引用上一层作用域链的 Arguments 对象;
  • 不能用作构造器,和 new 一起用会抛出错误;
  • 没有 prototype 属性。

Symbols

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值,类似于 UUID。

js
// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false
const s1 = Symbol("macOS");
const s2 = Symbol("macOS");
// Symbol.for 机制类似于单例模式
const s3 = Symbol.for("windows"); // 注册一个全局 Symbol
const s4 = Symbol.for("windows"); // 已存在相同名称的 Symbol,返回全局 Symbol
s1 === s2; // false
s3 === s4; // true
js
// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false
const s1 = Symbol("macOS");
const s2 = Symbol("macOS");
// Symbol.for 机制类似于单例模式
const s3 = Symbol.for("windows"); // 注册一个全局 Symbol
const s4 = Symbol.for("windows"); // 已存在相同名称的 Symbol,返回全局 Symbol
s1 === s2; // false
s3 === s4; // true
js
let key = Symbol("key");
function MyClass(privateData) {
// 注意,Symbol 值作为对象属性名时,不能用.去获取
this[key] = privateData;
}
MyClass.prototype = {
doStuff() {
console.log(this[key]);
},
};
typeof key; // symbol
let c = new MyClass("hello");
c.key; // undefined
c[key]; // hello
js
let key = Symbol("key");
function MyClass(privateData) {
// 注意,Symbol 值作为对象属性名时,不能用.去获取
this[key] = privateData;
}
MyClass.prototype = {
doStuff() {
console.log(this[key]);
},
};
typeof key; // symbol
let c = new MyClass("hello");
c.key; // undefined
c[key]; // hello

Symbol 的一些特性必须依赖于浏览器的原生实现,不可被 transpiled 或 polyfilled。

应用场景

  • 设计优雅的数据对象,让“对内操作”和“对外选择性输出”变得简便。

在实际应用中,我们经常会需要使用 Object.keys() 或者 for...in 来枚举对象的键名,那在这方面,Symbol 类型的 key 会有什么不同之处呢?看以下示例代码:

js
let obj = {
[Symbol("name")]: "一斤代码",
age: 18,
title: "Engineer",
};
Object.keys(obj); // ['age', 'title']
for (let p in obj) {
console.log(p); // 'age' 'title'
}
Object.getOwnPropertyNames(obj); // ['age', 'title']
js
let obj = {
[Symbol("name")]: "一斤代码",
age: 18,
title: "Engineer",
};
Object.keys(obj); // ['age', 'title']
for (let p in obj) {
console.log(p); // 'age' 'title'
}
Object.getOwnPropertyNames(obj); // ['age', 'title']

Symbol 类型的 key 是不能通过 Object.keys() 或者 for...in 来枚举的,所以,利用该特性,我们可以把一些 不需要对外操作和访问的属性 使用 Symbol 来定义。

因此,当使用 JSON.stringify() 将对象转换成 JSON 字符串的时候,Symbol 类型的 key 会被忽略:

js
JSON.stringify(obj); // {"age":18,"title":"Engineer"}
js
JSON.stringify(obj); // {"age":18,"title":"Engineer"}
  • 消除魔术字符串
js
const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";
function handleFileResource(resource) {
switch (resource.type) {
case TYPE_AUDIO:
playAudio(resource);
break;
case TYPE_VIDEO:
playVideo(resource);
break;
case TYPE_IMAGE:
previewImage(resource);
break;
default:
throw new Error("Unknown type of resource");
}
}
js
const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";
function handleFileResource(resource) {
switch (resource.type) {
case TYPE_AUDIO:
playAudio(resource);
break;
case TYPE_VIDEO:
playVideo(resource);
break;
case TYPE_IMAGE:
previewImage(resource);
break;
default:
throw new Error("Unknown type of resource");
}
}

上面的代码中那样,我们需要为常量赋一个唯一的值(比如这里的 'AUDIO'),'AUDIO' 就是一个魔术字符串,它本身没意义,只是为了保证常量唯一的关系,常量一多,就变得十分臃肿且难以维护。

现在有了 Symbol,我们大可不必这么麻烦了:

js
// 保证了三个常量的值是唯一的
const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();
js
// 保证了三个常量的值是唯一的
const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();

增强的对象字面量(Enhanced Object Literals)

js
const obj = {
// 允许设置原型
__proto__: theProtoObj,
// 允许覆盖属性
["__proto__"]: somethingElse,
// 属性简写,等于 ‘handler: handler’
handler,
// 计算 (动态) 属性名
["prop_" + (() => 42)()]: 42,
};
obj.prop_42; // 42
obj.__proto__; // somethingElse
js
const obj = {
// 允许设置原型
__proto__: theProtoObj,
// 允许覆盖属性
["__proto__"]: somethingElse,
// 属性简写,等于 ‘handler: handler’
handler,
// 计算 (动态) 属性名
["prop_" + (() => 42)()]: 42,
};
obj.prop_42; // 42
obj.__proto__; // somethingElse

__proto__ 需要原生支持,它在之前的 ECMAScript 版本中被移除,但大多数浏览器都实现了这一特性,包括 Node 环境

Map + Set + WeakMap + WeakSet

Set

Set 是 ES6 中新增的数据结构,它允许创建唯一值的集合。集合中的值可以是简单的基本类型(如字符串或数值),但更复杂的对象类型(如对象或数组)也可以,亦或是一个新的 Set

js
let animals = new Set();
animals.add("🐷");
animals.add("🐼");
animals.add("🐢");
animals.add("🐿");
console.log(animals.size); // 4
animals.add("🐼");
console.log(animals.size); // 4
console.log(animals.has("🐷")); // true
animals.delete("🐷");
console.log(animals.has("🐷")); // false
animals.forEach(animal => {
console.log(`Hey ${animal}!`);
});
// Hey 🐼!
// Hey 🐢!
// Hey 🐿!
animals.clear();
console.log(animals.size); // 0
js
let animals = new Set();
animals.add("🐷");
animals.add("🐼");
animals.add("🐢");
animals.add("🐿");
console.log(animals.size); // 4
animals.add("🐼");
console.log(animals.size); // 4
console.log(animals.has("🐷")); // true
animals.delete("🐷");
console.log(animals.has("🐷")); // false
animals.forEach(animal => {
console.log(`Hey ${animal}!`);
});
// Hey 🐼!
// Hey 🐢!
// Hey 🐿!
animals.clear();
console.log(animals.size); // 0

我们还可以传入一个数组来初始化集合

js
let myAnimals = new Set(["🐷", "🐢", "🐷", "🐷"]);
myAnimals.add(["🐨", "🐑"]);
myAnimals.add({ name: "Rud", type: "🐢" });
console.log(myAnimals.size); // 4
// Set 内置了遍历器,可以调用 forEach, for…of
myAnimals.forEach(animal => {
console.log(animal);
});
// 🐷
// 🐢
// ["🐨", "🐑"]
// Object { name: "Rud", type: "🐢" }
js
let myAnimals = new Set(["🐷", "🐢", "🐷", "🐷"]);
myAnimals.add(["🐨", "🐑"]);
myAnimals.add({ name: "Rud", type: "🐢" });
console.log(myAnimals.size); // 4
// Set 内置了遍历器,可以调用 forEach, for…of
myAnimals.forEach(animal => {
console.log(animal);
});
// 🐷
// 🐢
// ["🐨", "🐑"]
// Object { name: "Rud", type: "🐢" }

Map

与普通对象(Object)不同,Map 的键名(Key)可以是任何类型,不再局限于字符串(String),可以是对象、函数以及另一个 Map

js
let things = new Map();
const myFunc = () => "🍕";
things.set("🚗", "Car");
things.set("🏠", "House");
things.set("✈️", "Airplane");
things.set(myFunc, "😄 Key is a function!");
things.size; // 4
things.has("🚗"); // true
things.has(myFunc); // true
things.has(() => "🍕"); // false
things.get(myFunc); // '😄 Key is a function!'
things.delete("✈️");
things.has("✈️"); // false
things.clear();
things.size; // 0
// 链式设置键值对
things
.set("🔧", "Wrench")
.set("🎸", "Guitar")
.set("🕹", "Joystick");
const myMap = new Map();
// 甚至键名可以是另一个 Map
things.set(myMap, "Oh gosh!");
things.size; // 4
things.get(myMap); // 'Oh gosh!'
js
let things = new Map();
const myFunc = () => "🍕";
things.set("🚗", "Car");
things.set("🏠", "House");
things.set("✈️", "Airplane");
things.set(myFunc, "😄 Key is a function!");
things.size; // 4
things.has("🚗"); // true
things.has(myFunc); // true
things.has(() => "🍕"); // false
things.get(myFunc); // '😄 Key is a function!'
things.delete("✈️");
things.has("✈️"); // false
things.clear();
things.size; // 0
// 链式设置键值对
things
.set("🔧", "Wrench")
.set("🎸", "Guitar")
.set("🕹", "Joystick");
const myMap = new Map();
// 甚至键名可以是另一个 Map
things.set(myMap, "Oh gosh!");
things.size; // 4
things.get(myMap); // 'Oh gosh!'

可以通过传入包含两个元素的数组来初始化 Map

js
const funArray = [
["🍾", "Champagne"],
["🍭", "Lollipop"],
["🎊", "Confetti"],
];
let funMap = new Map(funArray);
funMap.get("🍾"); // Champagne
js
const funArray = [
["🍾", "Champagne"],
["🍭", "Lollipop"],
["🎊", "Confetti"],
];
let funMap = new Map(funArray);
funMap.get("🍾"); // Champagne

WeakMap

WeakMap 对象是一组键/值对的集合,其中的键值是弱引用的。其键名必须是对象,而值可以是任意的。它最重要的特性是 WeakMap 保持了对键名所引用的对象的弱引用

我们可以通过 Node 来证明一下这个问题:

js
// 允许手动执行垃圾回收机制
node --expose-gc
global.gc();
// 返回 Nodejs 的内存占用情况,单位是 bytes
process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M
let map = new Map();
let key = new Array(5 * 1024 * 1024); // new Array 当为 Obj
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M
// 所以当你设置 key = null 时,只是去掉了 key 对 Obj 的强引用
// 并没有去除 arr 对 Obj 的强引用,所以 Obj 还是不会被回收掉
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M
// 这句话其实是无用的,因为 key 已经是 null 了
map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M
js
// 允许手动执行垃圾回收机制
node --expose-gc
global.gc();
// 返回 Nodejs 的内存占用情况,单位是 bytes
process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M
let map = new Map();
let key = new Array(5 * 1024 * 1024); // new Array 当为 Obj
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M
// 所以当你设置 key = null 时,只是去掉了 key 对 Obj 的强引用
// 并没有去除 arr 对 Obj 的强引用,所以 Obj 还是不会被回收掉
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M
// 这句话其实是无用的,因为 key 已经是 null 了
map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M
js
node --expose-gc
global.gc();
process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M
const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M
// 当我们设置 key = null 的时候,就只有 wm 对所引用对象的弱引用
// 下次垃圾回收机制执行的时候,该引用对象就会被回收掉。
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M
js
node --expose-gc
global.gc();
process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M
const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M
// 当我们设置 key = null 的时候,就只有 wm 对所引用对象的弱引用
// 下次垃圾回收机制执行的时候,该引用对象就会被回收掉。
key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M

应用场景

传统使用 jQuery 的时候,我们会通过 $.data() 方法在 DOM 对象上储存相关信息(就比如在删除按钮元素上储存帖子的 ID 信息),jQuery 内部会使用一个对象管理 DOM 和对应的数据,当你将 DOM 元素删除,DOM 对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData() 方法才能删除掉相关联的数据,WeakMap 就可以简化这一操作:

js
let wm = new WeakMap(),
element = document.querySelector(".element");
wm.set(element, "data");
let value = wm.get(elemet);
console.log(value); // data
element.parentNode.removeChild(element);
element = null;
js
let wm = new WeakMap(),
element = document.querySelector(".element");
wm.set(element, "data");
let value = wm.get(elemet);
console.log(value); // data
element.parentNode.removeChild(element);
element = null;

WeakSet

特性与 WeakMap 相似

遍历器(Iterators + For..Of)

遍历器它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)

js
let fibonacci = {
[Symbol.iterator]() {
let pre = 0,
cur = 1;
return {
next() {
[pre, cur] = [cur, pre + cur]; // 数组解构
return { done: false, value: cur };
},
};
},
};
for (var n of fibonacci) {
// 当n超过1000时停止
if (n > 1000) break;
console.log(n);
}
js
let fibonacci = {
[Symbol.iterator]() {
let pre = 0,
cur = 1;
return {
next() {
[pre, cur] = [cur, pre + cur]; // 数组解构
return { done: false, value: cur };
},
};
},
};
for (var n of fibonacci) {
// 当n超过1000时停止
if (n > 1000) break;
console.log(n);
}

上面代码中,对象 fibonacci 是可遍历的,因为具有 Symbol.iterator 属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 value 和 done 两个属性。

JS 内置了许多具备 Iterator 接口的数据结构:

  • Array
  • Map、WeakMap
  • Set、WeakSet
  • String
  • TypedArray
  • 函数的 Arguments 对象
  • NodeList 对象

for...in 和 for..of 的差别

  • for...in 遍历键名(Key)并转化为字符串,for...of 遍历键值(Value);
  • for...in 语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非 Symbol 的属性;
  • for...in 更适合遍历对象,for...of 更适合遍历数组;
js
for (let i in [1, 2, 3]) {
console.log(typeof i); // string 数组下标被转化字符串
console.log(i); // '1', '2', '3'
}
var triangle = { a: 1, b: 2, c: 3 };
function ColoredTriangle() {
this.color = "red";
}
ColoredTriangle.prototype = triangle;
var obj = new ColoredTriangle();
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
// 如果去了 hasOwnProperty() 这个约束条件会怎么样?
console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red
}
}
js
for (let i in [1, 2, 3]) {
console.log(typeof i); // string 数组下标被转化字符串
console.log(i); // '1', '2', '3'
}
var triangle = { a: 1, b: 2, c: 3 };
function ColoredTriangle() {
this.color = "red";
}
ColoredTriangle.prototype = triangle;
var obj = new ColoredTriangle();
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
// 如果去了 hasOwnProperty() 这个约束条件会怎么样?
console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red
}
}

新增 API(Math + Number + String + Object APIs)

先来看看新增的 Number.EPSILON,确实挺懵逼的,WTF?

凡事必有因,呈上一道 JS 经典面试题:

js
> 0.1 + 0.2 === 0.30000000000000004;
> true
js
> 0.1 + 0.2 === 0.30000000000000004;
> true

这是因为 JS 的数值规范采用了 IEEE 754 标准,所以数字都是以 64 位双精度浮点数据类型储存。

即 JS 语言根本没有整数,所有数字都是小数!当我们以为在用整数进行计算时,都会被转换为小数。

而浮点数都是以多位二进制的方式进行存储的。

十进制的 0.1 用二进制表示为:0.0 0011 0011 0011 0011…,循环部分是 0011。 十进制 0.2 用二进制表示为:0.0011 0011 0011 0011…,循环部分是 0011。

由于计算机存储空间有限,最后会舍弃后面的数值,所以我们其实得到的是一个近似值。

新增的 Number.EPSILON 是一个常量,表示 1 与大于 1 的最小浮点数之间的差。

对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的 1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。

js
Number.EPSILON === Math.pow(2, -52);
// true
Number.EPSILON;
// 2.220446049250313e-16
Number.EPSILON.toFixed(20);
// "0.00000000000000022204"
js
Number.EPSILON === Math.pow(2, -52);
// true
Number.EPSILON;
// 2.220446049250313e-16
Number.EPSILON.toFixed(20);
// "0.00000000000000022204"

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。如果误差如果小于这个值,就可以认为不存在误差了。

js
0.1 + 0.2 - 0.3;
// 5.551115123125783e-17
(5.551115123125783e-17).toFixed(20);
// '0.00000000000000005551'
js
0.1 + 0.2 - 0.3;
// 5.551115123125783e-17
(5.551115123125783e-17).toFixed(20);
// '0.00000000000000005551'
js
0.00000000000000005551 < 0.00000000000000022204; // true
js
0.00000000000000005551 < 0.00000000000000022204; // true

显然,5.551115123125783e-17 这个误差已经没有存在的意义。

借助 Number.EPSILON 我们可以达到 0.1 + 0.2 === 0.3 的效果。

js
function withinErrorMargin(left, right) {
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3; // false
withinErrorMargin(0.1 + 0.2, 0.3); // true
js
function withinErrorMargin(left, right) {
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3; // false
withinErrorMargin(0.1 + 0.2, 0.3); // true

其他一些新增的 API

js
Number.isInteger(Infinity); // false
Number.isNaN("NaN"); // false
Math.sign(-5); // 判断一个数到底是正数、负数、还是零 -1
Math.hypot(3, 4); // 返回所有参数的平方和的平方根 5
Math.imul(-2, -2); // 返回两个数以 32 位带符号整数形式相乘的结果 4
"abcde".includes("cd"); // true
"abc".repeat(3); // "abcabcabc"
Array.from(document.querySelectorAll("*")); // 返回一个真正的数组
Array.of(1, 2, 3) // [1,2,3]
[(0, 0, 0)].fill(7, 1) // [0,7,7]
[(1, 2, 3)].findIndex(x => x == 2) // 1
[("a", "b", "c")].entries() // [0, "a"], [1,"b"], [2,"c"]
[("a", "b", "c")].keys() // 0, 1, 2
[("a", "b", "c")].values(); // "a", "b", "c"
Object.assign(Point, { origin: new Point(0, 0) }); // 合并对象
js
Number.isInteger(Infinity); // false
Number.isNaN("NaN"); // false
Math.sign(-5); // 判断一个数到底是正数、负数、还是零 -1
Math.hypot(3, 4); // 返回所有参数的平方和的平方根 5
Math.imul(-2, -2); // 返回两个数以 32 位带符号整数形式相乘的结果 4
"abcde".includes("cd"); // true
"abc".repeat(3); // "abcabcabc"
Array.from(document.querySelectorAll("*")); // 返回一个真正的数组
Array.of(1, 2, 3) // [1,2,3]
[(0, 0, 0)].fill(7, 1) // [0,7,7]
[(1, 2, 3)].findIndex(x => x == 2) // 1
[("a", "b", "c")].entries() // [0, "a"], [1,"b"], [2,"c"]
[("a", "b", "c")].keys() // 0, 1, 2
[("a", "b", "c")].values(); // "a", "b", "c"
Object.assign(Point, { origin: new Point(0, 0) }); // 合并对象

二进制和八进制字面量(Binary and Octal Literals)

js
0b111 === 7; // true 二进制
0o111 === 73; // true 八进制
0x111 === 273; // true 十六进制
js
0b111 === 7; // true 二进制
0o111 === 73; // true 八进制
0x111 === 273; // true 十六进制

进阶篇

尾递归(Tail Calls)

假设现在要实现一个阶乘函数,即 5!= 120,我们很容易想到递归实现。

js
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
js
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}

但递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

何为调用记录,在示例代码中,由于最后一步返回了一个表达式,内存会保留 n 这个变量的信息和 factorial(n - 1) 调用下一次函数的位置,形成一层层的调用栈

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身,返回函数本身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。尾递归优化如下:

js
function factorial(n, acc = 1) {
"use strict";
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}
factorial(100000);
js
function factorial(n, acc = 1) {
"use strict";
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}
factorial(100000);

由此可见,"尾调用优化"对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 也是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署"尾调用优化"。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

ES6 的尾调用优化只在严格模式下开启。

代理(Proxy)

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

js
// 代理一个对象
var target = {};
var handler = {
get: function(receiver, name) {
return `Hello, ${name}!`;
},
};
var p = new Proxy(target, handler);
p.world; // "Hello, world!"
js
// 代理一个对象
var target = {};
var handler = {
get: function(receiver, name) {
return `Hello, ${name}!`;
},
};
var p = new Proxy(target, handler);
p.world; // "Hello, world!"
js
// 代理一个函数
var target = function() {
return "I am the target";
};
var handler = {
apply: function(receiver, ...args) {
return "I am the proxy";
},
};
var p = new Proxy(target, handler);
p(); // "I am the proxy"
js
// 代理一个函数
var target = function() {
return "I am the target";
};
var handler = {
apply: function(receiver, ...args) {
return "I am the proxy";
},
};
var p = new Proxy(target, handler);
p(); // "I am the proxy"
js
// 代理会将所有应用到它的操作转发到 target 上
let target = {};
let p = new Proxy(target, {});
p.a = 37;
target.a; // 37
js
// 代理会将所有应用到它的操作转发到 target 上
let target = {};
let p = new Proxy(target, {});
p.a = 37;
target.a; // 37
js
// 如何实现 a == 1 && a == 2 && a == 3,利用 Proxy 的 get 劫持
const a = new Proxy(
{},
{
val: 1,
get() {
return () => this.val++;
},
}
);
a == 1 && a == 2 && a == 3; // true
js
// 如何实现 a == 1 && a == 2 && a == 3,利用 Proxy 的 get 劫持
const a = new Proxy(
{},
{
val: 1,
get() {
return () => this.val++;
},
}
);
a == 1 && a == 2 && a == 3; // true

Proxy 不能被 transpiled or polyfilled,亲自入的坑,由于在项目中使用了 Mobx5.x,其内部是用 Proxy 写的,结果 IE11 不支持 Proxy,只得回退版本 Mobx 到 4.x。

反射(Reflect)

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。

Reflect 对象的设计目的有这样几个:

  • 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上
  • 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。
js
var O = { a: 1 };
Reflect.defineProperty(O, "b", { value: 2 });
O[Symbol("c")] = 3;
O.__proto__.d = 4
Reflect.getOwnPropertyDescriptor(O, "b");
// { value: 2, writable: false, enumerable: false, configurable: false }
Reflect.ownKeys(O);
// ['a', 'b', Symbol(c)]
function C(a, b) {
this.c = a + b;
}
var instance = Reflect.construct(C, [20, 22]);
instance.c; // 42
js
var O = { a: 1 };
Reflect.defineProperty(O, "b", { value: 2 });
O[Symbol("c")] = 3;
O.__proto__.d = 4
Reflect.getOwnPropertyDescriptor(O, "b");
// { value: 2, writable: false, enumerable: false, configurable: false }
Reflect.ownKeys(O);
// ['a', 'b', Symbol(c)]
function C(a, b) {
this.c = a + b;
}
var instance = Reflect.construct(C, [20, 22]);
instance.c; // 42
js
// 获取键名的方法有很多,以上面的代码为例子,它们的区别如下:
Object.getOwnPropertyNames(O)
// [ 'a', 'b' ] 自身除 Symbol 外的属性
Object.getOwnPropertySymbols(O)
// [ Symbol(c) ] 自身 Symbol 属性
Object.keys(O)
// ['a'] 自身除 Symbol 外可枚举属性
Reflect.ownKeys(O)
// [ 'a', 'b', Symbol(c) ] 自身所有属性
for...in
// a d 自身和原型链,除 Symbol 外的可枚举属性
js
// 获取键名的方法有很多,以上面的代码为例子,它们的区别如下:
Object.getOwnPropertyNames(O)
// [ 'a', 'b' ] 自身除 Symbol 外的属性
Object.getOwnPropertySymbols(O)
// [ Symbol(c) ] 自身 Symbol 属性
Object.keys(O)
// ['a'] 自身除 Symbol 外可枚举属性
Reflect.ownKeys(O)
// [ 'a', 'b', Symbol(c) ] 自身所有属性
for...in
// a d 自身和原型链,除 Symbol 外的可枚举属性

生成器(Generators)

Generator 函数是 ES6 提供的一种异步编程解决方案。 Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function 关键字与函数名之间有一个星号(*)
  • 函数体内部使用 yield 表达式,定义不同的内部状态。
js
function* helloWorldGenerator() {
yield "hello"; // yield 使 Generator 函数暂停了执行,并将结果返回给调用者
yield "world"; // 当下一次调用时,从它中断的地方恢复执行
return "ending";
}
var hw = helloWorldGenerator();
a = hw.next(); // { value: 'hello', done: false }
b = hw.next(); // { value: 'world', done: false }
c = hw.next(); // { value: 'ending', done: true }
js
function* helloWorldGenerator() {
yield "hello"; // yield 使 Generator 函数暂停了执行,并将结果返回给调用者
yield "world"; // 当下一次调用时,从它中断的地方恢复执行
return "ending";
}
var hw = helloWorldGenerator();
a = hw.next(); // { value: 'hello', done: false }
b = hw.next(); // { value: 'world', done: false }
c = hw.next(); // { value: 'ending', done: true }

可以利用这种暂停执行的特性,来实现惰性求值

向 Generator 传递数据

js
function* sayFullName() {
const firstName = yield;
const secondName = yield;
console.log(firstName + " " + secondName);
}
let fullName = sayFullName();
fullName.next();
// 调用 next,函数开始执行,直到遇见 yield 暂停,返回 yield 后面的值
// {value: undefined, done: false}
fullName.next("Handsome");
// 调用 next,恢复函数执行,并传参 Handsome,yield 被 Handsome 替代,因此 firstName 的值变为 Handsome
// 遇到 yield 再次暂停执行
// {value: undefined, done: false}
fullName.next("Jack");
// 调用 next,恢复函数执行,传参 Jack,yield 被 Jack 替代,因此 secondName 的值变为 Jack
// Handsome Jack
// {value: undefined, done: true}
js
function* sayFullName() {
const firstName = yield;
const secondName = yield;
console.log(firstName + " " + secondName);
}
let fullName = sayFullName();
fullName.next();
// 调用 next,函数开始执行,直到遇见 yield 暂停,返回 yield 后面的值
// {value: undefined, done: false}
fullName.next("Handsome");
// 调用 next,恢复函数执行,并传参 Handsome,yield 被 Handsome 替代,因此 firstName 的值变为 Handsome
// 遇到 yield 再次暂停执行
// {value: undefined, done: false}
fullName.next("Jack");
// 调用 next,恢复函数执行,传参 Jack,yield 被 Jack 替代,因此 secondName 的值变为 Jack
// Handsome Jack
// {value: undefined, done: true}

使用 Generator 处理异步调用

js
let generator;
let getDataOne = () => {
setTimeout(() => {
generator.next("dummy data one");
}, 1000);
};
let getDataTwo = () => {
setTimeout(() => {
generator.next("dummy data one");
}, 1000);
};
function* main() {
let dataOne = yield getDataOne();
let dataTwo = yield getDataTwo();
console.log(dataOne, dataTwo);
}
generator = main();
generator.next();
// 执行 getDataOne(),然后 yield 暂停
// 直至一秒后 generator.next('dummy data one') 恢复代码执行,并赋值 dataOne
console.log("i am previous print");
// i am previous print
// dummy data one dummy data one
js
let generator;
let getDataOne = () => {
setTimeout(() => {
generator.next("dummy data one");
}, 1000);
};
let getDataTwo = () => {
setTimeout(() => {
generator.next("dummy data one");
}, 1000);
};
function* main() {
let dataOne = yield getDataOne();
let dataTwo = yield getDataTwo();
console.log(dataOne, dataTwo);
}
generator = main();
generator.next();
// 执行 getDataOne(),然后 yield 暂停
// 直至一秒后 generator.next('dummy data one') 恢复代码执行,并赋值 dataOne
console.log("i am previous print");
// i am previous print
// dummy data one dummy data one

Promises

Promises 是一个异步编程的解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

js
function timeout(duration = 0) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration);
});
}
var p = timeout(1000)
.then(() => {
return timeout(2000);
})
.then(() => {
throw new Error("hmm");
})
.catch(err => {
return Promise.all([timeout(100), timeout(200)]);
});
js
function timeout(duration = 0) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration);
});
}
var p = timeout(1000)
.then(() => {
return timeout(2000);
})
.then(() => {
throw new Error("hmm");
})
.catch(err => {
return Promise.all([timeout(100), timeout(200)]);
});

这里强调几点:

  • 不要剥夺函数 return 的能力,很多人写 Promise,照样有大量嵌套,掉进 Promise 地狱,要记得及时 return,避免嵌套
  • 当需要多个请求全部结束时,才更新数据,可以用 Promise.all(fetch1,fetch2)
  • 当需要从多个请求中,接受最先返回数据的那个请求,可以用 Promise.race(fetch1,fetch2)

总结

作为 ECMAScript 近几年最为重要的版本,学习它,能帮助你在实际项目中 以优雅的手段解决问题,简化代码逻辑,提高运行效率

附上一张我之前精心整理的思维导图,由于内容较多,可以选择在新标签页中打开,放大观看。

本文参考资料:


B2D1 (包邦东)

Written by B2D1 (包邦东)