美文网首页
创建对象(二)——原型模式

创建对象(二)——原型模式

作者: 薛普定朗谔克 | 来源:发表于2017-05-17 10:48 被阅读0次

    原文地址:创建对象(二)——原型模式

    理解原型对象

    我们创建的每一个函数都有一个默认的prototype属性,它指向一个对象,对象中默认的有一个叫做constructor的属性,指向这个函数本身。

    如上图,右侧的这个方框就是函数function的原型,也就是说prototype属性指向的这个对象就是原型。
    当构造函数创建出一个新实例后,该实例会默认具有一个__proto__属性,这个属性指向构造函数的原型对象。因此,如果我们把属性和方法都添加到原型对象中,不同的实例就可以访问到相同的属性和方法了。

        function Person() {
        }
        Person.prototype.name = "wanghan";
        Person.prototype.age = 20;
        Person.prototype.getName = function () {
            console.log(this.name);
        };
        var fun1 = new Person();
        var fun2 = new Person();
        console.log(fun1.name);   //wanghan
        console.log(fun2.age);    //20
        fun1.getName();           //wanghan
        fun2.getName();           //wanghan
        console.log(fun1.getName == fun2.getName);  //true
    

    我们先声明了一个空的构造函数Person,将一些属性和方法添加到了Person函数的prototype属性中,然后实例化了两个对象,根据后面的输出我们可以确定两个实例访问的是相同的对象,它们共享这些属性和方法。下图展示了例子中各个对象的关系:

    实例与原型中属性的纠葛

    当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。换句话说,添加这个属性只会阻止我们去访问原型中的那个属性,但不会修改那个属性。你也可以理解为,当我们访问实例对象时,会优先访问它自身的属性和方法。

        function Person() {
        }
        Person.prototype.name = "wanghan";
        Person.prototype.age = 20;
        Person.prototype.getName = function () {
            console.log(this.name);
        };
        var fun1 = new Person();
        var fun2 = new Person();
        fun1.name = "Tom";
        console.log(fun1.name);   //Tom
        console.log(fun2.name);   //wanghan
        fun1.getName();           //Tom
        fun2.getName();           //wanghan
    

    在这个例子中,我们给实例fun1添加了属性name="Tom"

    • 访问fun1.name时,先在实例fun1中寻找属性name,找到之后返回Tom就不必再搜索原型了。
    • 而访问fun2.name时,先在实例fun2中没有找到属性name,于是搜索原型,结果找到了值为wanghan的属性name,验证了在实例中添加属性不会修改原型中的同名属性。
    • 访问fun1.getName()时,在在实例fun1中没有找到方法getName(),搜索原型找到了这个方法,执行其中的代码,此时方法中的this已经指向了实例(回忆一下操作符new调用构造函数经历的过程),因此输出的是实例属性name的值Tom

    判断属性的位置

    • hasOwnProperty()方法只在给定属性存在于实例对象中时返回true
    • 单独使用in操作符时,无论属性存在于实例中还是原型中,只要能够访问到,就会返回true

    如果实例和原型中都存在着一个相同属性,结合这两种方法我们就可以判断,我们访问到的这个同名属性到底是实例中的还是原型中的。

        function Person() {
        }
        Person.prototype.name = "wanghan";
        Person.prototype.age = 20;
        var fun = new Person();
        fun.name = "Tom";
        console.log("name" in fun);               //true
        console.log(fun.hasOwnProperty("name"));  //true    --name来自实例
        console.log("age" in fun);                //true
        console.log(fun.hasOwnProperty("age"));   //false   --age来自原型
    

    更简单的原型语法

    在前面的例子中,我们给原型对象添加对象的时候,输入了好多遍Person.prototype,其实这些不必要的输入都是可以避免的,最常见的方法就是以对象字面量的形式创建对象。

        function Person() {
        }
        Person.prototype={
            name: "wanghan",
            age: 20,
            getName: function () {
                console.log(this.name)
            }
        };
        var fun = new Person();
        console.log(fun.name);  //wanghan
        console.log(fun.age);   //20
        fun.getName();           //wanghan
    

    这样创建对象是不是很轻松,而且输出的结果跟之前相比并没有什么变化。但是这里有一点需要注意,重写原型对象的实质是,我们重建了一个对象赋值给了函数的prototype,所以新建的这个原型对象中默认的constructor属性也是新建的,它不指向构造函数Person,但是必要情况下我们可以手动让它指向Person

        function Person() {
        }
        Person.prototype={
            constructor: Person,   //看这里
            name: "wanghan",
            age: 20,
            getName: function () {
                console.log(this.name)
            }
        };
    

    重写原型对象的弊端

    由于实例与原型之间的松散连接关系,即使我们先创建实例,再给原型对象添加属性,我们也照样可以访问到这些属性。

        function Person() {
        }
        var fun = new Person();
        Person.prototype.name ="wanghan";
        console.log(fun.name);  //wanghan
    

    但是重写原型对象之后就不一样了。

        function Person() {
        }
        var fun = new Person();
        Person.prototype={
            constructor: Person,
            name: "wanghan"
        };
        console.log(fun.name);   //undefined
    

    我们前面说过,重写原型对象实际上是新建了一个新的对象赋值给构造函数的prototype。如果是先创建一个实例,那么实例指向最开始的一个只含有constructor属性的原型对象,即使随后又新建了一个原型对象,它的指向也不会再发生变化。


    要想解决这个问题就要牢记,要在重写原型对象之后新建实例,这样实例指向的就是重写之后的原型对象。
        function Person() {
        }
        Person.prototype={
            constructor: Person,
            name: "wanghan"
        };
        var fun = new Person();
        console.log(fun.name);   //wanghan
    

    原型对象的问题

    原型模式也不是没有缺点。

    • 原型中的所有属性和方法都是被实例所共享的,共享方法(函数)是非常合适的,对于那些基本值的属性也还说的过去,但是对于包含引用类型值得属性来说,问题就非常突出了。
        function Person() {
        }
        Person.prototype={
            constructor: Person,
            name: "wanghan",
            friends: ["zhangmin","yangfan"]
        };
        var fun1 = new Person();
        var fun2 = new Person();
        fun1.friends.push("Tom");
        console.log(fun1.friends);   //["zhangmin", "yangfan", "Tom"]
        console.log(fun2.friends);   //["zhangmin", "yangfan", "Tom"]
    

    原型对象中有一个字符串数组,然后创建了两个实例,操作实例fun1向原型对象的数组中又添加了一个字符串Tom,由于数组是引用类型值,并且两个实例共享原型对象中的属性,所以我们刚刚的修改也会在实例fun2中体现出来。这个问题正是我们极少看到有人单独使用原型模式的原因所在。

    • 原型对象直接在原型对象里定义了属性值,使所有实例默认情况下都取得相同的属性值,要克服这一缺点可以组合使用原型模式和构造函数模式。

    后记

    我画框图是用电脑自带的画图软件画的你敢信,举得例子也是自己手打出来的,运行正确之后就剪切到markdown上面。眼看着快要写完了,时间也快凌晨两点了,喜滋滋地准备收尾,谁知道电脑突然一黑,重启了。再次打开markdown之后我就绝望了,就剩下前两段摆在上面。我冷静了二十分钟,发了个朋友圈,然后重写到凌晨四点。嗨呀,好气呀。

    相关文章

      网友评论

          本文标题:创建对象(二)——原型模式

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