简介

在 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 算法大致会检查下面这些内容

  1. 属性是否是有访问描述符?如果是并且存在 setter 就调用 setter。
  2. 属性的描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值

如果对象中不存在这个属性,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; //
分类: 你不知道的JavaScript 标签: 属性描述符

评论

暂无评论数据

暂无评论数据

目录