我每周都参加这个聚会,讨论《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]按如下方式求值:
- 让baserreference作为MemberExpression的求值结果
- 让baseValue为GetValue(baserreference)
- 让propertyNameReference作为表达式的求值结果。
- 设置propertyNameValue为GetValue(propertynamerence)。
- 调用CheckObjectCoercible (baseValue)。
- 设置propertyNameString为ToString(propertyNameValue)
- 如果正在计算的语法结果包含在严格模式代码中,则让strict为真,否则让strict为假。
- 返回一个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]]
内部方法,传递P、newDesc和Throw作为参数。
如果你仔细看一下规范,你会发现只有当继承的属性不是访问器属性时,赋值才会在对象上创建属性。
。下面的代码实际上不会创建实例属性:
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");