眾所周知军浆,vue3.0快要來(lái)了,用ts進(jìn)行了重構(gòu)椅挣,而且用Proxy替代了Object.defineProperty()头岔,所以在3.0真的到來(lái)之前,先了解一下Proxy鼠证。
Proxy
Proxy 用于修改某些操作的默認(rèn)行為峡竣,等同于在語(yǔ)言層面做出修改,所以屬于一種“元編程”量九,即對(duì)編程語(yǔ)言進(jìn)行編程适掰。
Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”荠列,外界對(duì)該對(duì)象的訪問(wèn)类浪,都必須先通過(guò)這層攔截,因此提供了一種機(jī)制肌似,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫费就。
ES6 原生提供 Proxy 構(gòu)造函數(shù),用來(lái)生成 Proxy 實(shí)例
var proxy = new Proxy(target, handler);
new Proxy()表示生成一個(gè)Proxy實(shí)例川队,target參數(shù)表示所要攔截的目標(biāo)對(duì)象力细,handler是一個(gè)配置的對(duì)象,對(duì)于每一個(gè)被代理的操作固额,需要提供一個(gè)對(duì)應(yīng)的處理函數(shù)艳汽,該函數(shù)將攔截對(duì)應(yīng)的操作。
要使得Proxy起作用对雪,必須針對(duì)Proxy實(shí)例(上例是proxy對(duì)象)進(jìn)行操作河狐,而不是針對(duì)目標(biāo)對(duì)象(上例是空對(duì)象)進(jìn)行操作。如果handler沒(méi)有設(shè)置任何攔截瑟捣,那就等同于直接通向原對(duì)象馋艺。
下面看下handler支持配置的鏈接操作
let obj = new Proxy({}, {
// 攔截對(duì)象屬性的讀取,比如proxy.foo和proxy['foo']
get: function (target, propKey, receiver) {
console.log('sonme code ....')
},
// 攔截對(duì)象屬性的設(shè)置,比如proxy.foo = v或proxy['foo'] = v迈套,返回一個(gè)布爾值
set: function (target, propKey, value, receiver) {
console.log('sonme code ....')
},
// 攔截propKey in proxy的操作捐祠,返回一個(gè)布爾值
has: function (target, propKey, receiver) {
console.log('sonme code ....')
},
// 攔截delete proxy[propKey]的操作,返回一個(gè)布爾值桑李。
deleteProperty: function (target, propKey) {
console.log('sonme code ....')
},
// 攔截Object.getOwnPropertyNames(proxy)踱蛀、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)贵白、for...in循環(huán)率拒,
// 返回一個(gè)數(shù)組。該方法返回目標(biāo)對(duì)象所有自身的屬性的屬性名禁荒,
// 而Object.keys()的返回結(jié)果僅包括目標(biāo)對(duì)象自身的可遍歷屬性猬膨。
ownKeys:function(target){
console.log('sonme code ....')
},
// 攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對(duì)象
getOwnPropertyDescriptor: function (target, propKey) {
console.log('sonme code ....')
},
// 攔截Object.defineProperty(proxy, propKey, propDesc)呛伴、Object.defineProperties(proxy, propDescs)勃痴,返回一個(gè)布爾值谒所。
defineProperty: function (target, propKey, propDesc) {
console.log('sonme code ....')
},
// 攔截Object.preventExtensions(proxy),返回一個(gè)布爾值沛申。
preventExtensions: function (target) {
console.log('sonme code ....')
},
// 攔截Object.getPrototypeOf(proxy)劣领,返回一個(gè)對(duì)象
getPrototypeOf: function (target) {
console.log('sonme code ....')
},
// 攔截Object.isExtensible(proxy),返回一個(gè)布爾值铁材。
isExtensible: function (target) {
console.log('sonme code ....')
},
// 攔截Object.setPrototypeOf(proxy, proto)剖踊,返回一個(gè)布爾值。
// 如果目標(biāo)對(duì)象是函數(shù)衫贬,那么還有兩種額外操作可以攔截德澈。
setPrototypeOf: function (target, proto) {
console.log('sonme code ....')
},
// 攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作,比如proxy(...args)固惯、
// proxy.call(object, ...args)梆造、proxy.apply(...)。
apply: function (target, object, args) {
console.log('sonme code ....')
},
// 攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作葬毫,比如new proxy(...args)镇辉。
construct:function (target, args) {
}
});
下面詳細(xì)介紹一下這些攔截方法
get()
get方法用于攔截某個(gè)屬性的讀取操作,可以接受三個(gè)參數(shù)贴捡,依次為目標(biāo)對(duì)象忽肛、屬性名和 proxy 實(shí)例本身(嚴(yán)格地說(shuō),是操作行為所針對(duì)的對(duì)象)烂斋,其中最后一個(gè)參數(shù)可選
下面的例子使用get攔截屹逛,實(shí)現(xiàn)數(shù)組讀取負(fù)數(shù)的索引。
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
set()
set方法用來(lái)攔截某個(gè)屬性的賦值操作汛骂,可以接受四個(gè)參數(shù)罕模,依次為目標(biāo)對(duì)象、屬性名帘瞭、屬性值和 Proxy 實(shí)例本身淑掌,其中最后一個(gè)參數(shù)可選。
假定Person對(duì)象有一個(gè)age屬性蝶念,該屬性應(yīng)該是一個(gè)不大于 200 的整數(shù)抛腕,那么可以使用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');
}
}
// 對(duì)于滿足條件的 age 屬性以及其他屬性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報(bào)錯(cuò)
person.age = 300 // 報(bào)錯(cuò)
apply()
apply方法攔截函數(shù)的調(diào)用媒殉、call和apply操作担敌。
apply方法可以接受三個(gè)參數(shù),分別是目標(biāo)對(duì)象适袜、目標(biāo)對(duì)象的上下文對(duì)象(this)和目標(biāo)對(duì)象的參數(shù)數(shù)組柄错。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
上面代碼中舷夺,變量p是 Proxy 的實(shí)例苦酱,當(dāng)它作為函數(shù)調(diào)用時(shí)(p())售貌,就會(huì)被apply方法攔截,返回一個(gè)字符串疫萤。
has()
has方法用來(lái)攔截HasProperty操作颂跨,即判斷對(duì)象是否具有某個(gè)屬性時(shí),這個(gè)方法會(huì)生效扯饶。典型的操作就是in運(yùn)算符恒削。
has方法可以接受兩個(gè)參數(shù),分別是目標(biāo)對(duì)象尾序、需查詢的屬性名钓丰。
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
如果原對(duì)象不可配置或者禁止擴(kuò)展,has攔截會(huì)報(bào)錯(cuò).
construct()
construct方法用于攔截new命令每币,construct方法可以接受三個(gè)參數(shù)携丁。
target:目標(biāo)對(duì)象
args:構(gòu)造函數(shù)的參數(shù)對(duì)象
newTarget:創(chuàng)造實(shí)例對(duì)象時(shí),new命令作用的構(gòu)造函數(shù)
construct方法返回的必須是一個(gè)對(duì)象兰怠,否則會(huì)報(bào)錯(cuò)梦鉴。
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value
// "called: 1"
// 10
deleteProperty()
deleteProperty方法用于攔截delete操作,如果這個(gè)方法拋出錯(cuò)誤或者返回false揭保,當(dāng)前屬性就無(wú)法被delete命令刪除
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
目標(biāo)對(duì)象自身的不可配置(configurable)的屬性肥橙,不能被deleteProperty方法刪除,否則報(bào)錯(cuò)
defineProperty()
defineProperty方法攔截了Object.defineProperty操作
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不會(huì)生效
defineProperty方法返回false秸侣,導(dǎo)致添加新屬性總是無(wú)效.
如果目標(biāo)對(duì)象不可擴(kuò)展(non-extensible)存筏,則defineProperty不能增加目標(biāo)對(duì)象上不存在的屬性,否則會(huì)報(bào)錯(cuò)味榛。另外方篮,如果目標(biāo)對(duì)象的某個(gè)屬性不可寫(writable)或不可配置(configurable),則defineProperty方法不得改變這兩個(gè)設(shè)置
getOwnPropertyDescriptor()
getOwnPropertyDescriptor方法攔截Object.getOwnPropertyDescriptor()励负,返回一個(gè)屬性描述對(duì)象或者undefined
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
getPrototypeOf()
getPrototypeOf()方法主要用來(lái)攔截獲取對(duì)象原型藕溅。具體來(lái)說(shuō),攔截下面這些操作继榆。
Object.prototype.proto
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
getPrototypeOf方法攔截Object.getPrototypeOf()巾表,返回proto對(duì)象
getPrototypeOf方法的返回值必須是對(duì)象或者null,否則報(bào)錯(cuò)略吨。另外集币,如果目標(biāo)對(duì)象不可擴(kuò)展(non-extensible), getPrototypeOf方法必須返回目標(biāo)對(duì)象的原型對(duì)象
isExtensible()
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
上面代碼設(shè)置了isExtensible方法翠忠,在調(diào)用Object.isExtensible時(shí)會(huì)輸出called鞠苟。
注意,該方法只能返回布爾值,否則返回值會(huì)被自動(dòng)轉(zhuǎn)為布爾值当娱。
這個(gè)方法有一個(gè)強(qiáng)限制吃既,它的返回值必須與目標(biāo)對(duì)象的isExtensible屬性保持一致,否則就會(huì)拋出錯(cuò)誤.
ownKeys()
ownKeys方法用來(lái)攔截對(duì)象自身屬性的讀取操作跨细。具體來(lái)說(shuō)鹦倚,攔截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in循環(huán)
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'a' ]
preventExtensions()
preventExtensions方法攔截Object.preventExtensions()冀惭。該方法必須返回一個(gè)布爾值震叙,否則會(huì)被自動(dòng)轉(zhuǎn)為布爾值。
這個(gè)方法有一個(gè)限制散休,只有目標(biāo)對(duì)象不可擴(kuò)展時(shí)(即Object.isExtensible(proxy)為false)媒楼,proxy.preventExtensions才能返回true,否則會(huì)報(bào)錯(cuò)
var proxy = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
// 為了防止出現(xiàn)這個(gè)問(wèn)題戚丸,通常要在proxy.preventExtensions方法里面匣砖,調(diào)用一次Object.preventExtensions
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(proxy)
// "called"
// Proxy {}
setPrototypeOf()
setPrototypeOf方法主要用來(lái)攔截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);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
Proxy.revocable()
Proxy.revocable方法返回一個(gè)可取消的 Proxy 實(shí)例
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable方法返回一個(gè)對(duì)象昏滴,該對(duì)象的proxy屬性是Proxy實(shí)例猴鲫,revoke屬性是一個(gè)函數(shù),可以取消Proxy實(shí)例谣殊。上面代碼中拂共,當(dāng)執(zhí)行revoke函數(shù)之后,再訪問(wèn)Proxy實(shí)例姻几,就會(huì)拋出一個(gè)錯(cuò)誤宜狐。
Proxy.revocable的一個(gè)使用場(chǎng)景是,目標(biāo)對(duì)象不允許直接訪問(wèn)蛇捌,必須通過(guò)代理訪問(wèn)抚恒,一旦訪問(wèn)結(jié)束,就收回代理權(quán)络拌,不允許再次訪問(wèn)
this問(wèn)題
Proxy 代理的情況下俭驮,目標(biāo)對(duì)象內(nèi)部的this關(guān)鍵字會(huì)指向 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
兼容性
本文參考:
ECMAScript6 入門