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 === 12return x + y;}f(3) === 15; // true
js
function f(x, y = 12) {// 当形参 y 没有被传递时,或传递 undefined, 那么 y === 12return 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; // trueb === 3; // truevar {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; // trueb === 3; // truevar {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 指向 bobhello();// hello, I am undefined// 作为普通函数调用,即 window.hello(),this 指向 globalhello.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 指向 bobhello();// hello, I am undefined// 作为普通函数调用,即 window.hello(),this 指向 globalhello.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
指针,通过call
或apply
调用,第一个参数会被忽略; - 不绑定
Arguments
对象,其引用上一层作用域链的Arguments
对象; - 不能用作构造器,和
new
一起用会抛出错误; - 没有
prototype
属性。
Symbols
ES6 引入了一种新的原始数据类型 Symbol
,表示独一无二的值,类似于 UUID。
js
// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 falseconst s1 = Symbol("macOS");const s2 = Symbol("macOS");// Symbol.for 机制类似于单例模式const s3 = Symbol.for("windows"); // 注册一个全局 Symbolconst s4 = Symbol.for("windows"); // 已存在相同名称的 Symbol,返回全局 Symbols1 === s2; // falses3 === s4; // true
js
// 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 falseconst s1 = Symbol("macOS");const s2 = Symbol("macOS");// Symbol.for 机制类似于单例模式const s3 = Symbol.for("windows"); // 注册一个全局 Symbolconst s4 = Symbol.for("windows"); // 已存在相同名称的 Symbol,返回全局 Symbols1 === s2; // falses3 === s4; // true
js
let key = Symbol("key");function MyClass(privateData) {// 注意,Symbol 值作为对象属性名时,不能用.去获取this[key] = privateData;}MyClass.prototype = {doStuff() {console.log(this[key]);},};typeof key; // symbollet c = new MyClass("hello");c.key; // undefinedc[key]; // hello
js
let key = Symbol("key");function MyClass(privateData) {// 注意,Symbol 值作为对象属性名时,不能用.去获取this[key] = privateData;}MyClass.prototype = {doStuff() {console.log(this[key]);},};typeof key; // symbollet c = new MyClass("hello");c.key; // undefinedc[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; // 42obj.__proto__; // somethingElse
js
const obj = {// 允许设置原型__proto__: theProtoObj,// 允许覆盖属性["__proto__"]: somethingElse,// 属性简写,等于 ‘handler: handler’handler,// 计算 (动态) 属性名["prop_" + (() => 42)()]: 42,};obj.prop_42; // 42obj.__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); // 4animals.add("🐼");console.log(animals.size); // 4console.log(animals.has("🐷")); // trueanimals.delete("🐷");console.log(animals.has("🐷")); // falseanimals.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); // 4animals.add("🐼");console.log(animals.size); // 4console.log(animals.has("🐷")); // trueanimals.delete("🐷");console.log(animals.has("🐷")); // falseanimals.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…ofmyAnimals.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…ofmyAnimals.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; // 4things.has("🚗"); // truethings.has(myFunc); // truethings.has(() => "🍕"); // falsethings.get(myFunc); // '😄 Key is a function!'things.delete("✈️");things.has("✈️"); // falsethings.clear();things.size; // 0// 链式设置键值对things.set("🔧", "Wrench").set("🎸", "Guitar").set("🕹", "Joystick");const myMap = new Map();// 甚至键名可以是另一个 Mapthings.set(myMap, "Oh gosh!");things.size; // 4things.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; // 4things.has("🚗"); // truethings.has(myFunc); // truethings.has(() => "🍕"); // falsethings.get(myFunc); // '😄 Key is a function!'things.delete("✈️");things.has("✈️"); // falsethings.clear();things.size; // 0// 链式设置键值对things.set("🔧", "Wrench").set("🎸", "Guitar").set("🕹", "Joystick");const myMap = new Map();// 甚至键名可以是另一个 Mapthings.set(myMap, "Oh gosh!");things.size; // 4things.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-gcglobal.gc();// 返回 Nodejs 的内存占用情况,单位是 bytesprocess.memoryUsage(); // heapUsed: 4640360 ≈ 4.4Mlet map = new Map();let key = new Array(5 * 1024 * 1024); // new Array 当为 Objmap.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-gcglobal.gc();// 返回 Nodejs 的内存占用情况,单位是 bytesprocess.memoryUsage(); // heapUsed: 4640360 ≈ 4.4Mlet map = new Map();let key = new Array(5 * 1024 * 1024); // new Array 当为 Objmap.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-gcglobal.gc();process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4Mconst 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-gcglobal.gc();process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4Mconst 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); // dataelement.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); // dataelement.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);// trueNumber.EPSILON;// 2.220446049250313e-16Number.EPSILON.toFixed(20);// "0.00000000000000022204"
js
Number.EPSILON === Math.pow(2, -52);// trueNumber.EPSILON;// 2.220446049250313e-16Number.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; // falsewithinErrorMargin(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; // falsewithinErrorMargin(0.1 + 0.2, 0.3); // true
其他一些新增的 API
js
Number.isInteger(Infinity); // falseNumber.isNaN("NaN"); // falseMath.sign(-5); // 判断一个数到底是正数、负数、还是零 -1Math.hypot(3, 4); // 返回所有参数的平方和的平方根 5Math.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); // falseNumber.isNaN("NaN"); // falseMath.sign(-5); // 判断一个数到底是正数、负数、还是零 -1Math.hypot(3, 4); // 返回所有参数的平方和的平方根 5Math.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 = 4Reflect.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 = 4Reflect.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') 恢复代码执行,并赋值 dataOneconsole.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') 恢复代码执行,并赋值 dataOneconsole.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 近几年最为重要的版本,学习它,能帮助你在实际项目中 以优雅的手段解决问题,简化代码逻辑,提高运行效率。
附上一张我之前精心整理的思维导图,由于内容较多,可以选择在新标签页中打开,放大观看。
本文参考资料: