Object.defineProperty和Proxy对比

Object.defineProperty

Object.defineProperty 会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

先看一个demo:

1
2
3
4
5
6
7
8
9
10
11
12
const object = {}

Object.defineProperty(object, 'property1', {
value: 20,
writable: false // 定义该属性不可改
})

console.log(object)
// { property1: 20 }

object.property1 = 30
// { property1: 20 }

语法

1
Object.defineProperty(obj, prop, descriptor)

参数

  • obj
    要定义的对象
  • prop
    要定义或修改的属性名称(或Symbol)
  • descriptor
    要定义或修改的属性描述符(数据描述符、存取描述符)

    数据描述符包含:

    • configurable(false)
      true 时,该属性的描述符才能够被改变,也能删除
    • enumerable(false)
      true 时,该属性才会出现在对象的枚举属性中(被 for...inObject.keys 访问)
    • value(undefined)
      该属性对应的值
    • writable(false)
      true 时,才可改

    存取描述符包含:

    • get(undefined)
      属性的 getter 函数,当访问该属性时,会调用此函数。
    • set(undefined)
      属性的 setter 函数,当属性值被修改时,会调用此函数。

注意:

  1. 如果一个描述符不具有 valuewritablegetset 中的任意一个键,它会被认为是一个数据描述符。
  2. 如果一个描述符同时拥有 valuewritablegetset 键,它会抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var o = {};
o.a = 1;
// 等同于:
Object.defineProperty(o, "a", {
value: 1,
writable: true,
configurable: true,
enumerable: true
});

Object.defineProperty(0, "a", { value: 1 })
// 等同于
Object.defineProperty(o, "a", {
value: 1,
writable: false,
configurable: false,
enumerable: false
});

自定义 SettersGetters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Archiver() {
var temp = null
var archive = []

Object.defineProperty(this, 'temp', {
get () {
console.log('get')
return temp
},
set (value) {
temp = value
archive.push({ val: temp })
}
})
this.getArchive = function () {
return archive
}
}

var arc = new Archiver()
arc.temp // 'get', null
arc.temp = 11
arc.temp = 13
arc.getArchive() // [{val: 11}, {val: 13}]

继承属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function MyClass() {}

var value;
Object.defineProperty(MyClass.prototype, 'x', {
get () {
return value
},
set (newValue) {
value = newValue
}
})

var a = new MyClass()
var b = new MyClass()
a.x = 1 // a.__proto__ 和 MyClass.prototype 相等

console.log(a.x, b.x) // 1 1

可以通过将值存储在另一个属性中解决。在 getset 方法中,this 指向某个被访问和修改属性的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function MyClass() {}

Object.defineProperty(MyClass.prototype, 'x', {
get () {
return this.stored_x
},
set (newValue) {
this.stored_x = newValue
}
})

var a = new MyClass()
var b = new MyClass()
a.x = 1

console.log(a.x, b.x) // 1 undefined

// this指向MyClass.prototype对象

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义。

语法

1
const p = new Proxy(target, handler)

参数

  • target
    要使用 Proxy 包装的目标对象,可以是对象、数组、函数,甚至是另外一个代理
  • handler
    一个通常以函数为属性的对象

基础示例

1
2
3
4
5
6
7
8
9
10
const p = new Proxy({}, {
get (obj, prop) {
return prop in obj ? obj[prop] : 30 // 存在即返回,不存在就返回30
}
})
p.a = 1
p.b = undefined

console.log(p.a, p.b) // 1, undefined
console.log('c' in p, p.c) // false, 30

无操作转发代理

1
2
3
4
5
const target = {}
const p = new Proxy(target, {})

p.a = 30 // p代理会将所有的操作都转发到这个对象上
console.log(p.a, target.a) // 30, 30

vue2.x

在 vue2.x 中,双向绑定是通过监听对象的get和set操作来实现的。即是 Object.defineProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
a: 1
}

let val = 1
let newVal = 2
Object.defineProperty(obj, 'a', {
get () {
console.log('数据被获取')
return val
},
set (newVal) {
console.log('数据被修改/设置')
val = newVal
}
})

每当我们获取 obj 的 a 属性或设置 a 属性时,都会被劫持。在 vue2.x 中正是采用这种监听方式使用发布-订阅模式,data 上的数据是发布方,而试图的修改是订阅方,每当数据发生修改时,就会发布信息,而订阅方接受到相应的信息,就会对视图进行修改。

下面简单模拟一下发布-订阅的流程:

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
44
45
46
47
const publisher = function () {
return {
currentList: [],
add (subscriber) {
this.currentList.push(subscriber)
console.log('添加订阅', this.currentList) // [{ receiveText(){} }, ..]
},
publish (key, content) {
const currentList = this.currentList
console.log('订阅集合', currentList)
// 遍历订阅者的数组
for (let i = 0; i < currentList.length; i++) {
currentList[i].receiveText(key, content)
}
}
}
}

const subscriber = function (name) {
return {
receiveText (key, content) {
console.log(`视图${key}已更新,相应的内容为${content}`)
}
}
}

// 将data作为发布方
const data = publisher()
// 将视图修改作为订阅方
const change = subscriber()
// 让视图修改订阅data的更新
data.add(change)
data._a = 1

Object.defineProperty(data, 'a', {
get () {
return data._a
},
set (newVal) {
data.publish('a', newVal)
data._a = newVal
}
})

// test
data.a // 1 返回data._a的数据
data.a = 10 //

vue3.0

虽然 Object.defineProperty 可以实现双向绑定的效果,但是在 Proxy 出现后,它就被替代掉了。
Object.defineProperty 只能一个属性一个属性的监听,也就是说,对于 data 对象,我们需要进行深度遍历,去监听每一个属性的变化,而一旦我们对data一个比较深的对象直接修改它的值,又得对其进行重新的遍历,非常损耗性能。而 Proxy 可以监听一整个对象,且基于 Proxy 的监听,只有当一个数据被用到的时候,才会去监听它,所以它在监听上大大降低了性能的损耗。

1
2
3
4
5
6
7
8
9
10
const p = new Proxy({}, {
get (target, property, receiver) {
console.log(`getting ${key}`)
return Reflect.get(target, property, receiver)
},
set (target, property, value, receiver) {
console.log(`setting ${key}`)
return Reflect.set(target, property, value, receiver)
}
})

模拟发布-订阅

1
var publisher