一混弥、Proxy概述
Proxy 用于修改某些操作的默認行為雨饺,等同于在語言層面做出修改,所以屬于一種“元編程”(meta programming)钠龙,即對編程語言進行編程炬藤。
Proxy 可以理解成,在目標對象之前架設(shè)一層“攔截”俊鱼,外界對該對象的訪問刻像,都必須先通過這層攔截畅买,因此提供了一種機制并闲,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理谷羞,用在這里表示由它來“代理”某些操作帝火,可以譯為“代理器”。
ES6 原生提供 Proxy 構(gòu)造函數(shù)湃缎,用來生成 Proxy 實例犀填。
var proxy = new Proxy(target, handler);
Proxy 對象的所有用法,都是上面這種形式嗓违,不同的只是handler參數(shù)的寫法九巡。其中,new Proxy()表示生成一個Proxy實例蹂季,target參數(shù)表示所要攔截的目標對象冕广,handler參數(shù)也是一個對象,用來定制攔截行為偿洁。
簡單demo(攔截讀取屬性行為):
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
上面代碼中撒汉,作為構(gòu)造函數(shù),Proxy接受兩個參數(shù)涕滋。第一個參數(shù)是所要代理的目標對象(上例是一個空對象)睬辐,即如果沒有Proxy的介入,操作原來要訪問的就是這個對象宾肺;第二個參數(shù)是一個配置對象溯饵,對于每一個被代理的操作,需要提供一個對應(yīng)的處理函數(shù)锨用,該函數(shù)將攔截對應(yīng)的操作瓣喊。比如,上面代碼中黔酥,配置對象有一個get方法藻三,用來攔截對目標對象屬性的訪問請求洪橘。get方法的兩個參數(shù)分別是目標對象和所要訪問的屬性】妹保可以看到熄求,由于攔截函數(shù)總是返回35,所以訪問任何屬性都得到35逗概。
注意弟晚,要使得Proxy起作用,必須針對Proxy實例(上例是proxy對象)進行操作逾苫,而不是針對目標對象(上例是空對象)進行操作卿城。
如果handler沒有設(shè)置任何攔截,那就等同于直接通向原對象铅搓。
demo2:
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代碼中瑟押,handler是一個空對象,沒有任何攔截效果星掰,訪問proxy就等同于訪問target多望。
技巧:一個技巧是將 Proxy 對象,設(shè)置到object.proxy屬性氢烘,從而可以在object對象上調(diào)用怀偷。
如下所示:
var object = { proxy: new Proxy(target, handler) };
Proxy 實例也可以作為其他對象的原型對象。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代碼中播玖,proxy對象是obj對象的原型椎工,obj對象本身并沒有time屬性,所以根據(jù)原型鏈蜀踏,會在proxy對象上讀取該屬性维蒙,導(dǎo)致被攔截。
同一個攔截器函數(shù)脓斩,可以設(shè)置攔截多個操作木西。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + 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);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
對于可以設(shè)置、但沒有設(shè)置攔截的操作随静,則直接落在目標對象上八千,按照原先的方式產(chǎn)生結(jié)果。
下面是 Proxy 支持的攔截操作一覽燎猛,一共 13 種恋捆。
- get(target, propKey, receiver):攔截對象屬性的讀取,比如proxy.foo和proxy['foo']重绷。
- set(target, propKey, value, receiver):攔截對象屬性的設(shè)置沸停,比如proxy.foo = v或proxy['foo'] = v,返回一個布爾值昭卓。
- has(target, propKey):攔截propKey in proxy的操作愤钾,返回一個布爾值瘟滨。
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布爾值能颁。
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)杂瘸、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)伙菊、for...in循環(huán)败玉,返回一個數(shù)組。該方法返回目標對象所有自身的屬性的屬性名镜硕,而Object.keys()的返回結(jié)果僅包括目標對象自身的可遍歷屬性运翼。
- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象兴枯。
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)血淌、Object.defineProperties(proxy, propDescs),返回一個布爾值念恍。
- preventExtensions(target):攔截Object.preventExtensions(proxy)六剥,返回一個布爾值晚顷。
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy)峰伙,返回一個對象。
- isExtensible(target):攔截Object.isExtensible(proxy)该默,返回一個布爾值瞳氓。
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值栓袖。如果目標對象是函數(shù)匣摘,那么還有兩種額外操作可以攔截。
- apply(target, object, args):攔截 Proxy 實例作為函數(shù)調(diào)用的操作裹刮,比如proxy(...args)音榜、proxy.call(object, ...args)、proxy.apply(...)捧弃。
- construct(target, args):攔截 Proxy 實例作為構(gòu)造函數(shù)調(diào)用的操作赠叼,比如new proxy(...args)。
Proxy this 問題
雖然 Proxy 可以代理針對目標對象的訪問违霞,但它不是目標對象的透明代理嘴办,即不做任何攔截的情況下,也無法保證與目標對象的行為一致买鸽。主要原因就是在 Proxy 代理的情況下涧郊,目標對象內(nèi)部的this關(guān)鍵字會指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
上面代碼中眼五,一旦proxy代理target.m妆艘,后者內(nèi)部的this就是指向proxy彤灶,而不是target。
下面是一個例子批旺,由于this指向的變化枢希,導(dǎo)致 Proxy 無法代理目標對象。
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
上面代碼中朱沃,目標對象jane的name屬性苞轿,實際保存在外部WeakMap對象_name上面,通過this鍵區(qū)分逗物。由于通過proxy.name訪問時搬卒,this指向proxy,導(dǎo)致無法取到值翎卓,所以返回undefined契邀。
此外,有些原生對象的內(nèi)部屬性失暴,只有通過正確的this才能拿到坯门,所以 Proxy 也無法代理這些原生對象的屬性。
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
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 === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
三黍檩、Proxy 與Object.defineProperty
使用Object.defineProperty
ES5 提供了 Object.defineProperty 方法叉袍,該方法可以在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性刽酱,并返回這個對象喳逛。
- Object.defineProperty(obj, prop, descriptor)
參數(shù):- obj: 要在其上定義屬性的對象。
- prop: 要定義或修改的屬性的名稱棵里。
- descriptor: 將被定義或修改的屬性的描述符润文。
下面一個簡單的輸入框變化,數(shù)據(jù)展示
var obj = {
value:''
}
var value = '';
Object.defineProperty(obj,"value",{
get:function(){
return value
},
set:function(newVal){
value = newVal
}
})
document.querySelector('#input').oninput = function(){
var value = this.value;
obj.value = value;
document.querySelector('#text').innerHTML = obj.value;
}
當然一般不會只是改變一個屬性,如下所示衍慎,遍歷劫持對象所有的屬性
//要劫持的對象
const data = {
name:''
}
//遍歷對象,對其屬性值進行劫持
Object.keys(data).forEach(function(key){
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
// 當屬性值發(fā)生變化時我們可以進行額外操作
console.log(`我修改了成了${newVal}`);
},
})
})
data.name = 'gg'
這個只是簡單的劫持
缺點:Object.defineProperty的第一個缺陷,無法監(jiān)聽數(shù)組變化
區(qū)別:
- Proxy可以直接監(jiān)聽對象而非屬性
- Proxy直接可以劫持整個對象,并返回一個新對象,不管是操作便利程度還是底層功能上都遠強于Object.defineProperty转唉。
- Proxy可以直接監(jiān)聽數(shù)組的變化
- Proxy有多達13種攔截方法,不限于apply、ownKeys稳捆、deleteProperty赠法、has等等是Object.defineProperty不具備的。