在16年的10月份,在校內雙選會找前端實習的時候,hr問了一個問題:JavaScript的面向對象理解嗎?我張口就說「JavaScript是基於原型的!」。而後就沒什麼好說的了,hr可能不知道原型,我也解釋不了,由於我也就知道這一點而已,至於JavaScript到底面不面向對象,如何基於原型的,我都不太清楚。最近又開始找工做了,在掘金看到面試題就趕快看一下,但是一些代碼卻使我更加的困惑了,決定深刻認真地學習一下JavaScipt面向對象的知識,花了幾天的時間看了MDN上的Javacript對象相關的內容仍存疑惑,因而求助於那本有名的書:《You-Dont-Know-JS》的一章 「this & Object Prototypes」連接在最下面(Github上的英文版),個人疑惑也獲得瞭解答,這個過程也是有點痛並快樂着的,寫下這篇博客與你們分享一下本身的收穫。前端
爲了可以清楚的解釋這一切,我先從對象講起。從其餘面嚮對象語言(如Java)而來的人可能認爲在JS裏的對象也是由類來實例化出來的,而且是由屬性和方法組成的。git
實際上在JS裏並非如你所想(我開始是這麼想的)那樣,對象或直接稱爲object,實際上只是一些映射對的集合,像Map,字典等概念。JS裏有大概7種類型(加上Symbol),數字、字符串、null、undefined、布爾、Symbol、對象。除對象之外的其餘類型屬於原始類型,就是說它們比較單純,包含的東西比較少,基本上就是字面量所表示的那些(像C語言中的一些類型,就是佔那麼多空間,沒有其餘的東西)。object基本上是一些鍵值對的集合,屬於引用類型,便是有一個名字去指向它來供別人使用的,就好像比較重的東西你拿不動,而只是拿了張記錄東西所在地的紙條。因此當A對象裏嵌套了B對象,僅表示A裏面有一個引用指向了B,並非真正把B包含在A裏面,雖然看起來是這樣(尤爲是從對象的字面量上來看),因此纔會有所謂的深拷貝與淺拷貝。github
有句話叫「JavaScript裏一切皆對象」,是由於在不少狀況下原始類型會被自動的轉爲對象,而函數實際上也是對象,這樣這句話看起來就頗有道理了。面試
說明對象的本質是爲了正確地認識對象,由於這關係到後面的理解。chrome
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
爲何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__指向那裏。
若是把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,這些原型鏈即圖中彩色的線條。
把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更使人迷惑和難以排查錯誤。
事實上,總有些事情被許多人搞得複雜,繁瑣。在《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,簡單的原型鏈能使你更容易分析本身的代碼,找出錯誤所在。
兩種組織代碼的方式孰優孰劣,大致上是看得出來的,只是面向對象的語法可能看起來令人更熟悉,但我相信不明白具體內在的人必定會迷惑的。
沒有其餘一門語言像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