JavaScript创建对象之原型模式

作者: iceman_dev | 来源:发表于2017-01-17 10:20 被阅读362次

    一、原型基础


    在之前的文章:《JavaScript创建对象之单例、工厂、构造函数模式》中详细介绍了构造函数模式创建对象的方式,构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的(实例识别)。

    但是利用构造函数模式创建出来的每个对象,都拥有一份自己的属性和方法,拥有自己的属性是无可厚非的,但是方法应该是要共有的,不应该每个实例都有一份,每个对象都拥有一份方法的话,也会多占用内存空间。

    于是基于构造函数的原型模式就有了,原型模式解决了方法或者属性不能共有的问题,在原型模式中,把实例之间相同的属性和方法提取成共有的属性和方法,即:想让谁共有,就把它放在类.prototype上。

    function CreateJsPerson(name, age) {
        this.name = name; // p1.name=name
        this.age = age;
    }
    
    CreateJsPerson.prototype.writeJs = function () {
        console.log(this.name + ' write js');
    };
    
    var p1 = new CreateJsPerson('iceman' , 25);
    var p2 = new CreateJsPerson('mengzhe' , 27);
        
    console.log(p1.writeJs === p2.writeJs); // true
    

    有三个非常重要的特性:

    • 每一个函数数据类型(普通函数、类)都有一个自带的属性:prototype(原型),并且这个属性是一个对象数据类型的值;

    • prototype上浏览器天生给它加了一个属性:constructor(构造函数),属性值是当前函数(类)本身;

    • 每一个对象数据类型(普通的对象、实例、prototype...)也天生自带一个属性:__proto__,属性值是当前实例所属的原型(prototype)。

    看完以上三句话,是不是有些想吐了呢?哈哈,刚接触的时候都会感到一头雾水,接下来会慢慢讲解。但是别问为什么会有这三个结论,这都是浏览器自带的哦!

    再看一个例子:

    function Fn() {
        this.x = 100;
        this.sum = function () {}
    }
    Fn.prototype.getX = function () {
        console.log(this.x);
    };
    Fn.prototype.sum = function () {
    };
    var f1 = new Fn();
    var f2 = new Fn();
    
    console.log(Fn.prototype.constructor === Fn); // true
    

    下图为该例子对应的图解(为了不增加难度,只画堆内存),注意联系上面的三个结论来理解哦:

    原型基础.png

    Object是JavaScript中所有数据类型的基类(最顶层的类):

    • f1 instanceof Object 输出true,因为f1通__proto__,可以向上级查找,不管多少级,最后总能找到Object;
    • 因为是最顶层的类了,所以Object.prototype上没有__proto__这个属性;

    二、原型链模式


    f1.hasOwnProperty('x'),f1能调用hasOwnProperty,那么hasOwnProperty是f1的一个属性。但是我们发现f1的私有属性上并没有这个方法,那如何处理的呢:

    • 通过 对象名.属性名 的方式获取属性值的时候,首先在对象的私有属性上进行查找,如果私有的属性中存在这个属性,则获取的是私有的属性值;

    • 如果私有的属性中没有,则通过__proto__找到所属类的原型(类的原型上定义的属性和方法都是当前实例的公有的属性和方法),原型上存在的话,获取的是原型上公有的属性值;

    • 如果原型上也没有,则继续通过原型上的__proto__继续向上查找,一直找到Object.prototype为止。

    以上的这种查找机制,就是原型链模式

    console.log(f1.getX == f2.getX); // true
    console.log(f1.__proto__.getX == f2.__proto__.getX); // true
    console.log(f1.getX === Fn.prototype.getX); // true
    
    console.log(f1.sum === f2.prototype.sum); // false
    console.log(f1.sum === Fn.prototype.sum); // false
    

    注意在IE浏览器中,原型模式也是这个原理,但是IE浏览器怕你通过__proto__把公有的修改,禁止我们使用__proto__

    三、原型模式中的this


    在原型模式中,this常见的情况有两种:

    • 在类中:this.xxx = xxx; this表示当前类的实例;

    • 在某一个方法中:要看"."前面是谁this就是谁,通过以下的三个步骤:

      • 需要先确定this的指向(this是谁);
      • 把this替换成对应的代码;
      • 按照原型链查找的机制,一步步的查找结果;
    function Fn() {
        this.x = 100;
        this.y = 200;
        this.getY = function () {
            console.log(this.y);
        }
    }
    Fn.prototype = {
        constructor:Fn,
        y:300,
        getX : function () {
            console.log(this.x);
        },
        getY : function () {
            console.log(this.y);
        }
    }
    var f = new Fn;
    f.getX(); // --> console.log(f.x) --> 100
    f.__proto__.getX(); // --> this是f.__proto__ --> console.log(f.__proto__.x) --> undefined
    
    Fn.prototype.getX(); // --> undefined
    
    f.getY(); // --> 200
    
    f.__proto__.getY(); // --> 300
    

    四、在内置类的原型上扩展方法


    在Array类的原型上扩展一个去重的方法:

    Array.prototype.myUnique = function () {
        var obj = {};
        for (var i = 0; i < this.length; i++) {
            var cur = this[i];
            if(obj[cur] == cur) {
                this[i] = this[this.length - 1];
                this.length --;
                i--;
                continue;
            }
            obj[cur] = cur;
        }
        obj = null;
        return this; // 返回this目的是为了实现链式写法
    };
    var ary = [12, 23, 23, 13, 12, 13, 23, 13];
    ary.myUnique();
    console.log(ary);
    ary.myUnique().sort(function (a, b) {
        return a - b;
    });
    console.log(ary);
    

    Array.prototype.myUnique(); // this --> Array.prototype

    链式写法:执行完数组的一个方法可以紧接着执行下一个方法(jQuery中实现了链式写法)。

    ary.sort(function (a, b) {
        return a - b;
    }).reverse().pop();
    console.log(ary);
    
    • ary为什么可以使用sort方法呢?因为sort是Array.prototype上的公有方法,而数组ary是Array这个类的一个实例,所以ary可以使用sort方法,也就是数组才能使用Array原型上定义的属性和方法;

    • sort执行完成的返回值是一个排序后数组,可以继续执行reverse;

    • reverse执行完成的返回值是一个数组,可以继续执行pop;

    • pop执行完成的返回值是被删除的那个元素,不是一个数组了,所以再执行push会报错。

    五、批量设置原型上的公有属性和方法


    5.1、为原有函数的prototype起一个别名

    function Fn() {
        this.x = 100;
    }
    var pro = Fn.prototype; // 把原来原型指向的地址赋值给我们的pro,现在它们操作的是同一个内存空间
    pro.getX = function () {
    };
    pro.getY = function () {
    };
    var f1 = new Fn();
    

    jQuery中就是这么实现的。

    5.2、重构原型对象的方式

    自己新开辟一个新内存,存储我们公有的属性和方法,把浏览器原来给Fn.rototype开辟的那个替换掉:

    function Fn() {
        this.x = 100;
    }
    Fn.prototype = {
        constructor:Fn,
        a:function () {
            
        },
        b:function () {
            
        }
    };
    var f = new Fn;
    
    批量修改原型的方法.png

    只有浏览器天生给Fn.prototype开辟的堆内存里面才有constructor,而我们自己开辟的这个堆内存没有这个属性,这样constructor指向的就不是Fn而是Object

    console.log(f.constructor); // --> 没做处理之前输出 Object
    

    为了和原来的保持一致,我们需要手动的增加constructor的指向:

    constructor:Fn
    

    注意:不能将这种方式用于给内置类增加公有的属性,例如:

    Array.prototype = {
        constructor:Fn,
        myUnique:function () {
        }
    };
    console.dir(Array.prototype);
    

    因为如果这种方式能用于内置类的话,会将之前内置类中已经存在于原型上的属性和方法给替换掉,所以浏览器是屏蔽这种方式修改内置类的

    所以如果想给内置类增加公有方法的话,应该使用如下方式:

    Array.prototype.myUnique = function () {
    
    };
    

    但是这种方式也是有危险的,因为我们可以一个一个的修改内置类的方法,当通过以下的方式在数组的原型上增加方法,如果方法名和原来内置的方法名重复,会把内置类内置的公有方法修改掉,所以以后在内置类的原型上增加方法的时候,命名都需要加特殊的前缀。

    Array.prototype.sort = function () {
        // .....
    };
    

    六、继承


    6.1、原型继承

    function A() {
        this.x = 100;
    }
    A.prototype.getX = function () {
        console.log(this.x);
    };
    function B() {
        this.x = 200;
    };
    B.prototype = new A();
    
    var n = new B;
    

    原型继承是JavaScript中最常用的一种方式。

    子类B想要继承父类A中的所有的属性和方法(私有+公有),只需要让B.prototype = new A; 即可。

    原型继承的特点:它是把父类中私有的+公有的都继承到了子类的原型上(子类公有的)。

    核心:原型继承,并不是把父类中的属性和方法克隆一份一模一样的给B,而是让B和A之间增加了原型链的连接,以后B的实例n想要用A中的getX方法,需要一级级的向上查找来使用。

    6.2、call继承

    function A() {
        this.x = 100;
    }
    A.prototype.getX = function () {
        console.log(this.x);
    }
    
    function B() {
        // this --> n
        A.call(this); // --> A.call(n) 把A执行,让A中的this变为了n
    }
    
    var n = new B;
    console.log(n.x);;
    

    call继承是把父类私有的属性和方法,克隆一份一模一样的作为自己的私有的属性,注意:只有私有的属性和方法才能继承。公有的属性和方法是没法继承的。所以如果执行n.getX()会报错:

    Uncaught TypeError: n.getX is not a function

    6.3、冒充对象继承

    function A() {
        this.x = 100;
    }
    A.prototype.getX = function () {
        console.log(this.x);
    }
    
    function B() {
        // this --> n
        var temp = new A;
        for (var key in temp) {
            this[key] = temp[key];
        }
        temp = null;
    }
        
    var n = new B;
    console.log(n.x);;
    

    冒充对象继承会把父类 私有的+公有的 都克隆一份一模一样的给子类私有的。

    6.4、混合模式继承

    function A() {
        this.x = 100;
    }
    A.prototype.getX = function () {
        console.log(this.x);
    }
    
    function B() {
        A.call(this); // --> n.x = 100;
    }
    B.prototype = new A; // --> B.prototype: x=100  getX
    B.prototype.constructor = B;
        
    var n = new B;
    n.getX();
    

    混合模式继承就是:原型继承+call继承。

    使用混合模式继承可以让子类即拥有父类私有的属性和方法(call继承的特点),又拥有父类公有的属性和方法(原型继承的特点),但是有一个问题是,父类中私有属性也会成为子类的公有属性,比如本例中的B类中,在私有属性和原型上都拥有一个x=100,虽然根据原型链搜索原则,在使用的没有影响,但是作为有一个代码洁癖的程序员还是觉得不妥,因为毕竟是占用了那么一丢丢的空间(哈哈),那么这时候就可以看接下来的寄生组合式继承了。

    6.5、寄生组合式继承

    function A() {
        this.x = 100;
    }
    
    A.prototype.getX = function () {
        console.log(this.x);
    }
    
    function B() {
        A.call(this);
    }
    //B.prototype = Object.create(A.prototype); // IE6、7、8不兼容
    B.prototype = objectCreate(A.prototype);
    B.prototype.constructor = B;
        
    var n = new B;
    console.dir(n);
    
    function objectCreate(o) {
        function fn() {}
        fn.prototype = o;
        return new fn;
    }
    

    6.6、中间类继承

    首先声明,这种中间类继承是不兼容的,但是可以用于移动端,因为移动端不用兼容IE,并且这种方式在大多数书中都没有介绍,算是一种奇技淫巧吧。

    function avgFn() {
        arguments.__proto__ = Array.prototype;
        arguments.sort(function (a, b) {
            return a-b;
        })
        arguments.pop();
        arguments.shift();
        return eval(arguments.join('+')) / arguments.length;
    }
    console.log(avgFn(10, 20, 30, 10, 30, 30, 40));
    

    个人公众号(icemanFE):分享更多的前端技术和生活感悟

    个人公众号.png

    相关文章

      网友评论

        本文标题:JavaScript创建对象之原型模式

        本文链接:https://www.haomeiwen.com/subject/usrsbttx.html