一、引言在16年的10月份,在校內(nèi)雙選會找前端實習(xí)的時候,hr問了一個問題:JavaScript的面向?qū)ο罄斫鈫??我張口就說“JavaScript是基于原型的!”。然后就沒什么好說的了,hr可能不知道原型,我也解釋不了,因為我也就知道這一點而已,至于JavaScript到底面不面向?qū)ο?,如何基于原型的,我都不太清楚。最近又開始找工作了,在掘金看到面試題就趕快看一下,可是一些代碼卻使我更加的困惑了,決定深入認真地學(xué)習(xí)一下JavaScipt面向?qū)ο蟮闹R,花了幾天的時間看了MDN上的Javacript對象相關(guān)的內(nèi)容仍存疑惑,于是求助于那本有名的書:《You-Dont-Know-JS》的一章 “this & Object Prototypes”鏈接在最下面(Github上的英文版),我的疑惑也得到了解答,這個過程也是有點痛并快樂著的,寫下這篇博客與大家分享一下自己的收獲。 二、JavaScript的對象為了能夠清楚的解釋這一切,我先從對象講起。從其他面向?qū)ο笳Z言(如Java)而來的人可能認為在JS里的對象也是由類來實例化出來的,并且是由屬性和方法組成的。 實際上在JS里并不是如你所想(我開始是這么想的)那樣,對象或直接稱為object,實際上只是一些映射對的集合,像Map,字典等概念。JS里有大概7種類型(加上Symbol),數(shù)字、字符串、null、undefined、布爾、Symbol、對象。除對象以外的其他類型屬于原始類型,就是說它們比較單純,包含的東西比較少,基本上就是字面量所表示的那些(像C語言中的一些類型,就是占那么多空間,沒有其他的東西)。object基本上是一些鍵值對的集合,屬于引用類型,即是有一個名字去指向它來供別人使用的,就好像比較重的東西你拿不動,而只是拿了張記錄東西所在地的紙條。所以當(dāng)A對象里嵌套了B對象,僅表示A里面有一個引用指向了B,并不是真正把B包含在A里面,雖然看起來是這樣(尤其是從對象的字面量上來看),所以才會有所謂的深拷貝與淺拷貝。 有句話叫“JavaScript里一切皆對象”,是因為在很多情況下原始類型會被自動的轉(zhuǎn)為對象,而函數(shù)實際上也是對象,這樣這句話看起來就很有道理了。 說明對象的本質(zhì)是為了正確地認識對象,因為這關(guān)系到后面的理解。 三、原型也是對象JS的世界里有一些對象叫原型,如果你有所懷疑,你可以在chrome終端下打出以下代碼來驗證它的存在: console.log(Object.prototype); //你可以理解prototype是指向原型的引用 和 console.log(typeof Object.prototype);//object 在看看: console.log(typeof {}.prototype);//undefined 為什么空對象{}沒有prototype對象呢,事實上prototype只是函數(shù)對象的一個屬性,而Array、Object卻是都是函數(shù),而不是對象或者類(class): console.log(typeof Object);//function 四、函數(shù),特殊的對象為什么JS里沒有函數(shù)這樣一種類型,而typeof輸出的卻是function,即JS把函數(shù)也看成了一種類型,這揭示了函數(shù)作為一種特殊對象的地位的超然性。 function foo(){console.log('inner foo');}; console.log(typeof foo);//function console.log(typeof []);//object 與數(shù)組這種內(nèi)建對象相比,說明了函數(shù)的地位非比尋常,實際上函數(shù)在JS中地位是一等的(或者說大家是平等的),函數(shù)可以在參數(shù)中傳遞也說明了這一點,這使得JS具備了一些屬于函數(shù)式語言的特性。 函數(shù)與普通對象的地位相等,使得函數(shù)中的"this"關(guān)鍵字極具迷惑性,可能很多人都知道了,this指向的是函數(shù)在運行時的上下文,既不是函數(shù)對象本身,也不是函數(shù)聲明時所在作用域,具體是如何指向某個對象的就不在本文的討論范疇了,感興趣的可以去看《You-Dont-Know-JS》。 查看如下代碼的輸出結(jié)果: console.log(foo.prototype); 可以看出foo.prototype是一個大概有兩個屬性的對象:constructor和__proto__。 console.log(foo.prototype.constructor === foo);//true 可以看出一個函數(shù)的原型的constructor屬性指向的是函數(shù)本身,你可以換成內(nèi)建的一些函數(shù):Object、String、Number,都是這樣的。 在觀察foo.prototype的__proto__之前,先考察下面看起來很面向?qū)ο蟮膸仔写a: var fooObj = new foo();//inner foo console.log(fooObj);//看得到,fooObj也有一個__proto__的屬性,那么__proto__是什么呢, console.log(fooObj.__proto__ === foo.prototype);//true 你知道了,對象的__proto__會指向其“構(gòu)造函數(shù)”的prototype(先稱之為構(gòu)造函數(shù))。 new 的作用實際上是,新創(chuàng)建一個對象,在這個對象上調(diào)用new關(guān)鍵字后面的函數(shù)(this指向此對象,雖然這里沒有用到),并將對象的__proto__指向了函數(shù)的原型,返回這個對象! 為了便于理解以上的內(nèi)容,我畫了這張圖:
用綠色表明了重點:foo.prototype,同時函數(shù)聲明可以這樣聲明: var bar = new Function("console.log('inner bar');"); 猜測console.log(foo.__proto__ === Function.prototype);輸出為true; 的確如此,于是再向圖片中加入一些東西: 看起來越來越復(fù)雜了,還是沒有講到foo.prototype的__proto__指向那里。 五、原型鏈的機制如果把prototype對象看成是一個普通對象的話,那么依據(jù)上面得到的規(guī)律: 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 先從圖上來看一下上面這些對象的關(guān)系: 為什么p1.nickName會輸出PersonPrototype,這是JS的內(nèi)在的原型鏈機制,當(dāng)訪問一個對象的屬性或方法時,JS會沿著__proto__指向的這條鏈路從下往上尋找,找不到就是undefined,這些原型鏈即圖中彩色的線條。 六、面向?qū)ο蟮恼Z法把JS中面向?qū)ο蟮恼Z法的內(nèi)容放到靠后的位置,是為了不給讀者造成更大的疑惑,因為只有明白了原型及原型鏈,這些語法的把戲你才能一目了然。 面向?qū)ο笥腥筇匦裕悍庋b、繼承、多態(tài) 封裝即隱藏對象的一些私有的屬性和方法,JS中通過設(shè)置對象的getter,setter方法來攔截你不想被訪問到的屬性或方法,具體有關(guān)對象的內(nèi)部的東西限于篇幅就不再贅述。 繼承是一個面向?qū)ο蟮恼Z言看起來很有吸引力的特性,之前看一些文章所謂的JS實現(xiàn)繼承的多種方式,只會使人更加陷入JS面向?qū)ο笏斐傻拿曰笾小?/p> 從原型鏈的機制出發(fā)來談繼承,加入Student要繼承Person,那么應(yīng)當(dāng)使Sudent.prototype.__proto__指向Person.prototype。 所以借助于__proto__實現(xiàn)繼承如下: 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 這時的原型鏈如圖所示: 多態(tài)意味著同名方法的實現(xiàn)依據(jù)類型有所改變,在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指向函數(shù)本身呢,這是為了當(dāng)你訪問p1.constructor時會指向Person函數(shù),即構(gòu)造器(不過沒什么實際意義),還有一個極具迷惑性的運算符:instanceof, instanceof從字面意上來說就是判斷當(dāng)前對象是否是后面的實例, 實際上其作用是判斷一個函數(shù)的原型是否在對象的原型鏈上: s1 instanceof Student;//true ES6新增的語法使用了 class 和extends來使得你的代碼更加的“面向?qū)ο蟆? 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這個關(guān)鍵字用來引用“父類”的constructor函數(shù),我是很懷疑這可能是上面所說的__proto__繼承方式的語法糖,不過沒有看過源碼,并不清楚哈。 你肯定已經(jīng)清楚地明白了JavaScript是如何“面向?qū)ο蟆钡牧?,諷刺地講,JavaScript不僅名字上帶了Java,現(xiàn)在就連語法也要看起來像Java了,不過這種掩蓋自身語言實現(xiàn)的真實特性,來偽裝成面向?qū)ο蟮恼Z法只會使得JavaScript更令人迷惑和難以排查錯誤。 七、另一種方式事實上,總有些事情被許多人搞得復(fù)雜,繁瑣。在《You-Dont-Know-JS》一書中,提供了另一種組織代碼的方式,拋去傳統(tǒng)面向?qū)ο箫L(fēng)格語法帶來的復(fù)雜的函數(shù)原型鏈,代之以簡單對象組成的原型鏈,稱其為行為委托(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的作用是以某一對象為原型來創(chuàng)建新的對象,可以簡單理解為向下擴展原型鏈的功能,即生成了一個__proto__指向源對象的新對象。 原型鏈如圖所示: 只是使用了一些對象,實現(xiàn)了和之前代碼的同樣的功能,并且具有更加簡單清晰的原型鏈,每個對象之間的關(guān)系一目了然,沒有了煩人的prototype,簡單的原型鏈能使你更容易分析自己的代碼,找出錯誤所在。 兩種組織代碼的方式孰優(yōu)孰劣,大體上是看得出來的,只是面向?qū)ο蟮恼Z法可能看起來使人更熟悉,但我相信不明白具體內(nèi)在的人一定會迷惑的。 八、總結(jié)沒有其他一門語言像JavaScript一樣會在語法層面上給人帶來極大的困惑,我想大概是因為JS不僅是原型與函數(shù)式的混合(已經(jīng)夠糟糕了),其還千方百計地偽裝成基于類的“面向?qū)ο蟆钡恼Z言,而且一些關(guān)鍵詞的含義與行為不符。 寫這篇文章大概耗費了我5天的時間和不少心血,但這個探索JS內(nèi)在機制的過程是令人興奮的,雖不至于深入到JS的本質(zhì),這是一種新奇的體驗,同時也使我明白了以后如何去了解一門新接觸的語言,透過語言的語法,看出使用某一門語言時的抽象化工作該如何去做,這其實體現(xiàn)了編程語言制造者的思維。 |
|