对象(中)

8/17/2020 js

# 5.属性描述符

在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。

但是从 ES5 开始,所有的属性都具备了属性描述符。

var myObject = { 
    a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
//      value: 2,
//      writable: true,
//      enumerable: true,
//      configurable: true 
// }
1
2
3
4
5
6
7
8
9
10

如你所见,这个普通的对象属性对应的属性描述符(也被称为“数据描述符”,因为它 只保存一个数据值)可不仅仅只是一个 2。它还包含另外三个特性:writable(可写)enumerable(可枚举)configurable(可配置)

在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..) 来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。

var myObject = {};
Object.defineProperty( myObject, "a", { 
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
myObject.a; // 2
1
2
3
4
5
6
7
8

我们使用 defineProperty(..) 给 myObject 添加了一个普通的属性并显式指定了一些特性。 然而,一般来说你不会使用这种方式,除非你想修改属性描述符。

  1. Writable

writable 决定是否可以修改属性的值。

var myObject = {};

Object.defineProperty( myObject, "a", { 
    value: 2,
    writable: false, // 不可写! 
    configurable: true, 
    enumerable: true
});
myObject.a = 3;

myObject.a; // 2 修改失败

"use strict"; // 在严格模式下面是会报错的
// TypeError 错误表示我们无法修改一个不可写的属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

之后我们会介绍 getter 和 setter,不过简单来说,你可以把 writable:false 看作是属性不可改变,相当于你定义了一个空操作setter。严格来说,如果要和 writable:false 一致的话,你的 setter 被调用时应当抛出一个 TypeError 错误。

  1. Configurable

只要属性是可配置的,就可以使用 defineProperty(..) 方法来修改属性描述符:

var myObject = { 
    a: 2
};
myObject.a = 3;
myObject.a; // 3

Object.defineProperty( myObject, "a", { 
    value: 4,
    writable: true,
    configurable: false, // 不可配置!
    enumerable: true 
});

myObject.a; // 4
myObject.a = 5;
myObject.a; // 5

Object.defineProperty( myObject, "a", { 
    value: 6,
    writable: true, 
    configurable: true, 
    enumerable: true
}); // TypeError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

最后一个 defineProperty(..) 会产生一个 TypeError 错误,不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。注意:如你所见,把 configurable 修改成 false 是单向操作,无法撤销!

要注意有一个小小的例外: 即便属性是 configurable:false,我们还是可以把 writable 的状态由 true 改为 false,但是无法由false 改为 true。

除了无法修改,configurable:false 还会禁止删除这个属性:

var myObject = { 
    a:2
};
myObject.a; // 2
delete myObject.a; 
myObject.a; // undefined
Object.defineProperty( myObject, "a", { 
    value: 2,
    writable: true, 
    configurable: false, 
    enumerable: true
});
myObject.a; // 2 
delete myObject.a; 
myObject.a; // 2
//Tip: 属性并不能被删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后一个 delete 语句(静默)失败了,因为属性是不可配置的

在本例中,delete 只用来直接删除对象的(可删除)属性。如果对象的某个属性是某个对象 / 函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象/函数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具(就像 C/C++ 中那 样),它就是一个删除对象属性的操作,仅此而已。

  1. Enumerable

从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for..in 循环。如果把 enumerable 设置成false,这个属性就不会出现在枚举中,虽然仍 然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。

用户定义的所有的普通属性默认都是 enumerable:true,这通常就是你想要的。但是如果你 不希望某些特殊属性出现在枚举中,那就把它设置成 enumerable:false。

# 6.不变性

有时候你会希望属性或者对象是不可改变(无论有意还是无意)的,在 ES5 中可以通过很多种方法来实现.

很重要的一点是,所有的方法创建的都是浅不变性,也就是说,它们只会影响目标对象和 它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的:

myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
1
2
3

假设代码中的 myImmutableObject 已经被创建而且是不可变的,但是为了保护它的内容 myImmutableObject.foo,你还需要使用下面的方法让 foo 也不可变。

在 JavaScript 程序中很少需要深不可变性。有些特殊情况可能需要这样做, 但是根据通用的设计模式,如果你发现需要密封或者冻结所有的对象,那你或许应当退一步,重新思考一下程序的设计,让它能更好地应对对象值的改变。

  1. 对象常量

结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除):

var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    value: 42,
    writable: false,
    configurable: false
});
1
2
3
4
5
6
  1. 禁止扩展 如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions(..):
var myObject = { 
    a: 2
};
Object.preventExtensions( myObject );

myObject.b = 3; 
myObject.b; // undefined
1
2
3
4
5
6
7

在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。

  1. 密封

Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。

所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)

  1. 冻结

Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。

你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..), 然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因 为这样做有可能会在无意中冻结其他(共享)对象

# 7.[[Get]]

属性访问在实现时有一个微妙却非常重要的细节,思考下面的代码:

var myObject = {
    a: 2
};
myObject.a; // 2
1
2
3
4

myObject.a 是一次属性访问,但是这条语句并不仅仅是在 myObjet 中查找名字为 a 的属性, 虽然看起来好像是这样

在语言规范中,myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作(有点像函数调 用:[Get])。对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。

然而,如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要的行为。我们会在第 5 章中介绍这个行为(其实就是遍历可能存在的 [[Prototype]] 链, 也就是原型链)

如果无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined:

var myObject = { 
    a:2
};
myObject.b; // undefined
1
2
3
4

注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的 变量,并不会像对象属性一样返回 undefined,而是会抛出一个 ReferenceError 异常:

var myObject = { 
    a: undefined
};
myObject.a; // undefined 
myObject.b; // undefined
1
2
3
4
5

从返回值的角度来说,这两个引用没有区别——它们都返回了 undefined。然而,尽管乍 看之下没什么区别,实际上底层的 [[Get]] 操作对 myObject.b 进行了更复杂的处理。

由于仅根据返回值无法判断出到底变量的值为 undefined 还是变量不存在,所以 [[Get]] 操作返回了 undefined。不过稍后我们会介绍如何区分这两种情况。

# 8.[[Put]]

既然有可以获取属性值的 [[Get]] 操作,就一定有对应的 [[Put]] 操作。

你可能会认为给对象的属性赋值会触发 [[Put]] 来设置或者创建这个属性。但是实际情况并不完全是这样。

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。

如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。

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

如果对象中不存在这个属性,[[Put]] 操作会更加复杂。我们会在第 5 章讨论 [[Prototype]] 时详细进行介绍。

# 9.Getter和Setter

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。

在语言的未来 / 高级特性中,有可能可以改写整个对象(不仅仅是某个属性)的默认 [[Get]] 和 [[Put]] 操作。这已经超出了本书的讨论范围,但是将来 “你不知道的 JavaScript”系列丛书中有可能会对这个问题进行探讨。

当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述 符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 setget(还有 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

不管是对象文字语法中的get a() { .. },还是defineProperty(..)中的显式定义,二者 都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数, 它的返回值会被当作属性访问的返回值

var myObject = {
    // 给 a 定义一个 getter 
    get a() {
        return 2; 
    }
};
myObject.a = 3;
myObject.a; // 2
1
2
3
4
5
6
7
8

由于我们只定义了 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
1
2
3
4
5
6
7
8
9
10
11
12
上次更新: 2/15/2025, 2:29:28 PM