相信我们都学过vue实现响应式的原理是对象属性上set和get的调用,这其中其实有很多问题,我们在JS中默认的属性值的设置和获取是怎么获取的?为什么有时候返回undefined?设置属性描述符为writable为false后为什么重写会直接失败?下面我们来深入探讨下
1.[[Get]]
属性访问在实现时有一个微妙却非常重要的细节,思考下面的代码:
var myObject = {
a: 2
};
myObject.a; // 2
myObject.a 是一次属性访问,但是这条语句并不仅仅是在myObjet中查找名字为a的属性, 虽然看起来好像是这样。
在语言规范中,myObject.a在myObject上实际上是实现了[[Get]]操作(有点像函数调 用:[[Get]]())。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。
然而,如果没有找到名称相同的属性,按照[[Get]]算法的定义会执行另外一种非常重要的行为(其实就是遍历可能存在的[[Prototype]]链, 也就是原型链)
如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值undefined:
var myObject = {
a:2
};
myObject.b; // undefined
注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是会抛出一个ReferenceError异常:
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
从返回值的角度来说,这两个引用没有区别——它们都返回了undefined。然而,尽管乍看之下没什么区别,实际上底层的[[Get]]操作对myObject.b进行了更复杂的处理。 由于仅根据返回值无法判断出到底变量的值为undefined还是变量不存在,所以[[Get]] 操作返回了undefined。不过稍后我们会介绍如何区分这两种情况。
2.[[Put]]
既然有可以获取属性值的[[Get]]操作,就一定有对应的[[Put]]操作。 你可能会认为给对象的属性赋值会触发[[Put]]来设置或者创建这个属性。但是实际情况并不完全是这样。
[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容。
1. 属性是否是访问描述符(下一节会给出这个词的解释)?如果是并且存在setter就调用setter。
2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在 严格模式下抛出TypeError异常。
3. 如果都不是,将该值设置为属性的值。 如果对象中不存在这个属性,[[Put]]操作会更加复杂。
3.Getter和Setter
对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。
在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法 应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
当你给一个属性定义getter、setter或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript会忽略它们的value和 writable 特性,取而代之的是关心set和get(还有configurable和enumerable)特性。
思考下面的代码:
var myObject = {
// 给a定义一个getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 给b设置一个getter
get: function(){ return this.a * 2 },
// 确保b会出现在对象的属性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
不管是对象文字语法中的get a() { .. },还是defineProperty(..)中的显式定义,二者 都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数, 它的返回值会被当作属性访问的返回值:
var myObject = {
// 给 a 定义一个getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
由于我们只定义了a的getter,所以对a的值进行设置时set操作会忽略赋值操作,不会抛出错误。而且即便有合法的setter,由于我们自定义的getter只会返回2,所以set操作是没有意义的。
为了让属性更合理,还应当定义setter,和你期望的一样,setter会覆盖单个属性默认的 [[Put]](也被称为赋值)操作。通常来说getter和setter是成对出现的(只定义一个的话 通常会产生意料之外的行为):
var myObject = {
// 给 a 定义一个getter
get a() {
return this._a_;
},
// 给 a 定义一个setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
4.存在性
前面我们介绍过,如myObject.a的属性访问返回值可能是undefined,但是这个值有可能 是属性中存储的undefined,也可能是因为属性不存在所以返回undefined。那么如何区分 这两种情况呢? 我们可以在不访问属性值的情况下判断对象中是否存在这个属性:
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in 操作符会检查属性是否在对象及其[[Prototype]]原型链中。相比之下, hasOwnProperty(..) 只会检查属性是否在myObject 对象中,不会检查[[Prototype]] 链。
所有的普通对象都可以通过对于Object.prototype的委托来访问 hasOwnProperty(..),但是有的对象可能没有连接到Object.prototype(通过 Object. create(null) 来创建)。在这种情况下,形如myObejct.hasOwnProperty(..) 就会失败。
这时可以使用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty. call(myObject,"a"),它借用基础的 hasOwnProperty(..) 方法并把它显式绑定到myObject上。
看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某 个属性名是否存在。对于数组来说这个区别非常重要,4 in [2, 4, 6]的结 果并不是你期待的True,因为[2, 4, 6]这个数组中包含的属性名是0、1、 2,没有4。
1. 枚举
先看代码:
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 让a像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让b不可枚举
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2
可以看到,myObject.b确实存在并且有访问值,但是却不会出现在for..in循环中(尽管 可以通过in操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。
也可以通过另一种方式来区分属性是否可枚举:
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 让a像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让b不可枚举
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足enumerable:true。
Object.keys(..) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。
in 和hasOwnProperty(..) 的区别在于是否查找[[Prototype]] 链,然而,Object.keys(..) 和Object.getOwnPropertyNames(..) 都只会查找对象直接包含的属性。
(目前)并没有内置的方法可以获取in操作符使用的属性列表(对象本身的属性以 及[[Prototype]] 链中的所有属性)。不过你可以递归遍历某个对象的整条 [[Prototype]] 链并保存每一层中使用Object.keys(..)得到的属性列表——只包含可枚举属性。