JavaScript 继承详解(六)

前端之家收集整理的这篇文章主要介绍了JavaScript 继承详解(六)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

在本章中,我们将分析Prototypejs中关于JavaScript继承的实现。 Prototypejs是最早的JavaScript类库,可以说是JavaScript类库的鼻祖。 我在几年前接触的第一个JavaScript类库就是这位,因此Prototypejs有着广泛的群众基础。

不过当年Prototypejs中的关于继承的实现相当的简单,源代码就寥寥几行,我们来看下。

早期Prototypejs中继承的实现 源码:

函数,此函数执行时将调用原型方法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;
};</pre>

调用方式:

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"</pre>

很原始的感觉对吧,在子类函数中没有提供调用父类函数的途径。

Prototypejs 1.6以后的继承实现 首先来看下调用方式:

函数 initialize: function(name) { this.name = name; },getName: function(prefix) { return prefix + this.name; } });
// Class.create的第一个参数是要继承的<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>
var Employee = Class.create(Person,{
  // 通过将子类<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>的第一个参数设为$super来引用<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>的同名<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>
  // 比较有创意,不过内部实现应该比较复杂,至少要用一个闭包来设置$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"</pre>

这里我们将Prototypejs 1.6.0.3中继承实现单独取出来, 那些不想引用整个prototype库而只想使用prototype式继承的朋友, 可以直接把下面代码拷贝出来保存为JS文件就行了。

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]; } });</pre>

首先,我们需要先解释下Prototypejs中一些方法的定义。

argumentNames: 获取函数的参数数组

bind: 绑定函数的上下文this到一个新的对象(一般是函数的第一个参数)

console.log(p.getName()); // "Lisi" console.log(p.getName.bind(window)()); // "window"</pre>

wrap: 把当前调用函数作为包裹器wrapper函数的第一个参数

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"</pre>

有一点绕口,对吧。这里要注意的是wrap和bind调用返回的都是函数,把握住这个原则,就很容易看清本质了。

对这些函数有了一定的认识之后,我们再来解析Prototypejs继承的核心内容。 这里有两个重要的定义,一个是Class.extend,另一个是Class.Methods.addMethods。

函数,则作为父类 var parent = null,properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift();
    // 子类构造<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>的定义
    function klass() {
      this.initialize.apply(this,arguments);
    }

    // 为子类<a href="/tag/tianjia/" target="_blank" class="keywords">添加</a>原型<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>Class.Methods.addMethods
    Object.extend(klass,Class.Methods);
    // 不仅为当前类保存<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>的引用,同时记录了所有子类的引用
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      // 核心<a href="/tag/daima/" target="_blank" class="keywords">代码</a> - 如果<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>存在,则实现原型的继承
      // 这里为创建类时不<a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a><a href="/tag/fulei/" target="_blank" class="keywords">父类</a>的构造<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>提供了一种新的途径
      // - 使用一个中间过渡类,这和我们以前使用全局initializing变量达到相同的目的,
      // - 但是<a href="/tag/daima/" target="_blank" class="keywords">代码</a>更优雅一点。
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    // 核心<a href="/tag/daima/" target="_blank" class="keywords">代码</a> - 如果子类拥有<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>相同的<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>,则特殊处理,将会在后面详解
    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    // 修正constructor指向<a href="/tag/cuowu/" target="_blank" class="keywords">错误</a>
    klass.prototype.constructor = klass;

    return klass;
  }
};</pre>

再来看addMethods做了哪些事情:

父类存在,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");
    // 循环子类原型定义的所有<a href="/tag/shuxing/" target="_blank" class="keywords">属性</a>,对于那些和<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>重名的<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>要重新定义
    for (var i = 0,length = properties.length; i < length; i++) {
      // property为<a href="/tag/shuxing/" target="_blank" class="keywords">属性</a>名,value为<a href="/tag/shuxing/" target="_blank" class="keywords">属性</a>体(可能是<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>,也可能是对象)
      var property = properties[i],value = source[property];
      // 如果<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>存在,并且当前当前<a href="/tag/shuxing/" target="_blank" class="keywords">属性</a>是<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>,并且此<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>的第一个参数为 $super
      if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") {
        var method = value;
        // 下面三行<a href="/tag/daima/" target="_blank" class="keywords">代码</a>是精华之所在,大概的意思:
        // - 首先创建一个自执行的匿名<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>返回另一个<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>,此<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>用于执行<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>的同名<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>
        // - (因为这是在循环中,我们曾多次指出循环中的<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>引用局部变量的问题)
        // - 其次把这个自执行的匿名<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>的作为method的第一个参数(也就是对应于形参$super)
        // 不过,窃以为这个地方作者有点走火入魔,完全没必要这么复杂,后面我会详细分析这段<a href="/tag/daima/" target="_blank" class="keywords">代码</a>。
        value = (function(m) {
          return function() { return ancestor[m].apply(this,arguments) };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        // 因为我们改变了<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>体,所以重新定义<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>的toString<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>
        // 这样<a href="/tag/yonghu/" target="_blank" class="keywords">用户</a><a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a><a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>的toString<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>时,返回的是原始的<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>定义体
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }
};</pre>

上面的代码中我曾有“走火入魔”的说法,并不是对作者的亵渎, 只是觉得作者对JavaScript中的一个重要准则(通过自执行的匿名函数创建作用域) 运用的有点过头。

其实这段代码和下面的效果一样:

value = ancestor[property].wrap(method);

我们把wrap函数展开就能看的更清楚了:

可以看到,我们其实为父类函数ancestor[property]通过自执行的匿名函数创建了作用域。 而原作者是为property创建的作用域。两则的最终效果是一致的。

我们对Prototypejs继承的重实现

分析了这么多,其实也不是很难,就那么多概念,大不了换种表现形式。 下面我们就用前几章我们自己实现的jClass来实现Prototypejs形式的继承。

代码,可以直接拷贝下来使用
// 这个<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>是借用Prototypejs中的定义
function argumentNames(fn) {
  var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g,');
  return names.length == 1 && !names[0] ? [] : names;
}


function jClass(baseClass,prop) {
  // 只接受一个参数的情况 - jClass(prop)
  if (typeof (baseClass) === "object") {
    prop = baseClass;
    baseClass = null;
  }

  // 本次<a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a>所创建的类(构造<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>)
  function F() {
    // 如果<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>存在,则实例对象的baseprototype指向<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>的原型
    // 这就提供了在实例对象中<a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a><a href="/tag/fulei/" target="_blank" class="keywords">父类</a><a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>的途径
    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;
  }

  // 覆盖<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>的同名<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>
  for (var name in prop) {
    if (prop.hasOwnProperty(name)) {
      // 如果此类继承自<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>baseClass并且<a href="/tag/fulei/" target="_blank" class="keywords">父类</a>原型中存在同名<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a>name
      if (baseClass &&
        typeof (prop[name]) === "function" &&
        argumentNames(prop[name])[0] === "$super") {
        // 重定义子类的原型<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>prop[name]
        // - 这里面有很多JavaScript方面的技巧,如果阅读有困难的话,可以参阅我前面关于JavaScript Tips and Tricks的系列<a href="/tag/wenzhang/" target="_blank" class="keywords">文章</a>
        // - 比如$super封装了<a href="/tag/fulei/" target="_blank" class="keywords">父类</a><a href="/tag/fangfa/" target="_blank" class="keywords">方法</a>的<a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a>,但是<a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a>时的上下文指针要指向当前子类的实例对象
        // - 将$super作为<a href="/tag/fangfa/" target="_blank" class="keywords">方法</a><a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a>的第一个参数
        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;
};</pre>

调用方式和Prototypejs的调用方式保持一致:

var Employee = jClass(Person,{ initialize: function($super,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"</pre>

经过本章的学习,就更加坚定了我们的信心,像Prototypejs形式的继承我们也能够轻松搞定。 以后的几个章节,我们会逐步分析mootools,Extjs等JavaScript类库中继承的实现,敬请期待。

猜你在找的JavaScript相关文章