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