javascript
详解JS原型链与继承
詳解JS原型鏈與繼承
- JavaScript
目錄
摘自JavaScript高級(jí)程序設(shè)計(jì):
繼承是OO語(yǔ)言中的一個(gè)最為人津津樂(lè)道的概念.許多OO語(yǔ)言都支持兩種繼承方式:?接口繼承?和?實(shí)現(xiàn)繼承?.接口繼承只繼承方法簽名,而實(shí)現(xiàn)繼承則繼承實(shí)際的方法.由于js中方法沒(méi)有簽名,在ECMAScript中無(wú)法實(shí)現(xiàn)接口繼承.ECMAScript只支持實(shí)現(xiàn)繼承,而且其?實(shí)現(xiàn)繼承?主要是依靠原型鏈來(lái)實(shí)現(xiàn)的.
概念
簡(jiǎn)單回顧下構(gòu)造函數(shù),原型和實(shí)例的關(guān)系:
每個(gè)構(gòu)造函數(shù)(constructor)都有一個(gè)原型對(duì)象(prototype),原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例(instance)都包含一個(gè)指向原型對(duì)象的內(nèi)部指針.
JS對(duì)象的圈子里有這么個(gè)游戲規(guī)則:
如果試圖引用對(duì)象(實(shí)例instance)的某個(gè)屬性,會(huì)首先在對(duì)象內(nèi)部尋找該屬性,直至找不到,然后才在該對(duì)象的原型(instance.prototype)里去找這個(gè)屬性.
如果讓原型對(duì)象指向另一個(gè)類型的實(shí)例…..有趣的事情便發(fā)生了.
即: constructor1.prototype = instance2
鑒于上述游戲規(guī)則生效,如果試圖引用constructor1構(gòu)造的實(shí)例instance1的某個(gè)屬性p1:
1).首先會(huì)在instance1內(nèi)部屬性中找一遍;
2).接著會(huì)在instance1.__proto__(constructor1.prototype)中找一遍,而constructor1.prototype 實(shí)際上是instance2, 也就是說(shuō)在instance2中尋找該屬性p1;
3).如果instance2中還是沒(méi)有,此時(shí)程序不會(huì)灰心,它會(huì)繼續(xù)在instance2.__proto__(constructor2.prototype)中尋找…直至Object的原型對(duì)象
搜索軌跡: instance1–> instance2 –> constructor2.prototype…–>Object.prototype
這種搜索的軌跡,形似一條長(zhǎng)鏈, 又因prototype在這個(gè)游戲規(guī)則中充當(dāng)鏈接的作用,于是我們把這種實(shí)例與原型的鏈條稱作?原型鏈?. 下面有個(gè)例子
function Father(){this.property = true; } Father.prototype.getFatherValue = function(){return this.property; } function Son(){this.sonProperty = false; } //繼承 Father Son.prototype = new Father();//Son.prototype被重寫,導(dǎo)致Son.prototype.constructor也一同被重寫 Son.prototype.getSonVaule = function(){return this.sonProperty; } var instance = new Son(); alert(instance.getFatherValue());//trueinstance實(shí)例通過(guò)原型鏈找到了Father原型中的getFatherValue方法.
注意: 此時(shí)instance.constructor指向的是Father,這是因?yàn)镾on.prototype中的constructor被重寫的緣故.
以上我們弄清楚了何為原型鏈,如有不清楚請(qǐng)盡量在下方給我留言
確定原型和實(shí)例的關(guān)系
使用原型鏈后, 我們?cè)趺慈ヅ袛嘣秃蛯?shí)例的這種繼承關(guān)系呢? 方法一般有兩種.
第一種是使用?instanceof?操作符, 只要用這個(gè)操作符來(lái)測(cè)試實(shí)例(instance)與原型鏈中出現(xiàn)過(guò)的構(gòu)造函數(shù),結(jié)果就會(huì)返回true. 以下幾行代碼就說(shuō)明了這點(diǎn).
alert(instance instanceof Object);//true alert(instance instanceof Father);//true alert(instance instanceof Son);//true由于原型鏈的關(guān)系, 我們可以說(shuō)instance 是 Object, Father 或 Son中任何一個(gè)類型的實(shí)例. 因此, 這三個(gè)構(gòu)造函數(shù)的結(jié)果都返回了true.
第二種是使用?isPrototypeOf()?方法, 同樣只要是原型鏈中出現(xiàn)過(guò)的原型,isPrototypeOf() 方法就會(huì)返回true, 如下所示.
alert(Object.prototype.isPrototypeOf(instance));//true alert(Father.prototype.isPrototypeOf(instance));//true alert(Son.prototype.isPrototypeOf(instance));//true原理同上.
原型鏈的問(wèn)題
原型鏈并非十分完美, 它包含如下兩個(gè)問(wèn)題.
問(wèn)題一: 當(dāng)原型鏈中包含引用類型值的原型時(shí),該引用類型值會(huì)被所有實(shí)例共享;
問(wèn)題二: 在創(chuàng)建子類型(例如創(chuàng)建Son的實(shí)例)時(shí),不能向超類型(例如Father)的構(gòu)造函數(shù)中傳遞參數(shù).
有鑒于此, 實(shí)踐中很少會(huì)單獨(dú)使用原型鏈.
為此,下面將有一些嘗試以彌補(bǔ)原型鏈的不足.
借用構(gòu)造函數(shù)
為解決原型鏈中上述兩個(gè)問(wèn)題, 我們開始使用一種叫做借用構(gòu)造函數(shù)(constructor stealing)的技術(shù)(也叫經(jīng)典繼承).
基本思想:即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù).
function Father(){this.colors = ["red","blue","green"]; } function Son(){Father.call(this);//繼承了Father,且向父類型傳遞參數(shù) } var instance1 = new Son(); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black"var instance2 = new Son(); console.log(instance2.colors);//"red,blue,green" 可見引用類型值是獨(dú)立的很明顯,借用構(gòu)造函數(shù)一舉解決了原型鏈的兩大問(wèn)題:
其一, 保證了原型鏈中引用類型值的獨(dú)立,不再被所有實(shí)例共享;
其二, 子類型創(chuàng)建時(shí)也能夠向父類型傳遞參數(shù).
隨之而來(lái)的是, 如果僅僅借用構(gòu)造函數(shù),那么將無(wú)法避免構(gòu)造函數(shù)模式存在的問(wèn)題–方法都在構(gòu)造函數(shù)中定義, 因此函數(shù)復(fù)用也就不可用了.而且超類型(如Father)中定義的方法,對(duì)子類型而言也是不可見的. 考慮此,借用構(gòu)造函數(shù)的技術(shù)也很少單獨(dú)使用.
組合繼承
組合繼承, 有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮兩者之長(zhǎng)的一種繼承模式.
基本思路: 使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承.
這樣,既通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能保證每個(gè)實(shí)例都有它自己的屬性. 如下所示.
function Father(name){this.name = name;this.colors = ["red","blue","green"]; } Father.prototype.sayName = function(){alert(this.name); }; function Son(name,age){Father.call(this,name);//繼承實(shí)例屬性,第一次調(diào)用Father()this.age = age; } Son.prototype = new Father();//繼承父類方法,第二次調(diào)用Father() Son.prototype.sayAge = function(){alert(this.age); } var instance1 = new Son("louis",5); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" instance1.sayName();//louis instance1.sayAge();//5var instance1 = new Son("zhai",10); console.log(instance1.colors);//"red,blue,green" instance1.sayName();//zhai instance1.sayAge();//10組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為 JavaScript 中最常用的繼承模式. 而且, instanceof 和 isPrototypeOf( )也能用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象.
同時(shí)我們還注意到組合繼承其實(shí)調(diào)用了兩次父類構(gòu)造函數(shù), 造成了不必要的消耗, 那么怎樣才能避免這種不必要的消耗呢, 這個(gè)我們將在后面講到.
原型繼承
該方法最初由道格拉斯·克羅克福德于2006年在一篇題為 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式繼承) 的文章中提出. 他的想法是借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象, 同時(shí)還不必因此創(chuàng)建自定義類型. 大意如下:
在object()函數(shù)內(nèi)部, 先創(chuàng)建一個(gè)臨時(shí)性的構(gòu)造函數(shù), 然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回了這個(gè)臨時(shí)類型的一個(gè)新實(shí)例.
function object(o){function F(){}F.prototype = o;return new F(); }從本質(zhì)上講, object() 對(duì)傳入其中的對(duì)象執(zhí)行了一次淺復(fù)制. 下面我們來(lái)看看為什么是淺復(fù)制.
var person = {friends : ["Van","Louis","Nick"] }; var anotherPerson = object(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.friends.push("Style"); alert(person.friends);//"Van,Louis,Nick,Rob,Style"在這個(gè)例子中,可以作為另一個(gè)對(duì)象基礎(chǔ)的是person對(duì)象,于是我們把它傳入到object()函數(shù)中,然后該函數(shù)就會(huì)返回一個(gè)新對(duì)象. 這個(gè)新對(duì)象將person作為原型,因此它的原型中就包含引用類型值屬性. 這意味著person.friends不僅屬于person所有,而且也會(huì)被anotherPerson以及yetAnotherPerson共享.
在 ECMAScript5 中,通過(guò)新增?object.create()?方法規(guī)范化了上面的原型式繼承.
object.create()?接收兩個(gè)參數(shù):
- 一個(gè)用作新對(duì)象原型的對(duì)象
- (可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象
object.create()?只有一個(gè)參數(shù)時(shí)功能與上述object方法相同, 它的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相同: 每個(gè)屬性都是通過(guò)自己的描述符定義的.以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性.例如:
var person = {name : "Van" }; var anotherPerson = Object.create(person, {name : {value : "Louis"} }); alert(anotherPerson.name);//"Louis"目前支持?Object.create()?的瀏覽器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.
提醒:?原型式繼承中, 包含引用類型值的屬性始終都會(huì)共享相應(yīng)的值, 就像使用原型模式一樣.
寄生式繼承
寄生式繼承是與原型式繼承緊密相關(guān)的一種思路, 同樣是克羅克福德推而廣之.
寄生式繼承的思路與(寄生)構(gòu)造函數(shù)和工廠模式類似, 即創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù),該函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象,最后再像真的是它做了所有工作一樣返回對(duì)象. 如下.
function createAnother(original){var clone = object(original);//通過(guò)調(diào)用object函數(shù)創(chuàng)建一個(gè)新對(duì)象clone.sayHi = function(){//以某種方式來(lái)增強(qiáng)這個(gè)對(duì)象alert("hi");};return clone;//返回這個(gè)對(duì)象 }這個(gè)例子中的代碼基于person返回了一個(gè)新對(duì)象–anotherPerson. 新對(duì)象不僅具有 person 的所有屬性和方法, 而且還被增強(qiáng)了, 擁有了sayH()方法.
注意:?使用寄生式繼承來(lái)為對(duì)象添加函數(shù), 會(huì)由于不能做到函數(shù)復(fù)用而降低效率;這一點(diǎn)與構(gòu)造函數(shù)模式類似.
寄生組合式繼承
前面講過(guò),組合繼承是 JavaScript 最常用的繼承模式; 不過(guò), 它也有自己的不足. 組合繼承最大的問(wèn)題就是無(wú)論什么情況下,都會(huì)調(diào)用兩次父類構(gòu)造函數(shù): 一次是在創(chuàng)建子類型原型的時(shí)候, 另一次是在子類型構(gòu)造函數(shù)內(nèi)部.?寄生組合式繼承就是為了降低調(diào)用父類構(gòu)造函數(shù)的開銷而出現(xiàn)的?.
其背后的基本思路是: 不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù)
function extend(subClass,superClass){var prototype = object(superClass.prototype);//創(chuàng)建對(duì)象prototype.constructor = subClass;//增強(qiáng)對(duì)象subClass.prototype = prototype;//指定對(duì)象 }extend的高效率體現(xiàn)在它沒(méi)有調(diào)用superClass構(gòu)造函數(shù),因此避免了在subClass.prototype上面創(chuàng)建不必要,多余的屬性. 于此同時(shí),原型鏈還能保持不變; 因此還能正常使用 instanceof 和 isPrototypeOf() 方法.
以上,寄生組合式繼承,集寄生式繼承和組合繼承的優(yōu)點(diǎn)于一身,是實(shí)現(xiàn)基于類型繼承的最有效方法.
下面我們來(lái)看下extend的另一種更為有效的擴(kuò)展.
function extend(subClass, superClass) {var F = function() {};F.prototype = superClass.prototype;subClass.prototype = new F(); subClass.prototype.constructor = subClass;subClass.superclass = superClass.prototype;if(superClass.prototype.constructor == Object.prototype.constructor) {superClass.prototype.constructor = superClass;} }我一直不太明白的是為什么要 “new F()“, 既然extend的目的是將子類型的 prototype 指向超類型的 prototype,為什么不直接做如下操作呢?
subClass.prototype = superClass.prototype;//直接指向超類型prototype顯然, 基于如上操作, 子類型原型將與超類型原型共用, 根本就沒(méi)有繼承關(guān)系.
new 運(yùn)算符
為了追本溯源, 我順便研究了new運(yùn)算符具體干了什么?發(fā)現(xiàn)其實(shí)很簡(jiǎn)單,就干了三件事情.
var obj = {}; obj.__proto__ = F.prototype; F.call(obj);第一行,我們創(chuàng)建了一個(gè)空對(duì)象obj;
第二行,我們將這個(gè)空對(duì)象的proto成員指向了F函數(shù)對(duì)象prototype成員對(duì)象;
第三行,我們將F函數(shù)對(duì)象的this指針替換成obj,然后再調(diào)用F函數(shù).
我們可以這么理解: 以 new 操作符調(diào)用構(gòu)造函數(shù)的時(shí)候,函數(shù)內(nèi)部實(shí)際上發(fā)生以下變化:
1、創(chuàng)建一個(gè)空對(duì)象,并且 this 變量引用該對(duì)象,同時(shí)還繼承了該函數(shù)的原型。
2、屬性和方法被加入到 this 引用的對(duì)象中。
3、新創(chuàng)建的對(duì)象由 this 所引用,并且最后隱式的返回 this.
__proto__ 屬性是指定原型的關(guān)鍵
以上, 通過(guò)設(shè)置 __proto__ 屬性繼承了父類, 如果去掉new 操作, 直接參考如下寫法
subClass.prototype = superClass.prototype;//直接指向超類型prototype那么, 使用 instanceof 方法判斷對(duì)象是否是構(gòu)造器的實(shí)例時(shí), 將會(huì)出現(xiàn)紊亂.
假如參考如上寫法, 那么extend代碼應(yīng)該為
function extend(subClass, superClass) {subClass.prototype = superClass.prototype;subClass.superclass = superClass.prototype;if(superClass.prototype.constructor == Object.prototype.constructor) {superClass.prototype.constructor = superClass;} }此時(shí), 請(qǐng)看如下測(cè)試:
function a(){} function b(){} extend(b,a); var c = new a(){}; console.log(c instanceof a);//true console.log(c instanceof b);//truec被認(rèn)為是a的實(shí)例可以理解, 也是對(duì)的; 但c卻被認(rèn)為也是b的實(shí)例, 這就不對(duì)了. 究其原因, instanceof 操作符比較的應(yīng)該是 c.__proto__ 與 構(gòu)造器.prototype(即 b.prototype 或 a.prototype) 這兩者是否相等, 又extend(b,a); 則b.prototype === a.prototype, 故這才打印出上述不合理的輸出.
那么最終,原型鏈繼承可以這么實(shí)現(xiàn),例如:
function Father(name){this.name = name;this.colors = ["red","blue","green"]; } Father.prototype.sayName = function(){alert(this.name); }; function Son(name,age){Father.call(this,name);//繼承實(shí)例屬性,第一次調(diào)用Father()this.age = age; } extend(Son,Father)//繼承父類方法,此處并不會(huì)第二次調(diào)用Father() Son.prototype.sayAge = function(){alert(this.age); } var instance1 = new Son("louis",5); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" instance1.sayName();//louis instance1.sayAge();//5var instance1 = new Son("zhai",10); console.log(instance1.colors);//"red,blue,green" instance1.sayName();//zhai instance1.sayAge();//10擴(kuò)展:
屬性查找
使用了原型鏈后, 當(dāng)查找一個(gè)對(duì)象的屬性時(shí),JavaScript 會(huì)向上遍歷原型鏈,直到找到給定名稱的屬性為止,到查找到達(dá)原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒(méi)有找到指定的屬性,就會(huì)返回 undefined. 此時(shí)若想避免原型鏈查找, 建議使用 **hasOwnProperty** 方法. 因?yàn)?**hasOwnProperty** 是 JavaScript 中唯一一個(gè)處理屬性但是不查找原型鏈的函數(shù). 如: console.log(instance1.hasOwnProperty('age'));//true對(duì)比:?isPrototypeOf?則是用來(lái)判斷該方法所屬的對(duì)象是不是參數(shù)的原型對(duì)象,是則返回true,否則返回false。如:
console.log(Father.prototype.isPrototypeOf(instance1));//trueinstanceof && typeof
上面提到幾次提到 instanceof 運(yùn)算符. 那么到底它是怎么玩的呢? 下面讓我們來(lái)趴一趴它的使用場(chǎng)景.
instanceof?運(yùn)算符是用來(lái)在運(yùn)行時(shí)指出對(duì)象是否是構(gòu)造器的一個(gè)實(shí)例, 例如漏寫了new運(yùn)算符去調(diào)用某個(gè)構(gòu)造器, 此時(shí)構(gòu)造器內(nèi)部可以通過(guò) instanceof 來(lái)判斷.(java中功能類似)
function f(){if(this instanceof arguments.callee)console.log('此處作為構(gòu)造函數(shù)被調(diào)用');elseconsole.log('此處作為普通函數(shù)被調(diào)用'); } f();//此處作為普通函數(shù)被調(diào)用 new f();//此處作為構(gòu)造函數(shù)被調(diào)用以上, this instanceof?arguments.callee 的值如果為 true 表示是作為構(gòu)造函數(shù)被調(diào)用的,如果為 false 則表示是作為普通函數(shù)被調(diào)用的。
對(duì)比:?typeof?則用以獲取一個(gè)變量或者表達(dá)式的類型, 一般只能返回如下幾個(gè)結(jié)果:
number,boolean,string,function(函數(shù)),object(NULL,數(shù)組,對(duì)象),undefined。
new運(yùn)算符
此處引用 艾倫的?JS 對(duì)象機(jī)制深剖——new 運(yùn)算符
接著上述對(duì)new運(yùn)算符的研究, 我們來(lái)考察 ECMAScript 語(yǔ)言規(guī)范中?new?運(yùn)算符的定義:
The new Operator
The production?NewExpression : new NewExpression?is evaluated as follows:Evaluate NewExpression.Call GetValue(Result(1)).If Type(Result(2)) is not Object, throw a TypeError exception.If Result(2) does not implement the internal?[[Construc]]?method, throw a TypeError exception.Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).Return Result(5).
其大意是,new 后必須跟一個(gè)對(duì)象并且此對(duì)象必須有一個(gè)名為 [[Construct]] 的內(nèi)部方法(其實(shí)這種對(duì)象就是構(gòu)造器),否則會(huì)拋出異常
根據(jù)這些內(nèi)容,我們完全可以構(gòu)造一個(gè)偽 [[Construct]] 方法來(lái)模擬此流程
function MyObject(age) {this.age = age; }MyObject.construct = function() {var o = {}, Constructor = MyObject;o.__proto__ = Constructor.prototype;// FF 支持用戶引用內(nèi)部屬性 [[Prototype]]Constructor.apply(o, arguments);return o; };var obj1 = new MyObject(10); var obj2 = MyObject.construct(10); alert(obj2 instanceof MyObject);// true不知不覺(jué)本文已經(jīng)寫了3天, 其實(shí)還有很多引申的東西沒(méi)有講出來(lái), 大家有什么問(wèn)題或好的想法歡迎在下方參與留言和評(píng)論.
本文作者:?louis
本文鏈接:?http://louiszhai.github.io/2015/12/15/prototypeChain/
轉(zhuǎn)載文章,如有侵權(quán),請(qǐng)告知,馬上刪除。
總結(jié)
以上是生活随笔為你收集整理的详解JS原型链与继承的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: apache https反向代理设置方案
- 下一篇: JavaScript 原型链和继承面试题