JavaScript继承详解(六)

在本章中,我们将分析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形式的继承我们也能够轻松搞定。

 

 

  • JavaScript继承详解(六)
  • 作者:三生石上  文章来源:www.cnblogs.com  发布日期:2009-07-16 

JavaScript继承详解 (六)

在本章中,我们将分析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: 获取函数的参数数组
posted @ 2010-01-27 17:44 華安 阅读(359) 评论(0) 编辑 收藏