ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # ES6系列 在我们开发的时候,可能认为应该默认使用 let 而不是 var ,这种情况下,对于需要写保护的变量要使用 const。然而另一种做法日益普及:默认使用 const,只有当确实需要改变变量的值的时候才使用 let。这是因为大部分的变量的值在初始化后不应再改变,而预料之外的变量之的改变是很多 bug 的源头。 ## 内建迭代器 为了更好的访问对象中的内容,比如有的时候我们仅需要数组中的值,但有的时候不仅需要使用值还需要使用索引,ES6 为数组、Map、Set 集合内建了以下三种迭代器: 1. entries() 返回一个遍历器对象,用来遍历\[键名, 键值\]组成的数组。对于数组,键名就是索引值。 2. keys() 返回一个遍历器对象,用来遍历所有的键名。 3. values() 返回一个遍历器对象,用来遍历所有的键值。 ~~~ let set = new Set(['a', 'b', 'c']); console.log(set.keys()); // SetIterator {"a", "b", "c"} 注意keys、values返回的是遍历器对象 console.log([...set.keys()]); // ["a", "b", "c"] ~~~ 因此我们可以用 for...of 配合内建迭代器来遍历 ~~~ var colors = ["red", "green", "blue"]; for (let index of colors.keys()) { console.log(index); } // 0 // 1 // 2 for (let color of colors.values()) { console.log(color); } // red // green // blue for (let item of colors.entries()) { console.log(item); } // [ 0, "red" ] // [ 1, "green" ] // [ 2, "blue" ] ~~~ ## WeakMap WeakMaps 保持了对键名所引用的对象的**弱引用**,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 也正是因为这样的特性,WeakMap 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakMap 不可遍历。 所以 WeakMap 不像 Map,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有 size 属性,也不支持 clear 方法,所以 WeakMap只有四个方法可用:get()、set()、has()、delete()。 **应用** ### 1\. 在 DOM 对象上保存相关数据 传统使用 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; ~~~ ### 2\. 数据缓存 从上一个例子,我们也可以看出,当我们需要关联对象和数据,比如在不修改原有对象的情况下储存某些属性或者根据对象储存一些计算的值等,而又不想管理这些数据的死活时非常适合考虑使用 WeakMap。数据缓存就是一个非常好的例子: ~~~js const cache = new WeakMap(); function countOwnKeys(obj) { if (cache.has(obj)) { console.log('Cached'); return cache.get(obj); } else { console.log('Computed'); const count = Object.keys(obj).length; cache.set(obj, count); return count; } } ~~~ ### 3\. 私有属性 WeakMap 也可以被用于实现私有变量,不过在 ES6 中实现私有变量的方式有很多种,这只是其中一种: ~~~js const privateData = new WeakMap(); class Person { constructor(name, age) { privateData.set(this, { name: name, age: age }); } getName() { return privateData.get(this).name; } getAge() { return privateData.get(this).age; } } export default Person; ~~~ ## Promise 红绿灯问题 题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现) 三个亮灯函数已经存在: ~~~js function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); } ~~~ 利用 then 和递归实现: ~~~js function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); } var light = function(timmer, cb){ return new Promise(function(resolve, reject) { setTimeout(function() { cb(); resolve(); }, timmer); }); }; var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(2000, green); }).then(function(){ return light(1000, yellow); }).then(function(){ step(); }); } step(); ~~~ ## Promise 的局限性 ### 1\. 错误被吃掉 首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗? 并不是,举个例子: ~~~js throw new Error('error'); console.log(233333); ~~~ 在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子: ~~~js const promise = new Promise(null); console.log(233333); ~~~ 以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。 然而再举个例子: ~~~js let promise = new Promise(() => { throw new Error('error') }); console.log(2333333); ~~~ 这次会正常的打印`233333`,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。 其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。 而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。 ### 2\. 单一值 Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。 说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值: ~~~js Promise.all([Promise.resolve(1), Promise.resolve(2)]) .then(([x, y]) => { console.log(x, y); }); ~~~ ### 3\. 无法取消 Promise 一旦新建它就会立即执行,无法中途取消。 ### 4\. 无法得知 pending 状态 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 # ES6 的 Class 与 ES5 的对应关系 ES6 中: ~~~js class Person { constructor(name) { this.name = name; } sayHello() { return 'hello, I am ' + this.name; } } var kevin = new Person('Kevin'); kevin.sayHello(); // hello, I am Kevin ~~~ 对应到 ES5 中就是: ~~~js function Person(name) { this.name = name; } Person.prototype.sayHello = function () { return 'hello, I am ' + this.name; }; var kevin = new Person('Kevin'); kevin.sayHello(); // hello, I am Kevin ~~~ 我们可以看到 ES5 的构造函数 Person,对应 ES6 的 Person 类的 constructor 方法。 值得注意的是:**类的内部所有定义的方法,都是不可枚举的(non-enumerable)** 以上面的例子为例,在 ES6 中: ~~~js Object.keys(Person.prototype); // [] Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"] ~~~ 然而在 ES5 中: ~~~js Object.keys(Person.prototype); // ['sayHello'] Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"] ~~~ ## 实例属性 以前,我们定义实例属性,只能写在类的 constructor 方法里面。比如: ~~~js class Person { constructor() { this.state = { count: 0 }; } } ~~~ 然而现在有一个提案,对实例属性和静态属性都规定了新的写法,而且 Babel 已经支持。现在我们可以写成: ~~~js class Person { state = { count: 0 }; } ~~~ 对应到 ES5 都是: ~~~js function Person() { this.state = { count: 0 }; } ~~~ ## 静态方法 所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 ES6 中: ~~~js class Person { static sayHello() { return 'hello'; } } Person.sayHello() // 'hello' var kevin = new Person(); kevin.sayHello(); // TypeError: kevin.sayHello is not a function ~~~ 对应 ES5: ~~~js function Person() {} Person.sayHello = function() { return 'hello'; }; Person.sayHello(); // 'hello' var kevin = new Person(); kevin.sayHello(); // TypeError: kevin.sayHello is not a function ~~~ ## 静态属性 静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。以前,我们添加静态属性只可以这样: ~~~js class Person {} Person.name = 'kevin'; ~~~ 因为上面提到的提案,现在可以写成: ~~~js class Person { static name = 'kevin'; } ~~~ 对应到 ES5 都是: ~~~js function Person() {}; Person.name = 'kevin'; ~~~ ## getter 和 setter 与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。 ~~~js class Person { get name() { return 'kevin'; } set name(newName) { console.log('new name 为:' + newName) } } let person = new Person(); person.name = 'daisy'; // new name 为:daisy console.log(person.name); // kevin ~~~ 对应到 ES5 中: ~~~js function Person(name) {} Person.prototype = { get name() { return 'kevin'; }, set name(newName) { console.log('new name 为:' + newName) } } let person = new Person(); person.name = 'daisy'; // new name 为:daisy console.log(person.name); // kevin ~~~ # Class extends 实现继承 ES6 ~~~ class Parent { constructor(name) { this.name = name; } } class Child extends Parent { constructor(name, age) { super(name); // 调用父类的 constructor(name) this.age = age; } } var child1 = new Child('kevin', '18'); console.log(child1); ~~~ 值得注意的是: super 关键字表示父类的构造函数,相当于 ES5 的 Parent.call(this)。 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。 也正是因为这个原因,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。 对应的 ES5 的寄生组合式继承 ~~~ function Parent (name) { this.name = name; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); var child1 = new Child('kevin', '18'); console.log(child1); ~~~ 原型链示意图: ![](https://box.kancloud.cn/b37f76b440aeb6b0bec9f8a7315fac55_584x497.png) ## 子类的 \_\_proto\_\_ 在 ES6 中,父类的静态方法,可以被子类继承。举个例子: ~~~js class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod(); // 'hello' ~~~ 这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 \_\_proto\_\_ 属性,因此同时存在两条继承链。 (1)子类的 \_\_proto\_\_ 属性,表示构造函数的继承,总是指向父类。 (2)子类 prototype 属性的 \_\_proto\_\_ 属性,表示方法的继承,总是指向父类的 prototype 属性。 ~~~js class Parent { } class Child extends Parent { } console.log(Child.__proto__ === Parent); // true console.log(Child.prototype.__proto__ === Parent.prototype); // true ~~~ ES6 的原型链示意图为: ![](https://box.kancloud.cn/d58f84991039ab6c134e0c4c898cda5e_612x497.png) 我们会发现,相比寄生组合式继承,ES6 的 class 多了一个`Object.setPrototypeOf(Child, Parent)`的步骤。 # defineProperty 与 Proxy ## definePropety ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。 **语法** > Object.defineProperty(obj, prop, descriptor) **参数** ~~~ obj: 要在其上定义属性的对象。 prop: 要定义或修改的属性的名称。 descriptor: 将被定义或修改的属性的描述符。 ~~~ 举个例子: ~~~js var obj = {}; Object.defineProperty(obj, "num", { value : 1, writable : true, enumerable : true, configurable : true }); // 对象 obj 拥有属性 num,值为 1 ~~~ 虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。 函数的第三个参数 descriptor 所表示的属性描述符有两种形式:**数据描述符和存取描述符**。 **两者均具有以下两种键值**: - configurable:当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。 - enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 **数据描述符同时具有以下可选键值**: - value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。 - writable:当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。 **存取描述符同时具有以下可选键值**: - get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。 - set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。 值得注意的是: **属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者**。这就意味着你可以: ~~~js Object.defineProperty({}, "num", { value: 1, writable: true, enumerable: true, configurable: true }); ~~~ 也可以: ~~~js var value = 1; Object.defineProperty({}, "num", { get : function(){ return value; }, set : function(newValue){ value = newValue; }, enumerable : true, configurable : true }); ~~~ 但是不可以: ~~~js // 报错 Object.defineProperty({}, "num", { value: 1, get: function() { return 1; } }); ~~~ 此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样: ~~~js var obj = Object.defineProperty({}, "num", {}); console.log(obj.num); // undefined ~~~ ## Setters 和 Getters 之所以讲到 defineProperty,是因为我们要使用存取描述符中的 get 和 set,这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做”存取器属性“。 当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。 举个例子: ~~~js var obj = {}, value = null; Object.defineProperty(obj, "num", { get: function(){ console.log('执行了 get 操作') return value; }, set: function(newValue) { console.log('执行了 set 操作') value = newValue; } }) obj.num = 1 // 执行了 set 操作 console.log(obj.num); // 执行了 get 操作 // 1 ~~~ ## proxy 使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。 Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法: ~~~js var proxy = new Proxy(target, handler); ~~~ proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。 ~~~js var proxy = new Proxy({}, { get: function(obj, prop) { console.log('设置 get 操作') return obj[prop]; }, set: function(obj, prop, value) { console.log('设置 set 操作') obj[prop] = value; } }); proxy.time = 35; // 设置 set 操作 console.log(proxy.time); // 设置 get 操作 // 35 ~~~ 简单地和上面使用 defineProperty 来实现比较一下,可以看出: - 使用 Proxy 一次性地拦截了所有的属性的设置与读取 - ???