Object.defineProperty 是 Js 中非常重要的一个属性,因为它是 Vue2 框架 实现响应式操作的底层原理, 它允许 精准地添加或修改 对象上的属性。
-
Object.defineproperty ()方法 接收3 个参数 -
第一个参数为
要操作的对象 -
第二个参数为
一个字符串 或 Symbol, 必须是这个对象的键名 -
第三个参数为
属性描述符 ( descriptor ), 也是最重要的参数
(1) 基本使用
const obj = {
name: 'Lily'
}
// 给对象添加 age 属性
Object.defineProperty (obj, 'age',{
value: 18
})
console.log (obj) // { name: 'Lily',age: 18 }
// 修改对象的属性
Object.defineProperty (obj, 'name',{
value: "Maria"
})
console.log (obj) // { name: 'Maria',age: 18 }
(2) 属性描述符 (descriptor)
Object.defineProperty () 的 属性描述符 一共有 6 种, 分别分 configurable 、 enumerable 、value 、 weitable 、 get 、set 。
它的基本使用就是通过 value 去添加或修改对象的属性值。
configurable
configurable 用于控制 是否能修改 当前对象的属性描述符, 默认为
const obj = {}
Object.defineProperty(obj, 'name', {
value: 'Lee'
})
console.log(obj) // {name: 'Lee'}
// 下面的方法都会报错
Object.defineProperty(obj, 'name', {
value: 'coder'
})
Object.defineProperty(obj, 'name', {
enumerable: true
})
Object.defineProperty(obj, 'name', {
configurable: true
})
Object.defineProperty(obj, 'name', {
writable: true
})
Object.defineProperty(obj, 'name', {
get: function() {
return this.a
}
})
Object.defineProperty(obj, 'name', {
set: function(value) {
this.a = value
}
})
-
它的默认值为 , 说明默认状态下是
不可以修改属性描述符的 -
即使是
只把configurable属性设置为, 也是不可以的, 这说明, 如果我们想要操作属性描述符必须在添加数据时就将configurable属性设置为。
enumerable
enumerable 可以控制当前 键名 是否 作为对象的可枚举属性( 是否能被 for… in/ Object.keys()/ 展开运算符等遍历出来 ), 默认值为 。
const obj = {}
Object.defineProperty(obj, 'name', {
value: 'Lily',
configurable: true
})
console.log(obj) // {name: 'Lily'}
for (let key in obj) {
// 空,因为此时name属性不是可枚举的
console.log(key)
}
Object.defineProperty(obj, 'name', {
// 将name属性变为可枚举属性
enumerable: true
})
// 可以被for...in遍历
for (let key in obj) {
console.log(key)
}
writable
writable 可以控制当前属性是否 可写 ( 即对该属性进行赋值操作 ), 默认为 , 该属性只可读不可写。
const obj = {}
Object.defineProperty(obj, 'name', {
value: 'Lily',
configurable: true
})
obj.name = 'Maria'
console.log(obj.name) // Lily,此时name是不可写的,因此进行赋值操作无效
Object.defineProperty(obj, 'name', {
// 设置name属性为可写属性
writable: true
})
obj.name = 'Maria'
console.log(obj.name) // Maria
get
get 是属性描述符中 最重要的两个属性之一, 它是用作 getter属性 的 一个函数, 默认值为 , 访问 被操作的属性时, 会触发 get函数 ,并且将 This 设置为操作对象( 当然,这是 get 函数没有使用箭头函数的前提下 ) , get 函数的 返回值 作为 该属性的值。
const obj = {
objName: 'obj'
}
Object.defineProperty(obj, 'name', {
configurable: true
})
Object.defineProperty(obj, 'name', {
get: function () {
console.log(this) // {objName: 'obj'}
return '我不叫Lucy'
}
})
console.log(obj.name) // '我不叫Lucy'
明明我们没有设置name的value属性,但是访问时,却变成了get函数的返回值, get 函数的作用就是如此,我们一旦设置了 get 属性描述符,get函数的返回值就是我们访问该属性的值(无论我们将它赋值成什么)。
set
set 是属性描述符中另一个重要的属性,它和get类似,只不过它是作用于setter属性 的一个函数,默认值也是 ,当我们给属性进行赋值操作时,set函数会被触发,它会接收一个参数,这个参数就是我们要进行赋值操作的值。
const obj = {
objName: 'obj'
}
Object.defineProperty(obj, 'name', {
configurable: true
})
Object.defineProperty(obj, 'name', {
get: function () {
return this.copyName
}
})
Object.defineProperty(obj, 'name', {
set: function (value) {
console.log(this)
this.copyName = value
}
})
obj.name = 1
console.log(obj.name)
当我们对obj.name进行赋值操作时,打印出来了this对象,和get函数一样,this指向当前操作的对象,我们可以通过监听给对象某个属性赋值,去改变对象中其他值,然后通过get函数被外界访问,从而替代value和writable描述符。
(3)数据描述符和访问器描述符指的是什么
-
Object.defineProperty() 的第三个参数为
属性描述符,属性描述符可分为数据描述符和访问器描述符。 -
访问器描述符只有get和set,其它四个为数据描述符。 -
当描述符
不具备value、writable、get、set中的任何一个(因为value和writable时可选的,描述符只有configurable和enumerable属性时),它一定就是数据描述符。 -
描述符
不可以同时拥有(value或writable)和(get或set)(也就是说value不能和get、set同时出现,其它同理),否则会报错;
const obj = {
objName: 'obj'
}
Object.defineProperty(obj, 'name', {
value: 'Lee',
configurable: true,
get: function (value) {
return this.objName
}
})
// 无效的属性描述符。不能同时指定访问器和值或可写属性,
Object.defineProperties()
- Object.defineProperties() 方法其实就是多个 Object.defineProperty() 的简便写法;
- Object.defineProperty() 只允许我们对某个对象的
某一个属性进行操作,而 Object.defineProperties() 允许我们传入一个对象,对该对象的所有属性分别进行操作。
const obj = {}
Object.defineProperties(obj, {
name: {
value: 'Lee'
},
age: {
value: 18
}
})
console.log(obj) // {name: 'Lee', age: 18}
Object.defineProperty()对数组对象的监听
const obj = {}
Object.defineProperty(obj, 'arr', {
configurable: true,
get: function () {
return this.copyArr
},
set: function (value) {
console.log(value)
this.copyArr = value
}
})
obj.arr = [1] // [1]
obj.arr.push(2) // 空
obj.arr = [1, 3] // [1, 3]
obj.arr.splice(0, 1) // 空
由此可见,当我们使用Object.defineProperty() 的访问器描述符监听数组时,数组的赋值我们是可以正确监听并处理的,但是调用数组原型上的一些方法(如pop、push、shift等)时,set属性描述符是无法监听到的,这也是为什么在vue2中,vue内部重写了部分数组原型的方法。
Object.defineProperty()的作用和意义
Object.defineProperty() 可以让我们更加灵活的去操作一个对象,可以限制对象的属性不被外部访问或修改,让一些属性可以变得更加安全,更加不容易被污染,于此同时,我们还可以通过其中的访问器描述符去拦截赋值和读取操作,并按照开发者的意愿对拦截之后的结果进行一些灵活的处理,能够完成更多复杂的操作。
Proxy
Proxy 对象用于创建一个对象(对象、数组、函数、甚至另一个代理对象)的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
Proxy 一共有两个参数。
-
target:是proxy的第一个参数,代表被代理的对象。 -
handler:一个通常以函数作为属性的对象,里面的每个属性分别代表了各种行为,目前handler一共有13个属性。
1. handler.apply()
apply 被用于函数劫持,该函数一共有三个参数。
target:被代理的对象;thisArg:被调用时的上下文对象(this);argumentsList:被调用时的传参列表;
function myFun(name) {
console.log(name)
}
const myFunProxy = new Proxy(myFun, {
apply(target, thisArg, argArray) {
console.log(target === myFun) // true
console.log(thisArg) // {name: 'obj1', fun: Proxy(Function)}
console.log(argArray) // ['Lee', 'Lee1']
console.log(argArray[0] + 'proxy') // Leeproxy
}
})
const obj1 = {
name: 'obj1',
fun: myFunProxy
}
obj1.fun('Lee', 'Lee1')
从代码得知,使用Proxy 代理myFun函数时,会返回一个新的函数,调用这个新的函数时,会被handler种的apply属性拦截,并将原函数、调用代理函数时的this指向、调用代理函数时的传参通过参数的形式放入apply函数中。
2. handler.construct()
construct 用于拦截new操作符,因此target必须是可以使用new操作符的,也就是说target最好是一个构造函数或者类,它一共有两个参数。
target:被代理的对象。argumentsList:调用代理对象时传入的参数列表。
function MyFun(name) {
this.name = name
}
const MyFunProxy = new Proxy(MyFun, {
construct(target, argArray) {
console.log(target === MyFun)
console.log('在执行构造函数之前做了一些操作')
return new target(...argArray)
}
})
const obj = new MyFunProxy('Lee')
console.log(obj)
// output: true -> 在执行构造函数之前做了一些操作 -> {name: 'Lee'}
construct的作用就是可以拦截我们的new 操作符,让我们在执行new之前,做一些其它的操作。
3. handler.defineProperty()
defineProperty 用于拦截对象的defineProperty操作,它的返回值必须是一个Boolean类型,用来表示是否成功操作代理对象。该方法一共有三个参数,三个参数和Object.defineProperty()的三个参数相同。
target:Object.defineProperty()中代表要操作的对象,而这里代表被代理的那个对象。property:对象的键名。descriptor:属性描述符。
const obj = {}
const objProxy = new Proxy(obj, {
defineProperty(target, key, descriptor) {
console.log(target === obj)
console.log(`对代理对象的${key}操作了`)
return Object.defineProperty(target, 'name', descriptor)
}
})
Object.defineProperty(objProxy, 'name', {
value: 'Lee',
configurable: true,
enumerable: true
})
console.log(obj)
// output: true -> 对代理对象的name操作了 -> {name: 'Lee'}
4. handler.deleteProperty()
deleteProperty 用于拦截对象的delete操作,它有两个参数:
target:被代理的对象。property:待删除的属性名。
const obj = {
name: 'Lee',
age: 18
}
const objProxy = new Proxy(obj, {
deleteProperty(target, key) {
console.log(target === obj)
console.log(`要删除对象的${key}了`)
return delete target[key]
}
})
delete objProxy.age
console.log(obj)
// output: true -> 要删除对象的age了 -> {name: 'Lee'}
5. has()
has 用于拦截in操作符,它有两个参数:
target:被代理的对象。property:in操作符之前的键名。
const obj = {
name: 'Lee'
}
const objProxy = new Proxy(obj, {
has(target, key) {
console.log(target === obj)
console.log(`判断key值:${key}是否为对象的属性`)
return key in target
}
})
console.log('name' in objProxy)
// output: true -> 判断key值:name是否为对象的属性 -> true
6. get()
get 用于拦截访问操作,对代理对象进行访问时,会触发get,它有三个参数:
target:被代理的对象。property:访问的键名。receiver:Proxy或继承Proxy的对象。
它有以下约束,否则会抛出异常:
- 如果要访问的目标属性是
不可写以及不可配置的,则返回的值必须与该目标属性的值相同。 - 如果要访问的目标属性没有配置访问方法,
即 get 方法是 undefined 的,则返回值必须为 。
const obj = {}
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
console.log(target === obj)
console.log(receiver === objProxy)
return 1
}
})
objProxy.name = 'Lee'
console.log(objProxy.name)
// output: true -> true -> 1
我们发现 Proxy 中的handler.get方法和 Object.defineProperty()中的 get 属性描述符作用相似,都是可以拦截访问操作,并以get方法最终的返回值作为访问的值。
约束:
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: false,
enumerable: false,
writable: false,
value: 'Lee'
})
// const objProxy = new Proxy(obj, {
// get(target, key, receiver) {
// return 1
// }
// })
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
return target[key]
}
})
console.log(objProxy.name)
// 第一种情况:Uncaught TypeError: 'get' on proxy: property 'name' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'Lee' but got '1')
// 第二种情况:Lee
7. set()
set 用于 拦截 对象或对象原型链上某个属性的赋值操作,它需要返回一个Boolean类型的否返回值,用于代表是否赋值成功。它有四个参数:
target:被代理的对象。property:被赋值的键名。value:将被赋值的新属性值。receiver:Proxy 对象或继承自 Proxy 对象的对象;
它拥有以下约束:
- 若被代理的对象是一个
不可写并且不可配置的,那么不能改变它的值; - 如果
赋值的属性,它的属性描述符set是一个undefined,那么不能设置它的值; - 严格模式下,如果
set()方法返回false,会抛出一个异常;
const obj = {}
const objProxy = new Proxy(obj, {
set(target, key, value, receiver) {
console.log(target === obj)
console.log(receiver === objProxy)
target[key] = value
return true
}
})
objProxy.name = 'Lee'
console.log(objProxy.name)
// output: true -> true -> Lee
约束:
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: false,
enumerable: false,
writable: false
})
const objProxy = new Proxy(obj, {
set(target, key, value, receiver) {
target[key] = value
return true
}
})
objProxy.name = 'Lee'
console.log(objProxy.name)
// output: Uncaught TypeError: 'set' on proxy: trap returned truish for property 'name' which exists in the proxy target as a non-configurable and non-writable data property with a different value
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: true,
set: undefined
})
const objProxy = new Proxy(obj, {
set(target, key, value, receiver) {
target[key] = value
return true
}
})
objProxy.name = 'Lee'
console.log(objProxy.name) // undefined 设置了也不管用
8. 其它方法
除此之外,还有其它6种方法,它们分别是:
-
handler.getOwnPropertyDescriptor():它是拦截**
Object.getOwnPropertyDescriptor()**(返回对象某个属性的属性描述符)操作的。 -
handler.getPrototypeOf():它是拦截
Object.getPrototypeOf()方法的,只有当获取代理对象的原型时才会被触发。
3.handler.isExtensible():用于拦截对对象的**Object.isExtensible()**(是否可以在对象上添加属性)方法。
-
handler.ownKeys():用于拦截 **
Reflect.ownKeys()**方法。 -
handler.preventExtensions():用于拦截**
Object.preventExtensions()**(防止该对象被扩展或者改变原型)方法。 -
handler.setPrototypeOf():主要用来拦截
Object.setPrototypeOf()(设置对象原型)方法。
Reflect
Reflect 是一个JavaScript内置的对象,它提供拦截JavaScript操作的方法。这些方法与proxy handler的方法相同。Reflect 不是一个函数对象,因此它是不可构造的(不能使用new操作符)。
Reflect和Proxy的异同
-
Reflect 对象也有
13种方法,并且方法名和 Proxy 相同。 -
Proxy 的
handler中的13种方法是一个个函数,函数的返回值需要我们设置,而 Reflect 中的13种方法,我们只需传入相应的参数,它会自动返回对应的值。 -
Proxy 的
handler方法中,如果我们返回值设置的不当,或者代理的对象不符合条件,可能系统会直接给我们抛出异常,我们必须去捕获这些异常,才能让后续的代码继续执行,而 Reflect方法正好弥补了这一点,面对一些不符合的场景,它会直接帮助我们返回 ,并Reflect方法中的返回值正好对应了 Proxy方法中需要的返回值,因此我们可以在 Proxy 的handler方法中返回一个Reflect对象相应的方法,去获取返回值。
Reflect中的get/set
Reflect 中有两个属性很重要,就是get和set。
-
get类似于target[key]访问对象属性,不过它是以函数返回值的方式返回的。 -
set类似于target.ket = value给对象赋值,不过它也是以函数的方式进行赋值,并返回是否赋值成功的一个Boolean值。 -
get/set函数的最后一个参数receiver是可选的,如果target对象设置了getter/setter函数,那么receiver就作为调用getter/setter函数时的this。
const obj = {
name: 'Lee'
}
console.log(obj.name) // Lee
console.log(Reflect.get(obj, 'name')) // Lee
obj.age = 18
// 等同于 obj.age = 19,覆盖了obj.age = 18
Reflect.set(obj, 'age', 19)
console.log(obj.age) // 19
Reflect中get/set的receiver参数
没有传入 receiver 时:
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: true,
get: function () {
console.log('当前的this为:', this) // 当前的this为: {name: undefined}
}
})
Reflect.get(obj, 'name')
默认情况下,get属性描述符中的 this 就是当前操作的对象,即obj对象。
传入 receiver 时:
const obj = {}
const obj1 = {
name: 'obj1'
}
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get: function () {
console.log('当前的this为:', this) // {name: 'obj1'}
}
})
Reflect.get(obj, 'name', obj1)
此时 get 属性描述符中的 this 就是传入的receiver对象。
也就是说,receiver参数 的作用就是,在对象的属性设置了get/set属性描述符时,可以给get/set属性描述符设置this。
它有什么用呢?
如果有以下场景:
-
原对象的某个属性a设置了**属性描述符get**。 -
对
原对象创建了一个代理对象,代理对象设置了**handler.get**方法。 -
访问
代理对象的a属性时,会去查看原对象的a属性值,此时触发原对象的getter函数。
代码如下:
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get: function () {
console.log('当前的this为:', this)
}
})
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
return target[key]
}
})
objProxy.name
obj.name
// output:
// 当前的this为: {}
// 当前的this为: {}
我们发现,不管是通过访问 代理对象 还是 原对象触发的getter函数,最终getter函数的this都指向原对象,如果我们想在访问代理对象时,getter 函数中的 this 指向代理对象呢? 这时候就可以用 Reflect.get 去实现。
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get: function () {
console.log('当前的this为:', this)
}
})
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
}
})
objProxy.name
obj.name
// output:
// 当前的this为: Proxy(Object) {}
// 当前的this为: {}
此时我们代理对象的handler.get方法不再直接通过访问原对象去获取值了,而是通过Reflect去帮助我们获取原对象的值,并且把receiver传递过去,作为原对象getter方法的this。