摘要:綜上所述有原型鏈繼承,構造函數繼承經典繼承,組合繼承,寄生繼承,寄生組合繼承五種方法,寄生組合式繼承,集寄生式繼承和組合繼承的優點于一身是實現基于類型繼承的最有效方法。
一、前言
繼承是面向對象(OOP)語言中的一個最為人津津樂道的概念。許多面對對象(OOP)語言都支持兩種繼承方式::接口繼承 和 實現繼承 。
接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。由于js中方法沒有簽名,在ECMAScript中無法實現接口繼承。ECMAScript只支持實現繼承,而且其 實現繼承 主要是依靠原型鏈來實現的。
二、概念 2.1簡單回顧下構造函數,原型和實例的關系:每個構造函數(constructor)都有一個原型對象(prototype),原型對象都包含一個指向構造函數的指針,而實例(instance)都包含一個指向原型對象的內部指針。
關系如下圖所示:
2.2什么是原型鏈每一個對象擁有一個原型對象,通過__proto__指針指向上一個原型,并從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層層的,最終指向null。這種關系成為原型鏈(prototype chain)。
假如有這樣一個要求:instance實例通過原型鏈找到了Father原型中的getFatherValue方法.
function Father(){ this.property = true; } Father.prototype.getFatherValue = function(){ return this.property; } function Son(){ this.sonProperty = false; } //繼承 Father Son.prototype = new Father();//Son.prototype被重寫,導致Son.prototype.constructor也一同被重寫 Son.prototype.getSonVaule = function(){ return this.sonProperty; } var instance = new Son(); console.log(instance.getFatherValue());//true
注意: 此時instance.constructor指向的是Father,這是因為Son.prototype中的constructor被重寫的緣故.
2.3確定原型和實例之間的關系使用原型鏈后, 我們怎么去判斷原型和實例的這種繼承關系呢? 方法一般有兩種.
1、第一種是使用 instanceof 操作符。只要用這個操作符來測試實例(instance)與原型鏈中出現過的構造函數,結果就會返回true. 以下幾行代碼就說明了這點。
console.log(instance instanceof Object);//true
console.log(instance instanceof Father);//true
console.log(instance instanceof Son);//true
由于原型鏈的關系, 我們可以說instance 是 Object, Father 或 Son中任何一個類型的實例. 因此, 這三個構造函數的結果都返回了true.
2、第二種是使用 isPrototypeOf() 方法,。同樣只要是原型鏈中出現過的原型,isPrototypeOf() 方法就會返回true
console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(Father.prototype.isPrototypeOf(instance));//true
console.log(Son.prototype.isPrototypeOf(instance));//true
原型鏈并非十分完美, 它包含如下兩個問題:
問題一: 當原型鏈中包含引用類型值的原型時,該引用類型值會被所有實例共享;
問題二: 在創建子類型(例如創建Son的實例)時,不能向超類型(例如Father)的構造函數中傳遞參數.
有鑒于此, 實踐中很少會多帶帶使用原型鏈。為此,下面將有一些嘗試以彌補原型鏈的不足。
三、借用構造函數(經典繼承)為解決原型鏈中上述兩個問題, 我們開始使用一種叫做借用構造函數(constructor stealing)的技術(也叫經典繼承).
基本思想:即在子類型構造函數的內部調用超類型構造函數。
function Father(){ this.colors = ["red","blue","green"]; } function Son(){ Father.call(this);//繼承了Father,且向父類型傳遞參數 } 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" 可見引用類型值是獨立的
很明顯,借用構造函數一舉解決了原型鏈的兩大問題:
其一, 保證了原型鏈中引用類型值的獨立,不再被所有實例共享;
其二, 子類型創建時也能夠向父類型傳遞參數.
隨之而來的是, 如果僅僅借用構造函數,那么將無法避免構造函數模式存在的問題–方法都在構造函數中定義, 因此函數復用也就不可用了.而且超類型(如Father)中定義的方法,對子類型而言也是不可見的. 考慮此,借用構造函數的技術也很少多帶帶使用。
四、組合繼承組合繼承, 有時候也叫做偽經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式.
基本思路: 使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承.
這樣,既通過在原型上定義方法實現了函數復用,又能保證每個實例都有它自己的屬性. 如下所示.
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);//繼承實例屬性,第一次調用Father() this.age = age; } Son.prototype = new Father();//繼承父類方法,第二次調用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();//5 var instance1 = new Son("zhai",10); console.log(instance1.colors);//"red,blue,green" instance1.sayName();//zhai instance1.sayAge();//10
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為 JavaScript 中最常用的繼承模式. 而且, instanceof 和 isPrototypeOf( )也能用于識別基于組合繼承創建的對象.
同時我們還注意到組合繼承其實調用了兩次父類構造函數, 造成了不必要的消耗, 那么怎樣才能避免這種不必要的消耗呢, 這個我們將在后面講到。
五、原型繼承該方法最初由道格拉斯·克羅克福德于2006年在一篇題為 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式繼承) 的文章中提出. 他的想法是借助原型可以基于已有的對象創建新對象, 同時還不必因此創建自定義類型. 大意如下:
在object()函數內部, 先創建一個臨時性的構造函數, 然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例。
function object(o){ function F(){} F.prototype = o; return new F(); }
從本質上講, object() 對傳入其中的對象執行了一次淺復制. 下面我們來看看為什么是淺復制.
在 ECMAScript5 中,通過新增 object.create() 方法規范化了上面的原型式繼承.
object.create() 接收兩個參數:1、一個用作新對象原型的對象;2、(可選的)一個為新對象定義額外屬性的對象
var person = { friends : ["Van","Louis","Nick"] }; var anotherPerson = Object.create(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.friends.push("Style"); alert(person.friends);//"Van,Louis,Nick,Rob,Style"
object.create() 只有一個參數時功能與上述object方法相同, 它的第二個參數與Object.defineProperties()方法的第二個參數格式相同: 每個屬性都是通過自己的描述符定義的.以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性.例如:
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.
提醒: 原型式繼承中, 包含引用類型值的屬性始終都會共享相應的值, 就像使用原型模式一樣.
六、寄生式繼承寄生式繼承是與原型式繼承緊密相關的一種思路, 同樣是克羅克福德推而廣之.
寄生式繼承的思路與(寄生)構造函數和工廠模式類似, 即創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象. 如下.
function createAnother(original){ var clone = object(original);//通過調用object函數創建一個新對象 clone.sayHi = function(){//以某種方式來增強這個對象 alert("hi"); }; return clone;//返回這個對象 }
這個例子中的代碼基于person返回了一個新對象–anotherPerson. 新對象不僅具有 person 的所有屬性和方法, 而且還被增強了, 擁有了sayH()方法.
注意: 使用寄生式繼承來為對象添加函數, 會由于不能做到函數復用而降低效率;這一點與構造函數模式類似.
七、寄生組合式繼承前面講過,組合繼承是 JavaScript 最常用的繼承模式; 不過, 它也有自己的不足. 組合繼承最大的問題就是無論什么情況下,都會調用兩次父類構造函數: 一次是在創建子類型原型的時候, 另一次是在子類型構造函數內部.
寄生組合式繼承就是為了降低調用父類構造函數的開銷而出現的
其背后的基本思路是: 不必為了指定子類型的原型而調用超類型的構造函數
function extend(subClass,superClass){ var prototype = object(superClass.prototype);//創建對象 prototype.constructor = subClass;//增強對象 subClass.prototype = prototype;//指定對象 }
extend的高效率體現在它沒有調用superClass構造函數,因此避免了在subClass.prototype上面創建不必要,多余的屬性. 于此同時,原型鏈還能保持不變; 因此還能正常使用 instanceof 和 isPrototypeOf() 方法。
綜上所述有:原型鏈繼承,構造函數繼承(經典繼承),組合繼承,寄生繼承,寄生組合繼承五種方法,寄生組合式繼承,集寄生式繼承和組合繼承的優點于一身,是實現基于類型繼承的最有效方法。
下面我們來看下extend的另一種更為有效的擴展.
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
顯然, 基于如上操作, 子類型原型將與超類型原型共用, 根本就沒有繼承關系.
為了追本溯源, 我順便研究了new運算符具體干了什么?發現其實很簡單,就干了三件事情.
var obj = {}; obj.__proto__ = F.prototype; F.call(obj);
第一行,我們創建了一個空對象obj;
第二行,我們將這個空對象的__proto__成員指向了F構造函數的prototype對象;
第三行,我們將F函數對象的this指針替換成obj,然后再調用F函數.
new 操作符調用構造函數的時候,函數內部實際上發生以下變化:
1、創建一個空對象,并且 this 變量引用該對象,同時還繼承了該函數的原型。
2、屬性和方法被加入到 this 引用的對象中。
3、新創建的對象由 this 所引用,并且最后隱式的返回 this.
九、屬性查找使用了原型鏈后, 當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性為止,到查找到達原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有找到指定的屬性,就會返回 undefined。
此時若想避免原型鏈查找, 建議使用hasOwnProperty方法. 因為hasOwnProperty是 JavaScript 中唯一一個處理屬性但是不查找原型鏈的函數。
console.log(instance1.hasOwnProperty("age"));//true 對比: isPrototypeOf 則是用來判斷該方法實例對象是不是參數的原型對象,是則返回true,否則返回false。如 console.log(Father.prototype.isPrototypeOf(instance1));//true十、instanceof && typeof
instanceof 運算符是用來在運行時指出對象是否是構造器的一個實例。
instanceof 可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。
例如漏寫了new運算符去調用某個構造器, 此時構造器內部可以通過 instanceof 來判斷.(java中功能類似)
function f(){ if(this instanceof arguments.callee) console.log("此處作為構造函數被調用"); else console.log("此處作為普通函數被調用"); } f();//此處作為普通函數被調用 new f();//此處作為構造函數被調用
對比: typeof 則用以獲取一個變量或者表達式的類型, 一般只能返回如下幾個結果:
1、typeof 對于基本類型,除了 null 都可以顯示正確的類型
2、typeof 對于對象,除了函數都會顯示 object
3、對于 null 來說,雖然它是基本類型,但是會顯示 object
總的來說:number,boolean,string,function(函數),object(NULL,數組,對象),undefined。
十一、參考1、《JavaScript高級程序設計》
2、深入理解JavaScript系列(5):強大的原型和原型鏈
【注:我是saucxs,也叫songEagle,松寶寫代碼,文章首發于sau交流學習社區(https://www.mwcxs.top),關注我們每天閱讀更多精彩內容】
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104690.html
摘要:在節中,我們學習到了通過構造函數創建對象的三個重要步驟,其中的一步是把構造函數的對象設置為創建對象的原型。利用而不是直接用創建一個實例對象的目的是,減少一次調用父構造函數的執行。 JavaScript語言不像面向對象的編程語言中有類的概念,所以也就沒有類之間直接的繼承,JavaScript中只有對象,使用函數模擬類,基于對象之間的原型鏈來實現繼承關系,ES6的語法中新增了class關鍵...
摘要:有了原型鏈,就有了繼承,繼承就是一個對象像繼承遺產一樣繼承從它的構造函數中獲得一些屬性的訪問權。這里其實就是一個原型鏈與繼承的典型例子,開發中可能構造函數復雜一點,屬性定義的多一些,但是原理都是一樣的。 作用域、原型鏈、繼承與閉包詳解 注意:本章講的是在es6之前的原型鏈與繼承。es6引入了類的概念,只是在寫法上有所不同,原理是一樣的。 幾個面試常問的幾個問題,你是否知道 insta...
摘要:有了原型鏈,就有了繼承,繼承就是一個對象像繼承遺產一樣繼承從它的構造函數中獲得一些屬性的訪問權。這里其實就是一個原型鏈與繼承的典型例子,開發中可能構造函數復雜一點,屬性定義的多一些,但是原理都是一樣的。 作用域、原型鏈、繼承與閉包詳解 注意:本章講的是在es6之前的原型鏈與繼承。es6引入了類的概念,只是在寫法上有所不同,原理是一樣的。 幾個面試常問的幾個問題,你是否知道 insta...
摘要:忍者級別的函數操作對于什么是匿名函數,這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數是一個很重要且具有邏輯性的特性。通常,匿名函數的使用情況是創建一個供以后使用的函數。 JS 中的遞歸 遞歸, 遞歸基礎, 斐波那契數列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執行機制 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果...
閱讀 815·2023-04-25 20:18
閱讀 2097·2021-11-22 13:54
閱讀 2536·2021-09-26 09:55
閱讀 3898·2021-09-22 15:28
閱讀 2978·2021-09-03 10:34
閱讀 1713·2021-07-28 00:15
閱讀 1635·2019-08-30 14:25
閱讀 1284·2019-08-29 17:16