JS 中的 Reflect 和 Proxy
Proxy
和Reflect
是 ES6 新增 API。
Reflect
Reflect
是一個內建的物件,它提供攔截 JavaScript 操作的方法。Reflect不是一個函式物件,因此它是不可構造的。Reflect
的所有的方法都是靜態的就和Math
一樣,目前它還沒有靜態屬性。
Reflect
物件的方法與Proxy
物件的方法相同。
Reflect
一共有13個靜態方法:
它可以分為一部分是是原來存在Object
上的方法,將它轉義到了Reflect
上,並作了小改動,讓方法更加合理。
-
defineProperty
與Object.defineProperty 類似,但是當物件無法定義時Object.defineProperty
會報錯而Reflect.defineProperty
不會,它會返回false
,成功時返回true
,如果不是物件還是會報錯。 -
getPrototypeOf(target)
與Object.getPrototypeOf
一樣,返回指定物件的原型。 -
setPrototypeOf(target, prototype)
與Object.setPrototypeOf
一樣,它將指定物件的原型設定為另外一個物件。 -
getOwnPropertyDescriptor()
與Object.getOwnPropertyDescriptor
一樣,如果在物件中存在,則返回給定的屬性的屬性描述符。 -
isExtensible(target)
與Object.isExtensible
類似,判斷一個物件是否可擴充套件(是否可以在它上面新增新的屬性),它們的不同點是,當引數不是物件時(原始值),Object
的將它強制轉變為一個物件,Reflect
是直接報錯。 -
preventExtensions(target)
與Object.preventExtensions
類似,阻止新屬性新增到物件,不同點和上一條一樣。 -
apply(func, thisArg, args)
與Function.prototype.apply.call(fn, obj, args)
一樣。 -
ownKeys(target)
與Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
一樣,返回一個包含所有自身屬性(不包含繼承屬性)的陣列
另一部分是將原來操作符的功能,變成函式行為。
-
has(target, key)
與in
操作符一樣,讓判斷操作都變成函式行為。 -
deleteProperty(target, key)
與delete
操作符一樣,讓刪除操作變成函式行為,返回布林值代表成功或失敗。 -
construct(target, argumentsList[, newTarget])
與new
操作符一樣,target
建構函式,第二引數是建構函式引數類陣列,第三個是new.target的值。 -
get(target, key[, receiver])
與obj[key]
一樣,第三個引數是當要取值的key
部署了getter
時,訪問其函式的this
繫結為receiver
物件。 -
set(target, key, value[, receiver])
設定target
物件的key
屬性等於value
,第三個引數和set
一樣。返回一個布林值。
// 老寫法 'assign' in Object // true // 新寫法 Reflect.has(Object, 'assign') // true // 老寫法 Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 // 新寫法 Reflect.apply(Math.floor, undefined, [1.75]) // 1 // 舊寫法 delete myObj.foo; // 新寫法 Reflect.deleteProperty(myObj, 'foo'); // new 的寫法 const instance = new Greeting('張三'); // Reflect.construct 的寫法 const instance = Reflect.construct(Greeting, ['張三']); // 舊寫法 Object.defineProperty(MyDate, 'now', { value: () => Date.now() }); // 新寫法 Reflect.defineProperty(MyDate, 'now', { value: () => Date.now() }); Reflect.get(1, 'foo') // 報錯 Reflect.get(false, 'foo') // 報錯 Reflect.set(1, 'foo', {}) // 報錯 Reflect.set(false, 'foo', {}) // 報錯 // --------------- var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; }, }; var myReceiverObject = { foo: 4, bar: 4, }; Reflect.get(myObject, 'baz', myReceiverObject) // 8 複製程式碼
Proxy
Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等),等同於在語言層面做出修改,所以屬於一種“超程式設計”(meta programming),即對程式語言進行程式設計。
Proxy 就像在目標物件之間的一個代理,任何對目標的操作都要經過代理。代理就可以對外界的操作進行過濾和改寫。
Proxy
是建構函式,它有兩個引數target
和handler
,
target
是用Proxy包裝的目標物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)。
handler
是一個物件,其屬性是當執行一個操作時定義代理的行為的函式。
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.set(target, key, value, receiver); } }); obj.count = 1 //setting count! ++obj.count //getting count! //setting count! //2 複製程式碼
Proxy
只有一個靜態方法revocable(target, handler)
可以用來建立一個可撤銷的代理物件。兩個引數和建構函式的相同。它返回一個包含了所生成的代理物件本身以及該代理物件的撤銷方法的物件。
一旦某個代理物件被撤銷,它將變的幾乎完全不可用,在它身上執行任何的可代理操作都會丟擲 TypeError 異常(注意,可代理操作一共有 14 種,執行這 14 種操作以外的操作不會丟擲異常)。一旦被撤銷,這個代理物件永遠不可能恢復到原來的狀態,同時和它關聯的目標物件以及處理器物件將有可能被垃圾回收掉。呼叫撤銷方法多次將不會有任何效果,當然,也不會報錯。
var revocable = Proxy.revocable({}, { get(target, name) { return "[[" + name + "]]"; } }); // revocable -> {"proxy": proxy, "revoke": revoke} var proxy = revocable.proxy; proxy.foo;// "[[foo]]" revocable.revoke();// 執行撤銷方法 proxy.foo;// TypeError proxy.foo = 1// 同樣 TypeError delete proxy.foo;// 還是 TypeError typeof proxy// "object",因為 typeof 不屬於可代理操作 複製程式碼
handler
引數是代理函式物件,它一共支援 13 種攔截函式。和Reflect
的相同。如果沒有定義某種操作,那麼這種操作會被轉發到目標物件身上。
const proxy = new Proxy({}, { get: function(target, property, receiver) { return receiver; // receiver 總是指向原始的讀操作所在的那個物件,一般情況下就是 Proxy 例項。 } }); proxy.getReceiver === proxy // true 複製程式碼
如果一個屬性不可配置(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 複製程式碼
apply
方法攔截函式的呼叫
、call
和apply
操作。
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" 複製程式碼
defineProperty
方法攔截了Object.defineProperty
操作。
var handler = { defineProperty (target, key, descriptor) { return false; } }; var target = {}; var proxy = new Proxy(target, handler); proxy.foo = 'bar' // 不會生效 // defineProperty 方法返回 false,導致新增新屬性總是無效。 複製程式碼
注意,如果目標物件不可擴充套件(non-extensible),則defineProperty不能增加目標物件上不存在的屬性,否則會報錯。另外,如果目標物件的某個屬性不可寫(writable)或不可配置(configurable),則defineProperty方法不得改變這兩個設定。
getPrototypeOf
方法主要用來攔截獲取物件原型,會以下這些操作:
Object.prototype.__proto__ Object.prototype.isPrototypeOf() Object.getPrototypeOf() Reflect.getPrototypeOf() instanceof
ownKeys
方法用來攔截物件自身屬性的讀取操作,會攔截以下操作:
Object.getOwnPropertyNames() Object.getOwnPropertySymbols() Object.keys() for...in
通過代理,你可以輕鬆地驗證向一個物件的傳值。
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'); } } // The default behavior to store the value obj[prop] = value; } }; let person = new Proxy({}, validator); person.age = 100; console.log(person.age); // 100 person.age = 'young'; // 丟擲異常: Uncaught TypeError: The age is not an integer person.age = 300; // 丟擲異常: Uncaught RangeError: The age seems invalid 複製程式碼
this 指向
雖然 Proxy 可以代理針對目標物件的訪問,但它不是目標物件的透明代理,即不做任何攔截的情況下,也無法保證與目標物件的行為一致。主要原因就是在 Proxy 代理的情況下,目標物件內部的this
關鍵字會指向 Proxy 代理。
const target = { m: function () { console.log(this === proxy); } }; const handler = {}; const proxy = new Proxy(target, handler); target.m() // false proxy.m()// true 複製程式碼
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 複製程式碼