这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
在本章中,我们将分析 Prototypejs 中关于 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 形式的继承我们也能够轻松搞定。 以后的几个章节,我们会逐步分析 mootools,Extjs 等 JavaScript 类库中继承的实现,敬请期待。
来源: http://www.phperz.com/article/17/0528/331273.html