引言

  初識 JavaScript 對象的時候,我以為 JS 是沒有繼承這種說法的,雖說 JS 是一門面向?qū)ο笳Z言,可是面向?qū)ο蟮囊恍┨匦栽?JS 中并不存在(比如多態(tài),不過嚴格來說也沒有繼承)。這就困惑了我很長的時間,當我學習到 JS 原型的時候,我才發(fā)現(xiàn)了 JS 的新世界。本篇文章講解了 JavaScript new 操作符與對象的關(guān)系、原型和對象關(guān)聯(lián)(也就是俗稱的繼承)的原理,適合有一定基礎(chǔ)的同學閱讀。

 一、JavaScript 的類與對象

  許多書籍上都會說到如何在 JS 當中定義“類”,通常來講就是使用如下代碼:

1 function foo () {2     this.x = 1;3     this.y = 2;4 }5 var obj = new foo();  //{x:1, y:2}

  實際上這一個很糟糕的語言機制,我們首先要明確,在 JS 當中根本沒有“類”這種東西。在了解它之前,我們要先來了解下 JS 的發(fā)展歷史。

  JavaScript 隨著互聯(lián)網(wǎng)和瀏覽器而誕生,在早些年代,互聯(lián)網(wǎng)還比較貧乏,上網(wǎng)的成本也比較高,網(wǎng)速非常的慢,通常需要花很長的時間才能傳輸完一個純文本的 HTML 文件。所以那時候 Netscape 就提出,需要有一種解決方案,能使一些操作在客戶端進行而不需要通過服務(wù)器處理,比如用戶在填寫郵箱的時候少寫了一個“@”,在客戶端就可以檢查出錯誤并提示用戶而不需要在服務(wù)器進行解析,這樣就可以極大的降低通信操作帶來了延遲和帶寬消耗。而那時候,正巧 JAVA 問世,火的那叫個一塌糊涂,所以 Netscape 決定和 SUN 合作,在瀏覽器當中植入 JAVA 小程序(后來稱Java applet)。不過后來就這一方案產(chǎn)生了爭議,因為瀏覽器本來只需要很小的操作,而 JAVA 語言本身太“重”了,用來處理什么表單驗證的問題實在是大材小用,所以決定開發(fā)一門新的語言來支持客戶端的輕量級操作,而又要借鑒 JAVA 的語法。于是乎 Netscape 開發(fā)出了一門新的輕量級語言,在語法方面偏向于 C 和 JAVA,在數(shù)據(jù)結(jié)構(gòu)方面偏向于 JAVA,這門語言最初叫做 Mocha,后來經(jīng)過多年的演變,變成了現(xiàn)在的 JavaScript。

  故事說道這里,好像和本文并沒有什么關(guān)系...別急,馬上就要說道點子上了。這個語言為什么要取名 JavaScript 呢,其實它和 JAVA 并沒有半毛錢的關(guān)系,只是因為在那點年代,面向?qū)ο蠓椒▎柺啦挪痪茫械某绦騿T都推崇學習面向?qū)ο蠓椒?,再加?JAVA 的橫空出世和大力宣傳,只要和 JAVA 沾邊的東西就像是往臉上貼了金一樣,自帶光環(huán)。所以便借助了 JAVA 的名氣來進行宣傳,不過光是嘴皮子宣傳還不行,因為面向?qū)ο蠓椒ǖ耐瞥纾蠹叶剂晳T于面向?qū)ο蟮恼Z法,也就是 new Class() 的方法編寫代碼。不過 JavaScript 語言本身并沒有類的概念,其是多種語言的大雜燴,為了更加貼合習慣了面向?qū)ο笳Z法的程序員,于是 new 操作符誕生了。

  好了,說了這么大一堆故事,就是想告訴同學們,new 操作符在 JavaScript 當中本身就是一個充滿歧義的東西,它并不存在類的概念,只是貼合程序員習慣而已。那么在 JavaScript 當中 new 操作符和對象究竟有什么關(guān)系呢?思考下面這一段代碼:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

1 function foo () {2     this.x = 1;3     this.y = 2;4     return {5         z:36     }7 }8 var obj = new foo();  //{z:3}

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  咦?發(fā)生了什么奇怪的事情,x 和 y 哪里去了?實際上 new 操作符并不是傳統(tǒng)面向?qū)ο笳Z言那樣,創(chuàng)建一個類的實例,new 操作符實際上只是在引擎內(nèi)部幫我們在函數(shù)的開始創(chuàng)建好了一個對象,然后將函數(shù)的上下文綁定到這個對象上面,并在函數(shù)的末尾返回這個對象。這里需要注意的問題是,如果我們手動的返回了一個對象,那么按照函數(shù)執(zhí)行機制,一旦返回了一個值,那么該函數(shù)也就執(zhí)行結(jié)束,后面的代碼將不會執(zhí)行,所以說在剛才的例子中我們得到的對象只是我們手動定義的對象,并不是引擎幫我們創(chuàng)建的對象。 new 操作符實際上類似于以下操作:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

1 function foo () {2     //新創(chuàng)建一個對象,將 this 綁定到該對象上3     4     //在這里編寫我們想要的代碼5 6     //return this;7 }

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  不過需要注意的是,new 操作符只接受 Object 類型的值,如果我們手動返回的是基本類型,則還是會返回 this :

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

1 function foo () {2     this.x = 1;3     this.y = 2;4     return 0;5 }6 var obj = new foo();  //{x:1, y:2}

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  現(xiàn)在我們現(xiàn)在可以將 new 操作符定義成以下方法:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 function newOpertor (cls, ...args) { 2     var obj = {}; 3     cls.apply(obj, args); 4     return obj; 5 } 6  7 function foo (x, y) { 8     this.x = x; 9     this.y = y;10 }11 12 var obj = newOpertor(foo, 1, 2);  //{x:1, y:2}

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 二、對象的原型

   JavaScript 中存在類似繼承的機制,但是又不是標準面向?qū)ο蟮睦^承,在 JS 中使用的是原型的機制。要記住,在 JS 中只有對象,沒有類,對象的繼承是由原型來實現(xiàn),籠統(tǒng)的來說可以這樣理解,一個對象是另一個對象的原型,那么便可以把它比作父類,子類既然也就繼承了父類的屬性和方法。

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

1 function foo () {2     this.x = 1;3     this.y = 2;4 }5 6 foo.prototype.z = 37 8 var obj = new foo();9 console.log(obj.z);  //3

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  [[prototype]] 是函數(shù)的一個屬性,這個屬性的值是一個對象,該對象是所有以該函數(shù)為構(gòu)造器創(chuàng)造的對象的原型??梢园阉频睦斫鉃楦割悓ο螅敲聪鄳?,子類自然會繼承父類的屬性和方法。不過為什么要區(qū)分原型繼承和類繼承的概念呢?標準的面向?qū)ο蠓椒ǎ愂遣痪哂袑嶋H內(nèi)存空間,只是一個事物的抽象,對象才是事物的實體,而通過繼承得到的屬性和方法,同屬于該對象,不同的對象各自都擁有獨立的繼承而來的屬性。不過在 JavaScript 當中,由于沒有類的概念,一直都是對象,所以我們“繼承”的,是一個具有實際內(nèi)存空間的對象,也是實體,也就是說,所有新創(chuàng)建的子對象,他們共享一個父對象(后面我統(tǒng)稱為原型),不會擁有獨立的屬性:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 function foo () { 2     this.x = 1; 3     this.y = 2; 4 } 5  6 foo.prototype.z = 3 7  8 var obj1 = new foo(); 9 10 console.log(obj1.z);  //311 12 foo.prototype.z = 213 14 console.log(obj1.z);  //2

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  還記得我們之前所說的 new 操作符的原理嗎?new 操作符的本質(zhì)不是實例化一個類,而是引擎貼合習慣了面向?qū)ο缶幊谭椒ǖ某绦騿T,所以說 [[prototype]] 屬性本質(zhì)上也是 new 操作符的一個副產(chǎn)物。這個屬性只在函數(shù)上面有意義,該屬性定義了 new 操作符產(chǎn)生的對象的原型。除了 [[prototype]] 可以訪問到對象原型以外,還有一個非標準的方法,在每一個對象中都有一個 __proto__ 屬性,這個屬性直接關(guān)聯(lián)到了該對象的原型。這種方法沒有寫入 W3C 的標準規(guī)范,但是卻得到了瀏覽器的廣泛支持,許多瀏覽器都提供了該方法以供訪問對象的原型。(個人覺得 __proto__ 比 [[prototype]] 更能體現(xiàn)原型鏈的本質(zhì))

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 function foo () { 2     this.x = 1; 3     this.y = 2; 4 } 5  6 foo.prototype.z = 3 7  8 var obj1 = new foo(); 9 10 console.log(obj1.__proto__);  //{z:3}

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  除了使用 new 操作符和函數(shù)的 [[prototype]] 屬性定義對象的原型之外,我們還可以直接在對象上顯示的通過 __proto_ 來定義,這種定義對象原型的方式更能夠體現(xiàn)出 JavaScript 語言的本質(zhì),更能夠使初學者理解原型鏈繼承的機制。

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

1 var father = {x:1};2 3 var child = {4     y:2,5     __proto__:father6 };7 8 console.log(child.x);  //1

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  現(xiàn)在我們來完成之前那個自定義 new 操作(如果你還不能理解這個函數(shù),沒有關(guān)系,跳過它,這并不影響你接下來的學習):

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 function newOpertor (cls, ...args) { 2     var obj = Object.create(cls.prototype); 3     cls.apply(obj, args); 4     return obj; 5 } 6  7 function foo (x, y) { 8     this.x = x; 9     this.y = y;10 }11 12 foo.prototype.z = 313 14 var obj1 = newOpertor(foo, 1, 2)15 16 console.log(obj1.z);  //3

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 三、原型鏈

  介紹完原型之后,同學們需要明確以下幾個概念:

  •   JavaScript 采用原型的機制實現(xiàn)繼承;

  •   原型是一個具有實際空間的對象,所有關(guān)聯(lián)的子對象共享一個原型;

  那么 JavaScript 當中的原型是如何實現(xiàn)相互關(guān)聯(lián)的呢?JS 引擎又是如何查找這些關(guān)聯(lián)的屬性呢?如何實現(xiàn)多個對象的關(guān)聯(lián)形成一條原型鏈呢?

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 var obj1 = { 2     x:1 3 } 4  5 var obj2 = { 6     y:2, 7     __proto__:obj1 8 } 9 10 var obj3 = {11     z:3,12     __proto__:obj213 }14 15 console.log(obj3.y);  //216 console.log(obj3.x);  //1

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  在上面這段代碼,我們可以看出,對象的原型可以實現(xiàn)多層級的關(guān)聯(lián)的操作,obj1 是 obj2 的原型, obj2 同時又是 obj3 的原型,這種多層級的原型關(guān)聯(lián),就是我們常說的原型鏈。在訪問一個處于原型鏈當中的對象的屬性,會沿著原型鏈對象一直向上查找,我們可以把這種原型遍歷操作看成是一個單向的鏈表,每一個處于原型鏈的對象都是鏈表當中的一個節(jié)點,JS 引擎會沿著這條鏈表一層一層的向下查找屬性,如果找到了一個與之匹配的屬性名,則返回該屬性的值,如果在原型鏈的末端(也就是 Object.prototype)都沒有找到與之匹配的屬性,則返回 undefined。要注意這種查找方式只會返回第一個與之匹配的屬性,所以會發(fā)生屬性屏蔽:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 var obj1 = { 2     x:1 3 } 4  5 var obj2 = { 6     x:2, 7     __proto__:obj1 8 } 9 10 var obj3 = {11     x:3,12     __proto__:obj213 }14 15 console.log(obj3.x);  //3

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  若要訪問原型的屬性,則需要一層的一層的先向上訪問原型對象:

1 console.log(obj3.__proto__.x);  //22 console.log(obj3.__proto__.__proto__.x);  //1

  要注意的一點是,原型鏈的遍歷只會發(fā)生在 [[getter]] 操作上,也就是取值操作,也可以稱之右查找(RHS)。相反,若是進行 [[setter]] 操作,也就是賦值操作,也可以稱作左查找(LHS),則不會遍歷原型鏈,這條原則保證了我們在對對象進行操作的時候不會影響到原型鏈:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 var obj1 = { 2     x:1 3 } 4  5 var obj2 = { 6     __proto__:obj1 7 } 8  9 console.log(obj2.x);  //110 11 obj2.x = 2;12 13 console.log(obj2.x);  //214 console.log(obj1.x);  //1(并沒有發(fā)生變化)

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

   在遍歷原型鏈中,如果訪問帶有 this 引用的方法,可能會發(fā)生令你意想不到的結(jié)果:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 var obj1 = { 2     x:1, 3     foo: function  () { 4         console.log(this.x); 5     } 6 } 7  8 var obj2 = { 9     x:2,10     __proto__:obj111 }12 13 obj2.foo();  //2

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  在上面的內(nèi)容中,我們討論過,對象的原型相當于父類,我們可以繼承它所擁有的屬性和方法,所以在我們訪問 foo() 函數(shù)的時候時候,實際上調(diào)用該方法的對象是 obj2 而不是 obj1。關(guān)于更詳細的內(nèi)容,需要了解 this 和上下文綁定,這不在本篇文章的討論范圍之內(nèi)。

  關(guān)于原型鏈的問題,大家需要理解的一點是,任何對象的原型鏈終點,都是 Object.prototype,可以把 Object 理解為所有對象的父類,類似于 JAVA 一樣,所以說所有對象都可以調(diào)用一些 Object.prototype 上面的方法,比如 Object.prototype.valueOf() 以及 Object.prototype.toString() 等等。所有的 string 類型,其原型為 String.prototype ,String.prototype 是一個對象,所以其原型也就是 Object.prototype。這就是我們?yōu)槭裁茨軌蛟谝粋€ string 類型的值上調(diào)用一些方法,比如 String.prototype.concat() 等等。同理所有數(shù)組類型的值其原型是 Array.prototype,數(shù)字類型的值其原型是 Number.prototype:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

1 console.log({}.__proto__ === Object.prototype);  //true2 3 console.log("hello".__proto__ === String.prototype);  //true4 5 console.log(1..__proto__ === Number.prototype);  //true6 //注意用字面量訪問數(shù)字類型方法時,第一個點默認是小數(shù)標志7 8 console.log([].__proto__ === Array.prototype);  //true

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

   理解了原型鏈的遍歷操作,我們現(xiàn)在就可以學習如何添加屬于自己的方法。我們現(xiàn)在知道了所有字符串的原型都是 String.prototype ,那么我們可以對其進行修改來設(shè)置我們自己的內(nèi)置方法:

1 String.prototype.foo = function () {2     return this + " foo";3 }4 5 console.log("bar".foo());  //bar foo

  所以說,在處理一些瀏覽器兼容性問題的時候,我們可以直接修改內(nèi)置對象來兼容一些舊瀏覽器不支持的方法,比如 String.prototype.trim() :

1 if (!String.prototype.trim) {2     String.prototype.trim = function() {3         return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');4     };5 }

  不過需要注意,切忌隨意修改內(nèi)置對象的原型方法,一是因為這會帶來額外的內(nèi)存消耗,二是這可能會在系統(tǒng)中造成一些隱患,一般只是用來做瀏覽器兼容的 polyfill 。

四、 有關(guān)原型的方法

   for ... in 語句會遍歷原型鏈上所有可枚舉的屬性(關(guān)于屬性的可枚舉性質(zhì),可以參考 《JavaScript 常量定義》),有時我們在操作的時候需要忽略掉原型鏈上的屬性,只訪問該對象上的屬性,這時候我們可以使用 Object.prototype.hasOwnProperty() 方法來判斷屬性是否屬于原型屬性:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 var obj1 = { 2     x:1, 3 } 4  5 var obj2 = { 6     y:2, 7     __proto__:obj1 8 } 9 10 for(var key in obj2){11     console.log(obj2[key]);  //2, 112 }13 14 for(var key in obj2){15     if(obj2.hasOwnProperty(key)){16         console.log(obj2[key]);  //217     }18 }

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  我們知道通過 new 操作符創(chuàng)建的對象可以通過 instanceof 關(guān)鍵字來查看對象的“類”:

1 function foo () {}2 3 var obj = new foo();4 5 console.log(obj instanceof foo);  //true

  實際上這個操作也是不嚴謹?shù)?,我們現(xiàn)在已經(jīng)知道了 new 操作符在 JavaScript 當中本是一個具有歧義設(shè)計,instanceof 操作符本身也是一個會讓人誤解的操作符,它并沒有實例這種說法,實際上這個操作符只是判斷了對象與函數(shù)原型的關(guān)聯(lián)性,也就是說其返回的是表達式 object.__proto__ === function.prototype 的值。

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

 1 function foo () {} 2  3 var bar = { 4     x:1 5 } 6  7 foo.prototype = bar 8  9 var obj = {10     __proto__: bar11 }12 13 console.log(obj instanceof foo);  //true

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓

  在這一段代碼中,我們可以看出 obj 和 foo 并沒有任何關(guān)系,只是 obj 的原型和 foo.prototype 關(guān)聯(lián)到了同一個對象上面,所以其結(jié)果會返回 true?! ?/span>

  不過對基本類型類型使用 instanceof 方法的話,可能會產(chǎn)生意外的結(jié)果:

1 console.log("1" instanceof String);  //false2 3 console.log(1 instanceof Number);  //false4 5 console.log(true instanceof Boolean);  //false

  但是我們同樣可以使用使用字面量調(diào)用原型的方法,這可能會讓人感到困惑,不過我們不用擔心它,并不是原型鏈出現(xiàn)什么毛病,而是在對基本類型進行字面量操作的時候,會涉及到隱式轉(zhuǎn)換的問題。JS 引擎會先將字面量轉(zhuǎn)換成內(nèi)置對象,然后在調(diào)用上面的方法,隱式轉(zhuǎn)換問題不在本文的討論范圍之類,大家可以參考 Kyle Simpson — 《你不知道的 JavaScript (中卷)》。

  實際對象的 Object.prototype.isPrototypeOf() 方法更能體現(xiàn)出對象原型鏈的關(guān)系,此方法判斷一個對象是否是另一個對象的原型,不同于 instanceof 的是,此方法會遍歷原型鏈上所有的節(jié)點,若有匹配項則返回 true:

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓        

http://www.cnblogs.com/dong-xu/p/7134195.html

電腦培訓,計算機培訓,平面設(shè)計培訓,網(wǎng)頁設(shè)計培訓,美工培訓,Web培訓,Web前端開發(fā)培訓