javascript
【面试必备】javascript的原型和继承
原型、閉包、作用域等知識可以說是js中面試必考的東西,通過你理解的深度也就能衡量出你基本功是否扎實。今天來復(fù)習(xí)一下javascript的原型和繼承,雖說是老生常談的話題,但對于這些知識,自己親手寫一遍能更加透徹的理解,能用自己的話說明白了,也就真正理解了。
原型是什么?
在javascript中,通過關(guān)鍵字new調(diào)用構(gòu)造器函數(shù)或者使用字面量聲明,我們可以得到一個對象實例。每個對象實例內(nèi)部都持有一個指針,指向一個普通的對象,這個普通的對象就是原型,這是天生的。為什么說它是普通的對象呢?因為它確實沒什么特別的地方,同樣也是某個構(gòu)造器函數(shù)的一個實例,這個構(gòu)造器可以是Object,可以是Array,也可以是其他你自己定義的構(gòu)造器函數(shù)。在js中,對象實例的原型是不可訪問的,不過在chrome和Firefox瀏覽器中,我們可以用一個名為__proto__的屬性來訪問到,來看一下所謂的原型長什么樣:
我用string的包裝類來創(chuàng)建了一個對象s,可以看到s的原型是一個對象,該對象上包含了一系列方法,比如我們熟悉的charAt。這里也就很明顯了,我們平時調(diào)用s.charAt(0),其實調(diào)用的是s的原型上的方法,也就是說,原型上的屬性可以被對象訪問到,就像是在訪問自身的屬性一樣。可以認(rèn)為原型就像孕婦肚子里的孩子一樣,孩子的胳膊也可以算是孕婦的胳膊,都在自己身上嘛。不過區(qū)別是這里的原型只是一個引用,并不是真正的包含這個對象。注意不要被__proto__后面的那個String迷惑到,s的原型是一個Object的實例,而不是String的實例。下面的代碼可以證明:
s.__proto__ instanceOf String; //false s.__proto__ instanceOf Object; //trues.hasOwnProperty('charAt'); //false s.__proto__.hasOwnProperty('charAt'); //true復(fù)制代碼要明白這個原型指針到底指向什么,就需要明白對象是如何創(chuàng)建出來的,所以接下來有必要了解一下構(gòu)造器函數(shù)。
javascript中沒有類,但可以把函數(shù)當(dāng)類使,被用來當(dāng)做類構(gòu)造器的函數(shù)就叫構(gòu)造器函數(shù),一般把首字母大寫來與普通函數(shù)進(jìn)行區(qū)別,其實就是豬鼻子插根蔥而已——裝象。js中一切都是對象,所以函數(shù)也是對象,所以函數(shù)也有一個原型指針。與實例對象不同的是,函數(shù)這種特殊的對象,它的原型可以通過prototype屬性顯式的訪問到,來看看String類的原型是啥樣的:
好像跟我們上面看到的s的原型是一模一樣的。。。是這樣嗎?驗證一下:
這是什么原因呢?我們就要細(xì)究一下var s = new String('s');在執(zhí)行的時候到底發(fā)生了什么,其實就是用new關(guān)鍵字調(diào)用函數(shù)String的時候發(fā)生了什么:
到這里就可以得出結(jié)論了:對象實例與它的構(gòu)造器函數(shù)擁有同一個原型,這個原型指向的是構(gòu)造器的父類的一個實例。
我第一次提到了“父類”,在面向?qū)ο蟮恼Z言中,如果B繼承自A,我們說A是B的父類。javascript是通過原型實現(xiàn)繼承的,所以我也可以說,我的原型指向誰,誰就是我的父類。通過上面的代碼我們可以得出:
String.prototype === s.__proto__ //true String.prototype instanceOf Object //true復(fù)制代碼可以用面向?qū)ο笳Z言的話說,Object就是String的父類。之所以這么說是因為這樣容易記住,再來重復(fù)一遍結(jié)論:對象實例與它的構(gòu)造器函數(shù)擁有同一個原型,這個原型指向的是構(gòu)造器的父類的一個實例。這個結(jié)論是非常有用的,由于對象實例的原型是不可訪問的(__proto__只是瀏覽器提供的能力),我們可以通過constructor屬性得到它的構(gòu)造器,然后用構(gòu)造器的prototype屬性來訪問到原型,像這樣:
s.constructor.prototype復(fù)制代碼理解的過程像是在做一道道證明題一樣。盡管有大師推薦在js中用構(gòu)造器函數(shù)這個稱呼來代替類,但為了便于理解和記憶,我還是這么叫吧~
原型的一些特性
明白是原型是什么東西,來看看原型都有哪些特性。其實也不能說是原型的特性,而是javascript語言的特性。
首先要看的就是所謂的原型鏈。每個對象都有原型,而對象的原型也是一個普通對象,那么就可以形成一個鏈,例如String對象的原型是Object類的一個實例,而Object對象的原型是一個空對象,空對象的原型是null。除去null不看的話,原型鏈的頂端是一個空對象{}
當(dāng)我們訪問對象的一個屬性時,會先從對象自身找,如過自身沒有,就會順著原型鏈一直往上找,直到找到為止。如果最后也沒找到,則返回undefined。這樣對象的內(nèi)容就會很“豐富”,我的是我的,原型的也是我的。通過修改原型的指向,對象可以獲得相應(yīng)原型上的屬性,js就是通過這種方式實現(xiàn)了繼承。
有一點需要注意的是,屬性的讀操作會順著原型鏈來查找,而寫操作卻不是。如果一個對象沒有屬性a,為該對象的a屬性賦值會直接寫在該對象上,而不是先在原型上找到該屬性然后修改值。舉個例子:
var s = new String('string'); s.charAt(0); //返回s s.hasOwnProperty('charAt'); //返回false 說明charAt不是自身的方法,而是原型上的 s.charAt = function(){return 1;} //為s的charAt賦值 s.hasOwnProperty('charAt'); //返回true 說明自身有了charAt方法 s.charAt(0); //返回1 這時候調(diào)用charAt找到了自身的方法 s.constructor.prototype.charAt.call(s,0); //返回s 調(diào)用原型上的charAt方法結(jié)果與原來一樣復(fù)制代碼上面的例子說明,為對象的屬性賦值是不會影響到原型的。這也是合理的,因為創(chuàng)建出來的對象s,它的原型是一個指針,指向了構(gòu)造器的原型。如果原型被修改,那么該類的其他實例也會跟著改變,這顯然是不愿意看到的。
我們愿意看到的是,修改了一個構(gòu)造器的原型,由它構(gòu)造出的實例也跟著動態(tài)變化,這是符合邏輯的。比如我們創(chuàng)建一個Person類,然后修改其原型上的屬性,觀察它的實例的變化:
function Person(name){this.name = name; } Person.prototype.age = 10; var p1 = new Person('p1'); console.log(p1.age); //10 Person.prototype.age = 11; console.log(p1.age); //11復(fù)制代碼這是因為age存在于原型上,p1只是擁有一個指針指向原型,原型發(fā)生改變后,用p1.age訪問該屬性必然也跟著變化。
用原型實現(xiàn)繼承
用原型實現(xiàn)繼承的思路非常簡單,令構(gòu)造函數(shù)的原型指向其父類的一個實例,這樣父類中的屬性和方法也就相當(dāng)于被引用到了,調(diào)用起來和調(diào)用自己的一樣。比如定義一個Programmer類繼承自Person:
function Person(name){this.name = name; } Person.prototype.age = 10;function Programmer(name){this.name = name; } Programmer.prototype = new Person(); Programmer.prototype.constructor = Programmer; var p1 = new Programmer('p1'); console.log(p1.age); //10復(fù)制代碼可以看到Programmer的實例p1繼承了Person的屬性age。另外需要注意的就是constructor的修正。因為我們new一個Person對象出來,它的constructor指向自身的構(gòu)造函數(shù)Person,所以在Programmer的原型中,這個constructor始終是Person,這與邏輯是不符的,所以必須顯式的“糾正”一下這個副作用,讓Programmer原型上的constructor指向自己。
以上代碼實現(xiàn)了一個基本的繼承。但其中還是有不少可以擴(kuò)展的地方,如果面試的時候只答出上面的這些,只能算是及格吧。關(guān)于如何優(yōu)化繼承的代碼,有位大牛的文章分析的十分詳細(xì),出于篇幅原因我在本篇就不再陳述。直接貼上鏈接地址:www.cnblogs.com/sanshi/arch…,共六篇系列博客,非常詳細(xì)。
----------------補(bǔ)充于2014.01.07---------------------
在上面的繼承實現(xiàn)方式中,有一個消耗內(nèi)存的地方,就是為子類指定原型時需要new一個父類的對象,有人做了比較好的處理,今天看到了代碼,據(jù)說是coffeescript中的,抄在這里:
var _hasProp = {}.hasOwnProperty; var extends = function(child,parent){for(var key in parent){if(_hasProp.call(parent,key)){child[key] = parent[key];}}function ctor(){this.constructor = child;}ctor.prototype = parent.prototype;child.prototype = new ctor();child._super_ = parnet.prototype;return child; }復(fù)制代碼是一個完整的實現(xiàn)繼承的方法。在內(nèi)部創(chuàng)建了一個最小化的對象,減少內(nèi)存消耗。
繼承的另一種實現(xiàn)方式
除了用原型,還有一種方式也可以實現(xiàn)繼承,叫做類復(fù)制。怎么個復(fù)制法呢,看下面的代碼:
function People(name){this.name = name;this.age = 11;this.getName = function(){return this.name;} }function Worker(name){People.call(this,name); }var w1 = new Worker('w1'); console.log(w1.getName()); //w1 console.log(w1.age); //11復(fù)制代碼在People構(gòu)造器中所有的屬性和方法都用this關(guān)鍵字定義在了自身,而不是放在它的原型上。在子類Worker中,用call把People當(dāng)作函數(shù)執(zhí)行了一下,并傳入this作為上下文對象。這樣就相當(dāng)于把People中的所有語句拿過來執(zhí)行一次,所有屬性的定義也都被復(fù)制過來了。同樣可以實現(xiàn)繼承。完全與原型無關(guān)。
那么這種方式與原型繼承有何區(qū)別呢?最大的區(qū)別就在于原型是一個引用,所有實例都引用一個共享的對象,每次創(chuàng)建出一個實例時,并不會復(fù)制原型的內(nèi)容,只是用一個指針指過去。而類復(fù)制的方法不存在共有的東西,每創(chuàng)建一個對象都把構(gòu)造器中的代碼執(zhí)行一次,當(dāng)構(gòu)造器中的方法較多時,會消耗很多的內(nèi)存。而原型繼承就不會了,只需一個指針指過去就完了。
由這種工作方式產(chǎn)生的另一個區(qū)別就是動態(tài)修改,我們知道在原型繼承中,只要修改了構(gòu)造器原型中的值,實例對象也跟著變化。但是類復(fù)制就不能了,每個對象都有自己的一份數(shù)據(jù),已創(chuàng)建出來的對象不會再受構(gòu)造器的影響了。
另外還有一點,就是屬性的訪問速度。類復(fù)制的方式,對象的屬性都在自身,所以在查找的時候可以立即找到,而原型繼承在查找的時候還得順著原型鏈向上查找,其訪問速度肯定不如類復(fù)制的快。
總結(jié)
以上是我理解到的原型與繼承的知識點,可能理解還是沒有那么透徹,只是從比較淺的層次梳理了一下。與原型相關(guān)的知識還有很多有深度的,還有待于繼續(xù)研究。這篇博客寫完我也感覺到,寫一篇基礎(chǔ)知識分析的文章真是挺困難的,需要你對每一個細(xì)節(jié)都掌握清楚,生怕稍不注意就給別人誤導(dǎo)。可能自己的水平也有待提高吧,本篇就先分析到這個程度,不知這個程度能否達(dá)到初級前端工程師的門檻。后續(xù)收集到了面試題,我會結(jié)合分析。
分類: javascript相關(guān),前端面試題 本文轉(zhuǎn)自呂大豹博客園博客,原文鏈接:http://www.cnblogs.com/lvdabao/p/3502944.html。總結(jié)
以上是生活随笔為你收集整理的【面试必备】javascript的原型和继承的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OSPF(Open Shortest P
- 下一篇: MYSQL使用inner join 进行