關於js中原生構造函數的繼承

前言

在現在快節奏的工做當中,不少基礎的東西會漸漸地被丟掉。就如繼承這個話題,寫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

  1. 仔細一看,這特麼不就是所謂的工廠函數模式嗎?工廠模式的幾個缺點它都存在,一種是建立的全部對象均爲Object類型,沒法進行類型識別;其次每次建立對象都會從新生成一個function用來建立sayName屬性,浪費內存。
  2. 這裏的new有什麼意義嗎?new的做用是生成一個對象,將當前上下文即this指向該對象,而後return該對象。可是此處return了一個o,new就徹底沒用了。
    帶着諸多的不解,又看到了做者提到了該模式的一個使用場景,看代碼:
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綁定

上面是典型的寄生組合式繼承的寫法,其存在幾個問題:數組

  1. new的行爲上面介紹過,它會返回對象類型,而咱們的SpecialArray但願像Array同樣,new的時候返回數組。
  2. 咱們先經過hasOwnProperty證實了length是Array的一個實例屬性,既然如此經過執行Array.apply(this, arguments)會將length綁定給SpecialArray的實例arr,可是實際arr上沒有length屬性,所以能夠證實Array無視apply方法的this綁定。

既然this沒法綁定,那咱們只能經過new一個Array來幫咱們構造一個數組實例並返回,此時咱們的構造函數應該像這樣:app

function SpecialArray() {
  var values = new Array()
  // 添加初始值
  values.push.apply(values, arguments);
  return values
};

這其實就是咱們上面提到的寄生構造函數模式,可是此時返回的values是Array的實例,其原型對象是Array.prototype。這樣會形成兩個問題:函數

  1. 沒法經過instanceof肯定實例的類型,它始終爲Array的實例
  2. 咱們但願將構造函數的方法放入prototype實現共享,而不是放入構造函數中,在每次生成實例都從新生成一個function

所以咱們要作的事情就是將生成的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

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中的數組

咱們知道在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高級程序設計引起的一些思考。所以,百忙之中,咱們也須要常常去溫習基礎知識,所謂溫故而知新,正是如此。

相關文章
相關標籤/搜索