ES6 Proxy攔截器詳解
Proxy 攔截器
如有錯誤,麻煩指正,共同學習
Proxy的原意是“攔截”,可以理解為對目標對象的訪問和操作之前進行一次攔截。提供了這種機制,所以可以對目標對象進行修改和過濾的操作。
const proxy = new Proxy({}, { get(target, proper(Key) { console.log(‘你的訪問被我攔截到了‘) return 1;s }, set(target, properKey, properValue) { console.log(‘你修改這個屬性被我攔截到了‘) } })
Proxy 實際上重載了點運算符,即用自己的定義覆蓋了語言的原始定義。
語法:
const proxy = new Proxy(target, hanlder)
new Proxy生成一個 proxy的實例, target表示要攔截的目標,可以是對象或函數等。凡是對目標對象的一些操作都會經過攔截器的攔截處理。 hanlder 參數也是一個對象,它表示攔截配置,就如上例所示。
Proxy
實例也可以作為其他對象的原型對象。對這個目標對象進行操作,如果它自身沒有設置這個屬性,就會去它的原型對象上面尋找,從而出發攔截行為。如下例:
const proxy = new Proxy({}, { get(target, key) { consoloe.log(`你訪問的屬性是${key}`) } }) const newObject = Object.create(proxy) newObject.a // 你訪問的屬性是a
提醒
同一個攔截器可以攔截多個操作,只需要在第二個參數(hanlder)配置添加
如果對這個目標對象沒有設置攔截行為,則直接落在目標對象上。
Proxy 支持的攔截操作
- get(target, propKey, receiver) 攔截對象屬性讀取
- set(target, propKey, value, receiver) 攔截對象的屬性設置
- has(target, propKey) 攔截
propkey in proxy
- deleteProperty(target, propKey) 攔截
delete proxy[propKey]
- ownKeys(target)
- getOwnPropertyDescriptor(target, propKey) 返回對象屬性的描述對象攔截
- defineProperty(target, propKey, propDesc)
- proventExtensions(target)
- getPrototypeOf(target)
- isExtensible(target)
- setPrototypeOf(target, proto)
- apply(target, object, args)
- construct(target, args) 攔截 proxy 實例作為構造函數調用的操作
Proxy 實例的方法
get(target, key): 當訪問目標對象屬性的時候,會被攔截。
target
: 目標對象
key
: 訪問的key值
const proxy = new Proxy({a:1,b:2}, {
get(target, key) {
console.log(‘called‘)
return target[key]
}
})
proxy.a // 1
// called 會被打印出來
上面的代碼中,當讀取代理對象屬性的時候,會被get方法攔截。所以可以在攔截前做一些事情,比如必須訪問這個對象存在的屬性,如果訪問對象不存在的屬性就拋出錯誤! 如下例:
const obj = {
name: ‘qiqingfu‘,
age: 21
}
const proxy = new Proxy(obj, {
get(target, key) {
if (key in target) {
return target[key]
} else {
throw Error(`${key}屬性不存在`)
}
}
})
以上代碼讀取代理對象的屬性,如果存在就正常讀取,負責提示錯誤訪問的key值不存在。
如果一個屬性不可配置(configurable), 或者不可寫(writeble),則該屬性不能被代理
const obj = Object.defineProperties({}, {
foo: {
value: ‘a‘,
writeble: false, // 不可寫
configurable: false, //不可配置
}
})
const proxy = new Proxy(obj, {
get(target, key) {
return ‘qiqingfu‘
}
})
proxy.value // 報錯
場景例子:
通過get()方法可以實現一個函數的鏈式操作
const pipe = (function(){
return function (value) {
const funcStack = []; // 存放函數的數組
const proxy = new Proxy({}, {
get(target, fnName) {
if (fnName === ‘get‘) {
return funcStack.reduce((val, nextfn) => {
return fn(val)
}, value)
}
funcStack.push(window[fnName])
return proxy //返回一個proxy對象,以便鏈式操作
}
})
return proxy
}
}())
var add = x => x * 2;
var math = y => y + 10;
pipe(3).add.math.get // 16
set(target, key, value)方法用於攔截某個屬性的賦值操作
target
: 目標對象
key
: 要設置的key值
value
: 設置的value值
返回值
: Boolean
假如有一個prosen對象,要設置的值不能小於100,那麽就可以使用 set
方法攔截。
const prosen = {
a: 101,
b: 46,
c: 200
}
const proxy = new Proxy(prosen, {
set(target, key, value) {
if (value < 100) {
throw Error(`${value}值不能小於100`)
}
target[key] = value
}
})
上面代碼對prosen對象賦值,我們可以攔截判斷它賦值如果小於100就給它提示錯誤。
使用場景
- 可以實現數據綁定,即數據發生變化時,我們可以攔截到,實時的更新DOM元素。
- 還可以設置對象的內部數據不可被修改,表示這些屬性不能被外部訪問和修改,這是可以使用
get
和set
, 如下例
規定對象的內部屬性以_開頭的屬性不能進行讀寫操作。
const obj = {
name: ‘qiqingfu‘,
age: 21,
_money: -100000,
_father: ‘xxx‘
}
function isSeal(key) {
if (key.charAl(0) === ‘_‘) {
return true
}
return false
}
const proxy = new Proxy(obj, {
get(target, key) {
if (isSeal(key)) {
throw Error(`${key},為內部屬性,不可以讀取`)
}
return target[key]
},
set(target, key, value) {
if (isSeal(key)) {
throw Error(`${key},為內部屬性,不可以修改`)
}
target[key] = value
return true
}
})
以上代碼obj對象設置了內部屬性,以_開頭的不支持讀寫。那麽可以使用Proxy對其進行攔截判斷。get和set中的key屬性如果是以_開頭的屬性就提示錯誤。 set
方法修改完值後,返回的是一個布爾值。 true成功,反則false為修改失敗。
apply(target, context, args) 方法可以攔截函數的調用,call()、apply()
target
: 目標對象,
context
: 目標對象的上下文對象
args
: 函數調用時的參數數組
const proxy = new Proxy(function(){}, {
apply(target, context, args) {
console.log(target, ‘target‘)
console.log(context, ‘context‘)
console.log(args, ‘args‘)
}
})
const obj = {
a: 1
}
proxy.call(obj,1,2,3)
上面的代碼是攔截一個函數的執行,分別打印:
target -> function(){}
: 目標對象
context -> {a: 1}
: 目標對象的上下文對象,也就是函數的調用者,這裏我們使用call
,讓obj對象來調用這個函數。
args -> [1,2,3]
: 目標對象函數調用時我們傳遞的參數,這裏會以數組的形式接受。
例子:
再說下面一個例子之前,先了解一下Reflect.apply()
, 下面是 MDN
的解釋
Reflect.apply() 通過指定的參數列表發起對目標(target)函數的調用。
語法: Reflect.apply(target, context, args)
target
: 目標函數
context
: 目標函數執行的上下文
args
: 函數調用時傳入的實參列表,該列表應該是一個類數組的對象
該方法和ES5的 function.prototype.apply()
方法類似。
下面對 sum
函數的調用進行攔截,並且將函數的執行結果 *2
const sum = (num1, num2) => {
return num1 + num2
}
const proxy = new Proxy(sum, {
apply(target, context, args) {
// 我們可以通過 Reflect.apply()來調用目標函數
return Reflect.apply(...arguments) * 2
}
})
proxy(3,4) // 14
以上代碼是對 sum
函數進行代理,並且將其執行結果 * 2
has(target, key ) 方法即攔截 hasProperty操作, 判斷對象是否具有某個屬性時,這個方法會生效。
target
: 目標對象,
key
: 對象的屬性
返回值是一個布爾值
如果原對象不可配置或者禁止擴展, 那麽has攔截會報錯。 for in循環雖然也有 in 操作符,但是has對 for in 循環不生效.
has在什麽情況下會進行攔截:
- 屬性查詢: 例如 foo in window
- 繼承屬性查詢: foo in Object.create(proxy)
- with檢查: with(proxy) {}
- Reflect.has()
例1:
使用 has方法隱藏屬性,使其不被 in
操作符發現。 就比如說對象以_開頭的屬性不能被發現。
const prosen = {
name: ‘qiqingfu‘,
_age: 21
}
const proxy = new Proxy(prosen, {
has(target, key) {
if (key.chatAt(0) === ‘_‘) {
return false
}
return key in target
}
})
例2: with檢查
with的定義總結
在with語句塊中,只是改變了對變量的遍歷順序,由原本的從執行環境開始變為從with語句的對象開始。當嘗試在with語句塊中修改變量時,會搜索with語句的對象是否有該變量,有就改變對象的值,沒有就創建,但是創建的變量依然屬於with語句塊所在的執行環境,並不屬於with對象。
- 離開with語句塊後,遍歷順序就會再次變成從執行環境開始。
with語句接收的對象會添加到作用域鏈的前端並在代碼執行完之後移除。
關於js with語句的一些理解
let a = ‘global a‘
const obj = {
a: 1,
b: 2
}
const fn = key => {
console.log(key)
}
const proxy = new Proxy(obj, {
has(target, key) {
console.log(target, ‘target‘)
console.log(key, ‘key‘)
}
})
with(proxy) {
fn(‘a‘)
}
//依此打印
// {a: 1, b: 2} target
// fn key
// a
以上代碼是對obj對象進行代理, 通過with
檢查, 訪問代理對象的 a
屬性會被 has
方法攔截。那麽攔截的第一個target
就是目標對象, 而第二個參數key
是訪問 a
時的with語句塊所在的執行環境。
construct(target, args) 方法用於攔截 new 命令。
target
: 目標函數,
args
: 構造函數的參數對象
返回值必須是一個 對象
, 否則會報錯。
const proxy = new Proxy(function() {}, {
construct(target, args) {
console.log(target, ‘target‘)
console.log(args, ‘args‘)
return new target(args)
}
})
new proxy(1,2)
// function() {} ‘target‘
// [1,2] ‘args‘
如果返回值不是對象會報錯
deleteProperty(target, key) 攔截對象的 delete操作
target
: 目標對象
key
: 刪除的哪個key值
返回值:
布爾值, true
成功,false
失敗
目標對象不可配置(configurable)屬性不能被deleteProperty刪除, 否則會報錯
const obj = Object.defineProperties({}, {
a: {
value: 1,
configurable: false,
},
b: {
value: 2,
configurable: true
}
})
const proxy = new Proxy(obj, {
deleteProperty(target, key) {
delete target[key]
return true;
}
})
delete proxy.a // 報錯
delete proxy.b // true
以上代碼攔截 obj
對象, 當進行刪除不可配置的屬性a
時,會報錯。刪除b
屬性時則成功。
應用場景:
我們可以指定內置屬性不可被刪除。如以_開頭的屬性不能被刪除
const obj = {
_a: ‘a‘,
_b: ‘b‘,
c: ‘c‘
}
const proxy = new Proxy(obj, {
deleteProperty(target, key) {
if (key.charAt(0) === ‘_‘) {
throw Error(`${key}屬性不可被刪除`)
return false
}
delete target[key]
return true
}
})
defindProperty(target, key, descriptor)方法攔截Object.defindProperty()操作
target
: 目標對象,
key
: 目標對象的屬性
descriptor
: 要設置的描述對象
返回值
: 布爾值, true
添加屬性成功, false
則會報錯
const proxy = new Proxy({}, {
defineProperty(target, key, descriptor) {
console.log(target, ‘target‘)
console.log(key, ‘key‘)
console.log(descriptor, ‘descriptor‘)
return true
}
})
Object.defineProperty(proxy, ‘a‘, {
value: 1
})
以上代碼是攔截一個對象的Object.defindProperty()
添加屬性的操作, 如果返回值為true,表示添加成功。返回值false則會報錯。
以上代碼的執行結果:
getPrototypeOf(target) 方法,用來攔截獲取對象原型。
target
: 代理對象
可以攔截一下獲取原型的操作:
- Object.prototype. __ proto __
- Object.prototype.isPrototypeOf()
- Object.getPrototypeOf() 獲取一個對象的原型對象
- instance 操作符
Object.prototype.isPrototypeOf() 方法
檢測一個對象的原型鏈上有沒有這個對象
語法: Objectactive.isPrototypeOf(object), 檢測object
對象的原型鏈上有沒有Objectactive
這個對象, 如果有返回true
, 否則返回false
const Objectactive = {a: 1}
const object = Object.create(Objectactive)
Objectactive.isPrototypeOf(object) // true
以上代碼 Objectactive
作為 object
的原型對象,然後通過 isPrototypeOf
檢測object
對象的原型鏈上有沒有Objectactive
這個對象。 理所當然返回 true
使用 getPrototypeOf()攔截
const Objectactive = {a: 1}
const object = Object.create(Objectactive)
const proxy = new Proxy(object, {
getPrototypeOf(target) {
console.log(target, ‘target‘)
return Object.getPrototypeOf(target)
}
})
let bl = Objectactive.isPrototypeOf(proxy)
console.log(bl)
// 依此打印結果:
/*
{
__proto__:
a: 1,
__proto__: Object
} ‘target‘
true
*/
以上代碼對 object
對象進行代理,當訪問原型對象時,通過getPrototypeOf()
方法攔截,target
就是代理對象。
getPrototypeOf()方法的返回值必須是 null 或者對象,否則報錯。
isExtensible(target)
ES6 Proxy攔截器詳解