• JavaScript几种类工厂实现原理剖析


    PS:
    本文参考了司徒正美的《JavaScript框架设计》,以及许多其它的书籍和网络文章,感谢作者们分享了这么好的学习资源!
    关于JS的类和继承的原理,见:JavaScript里的类和继承
    文中提到的库和测试文件戳这里:https://github.com/hellobugme/jsclass/

    1、基于拷贝继承


    Prototypehttp://prototypejs.org/)和 jQueryhttps://jquery.com/)都是风靡一时的库,功能大而全,并不是纯粹的类工厂,这里只是简单说下里面的继承。
    它们的 extend 都是基于拷贝继承,其中 jQuery 支持 深度拷贝(deep copy)
    拷贝继承的缺点在上篇文章中有提过:子类实例无法通过父类的 instanceof 验证

    Prototype 中类和继承相关的代码如下:

    var Class = {
        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;
    };

    是吧,够简单的… 只不过是使用 Class.create() 创建出来的类,在实例化时会主动帮你执行下初始化函数 initialize 而已。使用方法大致如下:

    var Person = Class.create();
    Object.extend(Person.prototype, {
        initialize: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    });
    var User = Class.create();
    User.prototype = Object.extend(new Person(), {
        initialize: function(name, password) {
            this.name = name;
            this.password = password;
        },
        getPassword: function() {
            return this.password;
        }
    });

    jQuery 在 extend 的实现中加入递归,对数组和对象等引用类型的属性值进行深度拷贝:

    ... 表示一大波的代码,下同

    jQuery.extend = jQuery.fn.extend = function() {
        //...
        for (name in options) {
            src = target[name];
            copy = options[name];
            //...
            if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
                if (copyIsArray) {
                    copyIsArray = false;
                    clone = src && jQuery.isArray(src) ? src : [];
                } else {
                    clone = src && jQuery.isPlainObject(src) ? src : {};
                }
                // 通过递归对数组和对象进行深度拷贝
                target[name] = jQuery.extend(deep, clone, copy);
            } else if (copy !== undefined) {
                target[name] = copy;
            }
        }
        //...
    };

    deep 是可选参数,指定是否进行深度拷贝。

    通过 jQuery.isPlainObject() 和 jQuery.isArray() 判断属性值是否为纯粹对象或数组,然后决定是递归操作还是直接赋值。

    2、Sugar


    道爷(Douglas Crockford)在自己的网站上提出了一套简单的方法来模拟类式继承—— Sugarhttp://javascript.crockford.com/inheritance.html)。

     1 // 辅助函数,可以将新函数绑定到对象的 prototype 上
     2 Function.prototype.method = function(name, func) {
     3     this.prototype[name] = func;
     4     return this;
     5 };
     6 
     7 // 从其它对象继承函数,同时仍然可以调用数据父对象的那些函数
     8 Function.method('inherits', function(parent) {
     9     // 继承父对象的方法
    10     this.prototype = new parent();
    11     this.prototype.constructor = parent;
    12 
    13     var d = {},
    14         p = this.prototype;
    15     // 创建一个新的特权函数'uber',调用它时会执行所有在继承时被重写的函数
    16     this.method('uber', function uber(name) {
    17         if (!(name in d)) {
    18             d[name] = 0;
    19         }
    20         var f, // 要执行的函数
    21             r, // 函数的返回值
    22             t = d[name], // 记录当前所在的父层次的级数
    23             v = parent.prototype; // 父对象的prototype
    24 
    25         // 如果已经在某个'uber'函数之内
    26         if (t) {
    27             // 上溯必要的t,找到原始的prototype
    28             while (t) {
    29                 v = v.constructor.prototype;
    30                 t -= 1;
    31             }
    32             // 从该prototype中获得函数
    33             f = v[name];
    34             // 否则这就是'uber'函数的第一次调用
    35         } else {
    36             // 从prototype获得要执行的函数
    37             f = p[name];
    38             // 如果此函数属于当前的prototype
    39             if (f == this[name]) {
    40                 // 则改为调用父对象的prototype
    41                 f = v[name];
    42             }
    43         }
    44 
    45         // 记录在继承堆栈中所在位置的级数
    46         d[name] += 1;
    47 
    48         // 使用除第一个以外所有的arguments调用此函数
    49         r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
    50         // 恢复继承堆栈
    51         d[name] -= 1;
    52 
    53         // 返回执行过的函数的返回值
    54         return r;
    55     });
    56     return this;
    57 });
    58 
    59 // 只继承父对象特定函数的函数(非使用new parent()继承所有的函数)
    60 Function.method('swiss', function(parent) {
    61     // 遍历所有要继承的方法
    62     for (var i = 1; i < arguments.length; i += 1) {
    63         // 需要导入的方法名
    64         var name = arguments[i];
    65         // 将此方法导入this对象的prototype中
    66         this.prototype[name] = parent.prototype[name];
    67     }
    68     return this;
    69 });
    View Code

    因为JS中的类本身就是一个函数,所以直接为 Function 的原型扩展了 method、inherits、swiss 三个方法,所有类也能获得这些方法。

    重点在于 inherits 中添加的特权方法“uber”(为什么不叫“super”叫“uber”?难道道爷是优步忠实粉?好吧,很冷…),它根据继承堆栈级数回溯到相应的祖先类原型方法,让你在子类中可以非常方便地调用被重写的父类原型方法。

    function Person(name) {
        this.name = name;
    }
    Person.method('getName', function() {
        return name;
    });
    
    function User(name, password) {
        this.name = name;
        this.password = password;
    }
    User.inherits(Person);
    User.method('getPassword', function() {
        return this.password;
    });
    // 重写父类原型方法
    User.method('getName', function() {
        // 通过 uber() 调用父类的 getName()
        return "My name is: " + this.uber('getName');
    });

     

    3、Base


    Basehttp://dean.edwards.name/base/)是 Dean Edwards 开发的一个类工厂库,它提供了一套比较直观的对象继承方法,对后来许多类工厂的实现有不小的影响,如:
    JS.Classhttp://dkraczkowski.github.io/js.class/)、
    simple-inheritancehttp://ejohn.org/blog/simple-javascript-inheritance/
    都借鉴了 Base 的继承系统。

    Base 的代码较长,忽略具体实现,只留关键步骤后大致如下:

     1 var Base = function() {
     2     // 并没有什么卵用
     3 };
     4 
     5 //Base.extend 方法功能:
     6 //1、创建子类;2、继承父类;3、返回子类
     7 Base.extend = function(_instance, _static) {
     8     var proto = new this; //原型继承,proto将作为子类的prototype
     9     Base.prototype.extend.call(proto, _instance); //给子类添加原型方法
    10     //...
    11     var klass = function() {}; //子类
    12     klass.prototype = proto;
    13     Base.prototype.extend.call(klass, _static); //给子类添加特权方法
    14     //...
    15     return klass; //返回子类
    16 };
    17 
    18 //Base.prototype.extend 方法功能:
    19 //复制新的成员到子类
    20 Base.prototype = {
    21     extend: function(source, value) {
    22         if (arguments.length > 1) {
    23             //传入的是2个参数,为键值对,直接给this的source赋值为value
    24             //该方法通过call来调用,这里的执行环境this为调用时传入的环境
    25             //...
    26             this[source] = value;
    27         } else {
    28             //传入的是1个参数,为对象,遍历对象,把对象的所有属性复制给this
    29             //...
    30             for (var key in source) {
    31                 Base.prototype.extend.call(this, key, source[key]);
    32             }
    33         }
    34         return this;
    35     }
    36 };
    37 
    38 //初始化Base
    39 Base = Base.extend({
    40     constructor: function() { /*...*/ }
    41 }, {
    42     ancestor: Object,
    43     version: "1.1"
    44         // ...
    45 });
    View Code

    Base 通过两个 extend 方法实现了类的创建和继承:

    1. Base.extend():这个直接添加在 Base 上的静态方法,实现了类的创建、类的继承;
    2. Base.prototype.extend():这个添加在 Base 原型上的方法,其实就是个工具函数,实现了属性拷贝。

    调用 Base.extends() 创建子类的过程如下:

    1. 创建子类原型,将父类的实例赋值给子类原型
    2. 将参数中传入的其它子类原型成员通过 Base.prototype.extend() 复制给子类原型
    3. 创建子类,将上面处理后的子类原型赋值给子类的原型属性
    4. 将参数中传入的初始化函数中的特权成员通过 Base.prototype.extend() 复制给子类

    Base 的使用方法如下:

    var Person = Base.extend({
        constructor: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    });
    var User = Person.extend({
        constructor: function(name, password) {
            this.base(name);
            this.password = password;
        },
        getPassword: function() {
            return this.password;
        }
    });

    一个代码块就创建了一个类并实现了继承,用起来是不是很爽?

    4、P.js


    P.jshttps://github.com/jneen/pjs) 是一个非常精巧的库,它最大的特点是直接把父类的原型抛出来,在调用父类方法时非常方便。

     1 var P = (function(prototype, ownProperty, undefined) {
     2     return function P(_superclass /* = Object */ , definition) {
     3         // 如果只有一个参数,表示没有父类
     4         if (definition === undefined) {
     5             definition = _superclass;
     6             _superclass = Object;
     7         }
     8 
     9         // C为要返回的子类,init为初始化函数
    10         function C() {
    11             var self = this instanceof C ? this : new Bare;
    12             self.init.apply(self, arguments);
    13             return self;
    14         }
    15 
    16         // Bare让C不用new就能返回实例
    17         function Bare() {}
    18         C.Bare = Bare;
    19 
    20         // 为了防止改动子类影响到父类,所以将父类的原型赋值给中介者Bare,然后再将Bare的实例作为子类的原型
    21         var _super = Bare[prototype] = _superclass[prototype];
    22         var proto = Bare[prototype] = C[prototype] = C.p = new Bare;
    23 
    24         var key;
    25 
    26         // 修正子类的构造器函数,使其指向自身
    27         proto.constructor = C;
    28 
    29         C.extend = function(def) {
    30             return P(C, def);
    31         }
    32 
    33         return (C.open = function(def) {
    34             // 如果def是函数,则直接调用,并传入子类原型、父类原型、子类构造器、父类构造器
    35             if (typeof def === 'function') {
    36                 def = def.call(C, proto, _super, C, _superclass);
    37             }
    38 
    39             // 如果def是对象,则是子类的扩展包,将其属性添加到子类原型
    40             if (typeof def === 'object') {
    41                 for (key in def) {
    42                     // ownProperty其实就是传入的Object.hasOwnProperty
    43                     if (ownProperty.call(def, key)) {
    44                         proto[key] = def[key];
    45                     }
    46                 }
    47             }
    48 
    49             // 确保有初始化函数可以调用
    50             if (!('init' in proto)) proto.init = _superclass;
    51 
    52             return C;
    53         })(definition);
    54     }
    55 
    56 })('prototype', ({}).hasOwnProperty);
    View Code

    里面有几个关键点:

    1. P 是一个函数,调用时会自动生成一个类
    2. 当给 P 传入一个参数且该参数为函数时,P 默认父类为 Object,然后调用该函数,并将创建的类的原型和父类原型传给它,所以你可以在该函数中使用第一个参数为创建的类添加原型成员
    3. 当给 P 传入两个参数时,第一个参数为父类,P 会通过原型式寄生组合继承实现原型继承,使创建的类继承父类原型;第二个参数为函数,原理同 2
    4. P 在处理完传入的参数后,会调用创建的类的原型方法 init() 进行初始化,从而实现特权成员的添加

    P.js 的使用方法如下:

    // 将一个函数传给P,P会先创建一个类,然后后调用该函数,并将创建的类的原型和父类原型传入,所以在该函数中可以通过第一个参数来给类添加原型成员
    var Person = P(function(proto, superProto) {
        // init为初始化函数,P会在完成类的处理后自动调用该方法
        proto.init = function(name) {
            // 添加特权成员
            this.name = name;
        };
        proto.getName = function() {
            return this.name;
        };
    });
    var User = P(Person, function(user, person) {
        user.init = function(name, password) {
            // 通过P传入的父类原型,可以很方便的访问父类原型成员
            person.init.call(this, arguments);
            this.password = password;
        };
        user.getPassword = function() {
            return this.password;
        };
    });

    妈妈再也不用担心我调不到父类方法了!

    5、def.js


    如果说有什么库最能体现 JS 的灵活性,def.jshttp://badassjs.com/post/811837523/def-js-ruby-style-inheritance-in-javascript) 肯定名列前茅!
    它试图在形式上模拟 Ruby 那种继承,让学过 Ruby 的人一眼就看到哪个是父类,哪个是子类。

    Ruby 的继承是这样的:

    class User < Person
        #...
    end

    而 def.js 能做到这样:

    def("Person")({
        init: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    });
    
    def("User") < Person({
        init: function(name, password) {
            this._super();
            this.password = password;
        },
        getPassword: function() {
            return this.password;
        }
    });

    是不是很神奇!!

    这里面有几个魔法:

    1. def()():能这么用,说明 def() 执行后返回的是一个函数
    2. “<”:就这么一个操作符,它是怎么实现继承的?
    3. _super():简单的一行 this._super(),就神奇地调用了父类同名方法!

    先来说说“<”操作符的运用。
    在 JS 中,当两个对象进行算术运算或大小比较时,如果它们不是数值,则会尝试将其转换为数值,即调用其 valueOf 方法。
    “<”操作符的目的就是强制两边计算自身,从而调用自己的 valueOf 方法,def.js 就是通过重写父类与子类的 valueOf,在里面偷偷实现了原型继承。
    先来看一个简单的例子:

    var a = {
            valueOf: function() {
                console.log("aaaaa");
            }
        },
        b = {
            valueOf: function() {
                console.log("bbbbb");
            }
        };
    console.log(a < b);

    执行结果如下:


    可以看到,先是执行了 a 的 valueOf(),然后执行了 b 的 valueOf(),最后返回比较结果false。

    再来看看 def.js 的源码,我们在其中几个关键点输出日志(代码中的console.log):

     1 (function(global) {
     2     // deferred 是整个库中最重要的部件,扮演3个角色
     3     // 1、def("SuperClass")时就是返回deferred,此时我们可以直接接括号对原型进行扩展
     4     // 2、在继承父类时 < 触发两者调用 valueOf,此时会执行 deferred.valueOf 里面的逻辑
     5     // 3、在继承父类时,父类的后面还可以接括号(此时构造器当普通函数使用),当作传送器,保存着父类与扩展包到 _super._props
     6     var deferred;
     7 
     8     // 扩展自定义的原型
     9     function extend(source) {
    10         var prop, target = this.prototype;
    11 
    12         for (var key in source)
    13             if (source.hasOwnProperty(key)) {
    14                 prop = target[key] = source[key];
    15                 if ('function' == typeof prop) {
    16                     // 在每个原型方法上添加2个自定义属性,保存其名字与当前类
    17                     prop._name = key;
    18                     prop._class = this;
    19                 }
    20             }
    21 
    22         return this;
    23     }
    24 
    25     function base() {
    26         // 取得调用 this._super() 这个函数本身,如果是在init内,那么就是当前类
    27         var caller = arguments.callee.caller;
    28         // 执行父类的同名方法,有2种形式,一是用户自己传,二是只能取当前函数的参数
    29         return caller._class._super.prototype[caller._name]
    30             .apply(this, arguments.length ? arguments : caller.arguments);
    31     }
    32 
    33     function def(context, klassName) {
    34         console.log("def called");
    35         klassName || (klassName = context, context = global);
    36         // 偷偷在给定的全局作用域或某对象上创建一个类
    37         var Klass = context[klassName] = function Klass() {
    38             if (context != this) {
    39                 // 如果不使用 new 操作符,大多数情况下 context 与 this 都为 window
    40                 return this.init && this.init.apply(this, arguments);
    41             }
    42             // 实现继承的第二步,复制自身与扩展包到 deferred
    43             console.log("Klass constructor called");
    44             deferred._super = Klass;
    45             deferred._props = arguments[0] || {};
    46         }
    47 
    48         // 让所有自定义类都共用一个 extend 方法
    49         Klass.extend = extend;
    50 
    51         // 实现继承的第一步,重写 deferred,为新创建的自定义类的扩展函数
    52         deferred = function(props) {
    53             return Klass.extend(props);
    54         };
    55 
    56         // 一个中介者,用于切断子类与父类的原型连接,会被反复读写
    57         function Subclass() {}
    58 
    59         // 实现继承的第三步,重写 valueOf,方便在 def("SubClass") < SuperClass({}) 执行它
    60         deferred.valueOf = function() {
    61             console.log("deferred.valueOf called");
    62             var Superclass = deferred._super;
    63 
    64             if (!Superclass) {
    65                 return Klass;
    66             }
    67             // 先将父类的原型赋给中介者,然后再将中介者的实例作为子类的原型
    68             Subclass.prototype = Superclass.prototype;
    69             var proto = Klass.prototype = new Subclass;
    70             // 引用自身与父类
    71             Klass._class = Klass;
    72             Klass._super = Superclass;
    73             // 获取该类名字
    74             Klass.toString = function() {
    75                 return klassName;
    76             };
    77             // 修复原型中 constructor 指向自身
    78             proto.constructor = Klass;
    79             // 让所有自定义类都共用这个 base 方法,它是构成方法链的关系
    80             proto._super = base;
    81             // 把父类后来传入的扩展包混入子类的原型中
    82             deferred(deferred._props);
    83         };
    84 
    85         return deferred;
    86     }
    87 
    88     global.def = def;
    89 }(this));
    View Code

    结果如下:

    第一个“def called”是创建 Person 时输出的,忽略。
    从接下来 3 个日志的输出顺序可以推测出,def("User") < Person({/*...*/}) 执行顺序如下:

    1. 执行 def("User"),创建了 User 类,重新擦写了 deferred 和 deferred.valueOf
    2. 执行 Person({/*...*/}),Person 作为普通函数接受子类的扩展包 {/*...*/},从而使 def.js 中的 deferred._super = Person,deferred._props = {/*...*/}
    3. “<”左边计算自身,执行 valueOf 操作,继承了 deferred._super 中存储的父类的原型,并拷贝了 deferred._props 中存储的子类扩展包

    太妙了!!这需要对 def("User") < Person({/*...*/}) 这一行代码各个部分的执行顺序有充分的了解,才能做出如此巧妙的设计。

    接下来说说第 3 点:_super 的原理。
    这里其实是对 arguments.callee.caller 的运用,通过 caller 可以获取到当前调用的原型方法的引用。
    而 def.js 在 extend() 中为每个原型方法都添加了 _name(方法名)和 _class(指向当前类)两个属性,因此,通过 caller._name 可以获得方法名,caller._class._super 可以获得父类引用,有了这两样东西,自然就能调用父类同名方法了。
    不过遗憾的是, caller 是被废弃的属性,在 ES5 的严格模式下不可用。当然也可以修改,只是会少了些智能化。

  • 相关阅读:
    All about Python
    All about TestComplete
    All about Ranorex
    围观大神们的博客
    CRP实施方法论(转)
    启发式测试策略模型(Heuristic Test Strategy Model,简称HTSM)(转)
    soapUI学习笔记---断言的小使用(转)
    soapUI学习笔记--用例字段参数化(转)
    常用功能测试点汇总(转)
    记一次性能测试实践1
  • 原文地址:https://www.cnblogs.com/kainanhong/p/5121804.html
Copyright © 2020-2023  润新知