在本章中,我們將分析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類庫中繼承的實現,敬請期待。