原型代理与原型克隆

一、原型代理

在之前的介绍中, 我们重点介绍了原型, 里面有熟悉的 __proto__prototype, 现在花一些时间来研究一下对象的拷贝问题。

let switchProto = {
    isOn: function isOn() { return this.state },
    toggle: function toggle() {
        this.state = !this.state;
        return this;
    },
    meta: { name: 'Light switch' },
    state: false
},
    switch1 = Object.create(switchProto),
    switch2 = Object.create(switchProto);

这里声明了一个作为原型材料的对象 switchProto, 它的两个拷贝对象为 switch1switch2, 现在要对两个对象做一些修改。

switch1.meta.name = "Henrenx";
console.log(switch1.meta.name);                    // Henrenx
console.log(switch2.meta.name);                    // Henrenx
console.log(switch1.meta === switch2.meta);        // true

两个拷贝的对象的属性竟然惊人的一致(其他属性也都是相同的)。

冷静下来, 我们从 Object.create() 函数开始, 逐步探究其中的秘密, 下面的代码是 MDN上对 Object.create() 方法的实现。

if (!Object.create) {
    Object.create = function (o) {
        if (arguments.length > 1) {
            throw new Error('Object.creaate implementation'
                + 'only accepts the first parameter');
        }
        function F() {};
        F.prototype = o;
        return new F();
    }
}

可以看到, switch1switch2 的拷贝过程其实就是把 switchProto 作为原型, 因此两个拷贝对象实际上是在共享一份属性 (原因不是这个)。

下面再做一次不同的修改:

switch1.meta = { name: 'Henrenx' };
console.log(switch1.meta.name);                    // Henrenx
console.log(switch2.meta.name);                    // Light switch
console.log(switch1.meta === switch2.meta);        // false

为什么这里两个拷贝对象就不同了呢?


划重点

如果你在实例中对原型上的一些引用类型的变量(对象或数组)做了修改, 那么此项修改会在所有使用原型的实例上生效, 但如果你直接替换了实例中的某个属性值, 所产生的修改仅限于实例本身。

可以把上面的话记为一条 rule , 配合这里的图就更好理解了, 其中[[Prototype]] 就是 __proto__

1527322805_459403.png

划重点


拿上面的例子来解释这段话:

  • 第一次修改的是一个对象 {name: 'Henrenx'}, meta 属性是在引用这个对象, 因此 switch1switch2 中都生效了。
  • 第二次修改的是 meta 属性, 将它引用到另外一个崭新的对象, 产生的修改仅限于 switch1 本身。

实际上 switch1 中有两个 meta, 一个在原型里, 一个在自身属性里。只是原型查找的过程中自身属性优先, 导致了这样的结果。

switch1的meta.png

二、原型克隆

把所有的属性(不包括方法<后面说原因>)放在原型上面共享是一种非常危险的编码模式, 很多代码的异常事故都源于对共享属性的意外修改。所以每个对象都拥有一份自己的属性显得格外重要。
在方法里, 我们往往使用 this.something 来操作数据, this 的指向往往就是实例本身, 因此很少对数据造成污染, 所以方法并不在其列。

Object.assign 方法就是在做上面期待的工作。

let switchProto = {
    // ... 此对象和前面的相同
},
switch1 = Object.assign({}, switchProto),
switch2 = Object.assign({}, switchProto);

Object.assign 方法在背地里做了如下的工作 (摘自 Underscore)

_.extend = function (obj) {
    each(slice.call(arguments, 1), function(source) {
           for(let prop in source) {
            obj[prop] = source[prop];
        } 
    });
    return obj;
}

可以明显且直观的看到通过这个方法拷贝出来的对象的属性是放在每个对象里的, 而原型代理是直接放在了 prototype 然后通过 new 实例化出来的。对于硬件部分来说, 原型克隆很明显占据了更多的内存。因此在实际应用中, 为了创建 共享方法私有属性, 常把这两者结合起来使用。


如果在原型克隆中做一些和原型代理一样的属性更改的小测试, 结果却是一样的, 这是上面划重点的那条规律所决定的, 与是否是原型代理还是原型克隆无关呢~

原型代理与原型克隆最主要的区别, 原型克隆中的实例属性都是经过拷贝得来的, 而原型代理在不同实例间只共享一份属性拷贝, 在对实例属性进行重写前, 外界访问到的永远是原型中所设置的属性值。

添加新评论