属性描述符
简介
在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。
但是从 ES5 开始,所有的属性都具备了属性描述符。
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
输出的这个对象被称为属性描述符,我们可以通过Object.defineProperty
方法去添加或者修改属性。
属性描述符有以下属性:
configurable
configurable
默认为true,表示该属性描述符是可修改的,且该描述符对应的属性是可以被删除的,configurable属性的修改是单向操作,当改为false时,就无法被再次修改了。
在严格模式下,去修改或者删除configurable为false的属性或者描述符对象,会产生 TypeError 的错误。
要注意有一个小小的例外:即便属性是 configurable:false,我们还是可以把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。
enumerable
enumerable
默认为true,表示该属性可枚举,比如一个键值对对象的for...in循环中,如果enumerable为false,那么这个属性就不会出现在该循环中,当我们使用Object.keys
去获取key数组时,不可枚举的属性也不会被获取到。
如果你不希望某些特殊属性出现在枚举中,那就把它设置成 enumerable:false
如果判断这个属性是否可枚举呢?
ES5提供了propertyIsEnumerable
方法用来判断属性是否可枚举,它返回一个布尔值,该方法挂载在最顶层的Object
原型中。
var myObject = {};
Object.defineProperty(myObject,
"b", {
enumerable: false, // 让 b 不可枚举
value: 3
}
);
myObject.propertyIsEnumerable("b"); //false
由于该方法存在于原型链中,那就可能会不安全,所以常见做法会通过借用的方式来使用:
Object.prototype.propertyIsEnumerable.call(myObject, "b"); //false
writable
writable
默认为true,表示该属性是可以被修改的。
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
});
myObject.a = 3;
myObject.a; // 2
如你所见,我们对于属性值的修改静默失败(silently failed)了。如果在严格模式下,这种方法会出错:
"use strict";
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
});
myObject.a = 3; // TypeError
TypeError 错误表示我们无法修改一个不可写的属性。
value
value
默认为undefined,是属性对应的值。
还有一些高阶属性,后面再说。
不变性
有时候我们希望属性或者对象是不可改变的,我们可以通过es5的多种方法来实现。
对象常量
结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除)
var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});
禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions()
var myObject = {
a: 2
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
注意,一般来说,不可扩展对象的属性可能仍然可被删除。
Object.preventExtensions()
仅阻止添加自身的属性。但其对象类型的原型依然可以添加新的属性。
该方法使得目标对象的 prototype
不可变;任何重新赋值 prototype
操作都会抛出 TypeError
。这种行为只针对内部的 prototype
属性,目标对象的其它属性将保持可变。
我们还可以通过Object.isExtensible()
方法来判断是否禁止扩展。
密封
Object.seal()
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
冻结
Object.freeze()
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal() 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)
你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。
高阶属性
get
var myObject = {
a: 2
};
myObject.a; // 2
当我们从myObject
对象上获取a
属性时,从代码上看,好像仅仅就是在myObject对象本身上去查找,但是实际上并不是这样的,要知道我们可是有原型链的。
在语言规范中,myObject.a
实际上就是触发了get
操作,有点像是函数调用,对象默认的内置 Get 操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值,如果没有找到,就会通过原型链向上查找,直到找到对应的属性,否则返回undefined。
get没有找到对应的属性,都会返回undefined值。
注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回 undefined,而是会抛出一个 ReferenceError 异常。
我们再看一个代码:
var myObject = {
a: undefined
};
myObject.a; // undefined
myObject.b; // undefined
由于get没有找到会返回undefined,所以从返回值的角度来看,我们无法通过返回值来判断这个属性是否存在。
所以,es5提供了hasOwnProperty
原型方法,存在于对象的原型中,一般都在最顶层的Object
上,所以我们可以通过该方法来进行判断属性是否存在。(使用in方法也是可以的)
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
需要注意的是:in
操作符会检查属性是否在对象及其原型链中,而hasOwnProperty则只会检测属性是否在myObject
对象中,不会去检测原型链。
由于hasOwnProperty
存在于原型链中,那么它就会有不安全性,所以在一些常见的用法上会通过借用的方式来保证结果的准确性。
Object.prototype.hasOwnProperty.call(myObject, "a");
put
既然有get获取属性的操作,就一定会有对应的put操作。
你可能会认为给对象的属性赋值会触发 Put 来设置或者创建这个属性。但是实际情况并不完全是这样。
Put被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
如果已经存在这个属性,Put 算法大致会检查下面这些内容
- 属性是否是有访问描述符?如果是并且存在 setter 就调用 setter。
- 属性的描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值
如果对象中不存在这个属性,Put 操作会更加复杂。后续再说。
上述流程不是单条单条,它是一个连贯的流程。
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(..) 中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。
const myObject = {
name: "a",
get myName() {
return this.name;
},
};
myObject.myName = "b";
console.log(myObject.myName); //a
由于我们只定义了myName
的getter,所以对它的赋值操作会被忽略,不会抛出错误。
为了让属性更加合理,还应当定义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; //
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据