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 相关文章推荐
把JS与CSS写在同一个文件里的书写方法
Jun 02 Javascript
JavaScript删除数组元素的方法
Mar 20 Javascript
js正则表达式验证邮件地址
Nov 12 Javascript
文本框只能输入数字的js代码(含小数点)
Jul 10 Javascript
Javascript在IE和Firefox浏览器常见兼容性问题总结
Aug 03 Javascript
浅谈jQuery为哪般去掉了浏览器检测
Aug 29 Javascript
JavaScript中的this陷阱的最全收集并整理(没有之一)
Feb 21 Javascript
微信小程序 向左滑动删除功能的实现
Mar 10 Javascript
基于LayUI分页和LayUI laypage分页的使用示例
Aug 02 Javascript
使用vue-resource进行数据交互的实例
Sep 02 Javascript
vue实现页面切换滑动效果
Jun 29 Javascript
vscode中使用npm安装babel的方法
Aug 02 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 动态添加记录
2009/03/10 PHP
php初始化对象和析构函数的简单实例
2014/03/11 PHP
ThinkPHP防止重复提交表单的方法实例分析
2018/05/10 PHP
goto语法在PHP中的使用教程
2020/09/17 PHP
jquery 模式对话框终极版实现代码
2009/09/28 Javascript
combox改进版 页面原型参考dojo的,比网上jQuery的那些combox功能强,代码更小
2010/04/15 Javascript
Javascript this 的一些学习总结
2012/08/02 Javascript
jquery实现弹出层遮罩效果的简单实例
2014/03/03 Javascript
Extjs的FileUploadField文件上传出现了两个上传按钮
2014/04/29 Javascript
javascript+css3 实现动态按钮菜单特效
2016/02/06 Javascript
javascript基础知识
2016/06/07 Javascript
强大Vue.js组件浅析
2016/09/12 Javascript
EditPlus 正则表达式 实战(3)
2016/12/15 Javascript
vue2.0获取自定义属性的值
2017/03/28 Javascript
jQuery+Ajax请求本地数据加载商品列表页并跳转详情页的实现方法
2017/07/12 jQuery
JS字符串常用操作方法实例小结
2019/06/24 Javascript
python实现用于测试网站访问速率的方法
2015/05/26 Python
基础的十进制按位运算总结与在Python中的计算示例
2016/06/28 Python
Python使用PDFMiner解析PDF代码实例
2017/03/27 Python
Python实现将HTML转成PDF的方法分析
2019/05/04 Python
python之信息加密题目详解
2019/06/26 Python
Django中的FBV和CBV用法详解
2019/09/15 Python
Python中bisect的使用方法
2019/12/31 Python
python 解决tqdm模块不能单行显示的问题
2020/02/19 Python
python数据分析:关键字提取方式
2020/02/24 Python
python中可以声明变量类型吗
2020/06/18 Python
Perfume’s Club澳大利亚官网:西班牙领先的在线美容店
2021/02/01 全球购物
工程地质勘察专业大学生求职信
2013/10/13 职场文书
中学生学习生活的自我评价
2013/10/26 职场文书
2014年母亲节寄语
2014/05/07 职场文书
一份没有按时交货失信于客户的检讨书
2014/09/19 职场文书
2015年乡镇信访工作总结
2015/04/07 职场文书
写给女朋友的检讨书
2015/05/06 职场文书
幼儿园班级管理心得体会
2016/01/07 职场文书
护士爱岗敬业心得体会
2016/01/25 职场文书
MySQL数据迁移相关总结
2021/04/29 MySQL