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 web页面刷新的方法收集
Jul 02 Javascript
js trim函数 去空格函数与正则集锦
Nov 20 Javascript
理解jQuery stop()方法
Nov 21 Javascript
JavaScript获取当前网页标题(title)的方法
Apr 03 Javascript
JavaScript中停止执行setInterval和setTimeout事件的方法
May 14 Javascript
javascript常用正则表达式汇总
Jul 31 Javascript
jQuery实现的背景动态变化导航菜单效果
Aug 24 Javascript
Angularjs在初始化未完毕时出现闪烁问题的解决方法分析
Aug 05 Javascript
bootstrap table表格插件使用详解
May 08 Javascript
开发Vue树形组件的示例代码
Dec 21 Javascript
Vue数据双向绑定原理实例解析
May 15 Javascript
JavaScript图片旋转效果实现方法详解
Jun 28 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数组 为文章加关键字连接 文章内容自动加链接
2011/12/29 PHP
php笔记之:初探PHPcms模块开发介绍
2013/04/26 PHP
简单的php文件上传(实例)
2013/10/27 PHP
zend framework框架中url大小写问题解决方法
2014/08/19 PHP
php获取url参数方法总结
2014/11/13 PHP
PHP标准库 (SPL)――Countable用法示例
2020/06/05 PHP
运算符&amp;&amp;的三个不同层次
2013/04/07 Javascript
js函数获取html中className所在的内容并去除标签
2013/09/08 Javascript
jquery submit ie6下失效的原因分析及解决方法
2013/11/15 Javascript
兼容主流浏览器的iframe自适应高度js脚本
2014/01/10 Javascript
JavaScript 模块的循环加载实现方法
2015/12/13 Javascript
JS获取html元素的标记名实现方法
2016/10/08 Javascript
让编辑器支持word复制黏贴、截屏的js代码
2016/10/17 Javascript
jquery+css3问卷答题卡翻页动画效果示例
2016/10/26 Javascript
浅谈Vue父子组件和非父子组件传值问题
2017/08/22 Javascript
SVG动画vivus.js库使用小结(实例代码)
2017/09/14 Javascript
详解Vue Elememt-UI构建管理后台
2018/02/27 Javascript
vue-cli项目中使用Mockjs详解
2018/05/14 Javascript
ES6中的迭代器、Generator函数及Generator函数的异步操作方法
2019/05/12 Javascript
vue 源码解析之虚拟Dom-render
2019/08/26 Javascript
基于Ionic3实现选项卡切换并重新加载echarts
2020/09/24 Javascript
Python @property原理解析和用法实例
2020/02/11 Python
如何基于python实现不邻接植花
2020/05/01 Python
Python命名空间namespace及作用域原理解析
2020/06/05 Python
Python函数参数定义及传递方式解析
2020/06/10 Python
解决keras模型保存h5文件提示无此目录问题
2020/07/01 Python
Python matplotlib图例放在外侧保存时显示不完整问题解决
2020/07/28 Python
Pycharm中如何关掉python console
2020/10/27 Python
迪斯尼商品官方网站:ShopDisney
2016/08/01 全球购物
玩具反斗城美国官网:Toys"R"Us
2016/09/17 全球购物
护理专业推荐信
2013/11/07 职场文书
信息技术专业个人自我评价
2013/12/11 职场文书
农业资源与环境专业自荐信范文
2013/12/30 职场文书
莫言诺贝尔获奖演讲稿
2014/05/21 职场文书
会计出纳岗位职责
2015/03/31 职场文书
CSS三大特性继承性、层叠性和优先级详解
2022/01/18 HTML / CSS