JavaScript繼承詳解(三)

來源:互聯網
上載者:User

在第一章中,我們使用建構函式和原型的方式在JavaScript的世界中實現了類和繼承, 但是存在很多問題。這一章我們將會逐一分析這些問題,並給出解決方案。

        註:本章中的jClass的實現參考了Simple            JavaScript Inheritance的做法。

首先讓我們來回顧一下第一章中介紹的例子:

 function Person(name) { this.name = name; } Person.prototype = { getName: function() { return this.name; } }  function Employee(name, employeeID) { this.name = name; this.employeeID = employeeID; } Employee.prototype = new Person(); Employee.prototype.getEmployeeID = function() { return this.employeeID; }; var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "ZhangSan"  

 

 

修正constructor的指向錯誤

 

從上一篇文章中關於constructor的描述,我們知道Employee執行個體的constructor會有一個指向錯誤,如下所示:

 var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.constructor === Employee); // false console.log(zhang.constructor === Object); // true  

我們需要簡單的修正:

 function Employee(name, employeeID) { this.name = name; this.employeeID = employeeID; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; Employee.prototype.getEmployeeID = function() { return this.employeeID; }; var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.constructor === Employee); // true console.log(zhang.constructor === Object); // false 

 

 

建立Employee類時執行個體化Person是不合適的

 

但另一方面,我們又必須依賴於這種機制來實現繼承。 解決辦法是不在建構函式中初始化資料,而是提供一個原型方法(比如init)來初始化資料。

 // 空的建構函式 function Person() { } Person.prototype = { init: function(name) { this.name = name; }, getName: function() { return this.name; } } // 空的建構函式 function Employee() { } // 建立類的階段不會初始化父類的資料,因為Person是一個空的建構函式 Employee.prototype = new Person(); Employee.prototype.constructor = Employee; Employee.prototype.init = function(name, employeeID) { this.name = name; this.employeeID = employeeID; }; Employee.prototype.getEmployeeID = function() { return this.employeeID; }; 

這種方式下,必須在執行個體化一個對象後手工調用init函數,如下:

 var zhang = new Employee(); zhang.init("ZhangSan", "1234"); console.log(zhang.getName()); // "ZhangSan" 

 

 

如何自動調用init函數?

 

必須達到兩個效果,構造類時不要調用init函數和執行個體化對象時自動調用init函數。看來我們需要在調用空的建構函式時有一個狀態標示。

 // 建立一個全域的狀態標示 - 當前是否處於類的構造階段 var initializing = false; function Person() { if (!initializing) { this.init.apply(this, arguments); } } Person.prototype = { init: function(name) { this.name = name; }, getName: function() { return this.name; } } function Employee() { if (!initializing) { this.init.apply(this, arguments); } } // 標示當前進入類的建立階段,不會調用init函數 initializing = true; Employee.prototype = new Person(); Employee.prototype.constructor = Employee; initializing = false; Employee.prototype.init = function(name, employeeID) { this.name = name; this.employeeID = employeeID; }; Employee.prototype.getEmployeeID = function() { return this.employeeID; };  // 初始化類執行個體時,自動調用類的原型函數init,並向init中傳遞參數 var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "ZhangSan" 

但是這樣就必須引入全域變數,這是一個不好的訊號。

 

 

如何避免引入全域變數initializing?

 

我們需要引入一個全域的函數來簡化類的建立過程,同時封裝內部細節避免引入全域變數。

 // 當前是否處於建立類的階段 var initializing = false; function jClass(baseClass, prop) { // 只接受一個參數的情況 - jClass(prop) if (typeof (baseClass) === "object") { prop = baseClass; baseClass = null; } // 本次調用所建立的類(建構函式) function F() { // 如果當前處於執行個體化類的階段,則調用init原型函數 if (!initializing) { this.init.apply(this, arguments); } } // 如果此類需要從其它類擴充 if (baseClass) { initializing = true; F.prototype = new baseClass(); F.prototype.constructor = F; initializing = false; } // 覆蓋父類的同名函數 for (var name in prop) { if (prop.hasOwnProperty(name)) { F.prototype[name] = prop[name]; } } return F; }; 

使用jClass函數來建立類和繼承類的方法:

 var Person = jClass({ init: function(name) { this.name = name; }, getName: function() { return this.name; } }); var Employee = jClass(Person, { init: function(name, employeeID) { this.name = name; this.employeeID = employeeID; }, getEmployeeID: function() { return this.employeeID; } });  var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "ZhangSan" 

OK,現在建立類和執行個體化類的方式看起來優雅多了。 但是這裡面還存在明顯的瑕疵,Employee的初始化函數init無法調用父類的同名方法。

 

 

如何調用父類的同名方法?

 

我們可以通過為執行個體化對象提供一個base的屬性,來指向父類(建構函式)的原型,如下:

 // 當前是否處於建立類的階段 var initializing = false; function jClass(baseClass, prop) { // 只接受一個參數的情況 - jClass(prop) if (typeof (baseClass) === "object") { prop = baseClass; baseClass = null; } // 本次調用所建立的類(建構函式) function F() { // 如果當前處於執行個體化類的階段,則調用init原型函數 if (!initializing) { // 如果父類存在,則執行個體對象的base指向父類的原型 // 這就提供了在執行個體對象中調用父類方法的途徑 if (baseClass) { this.base = baseClass.prototype; } this.init.apply(this, arguments); } } // 如果此類需要從其它類擴充 if (baseClass) { initializing = true; F.prototype = new baseClass(); F.prototype.constructor = F; initializing = false; } // 覆蓋父類的同名函數 for (var name in prop) { if (prop.hasOwnProperty(name)) { F.prototype[name] = prop[name]; } } return F; }; 

調用方式:

 var Person = jClass({ init: function(name) { this.name = name; }, getName: function() { return this.name; } }); var Employee = jClass(Person, { init: function(name, employeeID) { // 調用父類的原型函數init,注意使用apply函數修改init的this指向 this.base.init.apply(this, [name]); this.employeeID = employeeID; }, getEmployeeID: function() { return this.employeeID; }, getName: function() { // 調用父類的原型函數getName return "Employee name: " + this.base.getName.apply(this); } });  var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan" 

 

目前為止,我們已經修正了在第一章手工實現繼承的種種弊端。 通過我們自訂的jClass函數來建立類和子類,通過原型方法init初始化資料, 通過執行個體屬性base來調用父類的原型函數。

唯一的缺憾是調用父類的代碼太長,並且不好理解, 如果能夠按照如下的方式調用豈不是更妙:

 var Employee = jClass(Person, { init: function(name, employeeID) { // 如果能夠這樣調用,就再好不過了 this.base(name); this.employeeID = employeeID; } }); 

 

最佳化jClass函數

 

 // 當前是否處於建立類的階段 var initializing = false; function jClass(baseClass, prop) { // 只接受一個參數的情況 - jClass(prop) if (typeof (baseClass) === "object") { prop = baseClass; baseClass = null; } // 本次調用所建立的類(建構函式) function F() { // 如果當前處於執行個體化類的階段,則調用init原型函數 if (!initializing) { // 如果父類存在,則執行個體對象的baseprototype指向父類的原型 // 這就提供了在執行個體對象中調用父類方法的途徑 if (baseClass) { this.baseprototype = baseClass.prototype; } this.init.apply(this, arguments); } } // 如果此類需要從其它類擴充 if (baseClass) { initializing = true; F.prototype = new baseClass(); F.prototype.constructor = F; initializing = false; } // 覆蓋父類的同名函數 for (var name in prop) { if (prop.hasOwnProperty(name)) { // 如果此類繼承自父類baseClass並且父類原型中存在同名函數name if (baseClass && typeof (prop[name]) === "function" && typeof (F.prototype[name]) === "function") {  // 重定義函數name -  // 首先在函數上下文設定this.base指向父類原型中的同名函數 // 然後調用函數prop[name],返回函數結果  // 注意:這裡的自執行函數建立了一個上下文,這個上下文返回另一個函數, // 此函數中可以應用此上下文中的變數,這就是閉包(Closure)。 // 這是JavaScript架構開發中常用的技巧。 F.prototype[name] = (function(name, fn) { return function() { this.base = baseClass.prototype[name]; return fn.apply(this, arguments); }; })(name, prop[name]);  } else { F.prototype[name] = prop[name]; } } } return F; }; 

此時,建立類與子類以及調用方式都顯得非常優雅,請看:

 var Person = jClass({ init: function(name) { this.name = name; }, getName: function() { return this.name; } }); var Employee = jClass(Person, { init: function(name, employeeID) { this.base(name); this.employeeID = employeeID; }, getEmployeeID: function() { return this.employeeID; }, getName: function() { return "Employee name: " + this.base(); } });  var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan" 

 

至此,我們已經建立了一個完善的函數jClass, 協助我們在JavaScript中以比較優雅的方式實作類別和繼承。

在以後的章節中,我們會陸續分析網上一些比較流行的JavaScript類和繼承的實現。 不過萬變不離其宗,那些實現也無非把我們這章中提到的概念顛來簸去的“炒作”, 為的就是一種更優雅的調用方式。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.