JavaScript 继承详解(六)


Posted in Javascript onOctober 11, 2016

在本章中,我们将分析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类库中继承的实现,敬请期待。

Javascript 相关文章推荐
javascript 常用功能总结
Mar 18 Javascript
jquery的ajax异步请求接收返回json数据实例
Jun 16 Javascript
node.js中的fs.stat方法使用说明
Dec 16 Javascript
JavaScript实现MIPS乘法模拟的方法
Apr 17 Javascript
用JavaScript实现PHP的urlencode与urldecode函数
Aug 13 Javascript
浅析JSONP技术原理及实现
Jun 08 Javascript
jQuery实现鼠标经过购物车出现下拉框代码(推荐)
Jul 21 Javascript
微信小程序 教程之模板
Oct 18 Javascript
AngularJS 防止页面闪烁的方法
Mar 09 Javascript
javascript中toFixed()四舍五入使用方法详解
Sep 28 Javascript
vue-router路由模式详解(小结)
Aug 26 Javascript
javascript 关于赋值、浅拷贝、深拷贝的个人理解
Nov 01 Javascript
JavaScript 继承详解(五)
Oct 11 #Javascript
Javascript动画效果(4)
Oct 11 #Javascript
JavaScript中const、var和let区别浅析
Oct 11 #Javascript
对javascript继承的理解
Oct 11 #Javascript
Javascript动画效果(3)
Oct 11 #Javascript
JavaScript实现自动切换图片代码
Oct 11 #Javascript
Javascript动画效果(2)
Oct 11 #Javascript
You might like
用PHP调用Oracle存储过程的方法
2008/09/12 PHP
Optimizer与Debugger兼容性问题的解决方法
2008/12/01 PHP
php计算十二星座的函数代码
2012/08/21 PHP
smarty内置函数{loteral}、{ldelim}和{rdelim}用法实例
2015/01/22 PHP
精通Javascript系列之数据类型 字符串
2011/06/08 Javascript
javascript中的delete使用详解
2013/04/11 Javascript
使用JS 清空File控件的路径值
2013/07/08 Javascript
自己用jQuery写了一个图片的马赛克消失效果
2014/05/04 Javascript
jQuery中多个元素的Hover事件解决方案
2014/06/12 Javascript
nodejs中的fiber(纤程)库详解
2015/03/24 NodeJs
javascript元素动态创建实现方法
2015/05/13 Javascript
javascript汉字拼音互转的简单实例
2016/10/09 Javascript
AngularJS ng-repeat指令中使用track by子语句解决重复数据遍历错误问题
2017/01/21 Javascript
js实现旋转木马效果
2017/03/17 Javascript
获取url中用&amp;隔开的参数实例(分享)
2017/05/28 Javascript
JS基于正则表达式实现的密码强度验证功能示例
2017/09/21 Javascript
jQuery幻灯片插件owlcarousel参数说明中文文档
2018/02/27 jQuery
使用vue中的v-for遍历二维数组的方法
2018/03/07 Javascript
jsonp跨域获取数据的基础教程
2018/07/01 Javascript
vue-quill-editor+plupload富文本编辑器实例详解
2018/10/19 Javascript
React Native 混合开发多入口加载方式详解
2019/09/23 Javascript
纯JS实现五子棋游戏
2020/05/28 Javascript
使用go和python递归删除.ds store文件的方法
2014/01/22 Python
介绍Python中几个常用的类方法
2015/04/08 Python
Python3自动签到 定时任务 判断节假日的实例
2018/11/13 Python
python Gabor滤波器讲解
2020/10/26 Python
canvas进阶之如何画出平滑的曲线
2018/10/15 HTML / CSS
AmazeUI 图标的示例代码
2020/08/13 HTML / CSS
卡塔尔航空官方网站:Qatar Airways
2017/02/08 全球购物
给幼儿园老师的表扬信
2014/01/19 职场文书
交通事故调解协议书
2014/04/16 职场文书
接待员岗位职责范本
2015/04/15 职场文书
因个人原因离职的辞职信范文
2015/05/12 职场文书
MySQL中的布尔值,怎么存储false或true
2021/06/04 MySQL
mysql函数全面总结
2021/11/11 MySQL
解决Mysql多行子查询的使用及空值问题
2022/01/22 MySQL