在本章中,我們將分析Prototypejs中關於JavaScript繼承的實現。 Prototypejs是最早的JavaScript類庫,可以說是JavaScript類庫的鼻祖。 我在幾年前接觸的第一個JavaScript類庫就是這位,因此Prototypejs有著廣泛的群眾基礎。
不過當年Prototypejs中的關於繼承的實現相當的簡單,原始碼就寥寥幾行,我們來看下。
早期Prototypejs中繼承的實現
源碼:
var Class = { // Class.create僅僅返回另外一個函數,此函數執行時將調用原型方法initialize create: function() { return function() { this.initialize.apply(this, arguments); } } }; // 對象的擴充 Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; };
調用方式:
var Person = Class.create(); Person.prototype = { initialize: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }; var Employee = Class.create(); Employee.prototype = Object.extend(new Person(), { initialize: function(name, employeeID) { this.name = name; this.employeeID = employeeID; }, getName: function() { return "Employee name: " + this.name; } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
很原始的感覺對吧,在子類函數中沒有提供調用父類函數的途徑。
Prototypejs 1.6以後的繼承實現
首先來看下調用方式:
// 通過Class.create建立一個新類 var Person = Class.create({ // initialize是建構函式 initialize: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }); // Class.create的第一個參數是要繼承的父類 var Employee = Class.create(Person, { // 通過將子類函數的第一個參數設為$super來引用父類的同名函數 // 比較有創意,不過內部實現應該比較複雜,至少要用一個閉包來設定$super的上下文this指向當前對象 initialize: function($super, name, employeeID) { $super(name); this.employeeID = employeeID; }, getName: function($super) { return $super("Employee name: "); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
這裡我們將Prototypejs 1.6.0.3中繼承實現單獨取出來, 那些不想引用整個prototype庫而只想使用prototype式繼承的朋友, 可以直接把下面代碼拷貝出來儲存為JS檔案就行了。
var Prototype = { emptyFunction: function() { } }; var Class = { create: function() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); function klass() { this.initialize.apply(this, arguments); } Object.extend(klass, Class.Methods); klass.superclass = parent; klass.subclasses = []; if (parent) { var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; klass.prototype.constructor = klass; return klass; } }; Class.Methods = { addMethods: function(source) { var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } }; Object.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } Object.extend(Object, { keys: function(object) { var keys = []; for (var property in object) keys.push(property); return keys; }, isFunction: function(object) { return typeof object == "function"; }, isUndefined: function(object) { return typeof object == "undefined"; } }); Object.extend(Function.prototype, { argumentNames: function() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; }, bind: function() { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); } }, wrap: function(wrapper) { var __method = this; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } } }); Object.extend(Array.prototype, { first: function() { return this[0]; } });
首先,我們需要先解釋下Prototypejs中一些方法的定義。
- argumentNames: 擷取函數的參數數組
function init($super, name, employeeID) { } console.log(init.argumentNames().join(",")); // "$super,name,employeeID"
- bind: 綁定函數的上下文this到一個新的對象(一般是函數的第一個參數)
var name = "window"; var p = { name: "Lisi", getName: function() { return this.name; } }; console.log(p.getName()); // "Lisi" console.log(p.getName.bind(window)()); // "window"
- wrap: 把當前調用函數作為包裹器wrapper函數的第一個參數
var name = "window"; var p = { name: "Lisi", getName: function() { return this.name; } }; function wrapper(originalFn) { return "Hello: " + originalFn(); } console.log(p.getName()); // "Lisi" console.log(p.getName.bind(window)()); // "window" console.log(p.getName.wrap(wrapper)()); // "Hello: window" console.log(p.getName.wrap(wrapper).bind(p)()); // "Hello: Lisi"
有一點繞口,對吧。這裡要注意的是wrap和bind調用返回的都是函數,把握住這個原則,就很容易看清本質了。
對這些函數有了一定的認識之後,我們再來解析Prototypejs繼承的核心內容。 這裡有兩個重要的定義,一個是Class.extend,另一個是Class.Methods.addMethods。
var Class = { create: function() { // 如果第一個參數是函數,則作為父類 var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); // 子類建構函式的定義 function klass() { this.initialize.apply(this, arguments); } // 為子類添加原型方法Class.Methods.addMethods Object.extend(klass, Class.Methods); // 不僅為當前類儲存父類的引用,同時記錄了所有子類的引用 klass.superclass = parent; klass.subclasses = []; if (parent) { // 核心代碼 - 如果父類存在,則實現原型的繼承 // 這裡為建立類時不調用父類的建構函式提供了一種新的途徑 // - 使用一個中間過渡類,這和我們以前使用全域initializing變數達到相同的目的, // - 但是代碼更優雅一點。 var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } // 核心代碼 - 如果子類擁有父類相同的方法,則特殊處理,將會在後面詳解 for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; // 修正constructor指向錯誤 klass.prototype.constructor = klass; return klass; } };
再來看addMethods做了哪些事情:
Class.Methods = { addMethods: function(source) { // 如果父類存在,ancestor指向父類的原型對象 var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); // Firefox和Chrome返回1,IE8返回0,所以這個地方特殊處理 if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); // 迴圈子類原型定義的所有屬性,對於那些和父類重名的函數要重新定義 for (var i = 0, length = properties.length; i < length; i++) { // property為屬性名稱,value為屬性體(可能是函數,也可能是對象) var property = properties[i], value = source[property]; // 如果父類存在,並且當前當前屬性是函數,並且此函數的第一個參數為 $super if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; // 下面三行代碼是精華之所在,大概的意思: // - 首先建立一個自執行的匿名函數返回另一個函數,此函數用於執行父類的同名函數 // - (因為這是在迴圈中,我們曾多次指出迴圈中的函數引用局部變數的問題) // - 其次把這個自執行的匿名函數的作為method的第一個參數(也就是對應於形參$super) // 不過,竊以為這個地方作者有點走火入魔,完全沒必要這麼複雜,後面我會詳細分析這段代碼。 value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); // 因為我們改變了函數體,所以重新定義函數的toString方法 // 這樣使用者調用函數的toString方法時,返回的是原始的函數定義體 value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } };
上面的代碼中我曾有“走火入魔”的說法,並不是對作者的褻瀆, 只是覺得作者對JavaScript中的一個重要準則(通過自執行的匿名函數建立範圍) 運用的有點過頭。
value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method);
其實這段代碼和下面的效果一樣:
value = ancestor[property].wrap(method);
我們把wrap函數展開就能看的更清楚了:
value = (function(fn, wrapper) { var __method = fn; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } })(ancestor[property], method);
可以看到,我們其實為父類的函數ancestor[property]通過自執行的匿名函數建立了範圍。 而原作者是為property建立的範圍。兩則的最終效果是一致的。
我們對Prototypejs繼承的重實現
分析了這麼多,其實也不是很難,就那麼多概念,大不了換種表現形式。 下面我們就用前幾章我們自己實現的jClass來實現Prototypejs形式的繼承。
// 注意:這是我們自己實現的類似Prototypejs繼承方式的代碼,可以直接拷貝下來使用 // 這個方法是借用Prototypejs中的定義 function argumentNames(fn) { var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; } function jClass(baseClass, prop) { // 只接受一個參數的情況 - jClass(prop) if (typeof (baseClass) === "object") { prop = baseClass; baseClass = null; } // 本次調用所建立的類(建構函式) function F() { // 如果父類存在,則執行個體對象的baseprototype指向父類的原型 // 這就提供了在執行個體對象中調用父類方法的途徑 if (baseClass) { this.baseprototype = baseClass.prototype; } this.initialize.apply(this, arguments); } // 如果此類需要從其它類擴充 if (baseClass) { var middleClass = function() {}; middleClass.prototype = baseClass.prototype; F.prototype = new middleClass(); F.prototype.constructor = F; } // 覆蓋父類的同名函數 for (var name in prop) { if (prop.hasOwnProperty(name)) { // 如果此類繼承自父類baseClass並且父類原型中存在同名函數name if (baseClass && typeof (prop[name]) === "function" && argumentNames(prop[name])[0] === "$super") { // 重定義子類的原型方法prop[name] // - 這裡面有很多JavaScript方面的技巧,如果閱讀有困難的話,可以參閱我前面關於JavaScript Tips and Tricks的系列文章 // - 比如$super封裝了父類方法的調用,但是調用時的上下文指標要指向當前子類的執行個體對象 // - 將$super作為方法調用的第一個參數 F.prototype[name] = (function(name, fn) { return function() { var that = this; $super = function() { return baseClass.prototype[name].apply(that, arguments); }; return fn.apply(this, Array.prototype.concat.apply($super, arguments)); }; })(name, prop[name]); } else { F.prototype[name] = prop[name]; } } } return F; };
調用方式和Prototypejs的調用方式保持一致:
var Person = jClass({ initialize: function(name) { this.name = name; }, getName: function() { return this.name; } }); var Employee = jClass(Person, { initialize: function($super, name, employeeID) { $super(name); this.employeeID = employeeID; }, getEmployeeID: function() { return this.employeeID; }, getName: function($super) { return "Employee name: " + $super(); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
經過本章的學習,就更加堅定了我們的信心,像Prototypejs形式的繼承我們也能夠輕鬆搞定。 以後的幾個章節,我們會逐步分析mootools,Extjs等JavaScript類庫中繼承的實現,敬請期待。