• 第六章:类工厂


    类与继承在javascript的出现,说明javascript已经达到大规模开发的门槛了,在之前是ECMAScript4,就试图引入类,模块等东西,但由于过分引入太多的特性,搞得javascript乌烟瘴气,导致被否决。不过只是把类延时到ES6.到目前为止,javascript还没有正真意义上的类。不过我们可以模拟类,曾近一段时间,类工厂是框架的标配,本章会介绍各种类实现,方便大家在自己的框架中或选择时自己喜欢的那一类风格。

    1.javascript对类的支持

    在其它语言中 ,类的实例都要通过构造函数new出来。作为一个刻意模仿java的语言。javascript存在new操作符,并且所有函数都可以作为构造器。构造函数与普通的方法没有什么区别。浏览器为了构建它繁花似锦的生态圈,比如Node,Element,HTMLElement,HTMLParagraphElement,显然使用继承关系方便一些方法或属性的共享,于是javascript从其它语言借鉴了原型这种机制。Prototype作为一个特殊的对象属性存在于每一个函数上。当一个函数通过new操作符new出其“孩子”——“实例”,这个名为实例的对象就拥有这个函数的Prototype对象所有的一切成员,从而实现实现所有实例对象都共享一组方法或属性。而javascript所谓的“类”就是通过修改这个Prototype对象,以区别原生对象及其其它定义的“类”。在浏览器中,node这个类基于Object修改而来的,而Element则是基于Node,而HTMLElement又基于Element....相对我们的工作业务,我们可以创建自己的类来实现重用与共享

        function A(){
    
        }
        A.prototype = {
            aa:"aa",
            method:function(){
            }
        };
        var a = new A;
        var b = new A;
        console.log(a.aa === b.aa);
        console.log(a.method === b.method)

    一般地,我们把定义在原型上的方法叫原型方法,它为所有的实例所共享,这有好也有不好,为了实现差异化,javascript允许我们直接在构造器内指定其方法,这叫特权方法。如果是属性,就叫特权属性。它们每一个实例一个副本,各不影响。因此,我们通常把共享用于操作数据的方法放在原型,把私有的属性放在特权属性中。但放于this上,还是让人任意访问到,那就放在函数体内的作用域内吧。这时它就成为名副其实的私有属性。

        function A() {
            var count = 0;
            this.aa = "aa";
            this.method = function() {
                return count;
            }
            this.obj = {}
        }
        A.prototype = {
            aa:"aa",
            method:function(){
    
            }
        };
        var a = new A;
        var b = new A;
        console.log(a.aa === b.aa);//true 由于aa的值为基本类型,比较值
        console.log(a.obj === b.obj) //false 引用类型,每次进入函数体都要重新创建,因此都不一样。
        console.log(a.method === b.method); //false


    特权方法或属性只是只是遮住原型的方法或属性,因此只要删掉特权方法,就能方法到同名的原型方法或属性

        delete a.method;
        delete b.method;
        console.log(a.method === A.prototype.method);//true
        console.log(a.method === b.method); //true


    用java的语言来说,原型方法与特权方法都属性实例方法,在java中还有一种叫类方法与类属性的东西。它们用javascript来模拟也非常简单,直接定义在函数上就行了。

        A.method2 = function(){} //类方法
        var c = new A;
        console.log(c.method2); //undefined

    接下来,我们看下继承的实现,上面说过,Prototype上有什么东西,它的实例就有什么东西,不论这个属性是后来添加的,还是整个Prototype都置换上去的。如果我们将这个prototype对象置换为另一个类的原型,那么它就轻而易举的获得那个类的所有原型成员。

        function A() {};
        A.prototype = {
            aaa : 1
        }
        function B() {};
        B.prototype =  A.prototype;
        var b = new B;
        console.log(b.aaa); //=> 1;
        A.prototype.bb = 2;
        console.log(b.bb) //=> 2;

    由于是引用着同一个对象,这意味这,我们修改A类的原型,也等同于修该了B类的原型。因此,我们不能把一个对象赋值给两个类。这有两种办法,

    方法1:通过for in 把父类的原型成员逐一赋给子类的原型
    方法2是:子类的原型不是直接由父类获得,先将父类的原型赋值给一个函数,然后将这个函数的实例作为子类的原型。

    方法一,我们通常要实现mixin这样的方法,有的书称之为拷贝继承,好处就是简单直接,坏处就是无法通过instanceof验证。Prototype.js的extend方法就用来干这事。

        function extend (des, source) { //des = destination
            for (var property in source)
                des[property] = source[property];
            return des;
        }

    方法二,就在原型上动脑筋,因此称之为原型继承。下面是个范本

        function A() {};
        A.prototype = {
            aa:function(){
                alert(1)
            }
        }
        function bridge() {
    
        };
        bridge.prototype = A.prototype;
    
        function B() {}
        B.prototype = new bridge();
    
        var a = new A;
        var b = new B;
    
        console.log(a == b) //false 证明成功分开原型
        console.log(A.prototype == B.prototype) //true 子类共享父类的原型方法
        console.log(a.aa === b.aa); //为父类动态添加新的方法
        A.prototype.bb = function () {
            alert(2)
        }
        //true,继承父类的方法
        B.prototype.cc = function (){
            alert(3)
        }
        //false 父类未必有子类的new实例
        console.log(a.cc === b.cc)
        //并且它能够正常通过javascript自带的验证机制instanceof
        console.log(b instanceof A) ;//true
        console.log(b instanceof B) ; //true

    方法二能通过instanceof验证,es5就内置了这种方法来实现原型继承,它就是Object.create,如果不考虑第二个参数,它约等于下面的代码

        Object.create = function (o) {
            function F() {}
            F.prototype = o;
            return new F();
        }


    上面的方法,要求传入一个父类的原型作为参数,然后返回子类的原型

    不过,我们这样还是遗漏了一点东西——子类不只是继承父类的遗产,还应该有自己的东西,此外,原型继承并没有让子类继承父类的成员与特权成员。这些我们都得手动添加,如类成员,我们可以通过上面的extend方法,特权成员我们可以在子类构造器中,通过apply实现。

        function inherit(init, Parent, proto){
            function Son(){
                Parent.apply(this, argument); //先继承父类的特权成员
                init.apply(this, argument); //在执行自己的构造器
            }
        }
        //由于Object.create是我们伪造的,因此避免使用第二个参数
        Son.prototype = Object.create(Parent.prototype,{});
        Son.prototype.toString = Parent.prototype.toString; //处理IEbug
        Son.prototype.valueOf = Parent.prototype.valueOf; //处理IEbug
        Son.prototype.constructor = Son; //确保构造器正常指向,而不是Object
        extend(Son, proto) ;//添加子类的特有的原型成员
        return Son;

    下面,做一组实验,测试下实例的回溯机制。当我们访问对象的一个属性,那么他先寻找其特权成员,如果有同名就返回,没有就找原型,再没有,就找父类的原型...我们尝试将它的原型临时修改下,看它的属性会变成那个

        function A(){
    
        }
        A.prototype = {
            aa:1
        }
        var a = new A;
        console.log(a.aa) ; //=>1
    
        //将它的所有原型都替换掉
        A.prototype = {
            aa:2
        }
        console.log(a.aa); //=>1
    
        //于是我们想到每个实例都有一个constructor方法,指向其构造器
        //而构造器上面正好有我们的原型,javascript引擎是不是通过该路线回溯属性呢
        function B(){
    
        }
        B.prototype = {
            aa:3
        }
        a.constructor = B;
        console.log(a.aa) //1 表示不受影响


    因此类的实例肯定通过另一条通道进行回溯,翻看ecma规范可知每一个对象都有一个内部属性[[prototype]],它保存这我们new它时的构造器所引用的Prototype对象。在标准浏览器与IE11里,它暴露了一个叫__proto__属性来访问它。因此,只要不动__proto__上面的代码怎么动,a.aa始终坚定不毅的返回1.

    再看一下,new时操作发生了什么。

    1.创建了一个空对象 instance
    2.instance.__proto__ = intanceClass.prototype
    3.将构造函数里面的this = instance
    4.执行构造函数里的代码
    5.判定有没有返回值,没有返回值就返回默认值为undefined,如果返回值为复合数据类型,则直接返回,否则返回this
    于是有了下面的结果。

        function A(){
            console.log(this.__proto__.aa); //1
            this.aa = 2
        }
        A.prototype = {aa:1}
        var a = new A;
        console.log(a.aa)
        a.__proto__ = {
            aa:3
        }
        console.log(a.aa) //=>2
        delete a. aa; //删除特权属性,暴露原型链上的同名属性
        console.log(a.aa) //=>3

    有了__proto__,我们可以将原型设计继承设计得更简单,我们还是拿上面的例子改一改,进行试验

        function A() {}
        A.prototype = {
            aa:1
        }
        function bridge() {}
        bridge.prototype = A.prototype;
    
        function B(){}
        B.prototype = new bridge();
        B.prototype.constructor = B;
        var b = new B;
        B.prototype.cc = function(){
            alert(3)
        }
        //String.prototype === new String().__proto__  => true
        console.log(B.prototype.__proto__ === A.prototype) //true
        console.log(b.__proto__ == B.prototype); //true 
        console.log(b.__proto__.__proto__ === A.prototype); //true 得到父类的原型对象

    因为b.__proto__.constructor为B,而B的原型是从bridge中得来的,而bride.prototype = A.prototype,反过来,我们在定义时,B.prototype.__proto__ = A.prototype,就能轻松实现两个类的继承.

    __proto__属性已经加入es6,因此可以通过防止大胆的使用

    2.各种类工厂的实现。

    上节我们演示了各种继承方式的实现,但都很凌乱。我们希望提供一个专门的方法,只要用户传入相应的参数,或按照一定简单格式就能创建一个类。特别是子类


    由于主流框架的类工厂太依赖他们庞杂的工具函数,而一个精巧的类工厂也不过百行左右

    相当精巧的库,P.js

    https://github.com/jiayi2/pjs

    使用版:https://github.com/jiayi2/factoryjs

    这是一个相当精巧的库,尤其调用父类的同名方法时,它直接将父类的原型抛在你面前,连_super也省了

        var P = (function(prototype, ownProperty, undefined) {
      return function P(_superclass /* = Object */, definition) {
        // handle the case where no superclass is given
        if (definition === undefined) {
          definition = _superclass;
          _superclass = Object;
        }
    
        // C is the class to be returned.
        //
        // When called, creates and initializes an instance of C, unless
        // `this` is already an instance of C, then just initializes `this`;
        // either way, returns the instance of C that was initialized.
        //
        //  TODO: the Chrome inspector shows all created objects as `C`
        //        rather than `Object`.  Setting the .name property seems to
        //        have no effect.  Is there a way to override this behavior?
        function C() {
          var self = this instanceof C ? this : new Bare;
          self.init.apply(self, arguments);
          return self;
        }
    
        // C.Bare is a class with a noop constructor.  Its prototype will be
        // the same as C, so that instances of C.Bare are instances of C.
        // `new MyClass.Bare` then creates new instances of C without
        // calling .init().
        function Bare() {}
        C.Bare = Bare;
    
        // Extend the prototype chain: first use Bare to create an
        // uninitialized instance of the superclass, then set up Bare
        // to create instances of this class.
        var _super = Bare[prototype] = _superclass[prototype];
        var proto = Bare[prototype] = C[prototype] = C.p = new Bare;
    
        // pre-declaring the iteration variable for the loop below to save
        // a `var` keyword after minification
        var key;
    
        // set the constructor property on the prototype, for convenience
        proto.constructor = C;
    
        C.extend = function(def) { return P(C, def); }
    
        return (C.open = function(def) {
          if (typeof def === 'function') {
            // call the defining function with all the arguments you need
            // extensions captures the return value.
            def = def.call(C, proto, _super, C, _superclass);
          }
    
          // ...and extend it
          if (typeof def === 'object') {
            for (key in def) {
              if (ownProperty.call(def, key)) {
                proto[key] = def[key];
              }
            }
          }
    
          // if no init, assume we're inheriting from a non-Pjs class, so
          // default to using the superclass constructor.
          if (!('init' in proto)) proto.init = _superclass;
    
          return C;
        })(definition);
      }
    
      // as a minifier optimization, we've closured in a few helper functions
      // and the string 'prototype' (C[p] is much shorter than C.prototype)
    })('prototype', ({}).hasOwnProperty);

    我们尝试创建一个类:

        var Dog = P (function(proto, superProto){
            proto.init = function(name) { //构造函数
                this.name = name;
            }
            proto.move = function(meters){ //原型方法
                console.log(this.name + " moved " + meters + " m.")
            }
        });
        var a = new Dog("aaa")
        var b = new Dog("bbb"); //无实例变化
        a.move(1);
        b.move(2);

    我们在现在的情况下,可以尝试创建更简洁的定义方式

        var Animal = P (function(proto, superProto){
            proto.init = function(name) { //构造函数
                this.name = name;
            }
            proto.move = function(meters){ //原型方法
                console.log(this.name + " moved " + meters + " m.")
            }
        });
        var a = new Animal("aaa")
        var b = new Animal("bbb"); //无实例变化
        a.move(1);
        b.move(2);
        //...............
        var Snake = P (Animal, function(snake, animal){
            snake.init = function(name, eyes){
                animal.init.call(this, arguments); //调运父类构造器
                this.eyes = 2;
            }
            snake.move = function() {
                console.log('slithering...');
                animal.move.call(this, 5); //调运父类同名方法
            }
        });
        var s = new Snake("snake", 1);
        s.move();
        console.log(s.name);
        console.log(s.eyes);

    私有属性演示,由于放在函数体内集中定义,因此安全可靠!

        var Cobra = P (Snake, function(cobra){
            var age = 1;//私有属性
            //这里还可以编写私有方法
            cobra.glow = function(){ //长大
                return age++;
            }
        });
        var c = new Cobra("cobra");
        console.log(c.glow()); //1
        console.log(c.glow()); //2
        console.log(c.glow()); //3

    JS.Class

    从它的设计来看,让是继承Base2,相似的类工厂还有mootools。
    Base2的base2.__prototyping, mootools的klass.$protyping。它创建子类时页不通过中间的函数断开双方的原型链,而是使用父类的实例来做子类的原型,这点实现的非常精巧。

    simple-inheritance

    作者为john Resig ,特点是方法链实现的十分优雅,节俭。

    体现javascript的灵活性的库 def.js

    如果有什么库能体现javascript的灵活性,此库肯定名列前茅。它试图在形式上模拟Ruby的继承形式。让使用过ruby的人一眼看出,那个是父类,那个是子类。

    下面就是Ruby的继承示例:

        class child < Father
        #
        end

    def.js能做到这个程度

        def("Animal")({
            init:function(name){
                this.name = name;
            },
            speak:function(text){
                console.log('this is a' + this.name)
            }
        });
        var animal = new Animal("Animal");
        console.log(animal.name)
    
        def('Dog') < Animal({
            init:function(name,age){
                this._super();//魔术般的调运了父类
                this.age = age;
            },
            run:function(s){
                console.log(s)
            }
        });
        var dog = new Dog('wangwang');
        console.log(dog.name); //wangwang
    
        //在命名空间上创建子类
        var namespace = {};
        def(namespace,"Shepherd") < Dog({
            init:function(){
                this._super();
            }
        });
        var shepherd = new namespace.Shepherd("Shepherd")
        console.log(shepherd.name); 

    3.es5属性描述符对oo库的冲击

    es5最受人瞩目的升级是为对象引入属性描述符,让我们对属性有了更精细的控制,如这个属性是否可以修改,是否可以在for in中循环出来,是否可以删除。这些新增的API都集中定义在Object下,基本上除了Object.keys这个方法外,其它新API,旧版本的IE都无法模拟。于是新的API,基本很少有讲解的,我们在这里稍微解读下:

    Obejct提供以下几种新方法。

    Object.keys
    Object.getOwnPropertyNames
    Object.getPrototypeOf
    Object.defineProperty
    Object.defineProperties
    Object.getOwnPropertyDescriptor
    Object.create
    Object.seal
    Object.freeze
    Object.preventExtensions
    Object.isSealed
    Object.isFrozen
    Object.isExtensible

    其中,除了Object.keys外,旧版本的IE都无法模拟这些新API。旧版式的标准浏览器,可以用__peototype__实现Object.getPrototypeOf,结合__defineGetter__与defineSetter__来模拟Object.defineProperty。

    Obejct.keys用于收集当前对象的可遍历属性(不包括原型链上的)以数组形式返回。

    Object.getOwnPropertyNames用于收集当前对象不可遍历属性与可遍历属性,以数组形式返回。

        var obj = {
            aa : 1,
            toString : function() {
                return "1"
            }
        }
        if (Object.defineProperty && Object.seal) {
            Object.defineProperty(obj,"name",{
                value:2
            })
        }
        console.log(Object.getOwnPropertyNames(obj)); //=> ["aa", "toString", "name"]
        console.log(Object.keys(obj));//=> ["aa", "toString"]
    
        function fn(aa, bb){};
        console.log(Object.getOwnPropertyNames(fn));// => ["length", "name", "arguments", "caller", "prototype"]
        console.log(Object.keys(fn));//[]
        var reg = /w{2,}/i;
        console.log(Object.getOwnPropertyNames(reg)); //=> ["source", "global", "ignoreCase", "multiline", "lastIndex"]
        console.log(Object.keys(reg));//[]

    Object.prototypeOf返回参数对象内部属性[[Prototype]],它在标准浏览器中一直使用一个私有属性__proto__获取(IE9 10,opera都没有)。需要补充一下,Object的新API(除了Object.create外)有一个统一的规定,要求第一个参数不能为数字 ,字符串,布尔,null,undefeind这五种字面量,否则抛出TypeError异常。

        console.log(Object.getPrototypeOf(function(){}) == Function.prototype ); //=>true
        console.log(Object.getPrototypeOf({}) === Object.prototype);//=>true

    Object.definePrototype暴露了属性描述的接口,之前许多内建属性都是由JavaScript引擎在属下操作。如,for in循环为何不能遍历出函数的arguments、length、name等属性名,delete window.a为何返回false. 这些现象终究有个解释。它一共涉及六个可组合的配置项。
    是否可重写writable,当前值value
    读取时内部调用的函数set
    写入时内部调用函数get,
    是否可遍历enumerable,
    是否可以再次改动这些配置项configurable.

    比如我们随便写个对象

    var obj = {x:1}

    有了属性描述符,我们就清楚它在底下做的更多细节,它相当于es5的这个创建对象的式子:

        var obj = Object.create(Object.prototype,{
            x : {
                value : 1,
                writable : true,
                enumerable : true,
                configurable : true
            }
        })

    效果对比es3和es5,就很快明白,曾经的[[ReadOnly]] , [[DontEnum]], [[DontDlelete]]改成[[writable]], [[enumerable]],[[Configurable]]了。因此,configurable还有兼顾能否删除的职能

    这六个配置项将原有的本地属性拆分为两组。数据属性与访问器属性。我们之前的方法可以像数据属性那样定义。

    es3时代,我们的自定义类的属性可以统统看做是数据属性。

    像DOM中的元素节点的 innerHTML innerText cssText 数组的length则可归为访问器属性,对它们赋值不是单纯的赋值,还会引发元素其它功能的触发,而取值不一定直接返回我们之前给予的值。

    数据的属性有1、2、5、6这四个配置项,访问器有3、4、5、6这四个配置项、如果你设置了value与writable,就不能设置set,get,反之亦然。如果没有设置。2,3,4默认为false。第1,5,6项默认为false.

     关于对象属性特征,更多请参阅http://www.cnblogs.com/ahthw/p/4272663.html 第7小节:7.属性的特征

    var obj = {};
        Object.defineProperty(obj,"a",{
            value: 37,
            writable :true,
            enumerable :true,
            configurable: true
        });
    
        console.log(obj.a) //=> 37;
        obj.a = 40;
        console.log(obj.a) //=>40
        var name = "xxx";
        for(var i in obj){
            name = i
        }
        console.log(name);//=> a
    
        Object.defineProperty(obj,"a",{
            value:55,
            writable:false,
            enumerable:false,
            configurable:true
        })
    
        console.obj(obj.a);//=>55
        obj.a = 50;
    
        console.log(obj.a);//55
        name = "b";
        for (var i in obj){
            name = i
        }
        console.log(name); //b

    Object.defineProperties就是Object.defineProperty的加强版,它能一下子处理多个属性。因此,如果你能模拟Object.defineProperty,它就不是问题。

        if (typeof Object.defineProperties !== 'function'){
            Object.defineProperties = function(obj, descs){
                for(var prop in descs) {
                    if (descs.hasOwnProperty(porop)){
                        Object.defineProperty(obj, prop, descs[prop]);
                    }
                }
                return obj;
            }
        }

    使用示例

        var obj = {};
        Object.defineProperties(obj, {
            "value":{
                value :true,
                writable:false,
            },
            "name":{
                value:"John",
                writable:false
            }
        });
        var a = 1;
        for (var p in obj){
            a = p
        };
        console.log(a);// 1

    Object.getOwnPropertDescriptor用于获得某对象的本地属性的配置对象。其中,configurable,enumerable肯定包含其中。视情况再包括value,wirtable或set,get.
    ....
    Object.preventExtensions,它是三个封锁对象修改的API中程度最轻的,就是阻止添加本地属性,不过如果本地属性都被删除了,也无法再加回来。以前javascript对象的属性都是任意添加的,删除,修改其值。如果它原型改动。我们访问它还会有意外的惊喜。

    var a = {
            aa: "aa"
        }
        Object.preventExtensions(a)
        a.bb = 2;
        console.log(a.bb);//=> undefined
        a.aa = 3; 
        console.log(a.aa); //=>3 允许修改原有的属性
        delete a.aa;
        console.log(a.aa); //=> undefined 但允许它删除已有的属性
    
        Object.prototype.ccc = 4;
        console.log(a.ccc); //4 不能阻止添加原型属性
        a.aa = 5;
        console.log(a.aa); //=> dundeined 不吃回头草,估计里边是以白名单的方式实现的

    Object.seal比Object.preventExtensions更严格,它不准删除已有的本地属性,内部实现就是遍历一下,把本地属性的configurable改为false

    var a = {
            a : "aa"
        }
        Object.seal(a)
        a.bb = 2;
        console.log(a.bb); // =>undefined添加本地属性失败
        a.aa = 3;
        console.log(a.aa); //3 允许修改已有的属性
        delete a.aa;
        console.log(a.aa) ;//=>3 但不允许删除已有的属性

    Object.freeze无疑是最专制的(因此有人说过程式程序很专制,OO程序则自由些,显然道格拉斯的ecma262v5想把javascript引向前者),它连原有的本地属性也不让修改了。内部的实现就是遍历一下,把每个本地属性的writable也改成false.

        var a = {
            aa : "aa"
        };
        Object.freeze(a);
        a.bb = 2;
        console.log(a.bb) //undefined 添加本地属性失败
        a.aa =3;
        console.log(a.aa) //aa 不允许它修改已有的属性
        delete a.aa;
        console.log(a.aa); //aa 不允许删除已经有的属性

    (isPainObject用于判定目标是不是纯净的javascript对象,且不是其它自定义类的实例。用法与prototype.js的class.create一样,并参照 jQuery UI提供了完美的方法链与静态成员的继承。)

    总结:es5对javascript对象产生深刻的影响,Object.create让原型继承更方便了,但在增添的字类的专有原型成员或类成员时,如果它们的属性enumerable为false,单纯的for in循环已经不管用了,完美就要用到Object.getOwnPropertyNames。另外,访问器属性的复制只有通过Object.getOwnPropertyDescriptpor与Object.defineProperty才能完成

    (本章完结

    上一章:第五章:浏览器的嗅探和特征侦测  下一章: 第7章:选择器引擎

  • 相关阅读:
    Mac 上 Go 语言的安装以及编辑器的配置
    【Go学习】立Flag
    python 使用多线程同时执行多个函数
    python闭包
    mfs环境搭建之元数据服务器(master)节点安装-2
    mfs分布式文件系统介绍-01
    vscode中vim插件对ctrl键的设置
    文本截取{}的内容,生成新数组
    如何过滤a数组中b数组存在的值
    js动态加载js文件
  • 原文地址:https://www.cnblogs.com/ahthw/p/4591423.html
Copyright © 2020-2023  润新知