Proxy 用於修改某些操做的默認行爲,等同於在語言層面作出修改。chrome
Proxy 能夠理解在目標對象架設一個「攔截」層外界對該對象的訪問都必須先經過這層攔截,所以提供了一種機制能夠對外界的訪問進行過濾和修改。數據庫
var obj = new Proxy({}, { get: function(target, key, receiver) { console.log(`getting ${key}`) return Reflect.get(target, key, receiver) }, set: function(target, key, value, receiver) { console.log(`setting ${key}`) return Reflect.get(target, key, value, receiver) } }) obj.count = 1 // 觸發 set 事件 setting count ++obj.count // 觸發 get 和 set 事件 getting count setting count
Proxy 重載了點運算符,用本身的定義覆蓋了語言的原始定義。json
數組
/** * new Proxy() 生成一個 Proxy 實例 * target 參數表示所要攔截的目標對象 * handler 參數也是一個對象,用來定製攔截行爲 */ let proxy = new Proxy(target, handler)
注意:app
要使 Proxy 起做用,必須針對 Proxy 實例進行操做,而不是目標對象。函數
ui
例子:this
let target = {} let handler = {} var proxy = new Proxy(target, handler) proxy.count = 1 console.log(proxy.count) // 1 target.count = 2 console.log(target.count) // 2
如今有一個技巧:將 Proxy 設置到 object.proxy 屬性中,從而能夠在 object 對象上調用。spa
let object = { proxy: new Proxy(target, handler) }
也能夠將 proxy 直接掛載到原型上prototype
let proxy = new Proxy({}, { get: function(target, handler) { return 'houfee' } }) let obj = Object.create(proxy) console.log(obj.name) // houfee // proxy 是 obj 原型上的對象,根據原型鏈會在 proxy 對象讀取該屬性,致使被攔截
同一個攔截器函數設置多個操做:
var handler = { get: function(target, name) { if(name === 'prototype') { return Object.prototype } return `holle ${name}` }, apply: function(target, thisBinding, args) { return args[0] }, construct: function(target, args) { return {value: args[1]} } } var fproxy = new Proxy(function(x, y) { return x * y }, handler) console.log(fproxy(2, 5)) // 2 console.log(new fproxy(2, 5)) // {value: 5} console.log(fproxy.prototype === Object.prototype) // true console.log(fproxy.houfee) // holle houfee
1.get(target, propKey, receiver)
攔截對象屬性的讀取,好比 proxy.houfee 和 proxy['houfee'],最後一個參數receiver是一個可選參數。
2.set(target, propKey, value, receiver)
攔截對象屬性的設置,好比 proxy.houfee = v 和 proxy['houfee'] = v ,返回一個boolean值
3.has(target, propKey)
攔截 propKey in proxy 的操做,返回一個布爾值。
4.deleteProperty(target, propKey)
攔截 delete proxy[propKey] 的操做,返回一個布爾值。
5.ownKeys(target)
攔截 Object.getOwnPropertyNames(proxy)、 Object.getOwnPropertySymbols(proxy)、Object(proty),返回一個數組。
該方法返回目標對象全部自身屬性的屬性名,而 Object.keys() 的返回結果僅包括目標對象自身的可遍歷屬性。
6.getOwnPropertyDescriptor(target, propKey)
攔截 Object.getOwnPropertyDescriptor(target, propKey),返回屬性的描述對象。
7.defineProperty(target, propKey, propDesc)
攔截 Object.defineProperty(target, propKey, propDesc)、Object.defineProperties(proxy, propDesc),返回一個布爾值。
8.preventExtensions(target)
攔截 Object.preventExtensions(proxy),返回一個布爾值。
9.getPrototypeOf(target)
攔截 Object.getPrototypeOf(target),返回一個對象。
10.isExtensible(target)
攔截 Object.isExtensible(target),返回一個布爾值。
11.setPrototypeOf(target, proto)
攔截 Object.setPrototypeOf(target, proto),返回一個布爾值。若是目標對象是函數,呢麼還有2種額外操做能夠攔截。
12.apply(target, object, args)
攔截 Proxy 實例,並將其做爲函數調用,好比 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
13.construct(target, args)
攔截 Proxy 實例做爲構造函數調用的操做,好比 new proxy(...args)。
var person = { name: 'houfee' } var proxy = new Proxy(person, { get: function(target, property) { if(property in target) { return target[property] } else { return `person對象不存在${property}屬性` } } }) console.log(proxy.name) // houfee console.log(proxy.age) // person對象不存在age屬性
若是直接person.age
會返回
get
方法用於原型:
var proto = new Proxy({}, { get: function(target, propertyKey, receiver) { console.log('GET ' + propertyKey) return target[propertyKey] } }) let obj = Object.create(proto) console.log(obj.name) // GET name
function createArray(...elements) { let handler = { get: function(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') console.log(arr[-1]) // c 數組是特殊的對象!
其餘demo參看《ES6標準入門》。
攔截對象屬性的設置。
let validator = { set: function(obj, prop, value) { if(prop === 'age') { if(!Number.isInteger(value)) { throw new TypeError('這個數不是整數') } else { throw new TypeError('這個數是整數') } } // 對於 age 覺得的屬性,直接保存 obj[prop] = value } } let person = new Proxy({}, validator) person.age = 100 console.log(person.age) // TypeError: 這個數是整數 person.age = 10.5 console.log(person.age) // TypeError: 這個數不是整數 person.age = 'houfee' console.log(person.age) // TypeError: 這個數不是整數
利用set方法還能夠實現數據綁定,即便每當對象發生變化時,會自動更新DOM
let handler = { get: function(target, key) { invariant(key, 'get') return target[key] }, set: function(target, key, value) { invariant(key, 'set') target[key] = value return true } } function invariant(key, action) { if(key[0] === '_') { console.log(`${action} 私有${_prop}屬性的嘗試無效`); } } var target = {} var proxy = new Proxy(target, handler) console.log(proxy._prop) // get 私有 _prop 屬性的嘗試無效 proxy._prop = 'c' // set 私有 _prop 屬性的嘗試無效 // 只要讀/寫的屬性名的第一個字符是_,一概console,從而達到緊緻讀寫的目的。
apply 方法一概攔截函數的調用,call 和 appply 操做。
apply(target, object, args) target: 目標對象 object: 目標對象的上下文 this args: 目標對象的參數數組 var handler = { apply: function(target, ctx, args) { return Reflect.apply(..argments) } }
例子1:
var target = function() { return 'i am the target!'} var handler = { apply: function() { return 'i am the proxy!' } } var p = new Proxy(target, handler) console.log(p()) // i am the proxy! 函數P調用時,被apply方法攔截,趕回字符串
例子2:
var twice = { apply: function(target, ctx, args) { return Reflect.apply(...arguments) * 2 } } function sum(left, right) { return left + right } var proxy = new Proxy(sum, twice) console.log(proxy(1, 3)) // 8 console.log(proxy.call(null, 5, 3)) // 16 console.log(proxy.apply(null, [8, 9])) // 34 console.log(Reflect.apply(proxy, null, [5, 4])); // 18 Reflect.apply也會被攔截
has方法攔截的是 HasProperty 操做,判斷對象是否具備某個屬性,而不是 HasOwnProperty 操做,即 has 方法不判斷一個屬性是對象自身的屬性仍是繼承來的屬性。
下面的例子使用 has 方法影藏了某些操做,使其不被 in 運算符發現。
var handler = { has(target, key) { if(key[0] === '_') { return false } return key in target } } var target = {_prop: 'foo', prop: 'fee'} var proxy = new Proxy(target, handler) console.log('_prop' in proxy) // false
上面的代碼中,若是源對象的屬性名第一個字符是「_」,那麼就返回 false,從而不被 in 運算符發現。
若是源對象不可配置或者緊緻擴展,那麼這是 has 攔截會報錯。
var obj = {a: 10} Object.preventExtensions(obj) var p = new Proxy(obj, { has(target, prop) { return false } }) console.log('a' in p) // Uncaught TypeError: 'has' on proxy: trap returned falsish for property 'a' but the proxy target is not extensible 代理上的未捕獲類型錯誤:「has」:陷阱爲屬性「a」返回了虛假的,但代理目標不可擴展
上面的代碼中,obj對象緊緻擴展,結果使用 has 攔截會報錯。也就是說,若是某個屬性不可配置(或者目標對象不可擴展),則 has 方法就不得「隱藏」(即返回false)目標對象的該屬性。
let stu1 = {name: '張三', score: 59} let stu2 = {name: '李四', score: 89} let handler = { has(target, prop) { if(prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`) return false } return prop in target } } let proxy1 = new Proxy(stu1, handler) let proxy2 = new Proxy(stu2, handler) console.log('score' in proxy1) // 張三 不及格 false console.log('score' in proxy2) // true for(let a in proxy1) { console.log(proxy1[a]) // 張三 59 } for(let a in proxy2) { console.log(proxy2[a]) // 李四 89 }
上面的代碼中,has 攔截只對 in 循環生效,對 for ... in 循環不生效,致使不符合要求的屬性沒有被排除在 for...in 循環以外。
construct 用於攔截new 命令,下面是攔截的寫法:
let handler = { construct (target, args, newTarget) { return new target(...args) } } // target 目標對象 // args 構建函數的參數對象
例子:
var p = new Proxy(function(){}, { construct (target, args) { console.log(`called: ${args.join('-')}`) return {value: args[0] * 10} } }) console.log(new p(1).value) // called: 1 10
construct 方法返回值必須是一個對象,不然會報錯
deleteProperty 用於攔截 delete 操做,若是這個方法拋出錯誤或者返回false,當前屬性就沒法被delete命令刪除。
let handler = { deleteProperty(target, key) { invariant(key, 'delete') return true } } function invariant(key, action) { if(key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}"`) } } var target = {_prop: 'foo'} var proxy = new Proxy(target, handler) console.log(delete proxy._prop) // Invalid attempt to delete private "_prop"
上面代碼,deleteProperty 攔截了delete 操做符,刪除第一個字符爲「_」的屬性會報錯。
注意:目標對象的不可配置(configurable)的屬性不能被 deleteProperty 刪除,不然會報錯。
defineProperty 方法攔截 Object.defineProperty 操做。
let handler = { defineProperty(target, key, descriptor) { console.log(key) // foo return false } } var target = {} var proxy = new Proxy(target, handler) proxy.foo = 'bar' // 書上 解釋會報錯,可是在chrome 中沒有!!!
getOwnPropertyDescriptor 攔截 Object.getOwnPropertyDescriptor (),返回一個屬性描述對象 或者 undefined。
let handler = { getOwnPropertyDescriptor(target, key) { console.log(key) // wat // _foo // baz if(key[0] === '_') { return } return Object.getOwnPropertyDescriptor(target, key) } } var target = {_foo: 'bar', baz: 'tar'} var proxy = new Proxy(target, handler) let data1 = Object.getOwnPropertyDescriptor(proxy, 'wat') console.log(data1) // undefined let data2 = Object.getOwnPropertyDescriptor(proxy, '_foo') console.log(data2) // undefined let data3 = Object.getOwnPropertyDescriptor(proxy, 'baz') console.log(data3) // {value: "tar", writable: true, enumerable: true, configurable: true}
上面代碼中,handler.getOwnPropertyDescriptor 方法對於第一個字符爲下劃線的屬性名會返回 undefined。
getPrototypeOf 主要用來攔截獲取對象原型。攔截如下操做:
Object.prototype.__prop__ Object.prototype.isPrototypeOf() Object.getPrototypeOf() Reflect.getPrototypeOf() instanceof
例子:
var proto = {} var p = new Proxy({}, { getPrototypeOf(target) { return proto } }) console.log(Object.getPrototypeOf(p) === proto) // true
上面代碼 getPrototypeOf 方法攔截 Object.getPrototypeOf(), 返回 proto 對象。
注意:getPrototypeOf 方法的返回值必須是對象 或者是 null,不然會報錯。另外,若是目標對象不可擴展(extensible),getPrototypeOf 方法補習返回目標對象的原型對象。
isExtensible 攔截 Object.isExtensible 操做
var p = new Proxy({}, { isExtensible(target) { console.log('called') // called return true } }) console.log(Object.isExtensible(p)) // true
上面代碼,在調用Object.isExtensible() 方法會輸出 called。
注意 isExtensible 只會返回布爾值,不然會自動轉化爲布爾值
ownKeys 方法用來攔截對象自身屬性的讀取操做。具體來講,攔截如下操做:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
例子:
var target = {a: 1, b: 2, c: 3} let handler = { ownKeys(target) { return ['a'] } } let proxy = new Proxy(target, handler) console.log(Object.keys(proxy)) // ["a"]
上面代碼攔截了對於 target 對象的 Object.keys(proxy)值返回 a 屬性。
var target = {_a: 1, _b: 2, c: 3} let handler = { ownKeys(target) { return Reflect.ownKeys(target).filter(key => key[0] !== "_") } } let proxy = new Proxy(target, handler) for(let key of Object.keys(proxy)) { console.log(target[key]) } // 3
須要注意的是,使用 Object.keys 方法時,有三類屬性會被 ownKeys 自動過濾掉,不會返回。
目標對象不存在的屬性
屬性名爲 Symbol 的值
不可遍歷(enumerable)的屬性
例子3:
var target = {a: 1, b: 2, c: 3, [Symbol.for('secret')]: 4} Object.defineProperty(target, 'key', { enumerable: false, configurable: true, writable: true, value: 'static' }) let handler = { ownKeys(target) { return ['a', 'd', Symbol.for('secret'), 'key'] } } let proxy = new Proxy(target, handler) console.log(Object.keys(proxy)) // ["a"] // 不存在的屬性d Symbol屬性 不可遍歷的屬性 都被過濾掉了
例子4: ownKeys 還能夠攔截 Object.getOwnPropertyNames()
var p = new Proxy({}, { ownKeys(target) { return ['a', 'b', 'c',] } }) let s = Object.getOwnPropertyNames(p) console.log(s) // ["a", "b", "c"]
ownKeys 方法返回的數據成員只能是字符串 或 symbol 值,若是有其餘類型的值,或者返回的根本不是數組,就會報錯:
var p = new Proxy({}, { ownKeys(target) { return ['a', 'b', true, undefined, null, {}, []] } }) let s = Object.getOwnPropertyNames(p) console.log(s) // Uncaught TypeError: true is not a valid property name // 每個數組成員都不是字符串或者symbol值,所以會報錯
若是目標對象自身包含不可配置的屬性,則該屬性必須被ownKeys方法返回,不然會報錯:
var obj = {} Object.defineProperty(obj, 'a', { enumerable: true, configurable: false, value: 1000 }) var p = new Proxy(obj, { ownKeys(target) { return ['b'] } }) let s = Object.getOwnPropertyNames(p) console.log(s) // 'ownKeys' on proxy: trap result did not include 'a'
上面代碼,obj的a屬性不可配置,這時 ownKeys 方法返回的數組之中必須包含a,不然會報錯。
另外,若是目標對象是不可擴展的,這時 ownKeys 方法返回的數組中必須包含源對象的全部屬性,切不能包含多餘的屬性,不然會報錯:
var obj = {a : 123} Object.preventExtensions(obj) var p = new Proxy(obj, { ownKeys(target) { return ['a', 'b'] } }) let s = Object.getOwnPropertyNames(p) console.log(s) // TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
上面的對象obj是不可擴展的,這時 ownKeys 方法返回的數組之中包含了obj對象的多餘屬性,因此報錯了。
preventExtensions 方法攔截 Object.preventExtensions() 。該方法必須返回一個布爾值,不然會被自動轉爲布爾值。
var p = new Proxy({}, { preventExtensions: function(target) { return true; } }); console.log(Object.preventExtensions(p)) // preventExtensions' on proxy: trap returned truish but the proxy target is extensible //proxy.preventExtensions 方法返回 true ,但這時 Object.isExtensible(proxy) 會返回 true
爲了防止出現這個問題,一般要在 proxy.preventExtensions 方法裏面,調用一次 Object.preventExtensions 。
var p = new Proxy({}, { preventExtensions: function(target) { console.log('called') // called Object.preventExtensions(target) return true } }); console.log(Object.preventExtensions(p)) // Proxy {}
setPrototypeOf 方法主要用來攔截 Object.setPrototypeOf 方法。
var handler = { setPrototypeOf(target, proto) { throw new Error('Changing the prototype is forbidden') } } var proto = {}; var target = function() {} var proxy = new Proxy(target, handler) console.log(Object.setPrototypeOf(proxy, proto)) // Changing the prototype is forbiddenA
let target = {} let handler = {} let { proxy, revoke } = Proxy.revocable(target, handler) proxy.foo = 123 console.log(proxy.foo) // 123 revoke() console.log(proxy.foo) // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked // Proxy.revocable 方法返回一個對象,該對象的 proxy 屬性是 Proxy 實例, revoke 屬性是一個函數,能夠取消 Proxy 實例。當執行 revoke 函數以後,再訪問 Proxy 實例,就錯誤
Proxy.revocable 的一個使用場景是,目標對象不容許直接訪問,必須經過代理訪問,一旦訪問結束,就收回代理權,不容許再次訪問。
雖然 Proxy 能夠代理針對目標對象的訪問,但它不是目標對象的透明代理,即不作任何攔截的狀況下,也沒法保證與目標對象的行爲一致。主要緣由就是在 Proxy 代理的狀況下,目標對象內部的 this 關鍵字會指向 Proxy 代理。
const target = { m: function() { console.log(this === proxy) // false true false } } const handler = {}; const proxy = new Proxy(target, handler) console.log(target.m()) // undefined console.log(proxy.m()) // undefined console.log(target.m()) // undefined // 一旦 proxy 代理 target.m ,後者內部的 this 就指向 proxy 了,而不是 target
例子:因爲 this 指向變化致使 Proxy 沒法代理目標對象:
const _name = new WeakMap() class Person { constructor(name) { _name.set(this, name) } get name() { return _name.get(this) } } const jane = new Person('Jane'); console.log(jane.name) // 'Jane' const proxy = new Proxy(jane, {}); console.log(proxy.name) // undefined // 目標對象 jane 的 name 屬性,實際保存在外部 WeakMap 對象 _name 上面,經過 this 鍵區分。因爲經過 proxy.name 訪問時, this 指向 proxy ,致使沒法取到值,因此返回 undefined 。
此外,有些原生對象的內部屬性,只有經過正確的 this 才能拿到,因此 Proxy 也沒法代理這些原生對象的屬性:
const target = new Date() const handler = {} const proxy = new Proxy(target, handler) console.log(proxy.getDate()) // TypeError: this is not a Date object. // getDate 方法只能在 Date 對象實例上面拿到,若是 this 不是 Date 對象實例就會報錯。
這時, this 綁定原始對象,就能夠解決這個問題:
const target = new Date('2015-01-01') const handler = { get(target, prop) { if (prop === 'getFullYear') { return target.getFullYear.bind(target) } return Reflect.get(target, prop) } }; const proxy = new Proxy(target, handler) console.log(proxy.getFullYear()); // 2015
Proxy 對象能夠攔截目標對象的任意屬性,這使得它很合適用來寫 Web 服務的客戶端
const service = createWebService('http://example.com/data'); service.employees().then(json => { const employees = JSON.parse(json); // ··· });
上面代碼新建了一個 Web 服務的接口,這個接口返回各類數據。Proxy 能夠攔截這個對象的任意屬性,因此不用爲每一種數據寫一個適配方法,只要寫一個 Proxy 攔截就能夠了。
function createWebService(baseUrl) { return new Proxy({}, { get(target, propKey, receiver) { return () => httpGet(baseUrl + '/' + propKey); } }); }
同理,Proxy 也能夠用來實現數據庫的 ORM 層。