在现在快节奏的工做当中,不少基础的东西会渐渐地被丢掉。就如继承这个话题,写React的同窗应该都是class xxx extends React.Component,然而这能够理解为es5的一个语法糖,因此问题又回到了js如何实现继承。面试结束后,赶忙翻了翻积满灰尘的js高级程序设计,从新学习了一遍面向对象这一章,有一个建立对象的模式吸引到了我。javascript
在oo中咱们是经过类去建立自定义类型的对象,然而js中没有类的概念,在es5的时代,若是咱们要去模拟类,学过的同窗应该知道最好采用一种构造函数与原型混成的模式。而书中做者提到了一种有意思的模式,叫作寄生构造函数模式,代码以下:vue
function Person(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { alert(this.name); }; return o; } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); // "Nicholas"
对于这种模式有诸多不解:java
function SpecialArray() { // 建立数组 var values = new Array(); // 添加值 values.push.apply(values, arguments); // 添加方法 values.toPipedString = function() { return this.join("|"); }; // 返回数组 return values; } var colors = new SpecialArray("red", "blue", "green"); alert(colors.toPipedString()); // "red|blue|green"
从代码咱们得知,该构造函数是但愿建立一个具备额外方法的特殊数组,仔细想一想,这不就是继承嘛。继承在书中提到的最棒的方式是经过寄生组合式继承,那为何还要经过这种方式来实现Array继承,何况该方式有个很大的问题就是上面提到的类型没法经过instanceof来肯定。es6
咱们先来看看最经常使用的继承范式:寄生组合式继承,写法以下:面试
function SpecialArray() { // 调用Array函数,绑定给当前上下文 Array.apply(this, arguments); }; // 建立一个以Array.prototype为原型的对象做为SpecialArray的原型 SpecialArray.prototype = Object.create(Array.prototype); // constructor指向SpecialArray,默认状况[[enumerable]]为false Object.defineProperty(SpecialArray.prototype, "constructor", { enumerable: false, value: SpecialArray }); SpecialArray.prototype.toPipedString = function() { return this.join("|"); }; var arr = new SpecialArray(1, 2, 3); console.log(arr); // arr为SpecialArray {} console.log(new Array(1, 2, 3).hasOwnProperty('length')) // true 证实length是Array的实例属性 console.log(arr.hasOwnProperty('length')) // false 证实Array无视apply方法的this绑定
上面是典型的寄生组合式继承的写法,其存在几个问题:数组
既然this没法绑定,那咱们只能经过new一个Array来帮咱们构造一个数组实例并返回,此时咱们的构造函数应该像这样:app
function SpecialArray() { var values = new Array() // 添加初始值 values.push.apply(values, arguments); return values };
这其实就是咱们上面提到的寄生构造函数模式,可是此时返回的values是Array的实例,其原型对象是Array.prototype。这样会形成两个问题:函数
所以咱们要作的事情就是将生成的values实例的原型指向SpecialArray.prototype。咱们知道实例对象有一个__proto__属性,它指向其构造函数的原型,咱们能够经过修改该属性达到咱们的目的:学习
function SpecialArray() { var values = new Array() // 添加初始值 values.push.apply(values, arguments); // 将values的原型指向SpecialArray.prototype values.__proto__ = SpecialArray.prototype return values }; // 建立一个以Array.prototype为原型的对象做为SpecialArray的原型 SpecialArray.prototype = Object.create(Array.prototype); // constructor指向SpecialArray,默认状况[[enumerable]]为false Object.defineProperty(SpecialArray.prototype, "constructor", { enumerable: false, value: SpecialArray }); SpecialArray.prototype.toPipedString = function() { return this.join("|"); }; var arr = SpecialArray(1, 2, 3); // 不须要new console.log(arr.toPipedString()); // 1|2|3 console.log(arr instanceof SpecialArray) // true
咱们看到arr.toPipedString()能够返回正确的值了,且arr instanceof SpecialArray为true,即完成了继承。这种作法刚好和原型链继承相反,原型链继承是将父类实例做为子类的原型,而该方法是将父类实例的原型指针指向了子类的原型。可是,这种方法有一个很大的问题:__proto__属性是一个非标准属性,其在部分安卓机上未被实现,所以就有一种说法:ES5及如下的JS没法完美继承数组。this
es6的extends其实可以很方便的帮咱们完成Array继承:
class SpecialArray extends Array { constructor(...args) { super(...args) } toPipedString() { return this.join("|"); } } var arr = new SpecialArray(1, 2, 3) console.log(arr.toPipedString()) // 1|2|3 console.log(arr instanceof SpecialArray) // true
由于咱们调用super的时候是先新建父类的实例this,而后再用子类的构造函数SpecialArray来修饰this,这是es5当中作不到的一点。
咱们知道在vue中,push、pop、splice等方法能够触发响应式更新,而arr[0] = 1这种写法没法触发,缘由是defineProperty没法劫持数组类型的属性,那么vue是如何让经常使用的方法触发更新的呢,咱们看:
var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); });
这是vue的部分源码,咱们不用细看,看重点便可。咱们能够看到vue建立了一个对象arrayMethods,它是以Array.prototype做为原型的。而后改写了arrayMethods中的push、pop、shift等方法,即在原有功能的基础上触发ob.dep.notify()完成更新。那它是如何将咱们声明的数组指向arrayMethods的呢,咱们继续看:
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } }; /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment (target, src, keys) { /* eslint-disable no-proto */ target.__proto__ = src; /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; def(target, key, src[key]); } }
咱们看到vue先是作了个判断,即当前运行环境是否支持__proto__属性。若支持,执行protoAugment(),将target的__proto__指向arrayMethods,这其实就是咱们上面实现的es5的继承方式。若不支持,就将arrayMethods里的方法注入到target中完成mixin的操做。
寄生组合式继承虽然很完美,可是它没办法作到继承原生类型的构造函数,此时能够借用咱们实现的进化版的寄生构造函数模式完成继承。每一个阶段回头去看一些基础总会发现有不一样的收获,此次的分享内容也是看了js高级程序设计引起的一些思考。所以,百忙之中,咱们也须要常常去温习基础知识,所谓温故而知新,正是如此。