Object.definePrototy()与 Proxy 理解

67 阅读9分钟

Object.definePropertyJs 中非常重要的一个属性,因为它是 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 种, 分别分 configurableenumerablevalueweitablegetset

它的基本使用就是通过 value添加或修改对象的属性值。

configurable

configurable 用于控制 是否能修改 当前对象的属性描述符, 默认为 false\color{red}{false}

    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
        }
    })

  • 它的默认值为 false\color{red}{false}, 说明默认状态下是 不可以修改属性描述符的

  • 即使是 只把 configurable 属性设置为true\color{red}{true} , 也是不可以的, 这说明, 如果我们想要操作属性描述符 必须在 添加数据时就将configurable 属性设置为true\color{red}{true}

enumerable

enumerable 可以控制当前 键名 是否 作为对象的可枚举属性( 是否能被 for… in/ Object.keys()/ 展开运算符等遍历出来 ), 默认值为 false\color{red}{false}

    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 可以控制当前属性是否 可写 ( 即对该属性进行赋值操作 ), 默认为 false\color{red}{false}, 该属性只可读不可写

   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属性一个函数, 默认值为 undefined\color{red}{undefined}, 访问 被操作的属性时, 会触发 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'

明明我们没有设置namevalue属性,但是访问时,却变成了get函数的返回值get 函数的作用就是如此,我们一旦设置了 get 属性描述符get函数的返回值就是我们访问该属性的值(无论我们将它赋值成什么)

set

set 是属性描述符中另一个重要的属性,它和get类似,只不过它是作用于setter属性一个函数,默认值也是 undefined\color{red}{undefined},当我们给属性进行赋值操作时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函数被外界访问,从而替代valuewritable描述符。

(3)数据描述符和访问器描述符指的是什么

  • Object.defineProperty() 的第三个参数为属性描述符,属性描述符可分为数据描述符访问器描述符

  • 访问器描述符只有getset,其它四个为数据描述符

  • 当描述符不具备value、writable、get、set中的任何一个(因为valuewritable时可选的,描述符只有configurableenumerable属性时),它一定就是数据描述符

  • 描述符不可以同时拥有(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()的三个参数相同。

  • targetObject.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 的,则返回值必须为 undefined\color{red}{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:将被赋值的新属性值。
  • receiverProxy 对象或继承自 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种方法,它们分别是:

  1. handler.getOwnPropertyDescriptor():它是拦截**Object.getOwnPropertyDescriptor()**(返回对象某个属性的属性描述符)操作的。

  2. handler.getPrototypeOf():它是拦截 Object.getPrototypeOf() 方法的,只有当获取代理对象的原型时才会被触发

3.handler.isExtensible():用于拦截对对象的**Object.isExtensible()**(是否可以在对象上添加属性)方法。

  1. handler.ownKeys():用于拦截 **Reflect.ownKeys()**方法。

  2. handler.preventExtensions():用于拦截**Object.preventExtensions()**(防止该对象被扩展或者改变原型)方法。

  3. handler.setPrototypeOf():主要用来拦截 Object.setPrototypeOf()(设置对象原型)方法。

Reflect

Reflect 是一个JavaScript内置的对象,它提供拦截JavaScript操作的方法。这些方法与proxy handler的方法相同Reflect 不是一个函数对象,因此它是不可构造的(不能使用new操作符)。

Reflect和Proxy的异同

  • Reflect 对象也有13种方法,并且方法名和 Proxy 相同。

  • Proxyhandler中的13种方法是一个个函数,函数的返回值需要我们设置,而 Reflect 中的13种方法,我们只需传入相应的参数,它会自动返回对应的值

  • Proxyhandler方法中,如果我们返回值设置的不当,或者代理的对象不符合条件,可能系统会直接给我们抛出异常,我们必须去捕获这些异常,才能让后续的代码继续执行,而 Reflect方法正好弥补了这一点,面对一些不符合的场景,它会直接帮助我们返回 false\color{red}{false} ,并Reflect 方法中的返回值正好对应了 Proxy 方法中需要的返回值,因此我们可以在 Proxyhandler 方法中返回一个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