在本章中,我們將分析John Resig關於JavaScript繼承的一個實現 - Simple JavaScript Inheritance。 John Resig作為jQuery的創始人而聲名在外。是《Pro JavaScript Techniques》的作者,而且Resig將會在今年秋天推出一本書《JavaScript Secrets》,非常期待。
調用方式
調用方式非常優雅: 注意:代碼中的Class、extend、_super都是自訂的對象,我們會在後面的程式碼分析中詳解。
var Person = Class.extend({ // init是建構函式 init: function(name) { this.name = name; }, getName: function() { return this.name; } }); // Employee類從Person類繼承 var Employee = Person.extend({ // init是建構函式 init: function(name, employeeID) { // 在建構函式中調用父類的建構函式 this._super(name); this.employeeID = employeeID; }, getEmployeeID: function() { return this.employeeID; }, getName: function() { // 調用父類的方法 return "Employee name: " + this._super(); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
說實話,對於完成本系列文章的目標-繼承-而言,真找不到什麼缺點。方法一如jQuery一樣簡潔明了。
程式碼分析
為了一個漂亮的調用方式,內部實現的確複雜了很多,不過這些也是值得的 - 一個人的思考帶給了無數程式員快樂的微笑 - 嘿嘿,有點肉麻。 不過其中的一段代碼的確迷惑我一段時間:
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
我曾在幾天前的部落格中寫過一篇文章專門闡述這個問題,有興趣可以向前翻一翻。
// 自執行的匿名函數建立一個上下文,避免引入全域變數 (function() { // initializing變數用來標示當前是否處於類的建立階段, // - 在類的建立階段是不能調用原型方法init的 // - 我們曾在本系列的第三篇文章中詳細闡述了這個問題 // fnTest是一個Regex,可能的取值為(/\b_super\b/ 或 /.*/) // - 對 /xyz/.test(function() { xyz; }) 的測試是為了檢測瀏覽器是否支援test參數為函數的情況 // - 不過我對IE7.0,Chrome2.0,FF3.5進行了測試,此測試都返回true。 // - 所以我想這樣對fnTest賦值大部分情況下也是對的:fnTest = /\b_super\b/; var initializing = false, fnTest = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/; // 基類建構函式 // 這裡的this是window,所以這整段代碼就向外界開闢了一扇窗戶 - window.Class this.Class = function() { }; // 繼承方法定義 Class.extend = function(prop) { // 這個地方很是迷惑人,還記得我在本系列的第二篇文章中提到的麼 // - this具體指向什麼不是定義時能決定的,而是要看此函數是怎麼被調用的 // - 我們已經知道extend肯定是作為方法調用的,而不是作為建構函式 // - 所以這裡this指向的不是Object,而是Function(即是Class),那麼this.prototype就是父類的原型對象 // - 注意:_super指向父類的原型對象,我們會在後面的代碼中多次碰見這個變數 var _super = this.prototype; // 通過將子類的原型指向父類的一個執行個體對象來完成繼承 // - 注意:this是基類建構函式(即是Class) initializing = true; var prototype = new this(); initializing = false; // 我覺得這段代碼是經過作者最佳化過的,所以讀起來非常生硬,我會在後面詳解 for (var name in prop) { prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { return function() { var tmp = this._super; this._super = _super[name]; var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } // 這個地方可以看出,Resig很會偽裝哦 // - 使用一個同名的局部變數來覆蓋全域變數,很是迷惑人 // - 如果你覺得拗口的話,完全可以使用另外一個名字,比如function F()來代替function Class() // - 注意:這裡的Class不是在最外層定義的那個基類建構函式 function Class() { // 在類的執行個體化時,調用原型方法init if (!initializing && this.init) this.init.apply(this, arguments); } // 子類的prototype指向父類的執行個體(完成繼承的關鍵) Class.prototype = prototype; // 修正constructor指向錯誤 Class.constructor = Class; // 子類自動擷取extend方法,arguments.callee指向當前正在執行的函數 Class.extend = arguments.callee; return Class; }; })();
下面我會對其中的for-in迴圈進行解讀,把自執行的匿名方法用一個局部函數來替換, 這樣有利於我們看清真相:
(function() { var initializing = false, fnTest = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/; this.Class = function() { }; Class.extend = function(prop) { var _super = this.prototype; initializing = true; var prototype = new this(); initializing = false; // 如果父類和子類有同名方法,並且子類中此方法(name)通過_super調用了父類方法 // - 則重新定義此方法 function fn(name, fn) { return function() { // 將執行個體方法_super保護起來。 // 個人覺得這個地方沒有必要,因為每次調用這樣的函數時都會對this._super重新定義。 var tmp = this._super; // 在執行子類的執行個體方法name時,添加另外一個執行個體方法_super,此方法指向父類的同名方法 this._super = _super[name]; // 執行子類的方法name,注意在方法體內this._super可以調用父類的同名方法 var ret = fn.apply(this, arguments); this._super = tmp; // 返回執行結果 return ret; }; } // 拷貝prop中的所有屬性到子類原型中 for (var name in prop) { // 如果prop和父類中存在同名的函數,並且此函數中使用了_super方法,則對此方法進行特殊處理 - fn // 否則將此方法prop[name]直接賦值給子類的原型 if (typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])) { prototype[name] = fn(name, prop[name]); } else { prototype[name] = prop[name]; } } function Class() { if (!initializing && this.init) { this.init.apply(this, arguments); } } Class.prototype = prototype; Class.constructor = Class; Class.extend = arguments.callee; return Class; }; })();
寫到這裡,大家是否覺得Resig的實現和我們在第三章一步一步實現的jClass很類似。 其實在寫這一系列的文章之前,我已經對prototype、mootools、extjs、 jQuery-Simple-Inheritance、Crockford-Classical-Inheritance這些實現有一定的瞭解,並且大部分都在實際項目中使用過。 在第三章中實現jClass也參考了Resig的實現,在此向Resig表示感謝。 下來我們就把jClass改造成和這裡的Class具有相同的行為。
我們的實現
將我們在第三章實現的jClass改造成目前John Resig所寫的形式相當簡單,只需要修改其中的兩三行就行了:
(function() { // 當前是否處於建立類的階段 var initializing = false; jClass = function() { }; jClass.extend = function(prop) { // 如果調用當前函數的對象(這裡是函數)不是Class,則是父類 var baseClass = null; if (this !== jClass) { baseClass = this; } // 本次調用所建立的類(建構函式) function F() { // 如果當前處於執行個體化類的階段,則調用init原型函數 if (!initializing) { // 如果父類存在,則執行個體對象的baseprototype指向父類的原型 // 這就提供了在執行個體對象中調用父類方法的途徑 if (baseClass) { this._superprototype = baseClass.prototype; } this.init.apply(this, arguments); } } // 如果此類需要從其它類擴充 if (baseClass) { initializing = true; F.prototype = new baseClass(); F.prototype.constructor = F; initializing = false; } // 新建立的類自動附加extend函數 F.extend = arguments.callee; // 覆蓋父類的同名函數 for (var name in prop) { if (prop.hasOwnProperty(name)) { // 如果此類繼承自父類baseClass並且父類原型中存在同名函數name if (baseClass && typeof (prop[name]) === "function" && typeof (F.prototype[name]) === "function" && /\b_super\b/.test(prop[name])) { // 重定義函數name - // 首先在函數上下文設定this._super指向父類原型中的同名函數 // 然後調用函數prop[name],返回函數結果 // 注意:這裡的自執行函數建立了一個上下文,這個上下文返回另一個函數, // 此函數中可以應用此上下文中的變數,這就是閉包(Closure)。 // 這是JavaScript架構開發中常用的技巧。 F.prototype[name] = (function(name, fn) { return function() { this._super = baseClass.prototype[name]; return fn.apply(this, arguments); }; })(name, prop[name]); } else { F.prototype[name] = prop[name]; } } } return F; }; })(); // 經過改造的jClass var Person = jClass.extend({ init: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }); var Employee = Person.extend({ init: function(name, employeeID) { // 調用父類的方法 this._super(name); this.employeeID = employeeID; }, getEmployeeIDName: function() { // 注意:我們還可以通過這種方式調用父類中的其他函數 var name = this._superprototype.getName.call(this, "Employee name: "); return name + ", Employee ID: " + this.employeeID; }, getName: function() { // 調用父類的方法 return this._super("Employee name: "); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan" console.log(zhang.getEmployeeIDName()); // "Employee name: ZhangSan, Employee ID: 1234"
JUST COOL!