來自原形與原型鏈的拷問

本文由筆者師妹LazyCurry創做,收錄於筆者技術文章專欄下面試

前言

在JS中,咱們常常會遇到原型。字面上的意思會讓咱們認爲,是某個對象的原型,可用來繼承。可是其實這樣的理解是片面的,下面經過本文來了解原型與原型鏈的細節,再順便談談繼承的幾種方式。數組

原型

在講到原型以前,咱們先來回顧一下JS中的對象。在JS中,萬物皆對象,就像字符串、數值、布爾、數組等。ECMA-262把對象定義爲:無序屬性的集合,其屬性可包含基本值、對象或函數。對象是擁有屬性和方法的數據,爲了描述這些事物,便有了原型的概念。函數

不管什麼時候,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向該函數的原型對象。全部原型對象都會得到一個constructor屬性,這個屬性包含一個指向prototype屬性所在函數的指針。學習

這段話摘自《JS高級程序設計》,很好理解,以建立實例的代碼爲例。this

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        alert(this.name);
    };
}

const person1 = new Person("gali", 18);
const person2 = new Person("pig", 20);

avatar

上面例子中的person1跟person2都是構造函數Person()的實例,Person.prototype指向了Person函數的原型對象,而Person.prototype.constructor又指向Person。Person的每個實例,都含有一個內部屬性__proto__,指向Person.prototype,就像上圖所示,所以就有下面的關係。spa

console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

繼承

JS是基於原型的語言,跟基於類的面嚮對象語言有所不一樣,JS中並無類這個概念,有的是原型對象這個概念,原型對象做爲一個模板,新對象可從原型對象中得到屬性。那麼JS具體是怎樣繼承的呢?prototype

在講到繼承這個話題以前,咱們先來理解原型鏈這個概念。設計

原型鏈

構造函數,原型和實例的關係已經很清楚了。每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例對象都包含一個指向與原型對象的指針。這樣的關係很是好理解,可是若是咱們想讓原型對象等於另外一個類型的實例對象呢?那麼就會衍生出相同的關係,此時的原型對象就會含有一個指向另外一個原型對象的指針,而另外一個原型對象會含有一個指向另外一個構造函數的指針。若是另外一個原型對象又是另外一個類型的實例對象呢?這樣就構成了原型鏈。文字可能有點難理解,下面用代碼舉例。3d

function SuperType() {
    this.name = "張三";
}
SuperType.prototype.getSuperName = function() {
    return this.name;
};

function SubType() {
    this.subname = "李四";
}
SubType.prototype = new SuperType();
SubType.prototype.getSubName = function() {
    return this.subname;
};

const instance = new SubType();
console.log(instance.getSuperName()); // 張三

上述例子中,SubType的原型對象做爲SuperType構造函數的實例對象,此時,SubType的原型對象就會有一個__proto__屬性指向SuperType的原型對象,instance做爲SubType的實例對象,必然能共享SubType的原型對象的屬性,又由於SubType的原型對象又指向SuperType原型對象的屬性,所以可得,instance繼承了SuperType原型的全部屬性。指針

咱們都知道,全部函數的默認原型都是Object的實例,因此也能得出,SuperType的默認原型必然有一個__proto__指向Object.prototype。

圖中由__proto__屬性組成的鏈子,就是原型鏈,原型鏈的終點就是null

avatar

上圖可很清晰的看出原型鏈的結構,這不由讓我想到JS的一個運算符instanceof,instanceof可用來判斷一個實例對象是否屬於一個構造函數。

A instanceof B; // true

實現原理其實就是在A的原型鏈上尋找是否有原型等於B.prototype,若是一直找到A原型鏈的頂端null,仍然找不到原型等於B.prototype,那麼就可返回false。下面手寫一個instanceof,這個也是不少大廠經常使用的手寫面試題。

function Instance(left, right) {
    left = left.__proto__;
    right = right.prototype;
    while (true) {
        if (left === null) return false;
        if (left === right) return true;
        // 繼續在left的原型鏈向上找
        left = left.__propo__;
    }
}
原型鏈繼承

上面例子中,instance繼承了SuperType原型的屬性,其繼承的原理其實就是經過原型鏈實現的。原型鏈很強大,可用來實現繼承。但是單純的原型鏈繼承也是有問題存在的。

  • 實例屬性變成原型屬性,影響其餘實例
  • 建立子類型的實例時,不能向超類型的構造函數傳遞參數
function SuperType() {
    this.colorArr = ["red", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colorArr.push("black");
console.log(instance1.colorArr); // ["red", "blue", "green", "black"]

const instance2 = new SubType();
console.log(instance2.colorArr); // ["red", "blue", "green", "black"]

當SubType的原型做爲SuperType的實例時,此時SubType的實例對象經過原型鏈繼承到colorArr屬性,當修改了其中一個實例對象從原型鏈中繼承到的原型屬性時,便會影響到其餘實例。對instance1.colorArr的修改,在instance2.colorArr便能體現出來。

組合繼承

組合繼承指的是組合原型鏈和構造函數的技術,經過原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數實現對實例屬性的繼承。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    // 繼承屬性,借用構造函數實現對實例屬性的繼承
    SuperType.call(this, name);
    this.age = age;
}

// 繼承原型屬性及方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

const instance1 = new SubType("gali", 18);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // gali
instance1.sayAge(); // 18

const instance2 = new SubType("pig", 20);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // pig
instance2.sayAge(); // 20

上述例子中,借用構造函數繼承實例屬性,經過原型繼承原型屬性與方法。這樣就可以讓不一樣的實例分別擁有本身的屬性,又可共享相同的方法。而不會像原型繼承那樣,對實例屬性的修改影響到了其餘實例。組合繼承是JS最經常使用的繼承方式。

寄生組合式繼承

雖說組合繼承是最經常使用的繼承方式,可是有沒有發現,就上面的例子中,組合繼承中調用了2次SuperType函數。回憶一下,在第一次調用SubType時。

SubType.prototype = new SuperType();

這裏調用完以後,SubType.prototype會從SuperType繼承到2個屬性:name和colors。這2個屬性存在SubType的原型中。而在第二次調用時,就是在創造實例對象時,調用了SubType構造函數,也就會再調用一次SuperType構造函數。

SuperType.call(this, name);

第二次調用以後,便會在新的實例對象上建立了實例屬性:name和colors。也就是說,這個時候,實例對象跟原型對象擁有2個同名屬性。這樣實在是浪費,效率又低。

爲了解決這個問題,引入了寄生組合繼承方式。重點就在於,不須要爲了定義SubType的原型而去調用SuperType構造函數,此時只須要SuperType原型的一個副本,並將其賦值給SubType的原型便可。

function InheritPrototype(subType, superType) {
    // 建立超類型原型的一個副本
    const prototype = Object(superType.prototype);
    // 添加constructor屬性,由於重寫原型會失去constructor屬性
    prototype.constructor = subType;
    subType.prototype = prototype;
}

將組合繼承中的:

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

替換成:

InheritPrototype(SubType, SuperType);

寄生組合繼承的優勢在於,只須要調用一次SuperType構造函數。避免了在SubType的原型上建立多餘的沒必要要的屬性。

總結

溫故而知新,再次看回《JS高級程序設計》這本書的原型與原型鏈部分,發現不少之前忽略掉的知識點。而此次回看這個知識點,並輸出了一篇文章,對我來講受益不淺。寫文章每每不是爲了寫出怎樣的文章,其實中間學習的過程纔是最享受的。

相關文章
相關標籤/搜索