JavaScript学习笔记: Object.defineProperty()
前段时间在学习Vue的双向绑定原理及实现时,简单的知道Object.defineProperty()
有很大的用处。这个方法会直接在一个对象上定义一个新属性,或者修改一个对象现有的属性,并返回这个对象。感觉他非常强大,但并不知道其中原委。回过头来重新补一下这方面的基础知识。
对象定义属性和赋值
在对象中,我们有很多种方式给其定义属性和赋值。最常见的是obj.prop = value
和obj['prop'] = value
。比如:
let Person = {}
Person.name = '大漠'
Person['age'] = 35
console.log(Person)
除了上述的方式之外,还可以使用Object.defineProperty()
方法来定义和修改对象属性。下面我们就来好好的探讨一下这个方法。
Object.defineProperty()语法
Object.defineProperty()
的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象,我们先来看一下怎么使用这个方法:
Object.defineProperty(obj, prop, descriptor)
这个方法有三个参数:
obj
:需要被定义(或修改)属性的那个对象prop
:需要被定义(或修改)的属性名descriptor
:定义(或修改)的属性prop
的描述
其返回值是被传递给函数的对象。
该方法允许精确添加或修改对象的属性。一般情况下,为对象添加属性是通过赋值来创建并显示在属性枚举中(for...in
或Object.keys
方法),但这种方式添加的属性值可以被改变,也可以被删除。而使用Object.defineProperty()
则允许改变这些额外细节的默认设置。例如,默认情况下,使用Object.defineProperty()
增加的属性值是不可改变的。
属性特性和内部属性
JavaScript中有三种类型的属性:
- 命名数据属性:拥有一个确定的值的属性。这也是最常见的属性
- 命名访问器属性:通过
getter
和setter
进行读取和赋值的属性 - 内部属性:由JavaScript引擎内部使用的属性,不能通过JavaScript代码直接访问到,不过可以通过一些方法间接的读取和设置。比如,每个对象都有一个内部属性
[[Prototype]]
,你不能直接访问这个属性,但可以通过Object.getPrototypeOf()
方法间接的读取到它的值。虽然内部属性通常用一个双吕括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性名
属性特性
对象中每个属性都有四个特性。两种类型的属性一共有六个属性特性:
- 命名数据属性特有的特性:属性的值的
[[Value]]
特性和控制属性的值是否可以修改的[[Writable]]
特性 - 命名访问器属性特有的特性:存储着
getter
方法的[[Get]]
和存储着setter
方法的[[Set]]
- 两种属性都有的特性:如果一个属性是不可枚举的,则一些操作下,这个属性是不可见的,比如
for...in
和Object.keys
,那么可以通过[[Enumerable]]
特性来设置;如果一个属性是不可配置的,则该属性的所有特性([[Value]]
)都不可改变,那么可以通过[[Configurable]]
特性来设置
内部属性
除了上面所说的之外,下面几个是所有对象都有的内部属性:
[[Prototype]]
:对象的原型[[Extensible]]
:对象是否可扩展,也就是是否可以添加新的属性[[DefineOwnProperty]]
:定义一个属性的内部方法[[Put]]
:为一个属性赋值的内部方法
属性描述符
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。
- 数据描述符:是一个拥有可写或不可写值的属性
- 存取描述符:是由一对
getter
、setter
函数功能来描述的属性
描述符必须是两种形式之一;不能同时是两者。另外,属性描述符可以将一个属性的所有特性编码成一个对象并返回。例如:
Object.defineProperty(obj, 'key', {
enumerable: false,
configurable: false,
writable: false,
value: 'static'
})
属性描述符除了在Object.defineProperty()
中使用之外,还常常使用在Object.getOwnPropertyDescriptor()
和Object.create()
中。如果对象中的某个属性省略了属性描述符,则该属性会取一个默认值:
属性名 | 默认值 |
---|---|
value |
undefined |
get |
undefined |
set |
undefined |
writable |
false |
enumerable |
false |
configurable |
false |
先对属性描述符做一个简单的归纳,因为后面接下来的篇幅都将围绕着Object.defineProperty()
的属性描述符descriptor
来展开的。
数据描述符和存取描述均具有以下可选键值:
configurable
:这个特性决定了对象的属性是否可以被删除,以及除writable
特性外的其它特性是否可以被修改;并且writable
特性值只可以是false
。默认为false
。同样通过示例来帮助我们理解这个描述符的特性:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
configurable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => 大漠
可以看出,虽然执行了delete Person.name
,但name
的属性值并没有删除。这主要是因为,configurable
的值为false
,不允许删除。如果我们把其值设置为true
,但结果就不一样了:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
configurable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => undefined
在上面的示例基础上,咱们再添加writable
的描述符,首先来看writable
为true
的情况:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
configurable: false,
writable: true
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => 大漠
另外来看writable: true
的情况:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
configurable: false,
writable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => 大漠
再来看看另外两种情况:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
configurable: true,
writable: true
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => undefined
// 或
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
configurable: true,
writable: false
})
console.log(Person.name) // => 大漠
delete Person.name
console.log(Person.name) // => undefined
enumerable
:仅当该属性的enumerable
为true
时,该属性才能够出现在对象的枚举属性中。默认为false
。也就是说,当enumerable
的值为true
时,才可以使用for(prop in obj)
和Object.keys()
进行枚举。
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
enumerable: true // 可以枚举
})
Object.defineProperty(Person, 'age', {
value: 35,
enumerable: false // 不可枚举
})
Object.defineProperty(Person, 'title', {
value: '切图仔' // enumerable取默认值,为false
})
Person.from = 'W3cplus' // 如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable为true
for (let i in Person) {
console.log(i) // => name, from
}
Object.keys(Person)
Person.propertyIsEnumerable('name') // => true
Person.propertyIsEnumerable('age') // => false
Person.propertyIsEnumerable('title') // => false
Person.propertyIsEnumerable('from') // => true
数据描述符同时具有以下可选键值:
value
:该属性对应的值。可以是任何有效的JavaScript值(数值、对象、函数等)。默认为undefined
。来看一个小示例:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠'
})
console.log(Person)
从上面的结果中我们可以看到,我们给Person
定义了一个新的属性name
,然后我们打印这个对象就是我们预期的那样,其中对象Person.name
的值为'大漠'
。在上面的基础上,我们来通过普通的obj.name=value
这样的方式重新给对象Person
中的name
属性赋值:
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠'
})
console.log(Person)
Person.name = 'w3cplus_大漠'
console.log(Person)
正如大家所看到的,尽管我们使用Person.name='w3cplus_大漠'
的方式,给对象Person
中的name
属性重新赋值'w3cplus_大漠'
,却发现这个属性name
并没有得到改变,还是以第一次我们赋给它的值。主要原因是,这个属性的writable
默认值为false
,需要将writable
修饰符重置为true
。name
属性才可以被修改。
writable
:仅当该属性的writable
为true
时,该属性的属性值才能被改变。默认为false
。回到上面的示例中来,如果我们想把Person
对象中的name
值修改成我们所期望的属性值,那么就得在Object.defineProperty()
定义name
属性时,就指定该属性的writable
的描述符值为true
。
let Person = {}
Object.defineProperty(Person, 'name', {
value: '大漠',
writable: true
})
console.log(Person)
Person.name = 'w3cplus_大漠'
console.log(Person)
正如你所看到的结果,我们可以重新设置name
的属性值。
存取描述符同时具有以下可选键值:
get
:一个给属性提供getter
的方法,如果没有getter
则为undefined
。该方法返回值被用作属性值。默认为undefined
。
下面这个示例说明了如何实现自我存档的对象。当temperature
属性设置时,archive
数组会得到一个log
。
function Archiver () {
let temperature = null
let archive = []
Object.defineProperty(this, 'temperature', {
get: function () {
console.log('Get!')
return temperature
}
})
this.getArchive = function () {
return archive
}
}
let arc = new Archiver()
arc.temperature
arc.temperature = 11
arc.temperature = 13
arc.getArchive()
输出结果如下:
set
:一个给属性提供setter
的方法,如果没有setter
则为undefined
。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined
。
function Archiver () {
let temperature = null
let archive = []
Object.defineProperty(this, 'temperature', {
get: function () {
console.log('Get!')
return temperature
},
set: function (value) {
temperature = value
archive.push({
val: temperature
})
}
})
this.getArchive = function () {
return archive
}
}
let arc = new Archiver()
arc.temperature
arc.temperature = 11
arc.temperature = 13
arc.getArchive()
记住,这些选项不一定是自身属性,如果是继承来的也要考虑。为了确认保留这些默认值,你可能要在这之前冻结Object.prototype
,明确指定所有的选项,或者将__proto__
属性指向null
。
属性定义和属性赋值
@Dr. Axel Rauschmayer写了一篇文章,详细的阐述了JavaScript中的属性定义和赋值的区别。摘取文章中的内容,简单的看看JavaScript中的属性定义和属性赋值。
属性定义
定义属性是通过内部方法来进行操作的:
[[DefineOwnProperty]](P, Desc, Throw)
P
是要定义的属性名称,参数Throw
决定了在定义操作被拒绝的时候是否要抛出异常。如果Throw
为true
,则抛出异常;否则,操作只会静默失败。当调用[[DefineOwnProperty]]
时,具体会执行下面的操作步骤。
- 如果
this
没有名为P
的自身属性的话:如果this
是可扩展的话,则创建P
这个自身属性,否则拒绝 - 如果
this
已经有了名为P
的自身属性:则按照下面的步骤重新配置这个属性 - 如果这个已有的属性是不可配置的,则进行下面的操作会被拒绝:将一个数据属性转换成访问器属性,反之变然;改变
[[Configurable]]
或[[Enumerable]]
;改变[[Writable]]
;在[[Writable]]
为false
时改变[[Value]]
和改变[[Get]]
或[[Set]]
- 否则,这个已有的属性可以被重新配置
如果Desc
就是P
属性当前的属性描述符,则该定义操作永远不会被拒绝。
定义属性的函数有两个:Object.defineProperty
和Object.defineProperties
。例如:
Object.defineProperty(obj, propName, desc)
在引擎内部,会转换成这样的方法调用:
obj.[[DefineOwnProperty]](propName, desc, true)
属性赋值
为属性赋值是通过内部方法进行操作的:
[[Put]](P, Value, Throw)
参数P
以及Throw
和[[DefineOwnProperty]]
方法中的参数表现的一样。在调用[[Put]]
方法的时候,会执行下面这样的操作步骤:
- 如果在原型链上存在一个名为
P
的只读属性(只读的数据属性或者没有setter
的访问器属性),则拒绝 - 如果在原型链上存在一个名为
P
的且拥有setter
的访问器属性,则调用这个setter
- 如果没有名为
P
的自身属性,则如果这个对象是可扩展的,就使用下面的操作创建一个新属性,否则,如果这个对象是不可扩展的,则拒绝 - 否则,如果已经存在一个可写的名为
P
的自身属性,则调用this.[[DefineOwnProperty]](P, {value: Value}, Throw)
。该操作只会更改P
属性的值,其他的特性(比如可枚举性)都不会改变
赋值运算符=
就是在调用[[Put]]
。比如:
Obj.prop = value
在引擎内部,会转换成这样的方法调用:
Obj.[[Put]]('prop', value, isStrictModeOn)
isStrictModeOn
对应着参数Throw
。也就是说,赋值运算符只有在严格模式下才有可能抛出异常。[[Put]]
没有返回值,但赋值运算符有。
作用及影响
属性的定义操作和赋值操作各自有自己的作用和影响。
赋值可能会调用原型上的setter
,定义会创建一个自身属性。比如,给一个空对象obj
,他的原型proto
有一个名为foo
的访问器属性。
let proto = {
get foo() {
console.log('Getter!')
return 'a'
},
set foo(x) {
console.log(`Setter: ${x}`)
}
}
let obj = Object.create(proto)
console.log(obj)
那么,在obj
身上定义一个foo
属性和为obj
的foo
属性赋值有什么区别呢?
如果是定义操作的话,则会在obj
身上添加一个自身属性foo
:
Object.defineProperty(obj, 'foo', {
value: 'b'
})
console.log(obj.foo)
console.log(proto.foo)
但如果为foo
属性赋值的话,则意味着你想改变某个已经存在的属性的值。所以这次赋值操作会转交给原型proto
的foo
属性的setter
访问器来处理。下面代码的执行结果就能证明这一结论:
你还可以定义一个只读的访问器属性,办法是:只定义一个getter
,省略setter
。下面的例子中,proto
的bar
属性就是这样的只读属性,obj
继承了这个属性。
'use strict';
let proto = {
get bar() {
console.log('Getter!')
return 'a'
}
}
let obj = Object.create(proto)
console.log(obj)
在开启严格模式的话,下面的赋值操作会抛出异常。非严格模式的话,赋值操作只会静默失败(不起任何作用,也不报错)
obj.bar = 'b'
console.log(obj.bar)
我们还可以定义一个自身属性bar
,遮蔽从原型上继承的bar
属性:
Object.defineProperty(obj, 'bar', {
value: 'b'
})
console.log(obj.bar)
console.log(proto.bar)
原型链中的同名只读属性可能会阻止赋值操作,但不会阻止定义操作。如果原型链中存在一个同名的只读属性,则无法通过赋值的方式在原对象上添加这个自身属性,必须使用定义操作才可以。这项限制是在ECMAScript 5.1中引入的:
'use strict'
let proto = Object.defineProperties(
{},
{
foo: { // foo属性的特性
value: 'a', // foo属性的值
writable: false, // 只读
configurable: true // 可配置
}
}
)
let obj = Object.create(proto)
console.log(obj)
赋值操作会导致异常:
obj.foo = 'b'
console.log(obj.foo)
通过定义的方式,我们可以成功创建一个新的自身属性:
Object.defineProperty(obj, 'foo', {
value: 'b'
})
console.log(obj.foo) // => b
console.log(proto.foo) // => a
赋值运算符不会改变原型链上的属性。执行下面的代码,则obj
会从proto
上继承到foo
属性。
let proto = {
foo: 'a'
}
let obj = Object.create(proto)
console.log(obj)
不能通过为obj.foo
赋值来改变proto.foo
的值。这种操作只会在obj
上新建一个自身属性。
obj.foo = 'b'
console.log(obj.foo) // => b
console.log(proto.foo) // => a
只有通过定义操作,才能创建一个拥有指定特性的属性。如果通过赋值操作创建一个自身属性,则该属性始终拥有默认的特性。如果你想指定某个特性的值,必须通过定义操作。
对象字面量中的属性是通过定义操作添加的。下面的代码将变量obj
指向一个对象字面量:
let obj = {
name: '大漠'
}
这样的语句在引擎内部,可能会被转换成下面两种操作方式中的一种。首先可能是赋值操作:
let obj = new Object()
obj.name = '大漠'
其次,可能是个定义操作:
let obj = new Object()
Object.defineProperties(obj, {
name: {
value: '大漠',
enumerable: true,
configurable: true,
writable: true
}
})
到底是哪种呢?正确答案是第二种。因为第二种操作方式能够更好的表达出对象字面量的语义:创建新的属性Object.create()
接受一个属性描述符作为第二个可选参数,也是这个原因。
可以通过定义操作新建一个只读的方法属性:
'use strict'
function Stack() {
}
Object.defineProperties(Stack.prototype, {
push: {
writable: false,
configurable: true,
value: function (x) {
console.log(x)
}
}
})
目的是为了防止在实例身上发生意外的赋值操作。
let s = new Stack()
s.push = 5
不过,由于push
是可配置的,所以我们仍可以通过定义操作来为实例添加一个自身的push
方法。
let s = new Stack()
Object.defineProperty(s, 'push', {
value: function () {
return 'yes'
}
})
console.log(s.push()) // => yes
我们甚至可以通过定义操作来重新定义原型上的push
方法:Stack.prototype.push
。
添加多个属性和默认值
考虑特性被赋予的默认特性值非常重要,通常,使用点运算符和Object.defineProperty()
为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示。
let obj = {}
obj.name = '大漠'
上面的代码等同于:
Object.defineProperty(obj, 'name', {
value: '大漠',
writable: true,
configurable: true,
enumerable: true
})
另一主面:
Object.defineProperty(obj, 'name', {
value: '大漠'
})
等同于:
Object.defineProperty(obj, 'name', {
value: '大漠',
writable: false,
configurable: false,
enumerable: false
})
总结
这篇文章主要介绍了Object.defineProperty(obj, prop,descriptor)
。我想大家和我一样,对这个方法应该有了一定的了解。有了这个基础,咱们回过头来理解《学习Vue的双向绑定原理及实现》一文中提到的Vue的双向数据绑定的原理就变得容易一些了。而且这个属性也能更好的帮助我们后面理解Vue响应式视图,或者说Vue中的计算属性的奥秘。
由于作者也是这方面的初学者,如果文章中有不对请各路大婶拍正。如果你有更好的经验,欢迎在下面的评论中与我们一起分享。
如需转载,烦请注明出处:https://www.fedev.cn/javascript/object-defineproperty.htmlAir Max 1 Mid Liberty Max Kicks