JavaScript原型鏈及常見的繼承方法

原型鏈

原型鏈的概念

在JavaScript中,每一個構造函數都有一個原型,這個原型中有一個屬性constructor會再次指回這個構造函數,這個構造函數所創造的實例對象,會有一個指針(也就是我們說的隱式原型__proto__或者是瀏覽器中顯示的[[Prototype]])指向這個構造函數的原型對象。如果說該構造函數的原型對象也是由另外一個構造函數所創造的實例,那麼該構造函數的原型對象也會存在一個指針指向另外一個構造函數的原型對象,周而復始,就形成瞭一條原型鏈。 最特別的是所有的沒有經過再繼承函數都是由Function實例化來的,所有的除瞭函數外的對象都是由Object實例化來的,其中Object也是由Function實例化來的,但是Object.prototype.__proto__ === null 是成立的。

再強調一遍:原型鏈是沿著對象的隱式原型一層層的去尋找的,找到的是構造函數所創造的實例。例如下:

這個就是相當於由Studentnew 出來的實例s,查找自身的 name 屬性,然後沿著原型鏈查找,找到Student中的prototype當中,然後找到瞭name這個屬性。

而這個例子,由紅框框起來的代碼(寄生繼承的關鍵代碼),代替註釋掉的部分,最終s是找不到name屬性的,這是因為紅框中的代碼,僅僅是將Student的隱式原型指向瞭Person的顯示原型對象,未能創建任何的實例,當然就不會存在屬性這個說法。

原型鏈的問題

原型鏈的問題主要有兩個方面,第一個問題是,當原型中出現包含引用值(比如數組)的時候,所有在這條原型鏈中的實例會共享這個屬性,造成“一發而動全身”的問題。第二個問題就是子類在實例化時,不能夠給父類型的構造函數傳參,即

無法在不影響所有對象實例的情況下把參數傳遞進父類型的構造函數傳參

幾種常見的繼承方法

盜用構造函數

function SuperType() {
    this.friends = ['張三','李四']
}

function SubType() {
    SuperType.call(this);
}

const p1 = new SubType();
p1.friends.push('王武');

const p2 = new SubType();
console.log(p2.friends); // ['張三','李四', '王武']

盜用構造函數實現繼承在這個例子中有瞭充分的體現: 首先在子類的構造函數中調用父類的構造函數。因為畢竟函數就是特定上下文中執行代碼的簡單對象,所以可以使用call()方法以創建的對象為上下文執行的構造函數。

盜用構造函數的主要問題,也是創建對象的幾種方式中構造函數模式自定義類型的問題:必須在構造函數中定義方法,造成內存浪費。另外,子類也不能訪問父類原型上定義的方法,因此,盜用構造函數也不會單獨使用。

組合繼承

組合繼承也稱為偽經典繼承,綜合瞭原型鏈和構造函數,將兩者的有點結合起來。基本的思路就是使用原型鏈繼承原型上的屬性和方法,而通過盜用構造函數繼承實現實例的屬性。這樣就可以把方法定義在原型上實現復用,又可以讓每個實例有自己的屬性。

function SuperType(name) {
    this.name = name;
    this.friends = ['張三','李四'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
// 繼承方法
SubType.prototype = new SuperType();
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
const p1 = new SubType('趙六', 12);
const p2 = new SubType('趙六2', 22);
// 創建的 p1 和 p2 能夠擁有自己的屬性並且引用值屬性也是獨立的,此外,每一個實例能夠公用父類的方法。

組合繼承已經接近完美瞭,但是,我們發現,實現組合繼承就要調用兩次父類構造函數。在本質上,子類型最終是要包含超類對象的所有實例屬性,子類構造函數隻要在執行時重寫自己的原型就行瞭,這就為減少一次調用父類構造函數提供瞭思路。

原型式繼承

const person = {
    name: 'zs',
    friends: ['ls','ww']
}
// 創造出一個實例,這個實例的隱式原型指向 person
const anotherPerosn = Object.create(person);
anotherPerosn.name = 'xm'
anotherPerosn.friends.push('zl')

console.log(anotherPerosn.name) // xm
console.log(anotherPerosn.friends) // ['ls','ww', 'zl'];

const anotherPerosn2 = Object.create(person);
anotherPerosn.name = 'xh'
anotherPerosn.friends.push('dd')

console.log(anotherPerosn2.name) // xh
console.log(anotherPerosn2.friends) // ['ls','ww', 'zl', 'dd'];

對於原型鏈繼承就不再過多的解釋瞭。。。。

寄生式繼承

寄生式繼承與原型式繼承比較相似,都會存在屬性引用值共享的問題。

function createAnotherPerson(original) {
    const clone = Object.create(original); //通過調用函數創建一個新的對象
    clone.sayHi = function() { // 以某種方式增強這個對象
        console.log('Hi');
    }
    return clone;
}

寄生式繼承,不僅存在著屬性引用值共享的問題而且函數還不能進行復用。

寄生組合式繼承

// 實現瞭寄生式組合繼承的核心邏輯
function inheritPrototype(subFn, parentFn){
    subFn.prototype = Object.create(parentFn.prototype); // 創建賦值對象
    Object.defineProperty(subFn.prototype,'constructor', {   // 增強對象
        enumerable: false,
        writable: false,
        configurable: false,
        value: subFn,
    })
}
function Person(name, age, address) {
    this.name = name;
    this.age = age;
    this.address = address;
}
Person.prototype.eating = function() {
        console.log(this.name + "正在吃飯");
} // 共享方法

function Student(name, age, address, sno) {
    Person.call(this, name, age, address); // 綁定 this 確保創建出來的對象是相互獨立的
    this.sno = sno;
    this.studing = function() {
        console.log(`${this.name}正在學習`)
    }
}

function Teacher(name, age, address, tno) {
    Person.call(this, name, age, address)
    this.tno = tno;
}

寄生+組合式(構造函數+原型鏈)完美的解決瞭其他繼承出現的問題。

到此這篇關於JavaScript原型鏈及常見的繼承方法的文章就介紹到這瞭,更多相關JS原型鏈內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: