這次帶給大家關於JS繼承的詳解,使用JS繼承的注意事項有哪些,下面就是實戰案例,一起來看一下。
ECMAScript 實作繼承的方式不只一種。這是因為 JavaScript 中的繼承機制並不是明確規定的,而是透過模仿來實現的。這意味著所有的繼承細節並非完全由解釋程序處理。可依需求決定適合的繼承方式。
物件冒充
建構子使用this關鍵字給所有屬性與方法賦值(即採用類別宣告的建構子方式)。因為建構函數只是一個函數,所以可讓ClassA建構函數成為ClassB的方法,然後呼叫它。 ClassB就會收到ClassA的建構函式中定義的性質和方法。
function ClassA(name) { this.name = name; this.sayName = function () { console.log(this.name); }; }function ClassB(name,age) { this.classA = ClassA; this.classA(name); delete this.classA; this.age = age; this.sayAge = function(){ console.log(this.age); } }var tom = new ClassA('Tom');var jerry = new ClassB('Jerry',25); tom.sayName(); //'Tom'jerry.sayName(); //'Jerry'jerry.sayAge(); //25console.log(tom instanceof ClassA); //trueconsole.log(jerry instanceof ClassA); //falseconsole.log(jerry instanceof ClassB); //true
所有新屬性和新方法都必須在刪除了新方法的程式碼行後定義,因為可能會覆寫超類別的相關屬性和方法
物件冒充可以實現多重繼承
如果存在ClassA和ClassB,這時ClassC想繼承這兩個類,如下:
function ClassA(name){ this.name = name; this.sayName = function (){ console.log(this.name); } }function ClassB(age){ this.age = age; this.sayAge = function(){ console.log(this.age); } }function ClassC(name,age){ this.method = ClassA; this.method(name); this.method = ClassB; this.method(age); delete this.method; }var tom = new ClassC('Tom',25); tom.sayName(); //'Tom';tom.sayAge(); //25console.log(tom instanceof ClassA); //falseconsole.log(tom instanceof ClassB); //falseconsole.log(tom instanceof ClassC); //true
這種實作方式的缺陷是:如果兩個類ClassA和ClassB有同名的屬性或方法,ClassB具有高優先級,因為它從後面的類別繼承。
由於這種繼承方法的流行,ECMAScript的第三版為Function物件加入了兩個方法,即call()和apply()。
call方法是與經典的物件冒充方法最相似的方法。它的第一個參數用作this的物件,其他參數都直接傳遞給函數本身
function sayName(prefix) { console.log(prefix + this.name); };var tom = {}; tom.name = "Tom"; sayName.call(tom, 'This is '); //'This is Tom'
函數sayName在物件外定義,但也可以引用this。
call方法改寫物件冒充
function ClassA(name){ this.name = name; this.sayName = function(){ console.log(this.name); }}function ClassB(name,age){ //this.method = ClassA; //this.method(name); //delete this.method; ClassA.call(this,name); this.age = age; this.sayAge = function (){ console.log(this.age); }}var tom = new ClassB('Tom',25);tom.sayName(); //'Tom'tom.sayAge(); //25console.log(tom instanceof ClassA); //falseconsole.log(tom instanceof ClassB); //true
call方法取代了使用屬性引用ClassA的方式。
apply
apply方法有兩個參數,用作this的物件和要傳遞給函數的參數陣列
function sayName(prefex,mark) { console.log(prefex+ this.name+ mark); };var tom = {}; tom.name = 'Tom'; sayName.apply(tom, ['This is ','!']); //'This is Tom!'
同樣可以使用apply改寫物件冒充
function ClassA(name){ this.name = name; this.sayName = function(){ console.log(this.name); }}function ClassB(name,age){ ClassA.apply(this,arguments); this.age = age; this.sayAge = function (){ console.log(this.age); }}var tom = new ClassB('Tom',25);tom.sayName(); //'Tom'tom.sayAge(); //25 console.log(tom instanceof ClassA); //falseconsole.log(tom instanceof ClassB); //true
只有超類別中參數順序和子類別中的參數完全一致時才可以傳遞參數陣列
原型鏈
# prototype物件是個模板,要實例化的物件都以這個模板為基礎,prototype物件的任何屬性和方法都會傳遞給這個類別的所有實例,原型鏈就是利用這種功能來實現繼承機制。
function ClassA() {}ClassA.prototype.name = 'Tom';ClassA.prototype.sayName = function () { console.log(this.name);};function ClassB() {}ClassB.prototype = new ClassA();var tom = new ClassB();tom.sayName(); //'Tom'console.log(tom instanceof ClassA); //trueconsole.log(tom instanceof ClassB); //true
這裡把ClassB的prototype屬性設定稱ClassA的實例,避免逐一賦值prototpye屬性。
在呼叫ClassA時沒有設定參數,因為在原型鏈中要確保建構子是無參的。
在原型鏈中,instanceof的結果也有了變化,對於ClassA和ClassB都回傳了true。
因為prototype屬性的重指定,子類別中的新屬性都必須出現在prototype被賦值後。
function ClassA() {}ClassA.prototype.name = 'Tom';ClassA.prototype.sayName = function () { console.log(this.name);};function ClassB() {}ClassB.prototype = new ClassA();ClassB.prototype.age = 25;ClassB.prototype.sayAge = function () { console.log(this.age);};var tom = new ClassA();var jerry = new ClassB();tom.sayName(); //'Tom'jerry.sayName(); //'Tom'jerry.name = 'Jerry';tom.sayName(); //'Tom'jerry.sayName(); //'Jerry'jerry.sayAge(); //25console.log(tom instanceof ClassA); //trueconsole.log(jerry instanceof ClassA); //trueconsole.log(jerry instanceof ClassB); //true
原型鏈的缺陷是不能實現多重繼承,因為類別的prototype會被重寫。
混合方式
物件冒充的問題是必須使用建構函數方式,而使用原型鏈就無法使用帶參數的建構函數,不過,可以試試兩者結合。
用物件冒充繼承建構函式的屬性,用原型鏈繼承prototype的方法。
function ClassA(name) { this.name = name; } ClassA.prototype.sayName = function () { console.log(this.name); };function ClassB(name, age) { ClassA.call(this, name); this.age = age; } ClassB.prototype = new ClassA(); ClassB.prototype.sayAge = function () { console.log(this.age); };var tom = new ClassA('Tom');var jerry = new ClassB('Jerry',25);console.log(tom instanceof ClassA); //trueconsole.log(jerry instanceof ClassA); //trueconsole.log(jerry instanceof ClassB); //trueconsole.log(jerry.constructor === ClassA); //trueconsole.log(ClassB.prototype.constructor === ClassA); //true
在ClassB建構子中用物件冒充繼承了ClassA的name屬性,用原型鏈繼承了ClassA的sayName方法,由於使用了原型鏈繼承方式,instanceof運作方式正常。
但是constructor屬性揭露出了問題,每一個prototype物件都有一個constructor屬性指向它的建構函數,而ClassB實例的建構子卻指向了ClassA,這會導致繼承鏈的錯亂。可以手動修改constructor的指向。
function ClassA(name) { this.name = name; } ClassA.prototype.sayName = function () { console.log(this.name); };function ClassB(name, age) { ClassA.call(this, name); this.age = age; } ClassB.prototype = new ClassA(); ClassB.prototype.constructor = ClassB; ClassB.prototype.sayAge = function () { console.log(this.age); };var tom = new ClassA('Tom');var jerry = new ClassB('Jerry',25);console.log(tom instanceof ClassA); //trueconsole.log(jerry instanceof ClassA); //trueconsole.log(jerry instanceof ClassB); //trueconsole.log(ClassA.constructor === ClassB); //falseconsole.log(jerry.constructor === ClassA); //falseconsole.log(ClassB.prototype.constructor === ClassA); //false
直接繼承原型鏈
為了節省內存,可以不執行創建ClassA實例,直接讓ClassB的原型指向ClassA的原型
function ClassA(name) { this.name = name; } ClassA.prototype.sayName = function () { console.log(this.name); };function ClassB(name, age) { ClassA.call(this, name); this.age = age; } ClassB.prototype = ClassA.prototype; ClassB.prototype.constructor = ClassB; ClassB.prototype.sayAge = function () { console.log(this.age); };var tom = new ClassA('Tom');var jerry = new ClassB('Jerry',25);console.log(ClassA.prototype.hasOwnProperty('sayAge')); //trueconsole.log(ClassA.prototype.constructor === ClassB); //true
這樣的缺陷是由於直接修改原型鏈指向,對於ClassB原型鏈中的性質也會影響到ClassA上,於是就出現了ClassA具有sayAge方法、ClassA的建構子性質為ClassB。
空物件作中介
為解決直接繼承原型鏈的缺點,可以利用一個空物件作為中介。
function ClassA(name) { this.name = name; } ClassA.prototype.sayName = function () { console.log(this.name); };function ClassB(name, age) { ClassA.call(this, name); this.age = age; }var fn = function(){}; fn.prototype = ClassA.prototype; ClassB.prototype = new fn(); ClassB.prototype.constructor = ClassB; ClassB.prototype.sayAge = function () { console.log(this.age); };console.log(ClassA.prototype.hasOwnProperty('sayAge')); //falseconsole.log(ClassA.prototype.constructor === ClassB); //false
雖然還是建立了物件實例,但由於空物件幾乎不佔內存,修改ClassB的原型也不會影響到ClassA。
封裝成extends方法
function extends(child,parent){ var fn = function (){}; fn.prototype = parent.prototype; child.prototype = new fn(); child.prototype.constructor = child; child.super = parent.prototype; }
JS的靈活性使我們可以透過多種方式實現繼承,了解其中的原理和實作可以幫助我們在不同的場景中選擇適合的方法。
相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!
推薦閱讀:
#以上是關於JS繼承的詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!