在ES6之前, JS是无法通过继承的方式创建属于自己的特殊数组的, 也就是说可以使用原型链来继承数组的一些方法, 但是某些返回一个数组的方法返回的值还是一个Array的实例, 例如slice, length等, 还是Array的实例属性, 和当前原型链末尾的函数没什么关系, 只是借用了Array的方法。
1 // 实例: 2 // Array的行为 3 let colors = []; 4 colors[0] = ‘red‘; 5 console.log(colors.length); // 1 6 colors.length = 0; 7 console.log(colors[0]); // undefined 8 9 // 使用ES5方式继承数组 10 function MyArray() { 11 Array.apply(this, arguments); 12 } 13 MyArray.prototype = Object.create(Array.prototype, { 14 constructor: { 15 value: MyArray, 16 writable: true, 17 configurable: true, 18 enumerable: true 19 } 20 }); 21 colors = new MyArray(); 22 colors[0] = ‘red‘; 23 console.log(colors.length); // 0 24 colors.length = 0; 25 console.log(colors[0]); // ‘red‘ 26 colors = new MyArray(); 27 colors.push(1, 2, 3, 4, 5); 28 let cs = colors.slice(2, 3); 29 console.log(cs instanceof MyArray); // false
从上面代码可以看出, MyArray虽然可以使用数组上面的方法和属性, 但是其表现行为和Array是不一致的, 因为传统JS继承形式实现的数组继承没有从Array.apply或原型赋值中继承相关功能。
ES6和ES5继承的区别:
在ES5中继承是先由派生类型创建this的值, 然后调用基类型的构造函数, 也就是说this的值开始指向的是MyArray的实例, 但是随后被来自Array的其他属性所修饰。
在ES6中则正好相反, 先由基类创建this的值, 然后派生类的构造函数再修改这个值, 所有一开始可以通过this访问基类的所有内建功能, 然后再添加自己的功能, 这就是为什么派生类的constructor必须先写super() 方法。
1 // 实例: 2 class MyArray extends Array { 3 4 } 5 6 let colors = new MyArray(); 7 colors[0] = ‘red‘; 8 console.log(colors.length); // 1 9 colors.length = 0; 10 console.log(colors[0]); // undefined 11 colors.push(1, 2, 3, 4, 5); 12 let cs = colors.slice(2, 3); 13 console.log(cs instanceof MyArray); // true
由上面的例子可以看出, MyArray的实例不但可以使用数组的属性和方法, 还具有了数组的特性, slice返回的实例还是自身的实例。
这一行为的改变是通过Symbol.species属性实现的, 他被用于定义返回函数的静态访问器属性, 其作用其实是让子类可以自由的控制要返回自己的实例还是父类的实例。
1 // 内建类内部的大致实现方式 2 class MyClass { 3 static get[Symbol.species]() { 4 return this; 5 } 6 7 8 constructor(value) { 9 this.value = value; 10 } 11 12 13 clone() { 14 return new this.constructor[Symbol.species](this.value); 15 } 16 }
下面看一个栗子:
1 class c1 { 2 constructor(c) { 3 this.c = c; 4 } 5 static get[Symbol.species]() { 6 return this; 7 } 8 cc(c) { 9 console.log(this.constructor); 10 console.log(this.constructor[Symbol.species]); 11 return new this.constructor[Symbol.species](c); 12 } 13 } 14 15 16 // 如果c2想要自己控制自己所继承自c1的cc方法返回的是自己还是c1,那么就要通过Symbol.species来告 17 // 诉c1,我要返回什么,而c1给的默认选项是返回c2,也就是继承的属性,如果直接使用 18 // this.constructor,那么当我想要返回c1的实例,而不是c2自己时,我是没有办法改变的 19 // 此时就可以通过重写Symbol.species,来达到返回c1的实例的效果 20 class c2 extends c1 { 21 constructor(c) { 22 super(c); 23 } 24 static get[Symbol.species]() { 25 return c1; 26 } 27 } 28 29 30 let c = new c2(‘c2‘); 31 console.log(c.c); 32 console.log(c.cc(‘c22‘)); 33 34 35 /* 36 c2 37 [Function: c2] 38 [Function: c1] 39 c1 { c: ‘c22‘ } 40 */
通过上面栗子发现, 直接返回this.constructor也没有什么问题, 原先父类返回自身的方法能够返回现在的派生类, 而不是父类自己, 那么为什么不直接使用this.constructor呢? 因为这个必须要通过一种方式让子类可以进行自由控制, 写死之后如果我确实不想要返回自身的实例, 而是返回父类的实例呢? 那么写死后就没有任何办法进行修改了, 如果MyArray继承了Array, 那么slice永远都只能返回MyArray, 而不能返回一个Array的实例。
在下面的内建类中都已定义Symbol.species属性:
Array、 ArrayBuffer、 Map、 Promise、 Regexp、 Set、 Typed arrays
关于Symbol、 内部Symbol、 class等概念和详细用法, 请看《 深入理解ES6》 一书, 此文出处也是这本书。
关于原型链的继承相关知识请看《javascript高级教程第三版》其中有详细的讲解。