一文理解 ES6 中的代理模式——Proxy
一、介紹
定義: 用於定義基本操作的自定義行爲
本質: 修改的是程序默認形爲,就形同於在編程語言層面上做修改,屬於元編程(meta programming)
元編程(Metaprogramming,又譯超編程,是指某類計算機程序的編寫,這類計算機程序編寫或者操縱其它程序(或者自身)作爲它們的數據,或者在運行時完成部分本應在編譯時完成的工作
一段代碼來理解
#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
echo "echo $I" >>program
done
chmod +x program
這段程序每執行一次能幫我們生成一個名爲program
的文件,文件內容爲 1024 行echo
,如果我們手動來寫 1024 行代碼,效率顯然低效
- 元編程優點:與手工編寫全部代碼相比,程序員可以獲得更高的工作效率,或者給與程序更大的靈活度去處理新的情形而無需重新編譯
Proxy
亦是如此,用於創建一個對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等)
二、用法
Proxy
爲 構造函數,用來生成 Proxy
實例
var proxy = new Proxy(target, handler)
參數
target
表示所要攔截的目標對象(任何類型的對象,包括原生數組,函數,甚至另一個代理))
handler
通常以函數作爲屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p
的行爲
handler 解析
關於handler
攔截屬性,有如下:
-
get(target,propKey,receiver):攔截對象屬性的讀取
-
set(target,propKey,value,receiver):攔截對象屬性的設置
-
has(target,propKey):攔截
propKey in proxy
的操作,返回一個布爾值 -
deleteProperty(target,propKey):攔截
delete proxy[propKey]
的操作,返回一個布爾值 -
ownKeys(target):攔截
Object.keys(proxy)
、for...in
等循環,返回一個數組 -
getOwnPropertyDescriptor(target, propKey):攔截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回屬性的描述對象 -
defineProperty(target, propKey, propDesc):攔截
Object.defineProperty(proxy, propKey, propDesc)
,返回一個布爾值 -
preventExtensions(target):攔截
Object.preventExtensions(proxy)
,返回一個布爾值 -
getPrototypeOf(target):攔截
Object.getPrototypeOf(proxy)
,返回一個對象 -
isExtensible(target):攔截
Object.isExtensible(proxy)
,返回一個布爾值 -
setPrototypeOf(target, proto):攔截
Object.setPrototypeOf(proxy, proto)
,返回一個布爾值 -
apply(target, object, args):攔截 Proxy 實例作爲函數調用的操作
-
construct(target, args):攔截 Proxy 實例作爲構造函數調用的操作
Reflect
若需要在Proxy
內部調用對象的默認行爲,建議使用Reflect
,其是ES6
中操作對象而提供的新 API
基本特點:
-
只要
Proxy
對象具有的代理方法,Reflect
對象全部具有,以靜態方法的形式存在 -
修改某些
Object
方法的返回結果,讓其變得更合理(定義不存在屬性行爲的時候不報錯而是返回false
) -
讓
Object
操作都變成函數行爲
下面我們介紹proxy
幾種用法:
get()
get
接受三個參數,依次爲目標對象、屬性名和 proxy
實例本身,最後一個參數可選
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
return Reflect.get(target,propKey)
}
});
proxy.name // "張三"
get
能夠對數組增刪改查進行攔截,下面是試下你數組讀取負數的索引
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
注意:如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則會報錯
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failed
set()
set
方法用來攔截某個屬性的賦值操作,可以接受四個參數,依次爲目標對象、屬性名、屬性值和 Proxy
實例本身
假定Person
對象有一個age
屬性,該屬性應該是一個不大於 200 的整數,那麼可以使用Proxy
保證age
的屬性值符合要求
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 對於滿足條件的 age 屬性以及其他屬性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
如果目標對象自身的某個屬性,不可寫且不可配置,那麼set
方法將不起作用
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable: false,
});
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = 'baz';
}
};
const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"
注意,嚴格模式下,set
代理如果沒有返回true
,就會報錯
'use strict';
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
// 無論有沒有下面這一行,都會報錯
return false;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'
deleteProperty()
deleteProperty
方法用於攔截delete
操作,如果這個方法拋出錯誤或者返回false
,當前屬性就無法被delete
命令刪除
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
Reflect.deleteProperty(target,key)
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`無法刪除私有屬性`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 無法刪除私有屬性
注意,目標對象自身的不可配置(configurable)的屬性,不能被deleteProperty
方法刪除,否則報錯
取消代理
Proxy.revocable(target, handler);
三、使用場景
Proxy
其功能非常類似於設計模式中的代理模式,常用功能如下:
-
攔截和監視外部對對象的訪問
-
降低函數或類的複雜度
-
在複雜操作前對操作進行校驗或對所需資源進行管理
使用 Proxy
保障數據類型的準確性
let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("屬性只能是number類型");
}
return Reflect.set(target, key, value, proxy);
}
});
numericDataStore.count = "foo"
// Error: 屬性只能是number類型
numericDataStore.count = 333
// 賦值成功
聲明瞭一個私有的 apiKey
,便於 api
這個對象內部的方法調用,但不希望從外部也能夠訪問 api._apiKey
let api = {
_apiKey: '123abc456def',
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可訪問.`);
} return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`);
} return Reflect.get(target, key, value, proxy);
}
});
console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都拋出錯誤
還能通過使用Proxy
實現觀察者模式
觀察者模式(Observer mode)指的是函數自動觀察數據對象,一旦對象有變化,函數就會自動執行
observable
函數返回一個原始對象的 Proxy
代理,攔截賦值操作,觸發充當觀察者的各個函數
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
觀察者函數都放進Set
集合,當修改obj
的值,在會set
函數中攔截,自動執行Set
所有的觀察者
參考文獻
-
https://es6.ruanyifeng.com/#docs/proxy
-
https://vue3js.cn/es6
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/uMpveY8at1vkhbEZvktfQw