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 相关文章推荐
超级简单的图片防盗(HTML),好用
Apr 08 Javascript
jquery中focus()函数实现当对象获得焦点后自动把光标移到内容最后
Sep 29 Javascript
Enter转换为Tab的小例子(兼容IE,Firefox)
Nov 14 Javascript
jQuery+ajax实现无刷新级联菜单示例
May 21 Javascript
JS实现的另类手风琴效果网页内容切换代码
Sep 08 Javascript
JavaScript函数节流概念与用法实例详解
Jun 20 Javascript
jQuery图片渐变特效的简单实现
Jun 25 Javascript
jQuery文字提示与图片提示效果实现方法
Jul 04 Javascript
D3.js实现折线图的方法详解
Sep 21 Javascript
解决jquery appaend元素中id绑定事件失效的问题
Sep 12 jQuery
layui select获取自定义属性方法
Aug 15 Javascript
vue移动端下拉刷新和上拉加载的实现代码
Sep 08 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/11/30 PHP
php封装的mongodb操作类代码
2017/08/06 PHP
php 二维数组快速排序算法的实现代码
2017/10/17 PHP
使用SMB共享来绕过php远程文件包含的限制执行RFI的利用
2019/05/31 PHP
Web层改进II-用xmlhttp 无声息提交复杂表单
2007/01/22 Javascript
JQuery入门——用one()方法绑定事件处理函数(仅触发一次)
2013/02/05 Javascript
js中的push和join方法使用介绍
2013/10/08 Javascript
微信浏览器内置JavaScript对象WeixinJSBridge使用实例
2015/05/25 Javascript
js实现兼容性好的微软官网导航下拉菜单效果
2015/09/07 Javascript
分享五个有用的jquery小技巧
2015/10/08 Javascript
jQuery移动web开发中的页面初始化与加载事件
2015/12/03 Javascript
angularjs实现猜数字大小功能
2020/05/20 Javascript
微信小程序支付之c#后台实现方法
2017/10/19 Javascript
javaScript 连接打印机,打印小票的实例
2017/12/29 Javascript
微信小程序之事件交互操作实例分析
2018/12/03 Javascript
mongodb初始化并使用node.js实现mongodb操作封装方法
2019/04/02 Javascript
解决vue+ element ui 表单验证有值但验证失败问题
2020/01/16 Javascript
Python实现并行抓取整站40万条房价数据(可更换抓取城市)
2016/12/14 Python
python实现逻辑回归的方法示例
2017/05/02 Python
对numpy和pandas中数组的合并和拆分详解
2018/04/11 Python
Python的iOS自动化打包实例代码
2018/11/22 Python
深入了解Django中间件及其方法
2019/07/26 Python
Python Django简单实现session登录注销过程详解
2019/08/06 Python
python 视频逐帧保存为图片的完整实例
2019/12/10 Python
pymysql模块的操作实例
2019/12/17 Python
windows+vscode安装paddleOCR运行环境的步骤
2020/11/11 Python
预订奥兰多和佛罗里达州公园门票:FloridaTix
2018/01/03 全球购物
Under Armour安德玛德国官网:美国高端运动科技品牌
2019/03/09 全球购物
在家更换处方镜片:Lensabl
2019/05/01 全球购物
俄罗斯最大的在线手表商店:Bestwatch.ru
2020/01/11 全球购物
师范毕业生求职信
2014/07/11 职场文书
2015年元旦联欢晚会活动总结
2014/11/28 职场文书
经验交流材料格式
2014/12/30 职场文书
幼师自荐信范文
2015/03/06 职场文书
MySQL 四种连接和多表查询详解
2021/07/16 MySQL
解决vue中provide inject的响应式监听
2022/04/19 Vue.js