vue小技巧之数组继承

Array 继承

vue中监听数组变化的做法是重新包装数组的push、pop、shift等方法,每次调用这些方法时都会去通知相应的观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
const original = arrayProto[method]; // 原生数组方法
def(arrayMethods, method, function() {
// ......
const result = original.apply(this, args); // 先调用原生方法获取结果

const ob = this.__ob__;
/* 获取新插入的数据 */
let inserted;
switch (method) {
case 'push':
inserted = args;
break;
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}

if (inserted) ob.observerArray(inserted); // 监听新插入的数据

ob.dep.notify(); // 通知各观察者
return result;
});
});

最后在每次监听数组数据时,都会把arrayMethods当做数组数据的原型:

1
array.__proto__ = arrayMethods;

问题:

  1. 为什么不直接修改原生Array上的这些方法,如Array.prototype.push = function(){ /* ... */ }
  2. 上面的实现跟继承很相似,即子类的大部分方法跟父类一样,但也可以重写一些方法。可不可以实现一个自定义的Array子类,然后array.__proto__ = MyArray; ?

第一个问题很好回答,因为如果直接修改原生Array,会影响到所有使用数组的代码,肯定不希望在一些与vue无关的地方调用push结果还触发了数据监听。

第二个问题可以先做一个尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyArray() {
Array.apply(this, arguments);
}

MyArray.prototype = [];
MyArray.prototype.constructor = MyArray;

MyArray.prototype.push = function() {
Array.prototype.push.apply(this, arguments);
console.log(`加入了新数据`);
};

var myArr = new MyArray(1, 2, 3); // {}
myArr.push(4); // { length: 1, 0: 4 }

上面就是一个常见的寄生组合继承,打印一下myArr:

可以看到很奇怪的是myArr的值不是我们传入的1,2,3,4,这一点与原生Array的行为有差异:

1
2
var arr = new Array(1, 2, 3); // [1,2,3]
arr.push(4); // [1,2,3,4]

原因是因为 Array 构造函数不会对传进去的 this 做任何处理,其他原生类型的构造函数如 Error、Object 等也是这样:

1
2
3
4
var o = {};
Array.call({}, [1, 2, 3]); // o => {}
Array.call(undefined, [1, 2, 3]);
// => [1,2,3]

所以上面的myArr在初始化时就是一个空对象,不会被Array附加属性,调用push时也只是在操作这个空对象。从始至终,myArr就不是一个真正的数组。

数组有两个关键的的属性:

  1. 响应式的length属性,会自动根据元素增加而增加,并且如果减少 length,会自动删除多余的元素
  2. [[class]]内部属性(见baseGetTag的描述),他是Array.isArrayObject.prototype.toString.call的依据,我们无法改变它。

如果真的要在自定义的构造函数里获得一个原生的数组对象,只能直接在构造函数里初始化一个数组变量,然后设置这个变量的__proto__属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function MyArray2() {
var arr = [];
arr.push(...arguments);
arr.__proto__ = MyArray2.prototype;
return arr;
}

MyArray2.prototype = Object.create(Array.prototype);
MyArray2.prototype.constructor = MyArray2;

MyArray2.prototype.push = function() {
Array.prototype.push.apply(this, arguments);
console.log(`加入了新数据`);
};

var myArr2 = new MyArray2(1, 2, 3); // [1,2,3]
myArr2.push(4); // [1,2,3,4]

在 ES6 中情况有了一些改变,参考es6 入门

ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象 this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。

所以我们可以用 ES6 的extends来继承原生的Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyArray3 extends Array {
constructor(...args) {
super(...args);
}
push(...args) {
super.push(...args);
console.log(`加入了新数据~~~`);
}
}

var myArr3 = new MyArray3(1, 2, 3); // [1,2,3]
myArr3.push(4); // [1,2,3,4] 加入了新数据~~~
myArr3.push(5); // [1,2,3,4,5] 加入了新数据~~~
myArr3.length = 2; // [1,2]