为什么赋值影响实例属性而不是原型属性?



我每周都参加这个聚会,讨论《effective javascript: 68种方法》这本书。

在第36项:仅在实例对象上存储实例状态中,我们创建了以下示例来解释它。

function User() {}
User.prototype = {
    hobbies: [], // should be instance state!
    addHobby: function (x) {
        this.hobbies.push(x);
    }
};

我们实例化以下用户。

boy = new User();
// User {hobbies: Array[0], addHobby: function}
girl = new User();
// User {hobbies: Array[0], addHobby: function}
boy.addHobby("swimming"); 
girl.addHobby("running");
// undefined
boy.hobbies
// ["swimming", "running"]
girl.hobbies
// ["swimming", "running"]

可以看到,addHobby函数在原型级别影响爱好。

现在如果我把整个代码改成

function User() {}
    User.prototype = {
        hobbies: [], // should be instance state!
        addHobby: function (x) {
            newArr = new Array(x);
            this.hobbies = this.hobbies.concat(newArr);
        }
    };
boy = new User();
girl = new User();
boy.addHobby("swimming"); 
girl.addHobby("running");
boy.hobbies
//["swimming"]
girl.hobbies
//["running"]

我们知道原因是因为作业。我们正在寻找一个完整的解释,为什么this.hobbies = this.hobbies.concat(newArr);分配到实例级别,而不是在原型级别,尽管在两个实例中都使用了术语this.hobbies

这就是语言的定义方式。来自规范:

产品MemberExpression: MemberExpression [Expression]按如下方式求值:

  1. 让baserreference作为MemberExpression的求值结果
  2. 让baseValue为GetValue(baserreference)
  3. 让propertyNameReference作为表达式的求值结果。
  4. 设置propertyNameValue为GetValue(propertynamerence)。
  5. 调用CheckObjectCoercible (baseValue)。
  6. 设置propertyNameString为ToString(propertyNameValue)
  7. 如果正在计算的语法结果包含在严格模式代码中,则让strict为真,否则让strict为假。
  8. 返回一个Reference类型的值,其基值为baseValue,引用名称为propertyNameString,严格模式标志为strict。

那个Ecma月球语言没有提到在对象原型上查找属性。左值成员表达式总是引用直接涉及的基对象的属性。

使用"this"你不能给原型分配任何东西,但你可以从中读取。因此,当你做this.hobbies = x;时,你设置当前实例的属性"爱好"而不是原型的属性,然后隐藏了一个同名的原型级属性(即,boy.hobbies不再返回来自原型的数组,因为有一个直接的属性)。

concat()返回一个新数组,而不是对现有数组的引用,因此,您隐藏了原型级属性"嗜好"。

在下一次调用时,实例级数组"嗜好"被一个包含先前值和新值的新数组覆盖。

无论何时设置对象的属性值,该属性都是在对象本身定义的,无论该属性是否存在于对象的原型链中。

这在规范第8.7.2节中有描述:

4。如果IsPropertyReference(V),则
,,,,(a) If HasPrimitiveBase(V)为假,则设put为[[Put]]内部方法,否则设put为下面定义的特殊[[Put]]内部方法。
,,,,(b)调用put内部方法,使用base作为它的this值,传递GetReferencedName(V)作为属性名,传递W作为值,传递IsStrictReference(V)作为Throw标志。

[[Put]]方法在第8.12.5节中描述,其中重要的步骤是:

6。否则,在对象O上创建名为P的命名数据属性,如下所示
,,,,(a)让newDesc作为属性描述符{[[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}
,,,,(b)调用O[[DefineOwnProperty]]内部方法,传递PnewDescThrow作为参数。


如果你仔细看一下规范,你会发现只有当继承的属性不是访问器属性时,赋值才会在对象上创建属性。

。下面的代码实际上不会创建实例属性:

var name = 'foo';
function User() {} 
Object.defineProperty(User.prototype, 'name', {
    'get': function() { return name;},
    'set': function(val) { name = val;}
});
var u1 = new Users();
var u2 = new Users();
u1.name = 'bar';
console.log(u2.name); // shows 'bar'
console.log(u1) // shows 'User {}' instead of 'User {name: 'bar'}'

对我来说,这不是原型继承的最好例子。

这是你的例子,由我修改:

function User() {
     this.hobbies = [];
};
    User.prototype = {
        addHobby: function (x) {
            this.hobbies.push(x);
        }
    };
boy = new User();
girl = new User();
boy.addHobby("swimming"); 
girl.addHobby("running");

最新更新