Object.defineProperty

概述

Object.defineProperty() 方法用于在指定对象上定义一个新属性,或者修改一个对象的现有属性,并 返回该对象 。( 原地更改对象

语法

Object.defineProperty(obj, prop, descriptor)

  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:一个包含属性描述符的对象,决定属性的特征。

属性描述符descriptor

属性描述符是一个对象,包含以下可选键:

  • value:属性的值。
  • writable:如果为 true,属性的值可以被赋值操作改变。
  • get:一个函数,当属性被读取时调用,函数返回值就是属性的值。
  • set:一个函数,当属性被赋值时调用,接收赋值操作传入的值。
  • configurable:如果为 true,属性可以被删除,getset 访问器可以被修改。
  • enumerable:如果为 true,表示可以遍历,属性出现在 for...in 循环中。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const obj = {
a:1,
b:2,
c:{
a:1,
b:2
}
}

let v = obj.a
// 在外面获取a的初始值,是为了防止在get(a)时return了obj[a],导致死循环
Object.defineProperty(obj, 'a', {
get(){
console.log('获取了');
return v
},
set(value){
if(value !== v){
console.log('设置了');
v = value
}
}
})

console.log(obj.a) // 获取了 1
obj.a = 3 // 设置了
console.log(obj.a) // 获取了 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const obj = {
a: 1,
b: 2,
c: {
a: 1,
b: 2
}
}

function isObject(obj) {
return typeof obj === 'object' && obj !== null
}

// 深度劫持
function observe(obj) {
if (isObject(obj)) {
for (let key in obj) {
let v = obj[key] // 避免在get时return obj[key]导致死循环
if (isObject(v)) {
observe(v) // 递归进行深度劫持
}
Object.defineProperty(obj, key, {
get() {
console.log('获取了');
return v
},
set(value) {
if (value !== v) {
console.log('设置了');
v = value
}
}
})
}

}
}

observe(obj)
console.log(obj.a); // 获取了 1
console.log(obj.c.a); // 获取了 获取了 1

注意事项

  • get set 访问器中 this 关键字指向定义属性的上下文,通常是一个属性描述符对象,而不是目标对象。
  • 属性必须是 configurable: true 才能被 Object.defineProperty() 修改或使用 delete 删除

Proxy

概述

Proxy 对象用于创建一个对象的代理,从而在访问对象前可以进行某种操作。

语法

new Proxy(target, handler)

  • target:一个对象,它是代理对象的原始对象。
  • handler:一个对象,其属性是当操作代理对象时定义代理的行为的函数。

处理程序(Handler)

处理程序对象可以定义以下11个方法:

注意:receiver 参数是 Reflect.get 方法的第三个可选参数,它的作用是:

  • 当你访问的对象属性实际上是通过原型链从另一个对象继承的访问器属性(即有一个 getter 函数)时,**receiver 参数指定了调用这个 getter 函数时 this 应该指向的对象。**

在大多数简单的情况下,你可能不需要使用 receiver 参数,因为默认情况下,访问器属性的 getter 会在访问它的那个对象上被调用。

但是,如果你需要确保在访问继承的访问器属性时,this 绑定到特定的对象上,那么 receiver 参数就非常有用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const parent = {
_prop: 'value on parent',
get prop() {
console.log('get prop', this._prop);
return this._prop;
}
};

const child = Object.create(parent);
child._prop = 'value on child';

// 使用 Reflect.get 访问 child 对象的 prop 属性,确保 getter 调用时 this 指向 child
const value = Reflect.get(child, 'prop', child);
console.log(value); // "get prop value on child"
  • get(target, prop, receiver):获取属性时调用。
  • set(target, prop, value, receiver):设置属性时调用。
  • has(target, prop):检查属性是否存在时调用。
  • deleteProperty(target, prop):删除属性时调用。
  • ownKeys(target):获取对象所有键时调用。
  • getOwnPropertyDescriptor(target, prop):获取属性描述符时调用。
  • defineProperty(target, prop, descriptor):定义属性时调用。
  • preventExtensions(target):阻止对象扩展时调用。
  • getPrototypeOf(target):获取对象原型时调用。
  • isExtensible(target):检查对象是否可扩展时调用。
  • setPrototypeOf(target, proto):设置对象原型时调用。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const obj = {
a: 1,
b: 2,
c: {
a: 1,
b: 2
}
}

const proxy = new Proxy(obj, {
get(target, key) {
let temp = Reflect.get(target, key)
console.log(`正在读取${key}值,为:`);
return temp
},
set(target, key, value) {
console.log(`正在设置${key}`);
if (value !== Reflect.get(target, key)) {
Reflect.set(target, key, value);
}
},
deleteProperty(target, key){
console.log(`删除属性了${key}`);
}
})

console.log(proxy.a); // 正在读取a值,为: 1
proxy.a = 10 // 正在设置a
console.log(proxy.a); // 正在读取a值,为: 10
delete proxy.a // 删除属性了a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const obj = {
a: 1,
b: 2,
c: {
a: 1,
b: 2
}
}

function isObject(obj) {
return typeof obj === 'object' && obj !== null
}

function observe(obj){
const proxy = new Proxy(obj, {
get(target, key) {
let temp = Reflect.get(target, key)
if(isObject(temp)){
temp = observe(temp) // 只有在访问到对象的时候才会递归
}
console.log(`正在读取${key}值,为:`);
return temp
},
set(target, key, value) {
console.log(`正在设置${key}`);
if (value !== Reflect.get(target, key)) {
Reflect.set(target, key, value);
}
},
deleteProperty(target, key){
console.log(`删除属性了${key}`);
}
})
return proxy
}

let p = observe(obj)
console.log(p.a); // 正在读取a值,为: 1
p.a = 12 // 正在设置a
console.log(p.a); // 正在读取a值,为: 12
console.log(p.c.a); // 正在读取c值,为: 正在读取a值,为: 1
p.c.a = 13 // 正在读取c值,为: 正在设置a
console.log(p.c.a); // 正在读取c值,为: 正在读取a值,为: 13

注意事项

  • 代理对象的 deletedeletePropertyhassetdefineProperty 操作可以拦截对象的相应操作。
  • 代理可以创建一个对象的“安全”版本,隐藏内部的实现细节。
  • 代理可以用于懒加载或延迟初始化对象的属性

对比 Object.definePropertyProxy

  • Object.defineProperty 用于在对象上定义或修改单个属性
  • Proxy 可以用于整个对象,为对象的所有操作提供自定义行为。
  • Object.defineProperty实现对象的深度监听,需要一次性递归到底。对于层级比较深的数据来说,计算量比较大。
  • Object.defineProperty无法监听新增属性/删除属性, 因为监听在created之前就完成了。(但是vue2.0提供了另外的api,分别是Vue.set和Vue.delete
  • Proxy 可以监听新增属性/删除属性等。
  • 使用 Object.defineProperty 可以修改对象的现有属性(就地更改),而 Proxy 允许你从头开始捕获和自定义任何操作。