JavaScript的面向對象原理之原型鏈詳解

1、引言

在16年的10月份,在校內雙選會找前端實習的時候,hr問了一個問題:JavaScript的面向對象理解嗎?我張口就說「JavaScript是基於原型的!」。而後就沒什麼好說的了,hr可能不知道原型,我也解釋不了,由於我也就知道這一點而已,至於JavaScript到底面不面向對象,如何基於原型的,我都不太清楚。最近又開始找工做了,在掘金看到面試題就趕快看一下,但是一些代碼卻使我更加的困惑了,決定深刻認真地學習一下JavaScipt面向對象的知識,花了幾天的時間看了MDN上的Javacript對象相關的內容仍存疑惑,因而求助於那本有名的書:《You-Dont-Know-JS》的一章 「this & Object Prototypes」連接在最下面(Github上的英文版),個人疑惑也獲得瞭解答,這個過程也是有點痛並快樂着的,寫下這篇博客與你們分享一下本身的收穫。前端

2、JavaScript的對象

爲了可以清楚的解釋這一切,我先從對象講起。從其餘面嚮對象語言(如Java)而來的人可能認爲在JS裏的對象也是由類來實例化出來的,而且是由屬性和方法組成的。git

實際上在JS裏並非如你所想(我開始是這麼想的)那樣,對象或直接稱爲object,實際上只是一些映射對的集合,像Map,字典等概念。JS裏有大概7種類型(加上Symbol),數字、字符串、null、undefined、布爾、Symbol、對象。除對象之外的其餘類型屬於原始類型,就是說它們比較單純,包含的東西比較少,基本上就是字面量所表示的那些(像C語言中的一些類型,就是佔那麼多空間,沒有其餘的東西)。object基本上是一些鍵值對的集合,屬於引用類型,便是有一個名字去指向它來供別人使用的,就好像比較重的東西你拿不動,而只是拿了張記錄東西所在地的紙條。因此當A對象裏嵌套了B對象,僅表示A裏面有一個引用指向了B,並非真正把B包含在A裏面,雖然看起來是這樣(尤爲是從對象的字面量上來看),因此纔會有所謂的深拷貝與淺拷貝。github

有句話叫「JavaScript裏一切皆對象」,是由於在不少狀況下原始類型會被自動的轉爲對象,而函數實際上也是對象,這樣這句話看起來就頗有道理了。面試

說明對象的本質是爲了正確地認識對象,由於這關係到後面的理解。chrome

3、原型也是對象

JS的世界裏有一些對象叫原型,若是你有所懷疑,你能夠在chrome終端下打出如下代碼來驗證它的存在:編程

console.log(Object.prototype); //你能夠理解prototype是指向原型的引用數組

和 console.log(typeof Object.prototype);//object編程語言

在看看:函數

console.log(typeof {}.prototype);//undefined學習

爲何空對象{}沒有prototype對象呢,事實上prototype只是函數對象的一個屬性,而Array、Object倒是都是函數,而不是對象或者類(class):

console.log(typeof Object);//function

4、函數,特殊的對象

爲何JS裏沒有函數這樣一種類型,而typeof輸出的倒是function,即JS把函數也當作了一種類型,這揭示了函數做爲一種特殊對象的地位的超然性。

function foo(){console.log('inner foo');};

console.log(typeof foo);//function

console.log(typeof []);//object

與數組這種內建對象相比,說明了函數的地位非比尋常,實際上函數在JS中地位是一等的(或者說你們是平等的),函數能夠在參數中傳遞也說明了這一點,這使得JS具有了一些屬於函數式語言的特性。

函數與普通對象的地位相等,使得函數中的"this"關鍵字極具迷惑性,可能不少人都知道了,this指向的是函數在運行時的上下文,既不是函數對象自己,也不是函數聲明時所在做用域,具體是如何指向某個對象的就不在本文的討論範疇了,感興趣的能夠去看《You-Dont-Know-JS》。

查看以下代碼的輸出結果:

console.log(foo.prototype);

能夠看出foo.prototype是一個大概有兩個屬性的對象:constructor和__proto__。

console.log(foo.prototype.constructor === foo);//true

能夠看出一個函數的原型的constructor屬性指向的是函數自己,你能夠換成內建的一些函數:Object、String、Number,都是這樣的。

在觀察foo.prototype的__proto__以前,先考察下面看起來很面向對象的幾行代碼:

var fooObj = new foo();//inner foo

console.log(fooObj);//看獲得,fooObj也有一個__proto__的屬性,那麼__proto__是什麼呢,

console.log(fooObj.__proto__ === foo.prototype);//true

你知道了,對象的__proto__會指向其「構造函數」的prototype(先稱之爲構造函數)。

new 的做用其實是,新建立一個對象,在這個對象上調用new關鍵字後面的函數(this指向此對象,雖然這裏沒有用到),並將對象的__proto__指向了函數的原型,返回這個對象!

爲了便於理解以上的內容,我畫了這張圖:

 

用綠色代表了重點:foo.prototype,同時函數聲明能夠這樣聲明:

var bar = new Function("console.log('inner bar');");

猜想console.log(foo.__proto__ === Function.prototype);輸出爲true;

的確如此,因而再向圖片中加入一些東西:

看起來愈來愈複雜了,仍是沒有講到foo.prototype的__proto__指向那裏。

5、原型鏈的機制

若是把prototype對象當作是一個普通對象的話,那麼依據上面獲得的規律:

console.log(foo.prototype.__proto__ === Object.prototype);//true

是這樣的,從新看一個更常見的例子:

 1 function Person(name){  2     this.name = name;  3     var label = 'Person';  4 }  5 
 6 Person.prototype.nickName = 'PersonPrototype';  7 
 8 var p1 = new Person('p1');  9 
10 console.log(p1.name);//p1
11 console.log(p1.label);//undefined
12 console.log(p1.nickName);//PersonPrototype

先從圖上來看一下上面這些對象的關係:

爲何p1.nickName會輸出PersonPrototype,這是JS的內在的原型鏈機制,當訪問一個對象的屬性或方法時,JS會沿着__proto__指向的這條鏈路從下往上尋找,找不到就是undefined,這些原型鏈即圖中彩色的線條。

6、面向對象的語法

把JS中面向對象的語法的內容放到靠後的位置,是爲了避免給讀者形成更大的疑惑,由於只有明白了原型及原型鏈,這些語法的把戲你才能一目瞭然。

面向對象有三大特性:封裝、繼承、多態

封裝即隱藏對象的一些私有的屬性和方法,JS中經過設置對象的getter,setter方法來攔截你不想被訪問到的屬性或方法,具體有關對象的內部的東西限於篇幅就再也不贅述。

繼承是一個面向對象的語言看起來頗有吸引力的特性,以前看一些文章所謂的JS實現繼承的多種方式,只會令人更加陷入JS面向對象所形成的迷惑之中。

從原型鏈的機制出發來談繼承,加入Student要繼承Person,那麼應當使Sudent.prototype.__proto__指向Person.prototype。

因此藉助於__proto__實現繼承以下:

 1 function Person(name){  2     this.name = name;  3     var label = 'Person';  4 }  5 
 6 Person.prototype.nickName = 'PersonPrototype';  7 
 8 Person.prototype.greet = function(){  9     console.log('Hi! I am ' + this.name); 10 } 11 
12 function Student(name,school){ 13     this.name = name; 14     this.school = school; 15     var label = 'Student'; 16 } 17 
18 Student.prototype.__proto__ = Person.prototype;19 
20 var p1 = new Person('p1'); 21 var s1 = new Student('s1','USTB'); 22 p1.greet();//Hi! I am p1
23 s1.greet();//Hi! I am s1

這時的原型鏈如圖所示:

多態意味着同名方法的實現依據類型有所改變,在JS中只須要在「子類」Student的prototype定義同名方法便可,由於原型鏈是單向的,不會影響上層的原型。

1 Student.prototype.greet = function() 2 { 3     console.log('Hi! I am ' + this.name + ',my school is ' + this.school); 4 }; 5 s1.greet();//Hi! I am s1,my school is USTB

爲何Student和Person的prototype會有constructor指向函數自己呢,這是爲了當你訪問p1.constructor時會指向Person函數,即構造器(不過沒什麼實際意義),還有一個極具迷惑性的運算符:instanceof,

instanceof從字面意上來講就是判斷當前對象是不是後面的實例, 實際上其做用是判斷一個函數的原型是否在對象的原型鏈上:

s1 instanceof Student;//true
s1 instanceof Person;//true
s1 instanceof Object;//true

ES6新增的語法使用了 class 和extends來使得你的代碼更加的「面向對象」:

 1 class Person{  2  constructor(name){  3         this.name = name;  4  }  5 
 6  greet(){  7         console.log('Hello, I am ' + this.name);  8  }  9 } 10 
11 class Student extends Person{ 12  constructor(name, school){ 13  super(name); 14         this.school = school; 15  } 16 
17  greet(){ 18         console.log('Hello, I am '+ this.name + ',my school is ' + this.school); 19  } 20 } 21 
22 let p1 = new Person('p1'); 23 let s1 = new Student('s1', 'USTB'); 24 p1.greet();//Hello, I am p1
25 p1.constructor === Person;//true
26 s1 instanceof Student;//true
27 s1 instanceof Person;//true
28 s1.greet();//Hello, I am s1my school is USTB

super這個關鍵字用來引用「父類」的constructor函數,我是很懷疑這多是上面所說的__proto__繼承方式的語法糖,不過沒有看過源碼,並不清楚哈。

你確定已經清楚地明白了JavaScript是如何「面向對象」的了,諷刺地講,JavaScript不只名字上帶了Java,如今就連語法也要看起來像Java了,不過這種掩蓋自身語言實現的真實特性,來假裝成面向對象的語法只會使得JavaScript更使人迷惑和難以排查錯誤。

7、另外一種方式

事實上,總有些事情被許多人搞得複雜,繁瑣。在《You-Dont-Know-JS》一書中,提供了另外一種組織代碼的方式,拋去傳統面向對象風格語法帶來的複雜的函數原型鏈,代之以簡單對象組成的原型鏈,稱其爲行爲委託(Behavior Delegation)。

 1 var Person = {  2     init: function(name){  3         this.name = name;  4  },  5     greet: function(){  6         console.log('I am ' + this.name);  7  }  8 }  9 
10 
11 var Student = Object.create(Person); 12 
13 Student.init = function(name, school){ 14     Person.init.call(this, name); 15     this.school = school; 16 } 17 
18 Student.greet = function(){ 19     console.log('I am '+ this.name + ',my school is ' + this.school); 20 } 21 
22 var p1 = Object.create(Person); 23 var s1 = Object.create(Student); 24 p1.init('p1'); 25 p1.greet();//I am p1
26 s1.init('s1','USTB'); 27 s1.greet();//I am s1,my school is USTB

Object.create的做用是以某一對象爲原型來建立新的對象,能夠簡單理解爲向下擴展原型鏈的功能,即生成了一個__proto__指向源對象的新對象。

原型鏈如圖所示:

只是使用了一些對象,實現了和以前代碼的一樣的功能,而且具備更加簡單清晰的原型鏈,每一個對象之間的關係一目瞭然,沒有了煩人的prototype,簡單的原型鏈能使你更容易分析本身的代碼,找出錯誤所在。

兩種組織代碼的方式孰優孰劣,大致上是看得出來的,只是面向對象的語法可能看起來令人更熟悉,但我相信不明白具體內在的人必定會迷惑的。

8、總結

沒有其餘一門語言像JavaScript同樣會在語法層面上給人帶來極大的困惑,我想大概是由於JS不只是原型與函數式的混合(已經夠糟糕了),其還想方設法地假裝成基於類的「面向對象」的語言,並且一些關鍵詞的含義與行爲不符。

寫這篇文章大概耗費了我5天的時間和很多心血,但這個探索JS內在機制的過程是使人興奮的,雖不至於深刻到JS的本質,這是一種新奇的體驗,同時也使我明白了之後如何去了解一門新接觸的語言,透過語言的語法,看出使用某一門語言時的抽象化工做該如何去作,這其實體現了編程語言製造者的思惟。

 

參考文獻:

《You-Dont-Know-JS》 this & Object Prototypes一章 https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes

 MDN JavaScript對象入門 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects

相關文章
相關標籤/搜索