這篇博客是讀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編譯一下看看,我會在後面的博客中分析這個問題。