JavaScript-讀 You Dont Know JS,原型繼承不是繼承

這篇博客是讀You Dont Know JS系列書中this & Object Prototypes這本書後總結的第三篇博客,也是最後一篇(第一篇講this到底是什麼,第二篇講Object到底是什麼)。

本篇博客中涉及到原型繼承的鏈式結構、prototype與__proto__(也就是[[prototype]])區別等問題。

繼承的本質

在傳統的面向對象編程中,大家都習慣抽象一些類,裏面封裝一些“公共”行爲,然後通過該類實例化一些對象,對象上可以定義一些方法來覆蓋類上的同名方法,實現每個對象特有的行爲。

如果你認爲以上這段話沒有錯誤,那你真的需要好好閱讀下面的內容。

繼承的本質是拷貝。傳統的面嚮對象語言中的父類、子類、實例是基於拷貝的。父類會把自己的方法拷貝到子類中,子類會把自己的方法拷貝到實例中。但是JavaScript中,常用的原型繼承,是基於原型鏈的關聯關係,不是拷貝。所以,原型繼承不是傳統意義上的繼承。我們有時候會用mixin來模擬拷貝繼承。

原型繼承

[[prototype]]

JavaScript中的對象有一個內部屬性,在語言規範中稱爲[[Prototype]],它只是一個其他對象的引用。幾乎所有的對象在被創建時,它的這個屬性都被賦予了一個非null值。

function Book(){}
let js = new Book();
let java = {};
console.log('js.__proto__', js.__proto__);
console.log('java.__proto__', java.__proto__);

這裏寫圖片描述

這裏的__proto__就是對象內部屬性[[prototype]],兩種方式創建對象__proto__的區別在於:使用new創建的對象__proto__指向創建它的那個函數,字面量創建的對象指向Object。換句話說,new操作符會通過__proto__鏈接兩個我們自己創建的對象(函數是內建對象)。

原型鏈中的prototype與__proto__

當我們訪問對象的屬性時,默認會尋找該對象上是否存在該屬性,存在則返回;不存在則繼續尋找__proto__屬性指向的對象上是否存在該屬性;依次尋找,終點是Object.prototype。當然也有非默認的情況,就是ES6的Proxy,它可以一次修改一個對象上所有屬性的讀取和寫入操作。

let obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});
obj.count = 0;
obj.count++;

這裏寫圖片描述

至於這個prototype,因爲取了個和__proto__相似的名字而時常被誤會。prototype僅存在於函數上(普通內建對象是沒有這個屬性的),當聲明一個函數的時候會根據特定規則爲這個函數增加一個prototype屬性,這個屬性指向一個新對象,新對象內有一個屬性constructor,指向這個函數。好複雜的樣子,看圖:
這裏寫圖片描述
借用《JavaScript高級程序設計》中的圖。

function Book(){}
let js = new Book();
let java = {};

console.log('js.prototype', js.prototype);
console.log('java.prototype', java.prototype);

console.log('Book.prototype', Book.prototype);
console.log('Book.__proto__', Book.__proto__);

這裏寫圖片描述

只有函數纔有prototype,因爲函數是一類內建對象,所以它也有__proto__,也就是[[prototype]]
我有一張收藏多年的圖,仔細分析可以更理解__proto__ prototype之間的關係。

這裏寫圖片描述

原型鏈上的屬性不僅僅覆蓋那麼簡單

大多數開發者認爲,如果一個屬性已經存在於[[Prototype]]鏈的高層,那麼對它的賦值將總是造成遮蔽。但事實真心沒那麼簡單:

  • 如果一個普通的名爲foo的數據訪問屬性在[[Prototype]]鏈的高層某處被找到,而且沒有被標記爲只讀(writable:false),那麼一個名爲foo的新屬性就直接添加到myObject上,形成一個 遮蔽屬性。

  • 如果一個foo在[[Prototype]]鏈的高層某處被找到,但是它被標記爲 只讀(writable:false) ,那麼設置既存屬性和在myObject上創建遮蔽屬性都是 不允許 的。如果代碼運行在strict mode下,一個錯誤會被拋出。否則,這個設置屬性值的操作會被無聲地忽略。不論怎樣,沒有發生遮蔽。

  • 如果一個foo在[[Prototype]]鏈的高層某處被找到,而且它是一個setter,那麼這個setter總是被調用。沒有foo會被添加到(也就是遮蔽在)myObject上,這個foo setter也不會被重定義。

你要小心原型繼承

原型繼承有兩大缺點:

  • 繼承關係複雜,如下圖
  • 由於prototype的可寫性,造成自省複雜(自省指找出類與實例,實例與實例直接的關係)

繼承關係

原型繼承大家都寫過:

function SuperType(){
    this.name = 'super type';
}
SuperType.prototype.tellName = function(){
    console.log(this.name);
}
SuperType.prototype.newName = 'father';
function SubType(){
    this.name = 'sub type';
}

SubType.prototype = Object.create(SuperType.prototype);
// 或者
// SubType.prototype = new SuperType();
let instance = new SubType();

再借《JavaScript高級程序設計》中的一張圖:
這裏寫圖片描述

初學時候想必大家都對這個圖困惑不已,到現在也不一定可以不看書的情況下清晰完成此圖。

自省

然後,我們爲了知道實例所屬的類,常常需要進行自省。

如果希望知道類(也就是new操作符調用的那個函數,通常稱爲構造函數)與實例之間的關係:

// instanceof
console.log(instance instanceof SubType); //true
console.log(instance instanceof SuperType); //true

// isPrototypeOf
console.log(SubType.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true

instanceof回答的問題是:在instance的整個[[Prototype]]鏈中,有沒有出現被那個被Foo.prototype所隨便指向的對象?
isPrototypeOf(..)回答的問題是:在instance的整個[[Prototype]]鏈中,Foo.prototype出現過嗎?

如果想知道兩個實例對象間的關係:

let superInstance = SubType.prototype;
......
console.log(superInstance.isPrototypeOf(instance)); //true

作者建議你這樣理解JS中的繼承

繼承是爲了獲得其他對象上的屬性,JS中不是用拷貝完成這種“獲得”,JS使用原型鏈來“鏈接”一些對象(通過[[prototype]])。傳統的原型鏈接方式很複雜,既然僅僅是對象鏈接,就不要再考慮“類”這個概念,僅考慮如何更簡單的進行鏈接。作者建議我們這樣寫”繼承”:

let SuperType = {
    name: 'super type',
    tellName: function(){
        console.log(this.name);
    }
}

let SubType = Object.create(SuperType);
SubType.name = 'sub type';
SubType.sayHi = function(){};

let instance = Object.create(SubType);
instance.name = 'instance';

instance.tellName(); //instance
// 自省更簡單明瞭
console.log(SubType.isPrototypeOf(instance)); //true
console.log(SuperType.isPrototypeOf(instance)); //true

我畫了一個關於以上對象關係的圖:
這裏寫圖片描述

是不是簡單很多。

mixin模擬拷貝繼承

當你真的非常想使用傳統的拷貝繼承,或者需要多繼承這種技術,就可以考慮mixin。很多工具庫都有類似mixin或者extend的工具函數,這裏簡單介紹。

function mixin(source, target){
    for(var key in source){
        if(!(key in target)){
            target[key] = source[key];
        }
    }
    return target
}

這裏你可以控制source上的屬性會不會覆蓋target上的同名屬性。
這部分很簡單,更多mixin相關內容你可以自己學習,不贅述。

一個懸念

你知道ES6的Class和extend是如何實現的麼,不要說是語法糖、JS中沒有類,我想問你知道這顆糖裏到底包着什麼。可以用babel編譯一下看看,我會在後面的博客中分析這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章