
本文共 12895 字,大约阅读时间需要 42 分钟。
關於 JavaScript ES6 Proxy
Proxy 概念
Proxy 是用於修改某些操作的默認行為,等同於在語言層面上做了修改,所以算是一種 元編程(meta programming),意即對編程語言本身做編程。
Proxy 可以被理解為,在目標對象之前架設一層 “攔截”,外界對該對象的訪問都要經過這層 “攔截”,因此提供了一種機制,可以對外界的訪問進行一些修改或次過濾,但在外界的視角,卻感受不到這樣的中間處理。Proxy 的原意 “代理”,ES6 就解釋為代理某些操作。
話不多說,直接來看看一個例子:
let obj = new Proxy({ }, { get: function(target, key, receiver) { console.log(`getting ${ key}`) return Reflect(target, key, receiver) }, set: function(target, key, value, receiver) { console.log(`setting ${ key}`) return Reflect(target, key, value, receiver) }})
上面的代碼就是對一個空對象架設了一層攔截,重新定義了 get
, set
方法。這邊先不關心具體的語法,先試試看效果。
obj.count = 1console.log(obj.count)// setting count// getting count// 1
上面打印結果表明,Proxy 實際上是重載了 .
運算符,也就是說用自己的定義覆蓋了語言的原始定義。
Proxy 使用
ES6 提供了原生的 Proxy 構造函數,用來生成 Proxy 實例。
let proxy = new Proxy(target, handler)
Proxy 對象的所有用法都是上面這樣,不同的只是 handler 我們可以自己定義。new Proxy()
表示生成一個 Proxy 實例,target
表示要代理的目標對象,handler
也是一個對象,用來定義攔截行為。
下面再看一個攔截訪問屬性行為的例子:
let proxy = new Proxy({ }, { get: function(target, property) { return 30 }})proxy.time // 30proxy.name // 30
上面代碼中,Proxy 構造函數的第一個參數是 {}
,也就是說沒有 Proxy 介入,操作原本要訪問的就是這個對象。第二個參數是一個配置對象,對於每一個要被代理的操作,都要給出相應的處理函數,而該函數就會攔截對應的操作。比如上面,配置對象有一個 get
屬性,用於攔截對目標對象屬性的訪問操作,target
指的是目標對象,而 property
指的是要訪問的屬性,那因為該函數總是返回 30,所以無論訪問任何屬性都得到 30。
這邊要注意的是,要使 Proxy
起作用,必須是針對 proxy 實例(proxy
)進行操作,而不是針對目標對象({}
)進行操作。
如果我們的 handler 甚麼都不做,那就相當於直接通向目標對象,沒有攔截。
const target = { }const handler = { }let proxy = new Proxy(target, handler)proxy.a = 'b'target.a // b
上面代碼,handler 是個空對象,所以訪問 target 就相當於訪問 proxy。
Proxy 對象也可以作為其他對象的原型對象。
let proxy = new Proxy({ }, { get: function(target, property) { return 30 }})let proxy = new Proxy({ }, { get: function(target, property) { return 30 }})const obj = Object.create(proxy)const res = obj.timeconsole.log(res)// 30
上面代碼,proxy
對象其實是 obj
對象的一個原型,而 obj
對象本身是沒有 time 屬性的,所以會根據原型鏈找到 proxy
對象,並在對 proxy
對象訪問屬性時被攔截,所以 res
是 30。
看到這邊,相信你會好奇,那 handler 究竟可以攔截哪些操作呢 ? 下面先就來一一探討 Proxy 所支持的攔截操作。對於可以設置,但沒有設置的攔截操作,就會直接落在目標對象上,按照原始的方式來執行。
get(target, propKey, receiver)
get 操作可以攔截對於目標對象某個屬性的訪問操作。target
參數是代理的目標對象,propKey
是目標對象中的屬性的代稱,receiver
要配合 Reflect.get() 使用,下面還會再提到。
let person = { name: 'cclin'}let proxy = new Proxy(person, { get: function(target, prop) { if(prop in target) { return target[prop] }else { throw new ReferenceError('Property ' + prop + ' does not exist!') } }})console.log(proxy.name)console.log(proxy.age)// cclin// 拋出一個錯誤,ReferenceError: Property age does not exist!
上面代碼做的事情就是,攔截了訪問目標對象屬性的操作,如果屬性存在則返回,若不存在則拋出一個異常。這麼做的好處就是可以提示用戶異常了,否則 js 就只會返回 undefined。
get 方法也是可以繼承了。
let person = { name: 'cclin'}let proxy = new Proxy(person, { get: function(target, prop) { if(prop in target) { console.log('Property ' + prop + ' found') return target[prop] } }})let obj = Object.create(proxy)console.log(obj.name)// Property name found// cclin
可以看到,proxy
對象其實是 obj
對象的原型,所以如果訪問 obj
對象繼承的屬性 name 時,會循著原型鏈往上找到 proxy
,攔截依然會生效。
來看一個很經典的例子,實現用負數索引訪問數組。
function createArray(...elements) { const handler = { get: function(target, prop) { const index = Number(prop) if(index < 0) { return target[target.length + index] }else { return target[index] } } } const target = new Array() target.push(...elements) return new Proxy(target, handler)}const arr = createArray('a', 'b', 'c', 'd')console.log(arr[-1])console.log(arr[-3])// d// b
上面代碼,實現了一個可以用正常索引也可以用負數索引訪問的數組。arr[-1]
就會返回最後一個元素。
set(target, propKey, value, receiver)
set 操作可以攔截對於目標對象屬性的設置,並且返回一個 bool 表示設置成功與否。
假設有這麼一個情況,有個 Person
對象,他有一個 age
屬性,該屬性應該是一個不大於 130 的數字(好像世界上最長壽的人就是 128 歲吧),那麼這時候 Proxy 就可以派上用場了。
const validator = { set: function(target, prop, value) { if(prop === 'age') { if(!Number.isInteger(value)) { throw new TypeError('Age property should be an Integer!') } if(value > 130) { throw new RangeError('Age property should be under 130!') } } // 對於 age 以外的屬性不做檢查,直接保存 target[prop] = value }}let William = new Proxy({ }, validator)William.id = '181250168'William.age = 50console.log(William)// { id: '181250168', age: 50 }let Paul = new Proxy({ }, validator)Paul.id = '181250015'Paul.age = 200console.log(Paul)// 會報錯 : RangeError: Age property should be under 130!
上面代碼,因為設置了 set 函數,所以我們可以對 age 這個屬性做我們所需要的一些檢查。
對於 Vue 熟悉的應該也就知道,Vue 其實就是通過 Object.defineProperty
中的 set
方法實現了對數據的綁定,當數據更新時 DOM 也會同步更新。之後會再專門寫一篇關於 Vue 的雙向綁定。
回到 Proxy,有時候,我們希望在對象上面設置私有屬性,屬性名的第一個字符用 _
表示(約定俗成),表示這些屬性不應該被外界使用。結合 get 和 set,我們來看看怎麼實現。
function privatePropCheck(prop, action) { if(prop[0] === '_') { throw new Error(`Invalid attempt to ${ action} private ${ prop} property!`) }}const handler = { get: function(target, prop) { privatePropCheck(prop, 'get') return target[prop] }, set: function(target, prop, value) { privatePropCheck(prop, 'set') target[prop] = value return true }}const obj = { }let proxy = new Proxy(obj, handler)proxy._name// Error: Invalid attempt to get private _name property!proxy._age = 25// Error: Invalid attempt to set private _age property!
上面代碼就實現了禁止對任何內部屬性( _
開頭)的讀寫。
apply(target, object, args)
apply 方法用於攔截 Proxy 實例作為函數調用。
has(target, propKey)
has 用來攔截 propKey in
target 的操作,並且返回一個 bool。判斷一個對象是否擁有某屬性時,該攔截就會生效。典型的操作符就是 in
。來看一個經典的例子。
如果我們想要隱藏某些屬性,讓 in
操作符無法發現,我們可以這麼做。
const handler = { has: function(target, prop) { if(prop[0] === '_') { return false } return prop in target }}const obj = { prop: 'public', _prop: 'private'}let proxy = new Proxy(obj, handler)console.log('prop' in proxy)console.log('_prop' in proxy)// true// false
上面代碼中,我們隱藏了 _
開頭的屬性,不被 in
發現。
不過這邊稍微留意一下,如果今天目標對象是禁止擴展的,那代理 has 反而會報錯。
let obj = { a: 10 }Object.preventExtensions(obj)let proxy = new Proxy(obj, { has: function(target, prop) { return false }})console.log('a' in proxy)// TypeError: 'has' on proxy: trap returned falsish for property 'a' but the proxy target is not extensible
值得注意的一點是,has 攔截的是對象的 HasProperty
操作,而不是 HasOwnProperty
,也就是說,has 攔截只針對目標對象的一級屬性,至於繼承的屬性則不會起效果。
關於 has 最後關注 for...in
。雖然 for...in
也用到了 in
,但是 has 攔截不會對 for...in
有作用。或許是因為 for...in
是 ES5 的內容吧。這可能會導致用 for...in
遍歷代理對象時不按照我們的邏輯執行,所以如果用了 Proxy 的 has 攔截,就盡量避免使用 for...in
。推薦使用 for...of
。
let student1 = { name: 'csd', score: '99'}let student2 = { name: 'xrw', score: '1'}const handler = { has: function(target, prop) { if(prop === 'score' && target[prop] < 60) { console.log(`${ target.name} 不及格!`) return false } return prop in target }}let proxy1 = new Proxy(student1, handler)let proxy2 = new Proxy(student2, handler)console.log('score' in proxy1)// trueconsole.log('score' in proxy2)// xrw 不及格!// falsefor(let key in proxy1) { console.log(proxy1[key])}// csd// 99for(let key in proxy2) { console.log(proxy2[key])}// xrw// 1
可以看到雖然我們重定義了 has 攔截,但是對於 for...in
是不會起效果的。
constuct(target, args)
construct 方法用於攔截 new
命令。其中,target
指的是目標對象,args
則是 new 構造函數的參數列表。簡單來說,construct 用於當代理目標對象被 new 的情況。
由於是代理 new
創建對象的行為,所以 construct 必須返回一個對象。
const person = function(id = -1, name) { this.id = id this.name = name}const proxy = new Proxy(person, { construct: function(target, args) { console.log('A new instance!') return new target(...args) }})let person1 = new proxy(1, 'person1')console.log(person1)// A new instance!// person { id: 1, name: 'person1' }
deleteProperty(target, propKey)
deleteProperty 用於攔截 delete 操作,並返回一個 bool。如果這個方法拋出錯誤或是異常,就表示 delete 失敗了。
const validator = function(prop, action) { if(prop[0] === '_') { throw new Error(`Invalid attempt to ${ action} private ${ prop} property!`) }}const handler = { deleteProperty: function(target, prop) { validator(prop, 'delete') delete target[prop] return true }}const obj = { name: 'object', _id: 100 }const proxy = new Proxy(obj, handler)console.log(proxy)// { name: 'object', _id: 100 }delete proxy.nameconsole.log(proxy)//{ _id: 100 }delete proxy._idconsole.log(proxy)// Error: Invalid attempt to delete private _id property!
defineProperty(target, propKey, propDesc)
defineProperty 攔截了 Object.defineProperty
操作。
const handler = { defineProperty: function(target, prop, descriptor) { return false }}let proxy = new Proxy({ }, handler)proxy.foo = 'foo'// TypeError: proxy defineProperty handler returned false for property '"foo"'
getOwnPropertyDescriptor(target, propKey)
getOwnPropertyDescriptor 方法攔截 Object.getOwnPropertyDescriptor()
,返回一個屬性描述對象或者是 undefined。
const handler = { getOwnPropertyDescriptor: function(target, prop) { if(prop[0] === '_') { console.log(`${ prop} is a private property!`) return } return Object.getOwnPropertyDescriptor(target, prop) }}let obj = { _id: 200, name: 'Object'}let proxy = new Proxy(obj, handler)console.log(Object.getOwnPropertyDescriptor(proxy, 'yoyo'))console.log(Object.getOwnPropertyDescriptor(proxy, 'name'))console.log(Object.getOwnPropertyDescriptor(proxy, '_id'))// undefined/* { value: 'Object', writable: true, enumerable: true, configurable: true }_id is a private property! */// undefined
上面代碼中,因為我們對首字符為 _
的屬性做了 getOwnPropertyDescriptor 的攔截,所以當我們 Object.getOwnPropertyDescriptor 首字符為 _
的屬性時不會像訪問一般屬性時返回關於該屬性的描述。
getPrototypeOf(target)
getPrototypeOf 用於攔截 Object.getPrototypeOf()
操作,以及以下運算符:
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
instanceof
let obj = { }console.log(Object.getPrototypeOf(obj))// [Object: null prototype] {}
let obj = { }let proxy = new Proxy(obj, { getPrototypeOf(target) { let proto = { desc: "Don't have prototype" } if(target.__proto__ === Object.prototype) return proto else return target.__proto__ }})console.log(Object.getPrototypeOf(proxy))// { desc: "Don't have prototype" }
isExtensible(target)
isExtensible 攔截 Object.isExtensible
操作,判斷一個對象是否可擴展(可否添加新屬性),並返回一個 bool。
let proxy = new Proxy({ }, { isExtensible(target) { console.log('csd nb') return true }})console.log(Object.isExtensible(proxy))// csd nb// true
不過這個方法有一個限制,就是如果沒有滿足以下條件,就會拋出一個錯誤:
Object.isExtensible(proxy) === Object.isExtensible(target)
let proxy = new Proxy({ }, { isExtensible(target) { console.log('csd nb') return false }})console.log(Object.isExtensible(proxy))// TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
ownKeys(target)
ownKeys 可以攔截 Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
,返回的是一個數組。
下面例子攔截第一個字符為下滑線的屬性名:
let obj = { name: 'cclin', age: 20, _occupation: 'frontend', _company: 'tencent', _job: 'intern'}let handler = { ownKeys(target) { return Reflect.ownKeys(target).filter(k => { return k[0] === '_' }) }}const proxy = new Proxy(obj, handler)console.log(Object.keys(proxy))// [ '_occupation', '_company', '_job' ]
preventExtensions(target)
preventExtensions 攔截了 Object.preventExtensions()
,返回一個 bool。
而這個方法有一個限制就是,只有當 Object.isExtensible(proxy)
为false(即不可扩展)时,proxy.preventExtensions
才能返回true,否则会报错。
let obj = { name: 'cclin', age: 20, _occupation: 'frontend', _company: 'tencent', _job: 'intern'}const proxy = new Proxy(obj, { preventExtensions(target) { console.log('Prevent extensions!') return true }})Object.preventExtensions(proxy)// TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
一般的做法就是要在攔截器中手動添加 Object.preventExtensions
,使目標對象變得不可擴展。
let obj = { name: 'cclin', age: 20, _occupation: 'frontend', _company: 'tencent', _job: 'intern'}const proxy = new Proxy(obj, { preventExtensions(target) { Object.preventExtensions(target) console.log('Prevent extensions!') return true }})Object.preventExtensions(proxy)// Prevent extensions!
setPrototypeOf(target, proto)
setPrototypeOf 用於攔截 Object.setPrototypeOf
方法,返回ㄧ個 bool。
let obj = { name: 'cclin', age: 20, _occupation: 'frontend', _company: 'tencent', _job: 'intern'}let proxy = new Proxy(obj, { setPrototypeOf(target, proto) { throw new Error('You cannot change the prototype!') }})let proto = { }Object.setPrototypeOf(proxy, proto)// Error: You cannot change the prototype!
上述例子只要我們去試圖改變代理對象的 proto
就會拋出一個錯誤。
发表评论
最新留言
关于作者
