Proxy 是 ES6 為了操作對象引入的 API 。
Proxy 可以對目标對象的讀取、函數調用等操作進行攔截,然後進行操作處理。它不直接操作對象,而是像代理模式,通過對象的代理對象進行操作,在進行這些操作時,可以添加一些需要的額外操作。
基本用法
一個 Proxy 對象由兩個部分組成: target 、 handler 。在通過 Proxy 構造函數生成執行個體對象時,需要提供這兩個參數。 target 即目标對象, handler 是一個對象,聲明了代理 target 的指定行為。
let target = {
name: 'Tom',
age: 24
}
let handler = {
get: function(target, key) {
console.log('getting '+key);
return target[key]; // 不是target.key
},
set: function(target, key, value) {
console.log('setting '+key);
target[key] = value;
}
}
let proxy = new Proxy(target, handler)
proxy.name // 實際執行 handler.get
proxy.age = 25 // 實際執行 handler.set
// getting name
// setting age
// 25
target也可以為空對象
let targetEpt = {}
let proxyEpt = new Proxy(targetEpt, handler)
// 調用 get 方法,此時目标對象為空,沒有 name 屬性
proxyEpt.name // getting name
// 調用 set 方法,向目标對象中添加了 name 屬性
proxyEpt.name = 'Tom'
// setting name
// "Tom"
// 再次調用 get ,此時已經存在 name 屬性
proxyEpt.name
// getting name
// "Tom"
// 通過構造函數建立執行個體時其實是對目标對象進行了淺拷貝,是以目标對象與代理對象會互相
// 影響
targetEpt)
// {name: "Tom"}
// handler 對象也可以為空,相當于不設定攔截操作,直接通路目标對象
let targetEmpty = {}
let proxyEmpty = new Proxy(targetEmpty,{})
proxyEmpty.name = "Tom"
targetEmpty) // {name: "Tom"}
Proxy執行個體方法:
get( )
get方法用于攔截某個屬性的讀取操作,可以接收三個參數,依次為目标對象,屬性名和Proxy執行個體本身(操作行為是針對的對象),其中最後一個參數可選。
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 抛出一個錯誤
上面代碼表示如果有這個屬性則傳回屬性值,如沒有就會報錯,如果沒有這個攔截,沒有屬性會傳回undefined。
get方法也可以繼承
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
上面代碼中,攔截操作定義在prototype對象上面,是以如果讀取obj對象繼承的屬性時,攔截會生效。
利用proxy,可以将讀取某個屬性的操作(get),轉變為執行某個函數,進而實作屬性的鍊式操作。
var pipe = (function () {
return function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
如果一個屬性不可配置(configurable)且不可寫(writable),則Proxy不能修改該屬性,否則通過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方法用來攔截某個屬性的指派操作,可以接受4個參數,依次為目标對象,屬性名,屬性值和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,任何不符合要求的age屬性指派都會報錯,這是資料驗證的一種實作方法。利用set方法,還可以資料綁定,每當對象發生變化時,會自動更新DOM。
有時我們可以對一個對象設定内部屬性,屬性名的第一個字元使用下劃線開頭,表示這些屬性不應被外部使用,如:
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
上面代碼中,隻要讀寫的屬性名的第一個字元是下劃線,一律報錯,進而禁止讀寫内部屬性的目的。