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; } };