放棄class,一步一圖徹底理解Javascript的原型鏈

引言

  最近看了下ES6的class語法糖,越看越感覺這丫的就是個巨大的敗筆:外表包裝的像個類,底層就是原型鏈,但是卻不嚴格遵守原型鏈,語法十分混亂,有人說這對初學JS的新手很友好,我覺得這反而讓新手陷入誤解的深淵,作爲一名專注Java的後端程序員,都實在是看不下去了,於是連夜攻破了原型鏈,感覺原型和原型鏈比class清晰多了,並且更加強大和靈活,配合閉包,有什麼是不能完成的,況且要面試的時候,考的肯定還是原型鏈,所以這個class的存在感極低,想要簡化封裝和繼承,結果引入的問題其實更多,JS設計之初就根本沒有class的概念,如今非要逆天而行,揚湯止沸,還順便灑了自己一身。
  然而原型鏈和繼承真的困難到需要包一層這麼個蹩腳的語法糖嗎?
  本文講解自己對原型鏈的理解,個人習慣梳理知識點的細節,越詳細越好,這樣心中才有底氣。特記錄下個人對原型鏈的理解過程,方便日後參考。

__proto__, prototype和constructor

下面這三個屬性的定義非常重要,始終貫穿在原型中。
  prototype:此屬性只有構造函數纔有,它指向的是當前構造函數的原型對象
  __proto__:此屬性是任何對象在創建時都會有的一個屬性,它指向了產生當前對象的構造函數的原型對象,由於並非標準規定屬性,不要隨便去更改這個屬性的值,以免破壞原型鏈,但是可以藉助這個屬性來學習,所謂的原型鏈就是由__proto__連接而成的鏈。
  constructor:此屬性只有原型對象纔有,它默認指回prototype屬性所在的構造函數

一步一圖

  
在開始之前,一定要記住一句話,JS中不管是什麼類型的對象,都一定有構造函數,包括構造函數本身。

function Computer(){
    this.name = 'computer';
}

var c = new Computer();

我們現在來把原型鏈圖示一步一步地畫出來。

根據之前三個屬性的定義,實例c的__proto__是和Computer的prototype指向同一個對象,即Computer的原型對象,同時Computer的原型對象有constructor屬性,指回構造函數Computer,如下:
這裏寫圖片描述

驗證:

console.log(Computer.prototype === c.__proto__);            //true
console.log(Computer.prototype);                            //Object{...}
console.log(Computer.prototype.constructor === Computer);   //true

現在我們想繼續看下Computer的__proto__指向什麼,那我們要思維轉換一下,把Computer當成是一個實例對象,那麼應該有一個更底層的構造函數來產生Computer這個構造函數,Computer的__proto__就應該指向那個更底層構造函數的原型,所以我們推測應該還有一個構造函數,現在輸出來看下:

console.log(Computer.__proto__);            //function()
console.log(Computer.__proto__.constructor);//function Function()
console.log(Computer.__proto__.constructor === Function);//true
console.log(Computer.__proto__ === Function.prototype);//true

藉助原型對象的constructor可以看到那個底層構造函數是Function,所以繼續畫:
這裏寫圖片描述

再來看Function的__proto__,把Function當成一個實例,有一個更底層構造函數來產生Function,但是看Function這名字,它本來就是用來產生函數的,所以猜測應該沒有什麼更底層的構造函數了,它自己產生自己:

console.log(Function.__proto__);                //function ()
console.log(Function.__proto__.constructor);    //function Function()
console.log(Function.__proto__.constructor == Function);//true

這裏寫圖片描述

再來看Function.prototype.__proto__,原型對象就是個普通對象,它肯定有自己的構造函數,都知道JS是面向對象,Object差不多也該出現了:

console.log(Function.prototype.__proto__);                      //Object{...}
console.log(Function.prototype.__proto__.constructor);          //function Object()
console.log(Function.prototype.__proto__.constructor === Object);//true

console.log(Computer.prototype.__proto__);                      //Object{...}
console.log(Computer.prototype.__proto__.constructor);          //function Object()
console.log(Computer.prototype.__proto__.constructor === Object);//true

console.log(Function.prototype.__proto__ === Computer.prototype.__proto__);//true

所以:
這裏寫圖片描述

還剩最後一個,Object的__proto__指向什麼,同樣得找Object的構造函數。之前已經發現Function似乎充當了任何對象的構造函數,那麼猜測Object的__proto__應該是指向Function的原型:

console.log(Object.__proto__);                          //function ()
console.log(Object.__proto__ === Function.prototype);   //true
console.log(Object.__proto__.constructor);              //function Function()
console.log(Object.__proto__.constructor === Function); //true

再次證明就是這麼一回事:
這裏寫圖片描述
(Object的原型對象的__proto__到這裏已經到盡頭了,爲null,所以沒有畫出來)
這裏面主要是Function和Object比較特殊,它們都是由Function作爲構造函數產生,那麼__proto__都指向Function的原型,同時Function的原型的原型又是Object的原型,於是就出現了Function是Object,Object也是Function這種奇妙現象:

//instanceof的作用是判斷一個對象是否在另一個對象的原型鏈上
console.log(Object instanceof Function);    //true
console.log(Function instanceof Object);    //true

前面說了,原型鏈就是__proto__連接的鏈,從上面的圖中可以看到,如果選擇不同的起點,會有不同的原型鏈,比如下面藍色箭頭
這裏寫圖片描述

//以c爲起點
c -> Computer.prototype -> Object.prototype -> null
//以Computer爲起點
Computer -> Function.prototype -> Object.prototype -> null
//以Function爲起點
Function -> Function.prototype -> Object.prototype -> null
//以Object爲起點
Object -> Function.prototype -> Object.prototype -> null

感覺__proto__屬性這前後兩根下劃線還挺形象。

原型鏈繼承

就用上面的圖來繼承一個,分析一下:

function Laptop(){
    this.brand = 'acer';
}

Laptop.prototype = c;//也可以使用一箇中間空對象,如果你不想繼承父對象的實例成員的話
Laptop.prototype.constructor = Laptop;//這裏先不畫
var l = new Laptop();

這裏寫圖片描述

Laptop的構造函數爲Function,Laptop的__proto__指向構造函數Function的原型,那麼繼承之後,藍色的原型鏈爲:

l -> Laptop.prototype(也就是c) -> Computer.prototype -> Object.prototype -> null

注意了,現在Laptop.prototype(也就是c)還沒有constructor屬性,前面說了,原型對象一定要有一個constructor屬性,指回構造函數,但是Laptop.prototype是Computer的實例c,實例對象是沒有constructor屬性的,此時輸出c.constructor有結果,但是其實引用的是原型鏈上找到的Computer.prototype.constructor,這指向的是Computer:

console.log(c.constructor);             //function Computer()
console.log(c.constructor === Laptop.prototype.constructor);//true

所以我們要給Laptop的原型對象上加一個constructor屬性,指向Laptop:

Laptop.prototype.constructor = Laptop;

這裏可能有疑問了,剛剛還說這裏的Laptop原型(即實例c)的constructor是在原型鏈上引用的Computer.prototype.constructor,那把constructor改了,難道不會影響Computer的原型嗎?之前也是一直在這裏糾結很久,最後在《Javascript權威指南》的6.2.2節上找到原因,下面是原文:

Now suppose you assign to the property x of the object o. If o already has an own
(noninherited) property named x, then the assignment simply changes the value of this
existing property. Otherwise, the assignment creates a new property named x on the
object o. If o previously inherited the property x, that inherited property is now 
hidden by the newly created own property with the same name.
Property assignment examines the prototype chain to determine whether the assignment
is allowed. If o inherits a read-only property named x, for example, then the
assignment is not allowed.If the assignment is allowed, however, it always creates or
sets a property in the original object and never modifies the prototype chain. The fact 
that inheritance occurs when querying properties but not when setting them is a key 
feature of JavaScript because it allows us to selectively override inherited properties

翻譯一下:
現在假設你給o這個對象的x屬性賦值,如果o已經有了一個非繼承而來的屬性x,那麼賦值將僅僅是改變x的值。如果o本身並沒有x這個屬性,那麼賦值操作將在o對象上添加一個屬性x。如果o之前繼承了屬性x,那麼此時被繼承的屬性x將被新添加的同名屬性屏蔽。
屬性賦值操作會檢查原型鏈以確定是否允許賦值。比如,如果o對象繼承了一個只讀的屬性x,那麼賦值是禁止的。但是如果允許賦值,也總是只會在當前對象上添加或者修改屬性值,不會去改動原型鏈。繼承效果只會顯現在查詢屬性值而非修改屬性值的時候,此爲Javascript的特點,這讓我們可以有選擇性的覆寫繼承而來的屬性。

意思就是說在原型對象上,屬性的查找是沿着原型鏈一級一級向上找的,但是屬性的賦值就只發生在當前對象上。
所以上面那句代碼讓Laptop.prototype擁有了自己的constructor,最終圖爲:
這裏寫圖片描述

總結

1、prototype只有構造函數纔有,指向構造函數的原型。
2、__proto__任何對象都有,指向產生當前對象的構造函數的原型。
3、constructor只有原型對象纔有,默認指回prototype屬性所在的構造函數,使用原型鏈繼承之後,要給新的原型對象添加constructor屬性並指向構造函數。
4、任何對象都有產生自己的構造函數,包括構造函數自己。

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