本文详细解析 JavaScript 中 Object.defineProperty() 方法,涵盖数据描述符与访问器属性的定义、configurable 与 enumerable 等属性的行为控制,以及通过 Object.preventExtensions、Object.seal 和 Object.freeze 实现对象不可性的具体机制,并对比属性定义与赋值的底层差异。
前言
本文内容将依据以下结构展开讲解:

对象的定义与赋值
开发者通常使用obj.prop =value或obj['prop']=value这两种常见方式来定义和赋值对象属性。

Object.defineProperty()语法说明
Object.defineProperty()的主要功能是在对象上直接定义一个新属性,或者修改一个已存在的属性。
Object.defineProperty(obj, prop, desc)- obj 代表需要定义属性的目标对象。
- prop 表示当前需要定义的属性名称。
- desc 为属性描述符,用于指定属性的具体行为。
通常情况下,通过直接赋值方式设置的属性可以自由修改或删除;而使用 Object.defineProperty() 定义属性时,可以通过配置描述符对属性的行为进行更精细的控制。
属性的特性以及内部属性
JavaScript 中存在三种类型的属性:
- 命名数据属性:拥有确定值的属性,这是最常见的属性类型。
- 命名访问器属性:通过
getter和setter函数进行读取和赋值的属性。 - 内部属性:由 JavaScript 引擎内部使用的属性,无法通过 JavaScript 代码直接访问,但可通过特定方法间接读取或设置。例如,每个对象都包含一个内部属性
[[Prototype]],虽然不能直接访问,但可通过Object.getPrototypeOf()方法间接获取其值。尽管内部属性通常用双括号包围的名称表示,但这并非其真实名称,而是一种抽象操作概念,不可见且不具备字符串类型属性的特征。
属性描述符
使用 Object.defineProperty() 定义属性时,存在两种互斥的形式:数据描述符和存取描述符,二者不可混合使用。以下分别阐述两者的区别:
数据描述符 --特有的两个属性(value,writable)
let Person = {}
Object.defineProperty(Person, 'name', {
value: 'jack',
writable: true // 是否可以改变
})

需要注意的是,如果描述符中省略了某些属性,系统将应用以下默认规则:

存取描述符 --是由一对 getter、setter 函数功能来描述的属性
get:一个提供getter功能的函数,若未指定getter则默认为undefined。该函数的返回值将作为属性值使用,默认值为undefined。
set:一个提供setter功能的函数,若未指定setter则默认为undefined。该函数接收唯一参数,并将该新值分配给属性,默认值为undefined。
let Person = {}
let temp = null
Object.defineProperty(Person, 'name', {
get: function () {
return temp
},
set: function (val) {
temp = val
}
})

数据描述符和存取描述均具有以下描述符
- configrable 描述属性是否可配置,以及是否可以被删除。
- enumerable 描述属性是否会出现在 for in 循环或 Object.keys() 的遍历结果中。
configrable 代码片段分析
当 configurable 为 false 时,无法删除该属性:

当 configurable 为 false 时,无法重新定义该属性:

上述代码等价于以下写法:

与前述代码进行对比:

当 configurable 为 true 时,可以删除该属性:

当 configurable 为 true 时,可以重新定义该属性:

将 configurable 设为 false 与上图进行对照:

根据上述代码运行结果分析总结可知:
当 configurable 为 false 时,无法删除当前属性,且不能重新配置当前属性的描述符(存在一个例外:可以将 writable 的状态由 true 改为 false,但无法由 false 改为 true);但在 writable 为 true 的情况下,可以修改 value 的值。
当 configurable 为 true 时,可以删除当前属性,也可以配置当前属性的所有描述符。
enumerable 代码片段分析

注意:以下两种情况存在区别


不变性
对象常量
结合 writable 为 false 和 configurable 为 false,可以创建一个真正的常量属性(即不可修改、不可重新定义且不可删除)。

对象常量
禁止扩展:
若希望禁止向对象添加新属性并保留现有属性,可使用 Object.preventExtensions(...) 方法。

禁止扩展片段1

禁止扩展片段2
在非严格模式下,尝试创建属性 gender 会静默失败;而在严格模式下,将会抛出异常。
密封
Object.seal() 会创建一个密封对象,该方法实际上是在现有对象上调用 object.preventExtensions(...),并将所有现有属性标记为 configurable 为 false。

密封:因此,密封后不仅无法添加新属性,也不能重新配置或删除任何现有属性(尽管仍可修改属性的值)。
冻结:Object.freeze() 会创建一个冻结对象,该方法实际上是在现有对象上调用 Object.seal(),并将所有现有属性标记为 writable 为 false,从而禁止修改其值。

冻结:
这是在对象上应用最高级别的不可变性,它禁止对对象本身及其任意直接属性进行任何修改(但对象引用的其他对象不受影响)。可以深度冻结一个对象,具体做法是首先对该对象调用 Object.freeze(),然后遍历其引用的所有对象并在这些对象上调用 Object.freeze()。但需格外小心,因为这可能会无意中冻结其他共享对象。
属性定义和属性赋值
最后一小节,对上述内容进行总结。
属性定义,通过Object.defineProperty()形式
- 如果 Obj 没有名为 Prop 的自身属性:若 Obj 是可扩展的,则创建 Prop 这个自身属性;否则拒绝操作。
- 如果 Obj 已经有了名为 Prop 的自身属性:则按照以下步骤重新配置该属性
- 如果这个已有的属性是不可配置的,则进行下面的操作会被拒绝
- 1: 将一个数据属性转换成访问器属性,反之亦然
- 2: 改变`[[Configurable]]`或`[[Enumerable]]`
- 3: 改变[[Writable]]由false变为true
- 4: 在`[[Writable]]`为`false`时改变`[[Value]]`
- 5: 改变[[Get]]或[[Set]]
- 否则这个已有的属性可以被重新配置
属性赋值,通过obj.prop = ''prop"形式
- 如果在原型链上存在一个名为
P的只读属性(只读的数据属性或者没有setter的访问器属性),则拒绝赋值。 - 如果在原型链上存在一个名为
P的且拥有setter的访问器属性,则调用这个setter。 - 如果没有名为
P的自身属性,则如果这个对象是可扩展的,就创建一个新属性;否则,如果这个对象是不可扩展的,则拒绝操作。 - 如果已经存在一个可写的名为 P 的自身属性,则调用 Object.defineProperty(),该操作只会更改
P属性的值,其他特性(比如可枚举性)都不会改变。
作用以及影响
属性的定义操作和赋值操作各自具有不同的作用和影响。
赋值可能会调用原型上的setter,而定义会创建一个自身属性。
原型链中的同名只读属性可能会阻止赋值操作,但不会阻止定义操作。如果原型链中存在一个同名的只读属性,则无法通过赋值的方式在原对象上添加这个自身属性,必须使用定义操作才可以。这项限制是在 ECMAScript 5.1 中引入的。




赋值运算符不会改变原型链上的属性
不能通过为obj.foo赋值来改变proto.foo的值。这种操作只会在obj上新建一个自身属性。

对象字面量中的属性是通过定义操作添加的。

再次强调,请牢记以下两种形式的区别:

上面的代码等同于:

另一方面:

上面的代码等同于:

到此这篇关于深入浅出JS的Object.defineProperty()的文章就介绍到这了,更多相关JS的Object.defineProperty()内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!