乐趣区

关于javascript:转载深入理解Proxy和defineProperty

据悉 Vue3.0 的正式版将要在本月 (8 月) 公布,从公布到正式投入到正式我的项目中,还须要肯定的过渡期,但咱们不能始终等到 Vue3 正式投入到我的项目中的时候才去学习,提前学习,让你更快一步把握 Vue3.0, 升职加薪迎娶白富美就靠它了。不过在学习Vue3 之前,还须要先理解一下 Proxy, 它是Vue3.0 实现数据双向绑定的根底。

理解代理模式

一个例子

作为一个独身钢铁直男程序员,小王最近逐步喜爱上了前台小妹,不过呢,他又和前台小妹不熟,所以决定委托与前端小妹比拟熟的 UI 小姐姐帮忙给本人搭桥引线。小王于是请 UI 小姐姐吃了一顿大餐,而后拿出一封情书委托它转交给前台小妹,情书上写的 我喜爱你,我想和你睡觉 ,不愧钢铁直男。不过这样写必定是没戏的,UI 小姐姐吃人嘴短,于是帮忙改了情书,改成了 我喜爱你,我想和你一起在晨辉的沐浴下起床,而后交给了前台小妹。尽管有没有撮合胜利不分明啊,不过这个故事通知咱们,小王活该独身狗。

其实下面就是一个比拟典型的代理模式的例子,小王想给前台小妹送情书,因为不熟所以委托 UI 小姐姐UI 小姐姐相当于代理人,代替小王实现了送情书的事件。

作者:前端有的玩
链接:https://zhuanlan.zhihu.com/p/…
起源:知乎
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

引申

通过下面的例子,咱们想想 Vue 的数据响应原理,比方上面这段代码

const xiaowang = {love: '我喜爱你,我想和你睡觉'}
// 送给小姐姐情书
function sendToMyLove(obj) {console.log(obj.love)
 return '流氓,滚'
}
console.log(sendToMyLove(xiaowang))

如果没有 UI 小姐姐代替送情书,显示终局是悲惨的,想想 Vue2.0 的双向绑定,通过 Object.defineProperty 来监听的属性 get,set办法来实现双向绑定, 这个 Object.defineProperty 就相当于 UI 小姐姐

const xiaowang = {loveLetter: '我喜爱你,我想和你睡觉'}
// UI 小姐姐代理
Object.defineProperty(xiaowang,'love', {get() {return xiaowang.loveLetter.replace('睡觉','一起在晨辉的沐浴下起床')
  }
})

// 送给小姐姐情书
function sendToMyLove(obj) {console.log(obj.love)
 return '小伙子还挺有诗情画意的么,不过老娘不喜爱,滚'
}
console.log(sendToMyLove(xiaowang))

尽管仍然是一个悲惨的故事,因为送飞驰的成功率可能会更高一些。然而咱们能够看到,通过 Object.defineproperty 能够对对象的已有属性进行拦挡,而后做一些额定的操作。

存在的问题

Vue2.0 中,数据双向绑定就是通过 Object.defineProperty 去监听对象的每一个属性,而后在 get,set 办法中通过公布订阅者模式来实现的数据响应,然而存在肯定的缺点,比方只能监听已存在的属性,对于新增删除属性就无能为力了,同时无奈监听数组的变动,所以在 Vue3.0 中将其换成了性能更弱小的Proxy

理解 Proxy

ProxyES6 新推出的一个个性,能够用它去拦挡 js 操作的办法,从而对这些办法进行代理操作。

用 Proxy 重写下面的例子

比方咱们能够通过 Proxy 对下面的送情书情节进行重写:

const xiaowang = {loveLetter: '我喜爱你,我想和你睡觉'}
const proxy = new Proxy(xiaowang, {get(target,key) {if(key === 'loveLetter') {return target[key].replace('睡觉','一起在晨辉的沐浴下起床')
    }
  }
})
// 送给小姐姐情书
function sendToMyLove(obj) {console.log(obj.loveLetter)
 return '小伙子还挺有诗情画意的么,不过老娘不喜爱,滚'
}
console.log(sendToMyLove(proxy))

作者:前端有的玩
链接:https://zhuanlan.zhihu.com/p/…
起源:知乎
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

再看这样一个场景

请别离应用 Object.definePropertyProxy欠缺上面的代码逻辑.

function observe(obj, callback) {}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {console.log(` 属性 [${key}] 的值被批改为[${value}]`)
  }
)

// 这段代码执行后,输入 属性 [name] 的值被批改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输入 属性 [sex] 的值被批改为[女]
obj.sex = '女'

看了下面的代码,心愿大家能够先自行实现以下,上面咱们别离用 Object.definePropertyProxy去实现下面的逻辑.

  1. 应用Object.defineProperty
/**
 * 请实现这个函数,使上面的代码逻辑失常运行
 * @param {*} obj 对象
 * @param {*} callback 回调函数
 */
function observe(obj, callback) {const newObj = {}
  Object.keys(obj).forEach(key => {
    Object.defineProperty(newObj, key, {
      configurable: true,
      enumerable: true,
      get() {return obj[key]
      },
      // 当属性的值被批改时,会调用 set,这时候就能够在 set 外面调用回调函数
      set(newVal) {obj[key] = newVal
        callback(key, newVal)
      }
    })
  })
  return newObj
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {console.log(` 属性 [${key}] 的值被批改为[${value}]`)
  }
)

// 这段代码执行后,输入 属性 [name] 的值被批改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输入 属性 [sex] 的值被批改为[女]
obj.name = '女'
  1. 应用Proxy
function observe(obj, callback) {
  return new Proxy(obj, {get(target, key) {return target[key]
    },
    set(target, key, value) {target[key] = value
      callback(key, value)
    }
  })
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {console.log(` 属性 [${key}] 的值被批改为[${value}]`)
  }
)

// 这段代码执行后,输入 属性 [name] 的值被批改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输入 属性 [sex] 的值被批改为[女]
obj.name = '女'

通过下面两种不同实现形式,咱们能够大略的理解到 Object.definePropertyProxy的用法,然而当给对象增加新的属性的时候,区别就进去了,比方

// 增加公众号字段
obj.gzh = '前端有的玩'

应用 Object.defineProperty 无奈监听到新增属性,然而应用 Proxy 是能够监听到的。比照下面两段代码能够发现有以下几点不同

  • Object.defineProperty监听的是对象的每一个属性,而 Proxy 监听的是对象本身
  • 应用 Object.defineProperty 须要遍历对象的每一个属性,对于性能会有肯定的影响
  • Proxy对新增的属性也能监听到,但 Object.defineProperty 无奈监听到。

初识Proxy

概念与语法

MDN 中,对于 Proxy 是这样介绍的: Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。什么意思呢?Proxy就像一个拦截器一样,它能够在读取对象的属性,批改对象的属性,获取对象属性列表,通过 for in 循环等等操作的时候,去拦挡对象下面的默认行为,而后本人去自定义这些行为,比方下面例子中的 set, 咱们通过拦挡默认的set, 而后在自定义的set 外面增加了回调函数的调用

Proxy的语法格局如下

/**
* target: 要兼容的对象,能够是一个对象,数组, 函数等等
* handler: 是一个对象,外面蕴含了能够监听这个对象的行为函数,比方下面例子外面的 `get` 与 `set`
* 同时会返回一个新的对象 proxy, 为了可能触发 handler 外面的函数,必须要应用返回值去进行其余操作,比方批改值
*/
const proxy = new Proxy(target, handler)

在下面的例子外面,咱们曾经应用到了 handler 外面提供的 getset办法了,接下来咱们一一看一下 handler 外面的办法。

作者:前端有的玩
链接:https://zhuanlan.zhihu.com/p/…
起源:知乎
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

handler 外面的办法列表

handler外面的办法能够有以下这十三个,每一个都对应的一种或多种针对 proxy 代理对象的操作行为

  1. handler.get
    当通过 proxy 去读取对象外面的属性的时候,会进入到 get 钩子函数外面
  2. handler.set
    当通过 proxy 去为对象设置批改属性的时候,会进入到 set 钩子函数外面
  3. handler.has
    当应用 in 判断属性是否在 proxy 代理对象外面时,会触发has,比方
    const obj = {name: '子君'}  
    console.log('name' in obj)  
  1. handler.deleteProperty
    当应用 delete 去删除对象外面的属性的时候,会进入 deleteProperty 钩子函数
  2. handler.apply
    proxy 监听的是一个函数的时候,当调用这个函数时,会进入 apply 钩子函数
  3. handle.ownKeys
    当通过 Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys 去获取对象的信息的时候,就会进入 ownKeys 这个钩子函数
  4. handler.construct
    当应用 new 操作符的时候,会进入 construct 这个钩子函数
  5. handler.defineProperty
    当应用 Object.defineProperty 去批改属性修饰符的时候,会进入这个钩子函数
  6. handler.getPrototypeOf
    当读取对象的原型的时候,会进入这个钩子函数
  7. handler.setPrototypeOf
    当设置对象的原型的时候,会进入这个钩子函数
  8. handler.isExtensible
    当通过 Object.isExtensible 去判断对象是否能够增加新的属性的时候,进入这个钩子函数
  9. handler.preventExtensions
    当通过 Object.preventExtensions 去设置对象不能够批改新属性时候,进入这个钩子函数
  10. handler.getOwnPropertyDescriptor
    在获取代理对象某个属性的属性形容时触发该操作,比方在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时会进入这个钩子函数

Proxy提供了十三种拦挡对象操作的办法,本文次要筛选其中一部分在 Vue3 中比拟重要的进行阐明,其余的倡议能够间接浏览 MDN 对于 Proxy 的介绍。

具体介绍

get

当通过 proxy 去读取对象外面的属性的时候,会进入到 get 钩子函数外面
当咱们从一个 proxy 代理下面读取属性的时候,就会触发 get 钩子函数,get函数的构造如下

/**
 * target: 指标对象,即通过 proxy 代理的对象
 * key: 要拜访的属性名称
 * receiver: receiver 相当于是咱们要读取的属性的 this, 个别状况
 *           下他就是 proxy 对象自身,对于 receiver 的作用,后文将具体解说
 */
handle.get(target, key, receiver)

示例

咱们在工作中常常会有封装 axios 的需要,在封装过程中,也须要对申请异样进行封装,比方不同的状态码返回的异样信息是不同的,如下是一部分状态码及其提示信息:

// 状态码提示信息
const errorMessage = {
  400: '谬误申请',
  401: '零碎未受权,请从新登录',
  403: '回绝拜访',
  404: '申请失败,未找到该资源'
}

// 应用形式
const code = 404
const message = errorMessage
console.log(message)

但这存在一个问题,状态码很多,咱们不可能每一个状态码都去枚举进去,所以对于一些异样状态码,咱们心愿能够进行对立提醒,如提醒为 零碎异样,请分割管理员 ,这时候就能够应用Proxy 对错误信息进行代理解决

// 状态码提示信息 
const errorMessage = { 
    400: '谬误申请', 
    401: '零碎未受权,请从新登录', 
    403: '回绝拜访', 
    404: '申请失败,未找到该资源' 
} 
const proxy = new Proxy(errorMessage, {get(target,key) {const value = target[key] 
        return value || '零碎异样,请分割管理员' 
    } 
}) 
// 输入 谬误申请 
console.log(proxy[400]) 
// 输入 零碎异样,请分割管理员 
console.log(proxy[500])

set

当为对象外面的属性赋值的时候,会触发set

当给对象外面的属性赋值的时候,会触发 set,set 函数的构造如下

/**
 * target: 指标对象,即通过 proxy 代理的对象
 * key: 要赋值的属性名称
 * value: 指标属性要赋的新值
 * receiver: 与 get 的 receiver 基本一致
 */
handle.set(target,key,value, receiver)

示例

某零碎须要录入一系列数值用于数据统计,然而在录入数值的时候,可能录入的存在一部分异样值,对于这些异样值须要在录入的时候进行解决, 比方大于 100 的值,转换为 100, 小于0 的值,转换为 0, 这时候就能够应用proxyset,在赋值的时候,对数据进行解决

const numbers = []
const proxy = new Proxy(numbers, {set(target,key,value) {if(value < 0) {value = 0}else if(value > 100) {value = 100}
    target[key] = value
    // 对于 set 来说,如果操作胜利必须返回 true, 否则会被视为失败
    return true
  }
})

proxy.push(1)
proxy.push(101)
proxy.push(-10)
// 输入 [1, 100, 0]
console.log(numbers)

比照Vue2.0

在应用 Vue2.0 的时候,如果给对象增加新属性的时候,往往须要调用 $set, 这是因为Object.defineProperty 只能监听已存在的属性,而新增的属性无奈监听,而通过 $set 相当于手动给对象新增了属性,而后再触发数据响应。然而对于 Vue3.0 来说,因为应用了 Proxy,在他的set 钩子函数中是能够监听到新增属性的,所以就不再须要应用$set

const obj = {name: '子君'}
const proxy = new Proxy(obj, {set(target,key,value) {if(!target.hasOwnProperty(key)) {console.log(` 新增了属性 ${key}, 值为 ${value}`)
    }
    target[key] = value
    return true
  }
})
// 新增 公众号 属性
// 输入 新增了属性 gzh, 值为前端有的玩
proxy.gzh = '前端有的玩'

作者:前端有的玩
链接:https://zhuanlan.zhihu.com/p/...
起源:知乎
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

has

当应用 in 判断属性是否在 proxy 代理对象外面时,会触发has

/**
 * target: 指标对象,即通过 proxy 代理的对象
 * key: 要判断的 key 是否在 target 中
 */
 handle.has(target,key)

示例

个别状况下咱们在 js 中申明公有属性的时候,会将属性的名字以 _ 结尾,对于这些公有属性,是不须要内部调用,所以如果能够暗藏掉是最好的,这时候就能够通过 has 在判断某个属性是否在对象时,如果以 _ 结尾,则返回false

const obj =  {publicMethod() {},
  _privateMethod(){}
}
const proxy = new Proxy(obj, {has(target, key) {if(key.startsWith('_')) {return false}
    return Reflect.get(target,key)
  }
})

// 输入 false
console.log('_privateMethod' in proxy)

// 输入 true
console.log('publicMethod' in proxy)

作者:前端有的玩
链接:https://zhuanlan.zhihu.com/p/...
起源:知乎
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

deleteProperty

当应用 delete 去删除对象外面的属性的时候,会进入 deleteProperty` 拦截器

/**
 * target: 指标对象,即通过 proxy 代理的对象
 * key: 要删除的属性
 */
 handle.deleteProperty(target,key)

示例

当初有一个用户信息的对象,对于某些用户信息,只容许查看,但不能删除或者批改,对此应用 Proxy 能够对不能删除或者批改的属性进行拦挡并抛出异样,如下

const userInfo = {
  name: '子君',
  gzh: '前端有的玩',
  sex: '男',
  age: 22
}
// 只能删除用户名和公众号
const readonlyKeys = ['name', 'gzh']
const proxy = new Proxy(userInfo, {set(target,key,value) {if(readonlyKeys.includes(key)) {throw new Error(` 属性 ${key}不能被批改 `)
    }
    target[key] = value
    return true
  },
   deleteProperty(target,key) {if(readonlyKeys.includes(key)) {throw new Error(` 属性 ${key}不能被删除 `)
      return
    }
    delete target[key]
    return true
  }
})
// 报错 
delete proxy.name

比照Vue2.0

其实与 $set 解决的问题相似,Vue2.0是无奈监听到属性被删除的,所以提供了 $delete 用于删除属性,然而对于 Proxy,是能够监听删除操作的,所以就不须要再应用$delete

其余操作

在上文中,咱们提到了 Proxyhandler提供了十三个函数,在下面咱们列举了最罕用的三个,其实每一个的用法都是基本一致的,比方 ownKeys,当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys 去获取对象的信息的时候,就会进入 ownKeys 这个钩子函数,应用这个咱们就能够对一些咱们不像裸露的属性进行爱护,比方个别会约定 _ 结尾的为公有属性,所以在应用 Object.keys 去获取对象的所有 key 的时候,就能够把所有 _ 结尾的属性屏蔽掉。对于残余的那些属性,倡议大家多去看看 MDN 中的介绍。

Reflect

在下面,咱们获取属性的值或者批改属性的值都是通过间接操作 target 来实现的,但实际上 ES6 曾经为咱们提供了在 Proxy 外部调用对象的默认行为的API, 即Reflect。比方上面的代码

const obj = {}
const proxy = new Proxy(obj, {get(target,key,receiver) {return Reflect.get(target,key,receiver)
  }
})

大家可能看到下面的代码与间接应用 target[key] 的形式没什么区别,但实际上 Reflect 的呈现是为了让 Object 下面的操作更加标准,比方咱们要判断某一个 prop 是否在一个对象中,通常会应用到in, 即

const obj = {name: '子君'}
console.log('name' in obj)

但下面的操作是一种命令式的语法,通过 Reflect 能够将其转变为函数式的语法,显得更加标准

Reflect.has(obj,'name')

除了 has,get 之外,其实 Reflect 下面总共提供了十三个静态方法,这十三个静态方法与 Proxyhandler下面的十三个办法是一一对应的,通过将 ProxyReflect相结合,就能够对对象下面的默认操作进行拦挡解决,当然这也就属于函数元编程的领域了。

总结

有的同学可能会有纳闷,我不会 ProxyReflect就学不了 Vue3.0 了吗?其实懂不懂这个是不影响学习 Vue3.0 的,然而如果想深刻 去了解 Vue3.0,还是很有必要理解这些的。比方常常会有人在应用Vue2 的时候问,为什么我数组通过索引批改值之后,界面没有变呢?当你理解到 Object.defineProperty 的应用形式与限度之后,就会豁然开朗,原来如此。

退出移动版