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 DOM学习第一章 W3C DOM简介
Feb 19 Javascript
JavaScript高级程序设计(第3版)学习笔记4 js运算符和操作符
Oct 11 Javascript
JS模板实现方法
Apr 03 Javascript
JavaScript+html5 canvas绘制的小人效果
Jan 27 Javascript
JS结合bootstrap实现基本的增删改查功能
Jul 22 Javascript
Backbone View 之间通信的三种方式
Aug 09 Javascript
AngularJs directive详解及示例代码
Sep 01 Javascript
JavaScript日期对象(Date)基本用法示例
Jan 18 Javascript
微信小程序调用微信支付接口的实现方法
Apr 29 Javascript
通过实例解析JavaScript for in及for of区别
Jun 15 Javascript
JavaScript严格模式不支持八进制的问题讲解
Nov 07 Javascript
uniapp开发打包多端应用完整方法指南
Dec 24 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中Memcache操作类及用法实例
2014/12/12 PHP
IE php关于强制下载文件的代码
2008/08/23 Javascript
js showModalDialog弹出窗口实例详解
2014/01/07 Javascript
JS+CSS实现另类带提示效果的竖向导航菜单
2015/10/15 Javascript
js中数组插入、删除元素操作的方法
2017/02/15 Javascript
javascript设计模式之策略模式学习笔记
2017/02/15 Javascript
用JavaScript和jQuery实现瀑布流
2017/03/19 Javascript
详解基于angular-cli配置代理解决跨域请求问题
2017/07/05 Javascript
Vue的MVVM实现方法
2017/08/16 Javascript
详解在vue-cli中使用路由
2017/09/25 Javascript
详解Nodejs 通过 fs.createWriteStream 保存文件
2017/10/10 NodeJs
AngularJS实现的获取焦点及失去焦点时的表单验证功能示例
2017/10/25 Javascript
浅析JS抽象工厂模式
2017/12/14 Javascript
关于Webpack dev server热加载失败的解决方法
2018/02/22 Javascript
微信小程序开发实现的IP地址查询功能示例
2019/03/28 Javascript
vue中选中多个选项并且改变选中的样式的实例代码
2020/09/16 Javascript
[00:44]2016完美“圣”典 风云人物:Mikasa宣传片
2016/12/07 DOTA
python使用PyFetion来发送短信的例子
2014/04/22 Python
Python利用QQ邮箱发送邮件的实现方法(分享)
2017/06/09 Python
Python networkx包的实现
2020/02/14 Python
解决Jupyter NoteBook输出的图表太小看不清问题
2020/04/16 Python
Python sublime安装及配置过程详解
2020/06/29 Python
加拿大购物频道:The Shopping Channel
2016/07/21 全球购物
ONLY瑞典官网:世界知名服装品牌
2018/06/19 全球购物
物业电工岗位职责
2013/11/20 职场文书
员工薪酬福利制度
2014/01/17 职场文书
《散步》教学反思
2014/03/02 职场文书
法律进企业活动方案
2014/03/04 职场文书
房屋委托书范本
2014/04/04 职场文书
酒店爱岗敬业演讲稿
2014/09/02 职场文书
2014年业务员工作总结范文
2014/11/17 职场文书
家装业务员岗位职责
2015/04/03 职场文书
行政介绍信范文
2015/05/04 职场文书
新郎接新娘保证书
2015/05/08 职场文书
jQuery实现影院选座订座效果
2021/04/13 jQuery
Linux中各个目录的作用与内容
2022/06/28 Servers