午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

JavaScript是如何面向?qū)ο蟮?/span>

 司馬小賊 2018-02-27

一、引言

在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

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

復(fù)制代碼
 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
復(fù)制代碼

先從圖上來看一下上面這些對象的關(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)繼承如下:

復(fù)制代碼
 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
復(fù)制代碼

這時的原型鏈如圖所示:

多態(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
s1 instanceof Person;//true
s1 instanceof Object;//true

ES6新增的語法使用了 class 和extends來使得你的代碼更加的“面向?qū)ο蟆?

復(fù)制代碼
 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
復(fù)制代碼

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)。

復(fù)制代碼
 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
復(fù)制代碼

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)了編程語言制造者的思維。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多