前提摘要 尤大大的vue3.0即將到來,雖然學不動了,但是還要學的啊,據說vue3.0是基於proxy來進行對值進行攔截並操作,所以es6的proxy也是要學習一下的。 一 什麼是proxy Proxy 對象用於定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等) 摘自MDN Proxy ...
前提摘要
尤大大的vue3.0即將到來,雖然學不動了,但是還要學的啊,據說vue3.0是基於proxy來進行對值進行攔截並操作,所以es6的proxy也是要學習一下的。
一 什麼是proxy
Proxy 對象用於定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等) --摘自MDN
Proxy 用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“元編程”(meta programming),即對編程語言進行編程。 --摘自阮一峰的ES6入門
Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,可以譯為“代理器”。
Proxy 也可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
總結來說:Proxy對象就是要在目標對象上設置自定義的規則和方法,讓它按照自己定義的規則去實行某些操作。
二 Proxy聲明
ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例,所以可以按照構造函數創建對象的形式去實例化一個Proxy對象。
var proxy = new Proxy({},{})
console.log(proxy) // Proxy{}
註意點:
1 實例化一個Proxy對象時,必須要傳兩個參數對象,否則會報錯:Uncaught TypeError: Cannot create proxy with a non-object as target or handler,不能創建沒有對象的proxy對象。
2 傳兩個空對象時,預設的是簡單聲明瞭一個Proxy實例,(好像沒啥卵用……)
參數對象解釋:
第一個參數:target,目標對象,是你要代理的對象.它可以是JavaScript中的任何合法對象.如: (數組, 對象, 函數等等)
tip:var arr = [] var obj = {} var Person = class{} var foo = function (){} console.log(Person instanceof Object) // true console.log(foo instanceof Object) // true console.log(arr instanceof Object) // true console.log(obj instanceof Object) // true
第二個參數:handler,配置對象,用來定製攔截行為,對於每一個被代理的操作,需要提供一個對應的處理函數,該函數將攔截對應的操作。
Proxy支持的攔截操作,有13種,使用方法可以參考 阮一峰的ES6入門
三 常見情況
3.1 當目標對象為空時
var proxy = new Proxy({},handler)
這樣直接代表著,攔截的對象是空的,所以直接對proxy對象進行操控。
var target = {};
var handler = {
get(target,propKey,receiver){
return 'peter'
}
};
var proxy = new Proxy(target, handler);
proxy.name = 'tom';
console.log(proxy.name) // tom
console.log(target.name) // undefined
上面的代碼說明瞭:target是個空對象,但是操作了proxy,也影響不了target
ps:要使得Proxy起作用,必須針對Proxy實例進行操作,而不是針對目標對象進行操作
3.2 當攔截對象為空時
var proxy = new Proxy(target,{})
handler沒有設置任何攔截,那就等同於直接通向原對象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.name = 'peter';
console.log(proxy.name) // peter
console.log(target.name) // peter
上面的代碼說明瞭:handler是一個空對象,沒有任何攔截效果,訪問proxy就等同於訪問target
四 方法解析
Proxy實例化的對象預設帶有get和set方法。也可以在這些基礎上進行攔截操作,其他的13種方法也是如此。
- get() 用於攔截某個屬性的讀取(read)操作,換句話講,就是在讀取目標對象的屬性之前,操作該屬性。
參數解釋:- target:目標對象
- property:屬性名
receiver: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 // Property "age" does not exist.
參考阮一峰的例子,上述說明瞭,如果輸入目標函數不存在的屬性,就直接報錯。
- set() 用來攔截目標對象的賦值(write)操作
參數解釋:- target:目標對象
- propertyName:屬性名
- propertyValue:屬性值
receiver:Proxy實例本身
例子:var target = {} var handler = { set(target, propKey, value, receiver) { if (typeof value !== 'string') { target[propKey] = String(value); }else{ target[propKey] = value; } } } var proxy = new Proxy(target, handler) proxy.name = 'peter' proxy.age = 25 console.log(typeof proxy.name) // string console.log(typeof proxy.age) // string
上面例子就是攔截對象是不是字元串,不是字元串的話會強制轉化為字元串。
- apply() 用來攔截函數的調用、call和apply操作
參數解釋:- target:目標對象
- context:目標對象的上下文對象(this)
arguments:目標對象的參數數組
例子:var target = function(a,b){ return 10 + a + b } var handler = { apply(target,context,arguments){ arguments[0] = 10 arguments[1] = 20 return arguments.reduce(function(prev, curr, idx, arr){ return prev + curr; }); } } var proxy = new Proxy(target,handler) console.log(proxy(1,2)) // 30
上面的例子,就是目標函數是要傳兩個參數,並且返回之和,攔截目標做的就是改變目標對象的參數,並且求和,所以這樣寫觸發了apply方法,返回30,而不是13
- has() 用來攔截hasProperty操作,即判斷對象是否具有某個屬性時,這個方法會生效。典型的操作就是in運算符。
參數解釋:- target:目標對象
key: 需查詢的屬性名,是一個字元串!!!!!
例子:var target = { name: 'peter', age:25 } var handler = { has(target,key){ return key in target; } } var proxy = new Proxy(target,handler) console.log('age' in proxy) // true console.log('colors' in proxy) // false
上面的例子是典型的has的方法,判斷所要查詢的屬性名是不是在目標對象上的屬性名,返回布爾值。
ps:has攔截對for...in迴圈不生效。
- construct() 用於攔截new命令,要返回是一個對象,否則會報錯
參數解釋:- target:目標對象
- args:構造函數的參數對象
newTarget:創造實例對象時,new命令作用的構造函數
例子: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
由此可見,是針對構造函數而言的,對目標對象的構造函數進行攔截。
- defineProperty() 攔截了Object.defineProperty操作,在聲明時進行攔截,設置的是一個布爾值
- 參數解釋:
- target:目標對象
- key:要定義或修改的屬性的名稱
- descriptor: 將被定義或修改的屬性描述符,是一個對象
拓展:
Object.defineProperty(),聲明對象的屬性,參數說明和上述一樣
例子:var obj = {} Object.defineProperty(obj, "key", { enumerable: false, configurable: false, writable: false, value: "static" });
例子:
var target = { name: 'peter', age:25 } var handler = { defineProperty(target,key,descriptor){ if(key === 'color'){ throw new Error('不能定義顏色') } Object.defineProperty(target, key, descriptor) // return true } } var proxy = new Proxy(target,handler) var descriptor = { writable : true, enumerable : true, configurable : true } descriptor.value = 'sport' Object.defineProperty(proxy, 'favor', descriptor) console.log(proxy.favor) // sport descriptor.value = 'red' Object.defineProperty(proxy, 'color', descriptor) // 不能定義顏色 console.log(proxy.color)
如果目標對象不可擴展(non-extensible),則defineProperty不能增加目標對象上不存在的屬性,否則會報錯。另外,如果目標對象的某個屬性不可寫(writable)或不可配置(configurable),則defineProperty方法不得改變這兩個設置。
- 參數解釋:
- deleteProperty() 用於攔截delete操作,如果這個方法拋出錯誤或者返回false,當前屬性就無法被delete命令刪除。
- 參數解釋:
- target:目標對象
- key:要刪除的屬性名
(delete是關鍵字,目前用到的就是刪除對象的某個屬性)
例子:
var target = { _prop: 'foo' }; var handler = { deleteProperty (target, key) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${target} private "${key}" property`); } delete target[key]; return true; } }; var proxy = new Proxy(target, handler); delete proxy._prop // Error: Invalid attempt to delete private "_prop" property
上面代碼中,deleteProperty方法攔截了delete操作符,刪除第一個字元為下劃線的屬性會報錯。
註意,目標對象自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯。
- 參數解釋:
- getOwnPropertyDescriptor() 攔截Object.getOwnPropertyDescriptor(),返回一個屬性描述對象或者undefined。
- 參數解釋:
- target:目標對象
- key: 屬性名
- 拓展:
Object.getOwnPropertyDescriptor(obj,prop) 返回指定對象上一個自有屬性對應的屬性描述符- 參數解釋:
- obj:需要查找的目標對象
- prop: 目標對象內屬性名稱
- 返回值:
如果指定的屬性存在於對象上,則返回其屬性描述符對象(property descriptor),否則返回 undefined。 例子:
o = { bar: 42 }; d = Object.getOwnPropertyDescriptor(o, "bar"); console.log(d) // d { // configurable: true, // enumerable: true, // value: 42, // writable: true // }
- 參數解釋:
例子:
var target = { _foo: 'bar', baz: 'tar' }; var handler = { getOwnPropertyDescriptor (target, key) { if (key[0] === '_') { return; } return Object.getOwnPropertyDescriptor(target, key); } }; 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 }
上述說明:對於第一個字元為下劃線的屬性名會返回undefined。
- 參數解釋:
- getPrototypeOf() 用來攔截獲取對象原型,主要攔截以下操作:
- 如下:
- Object.prototype.__proto__
-- 該特性已經從 Web 標準中刪除 - Object.prototype.isPrototypeOf()
用於測試一個對象是否存在於另一個對象的原型鏈上。- 參數:
- object 在該對象的原型鏈上搜尋
- 返回值:
- 返回值 表示調用對象是否在另一個對象的原型鏈上。布爾值
例子:
function Baz() {} var baz = new Baz(); console.log(Baz.prototype.isPrototypeOf(baz)); // true
- 參數:
- Object.getPrototypeOf(obj) 返回指定對象的原型(內部[[Prototype]]屬性的值)
- 參數:
- obj 返回其原型的對象
- 返回值:
給定對象的原型。如果沒有繼承屬性,則返回 null 。 例子:
var proto = {}; var obj = Object.create(proto); Object.getPrototypeOf(obj) === proto; // true
- 參數:
- Reflect.getPrototypeOf()
- instanceof
- Object.prototype.__proto__
例子:
上面代碼中,getPrototypeOf方法攔截Object.getPrototypeOf(),返回proto對象。var proto = {}; var p = new Proxy({}, { getPrototypeOf(target) { return proto; } }); Object.getPrototypeOf(p) === proto // true
ps:- getPrototypeOf方法的返回值必須是對象或者null,否則報錯
- 如果目標對象不可擴展(non-extensible), getPrototypeOf方法必須返回目標對象的原型對象。
- 如下:
- isExtensible() 攔截Object.isExtensible()操作
- Object.isExtensible() 判斷是否可以為對象添加新的屬性
參數:- object 要進行判斷的對象
返回值: 是個布爾值,true是可以添加,false不可以添加
例子:let obj = { name: 'peter', age:25 } console.log(Object.isExtensible(obj)) // true
- object 要進行判斷的對象
例子:
var p = new Proxy({}, { isExtensible: function(target) { console.log("called"); return true; } }); Object.isExtensible(p) // called
這個方法有一個強限制,它的返回值必須與目標對象的isExtensible屬性保持一致,否則就會拋出錯誤。
即:Object.isExtensible(proxy) === Object.isExtensible(target)
- Object.isExtensible() 判斷是否可以為對象添加新的屬性
- preventExtensions() 攔截Object.preventExtensions()操作
- Object.preventExtensions(object) 不能再為此對象添加新的屬性或者方法
- 參數:object,要成為不可擴展的對象的對象
- 無返回值
例子
let obj = { name: 'peter', age:25 } Object.preventExtensions(obj) obj.color = 'red' // object is not extensible
- 例子
————————————
- Object.preventExtensions(object) 不能再為此對象添加新的屬性或者方法
- setPrototypeOf() 攔截Object.setPrototypeOf方法
- Object.setPrototypeOf(obj,proto) 設置對象的原型
此方法修改的是對象實例的內部屬性[[Prototype]],也就是__proto__屬性所指向的對象,它只是修改了特定對象上的原型對象,對於構造函數的prototype指向的原型對象沒有影響- 參數:
- obj 對其設置原型的對象
- proto 新的原型對象
例子:
let proto = {
color: red
};
let obj = {
name: 'peter'
age: 26
};
Object.setPrototypeOf(obj, proto);
console.log(obj.color); // red
- 參數:
例子
上面代碼中,只要修改target的原型對象,就會報錯。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
- ps
- 該方法只能返回布爾值,否則會被自動轉為布爾值
- 如果目標對象不可擴展(non-extensible),setPrototypeOf方法不得改變目標對象的原型
- Object.setPrototypeOf(obj,proto) 設置對象的原型
- ownKeys() 用來攔截對象自身屬性的讀取操作
- 如下:
- Object.getOwnPropertyNames(obj) 獲取對象的屬性名稱,並存儲在數組中。
- 參數解釋: obj 要獲取屬性名稱的對象
- 返回值:存放屬性名稱的數組
- 只能拿到對象的自有屬性,拿不到原型上的屬性,另外,defineProperty的值也可以拿到
例子:
function Person(){ this.name = 'peter' this.age = 26 } Person.prototype={ address:"北京" } let peter=new Person(); console.log(Object.getOwnPropertyNames(peter)); // ['name','age']
- Object.getOwnPropertySymbols(obj) 返回一個給定對象自身的所有 Symbol 屬性的數組
- 參數解釋: obj 要返回 Symbol 屬性的對象
- 返回值: 在給定對象自身上找到的所有 Symbol 屬性的數組。
例子:
var obj = {}; var a = Symbol("a"); var b = Symbol.for("b"); obj[a] = "localSymbol"; obj[b] = "globalSymbol"; var objectSymbols = Object.getOwnPropertySymbols(obj); console.log(objectSymbols.length); // 2 console.log(objectSymbols) // [Symbol(a), Symbol(b)] console.log(objectSymbols[0]) // Symbol(a)
- Object.keys() 返回一個由一個給定對象的自身可枚舉屬性組成的數組,數組中屬性名的排列順序和使用 for...in 迴圈遍歷該對象時返回的順序一致
- 參數:obj 要返回其枚舉自身屬性的對象
- 返回值: 一個表示給定對象的所有可枚舉屬性的字元串數組
例子:
var obj = { 0: 'a', 1: 'b', 2: 'c' }; console.log(Object.keys(obj)); // ['0', '1', '2']
- for...in迴圈
————————————
- Object.getOwnPropertyNames(obj) 獲取對象的屬性名稱,並存儲在數組中。
例子之一:
var p = new Proxy({}, { ownKeys: function(target) { return ['a', 'b', 'c']; } }); Object.getOwnPropertyNames(p) // [ 'a', 'b', 'c' ]
- 如下:
五 Proxy.revocable(target, handler)
返回一個可取消的 Proxy 實例
- 參數解釋:
- target 用Proxy包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
- handler 攔截對象,其屬性是當執行一個操作時定義代理的行為的函數。
- 返回值
返回一個包含了所生成的代理對象本身以及該代理對象的撤銷方法的對象
其結構為: {"proxy": proxy, "revoke": revoke},其中:- proxy
表示新生成的代理對象本身,和用一般方式 new Proxy(target, handler) 創建的代理對象沒什麼不同,只是它可以被撤銷掉 - revoke
撤銷方法,調用的時候不需要加任何參數,就可以撤銷掉和它一起生成的那個代理對象
- proxy
例子:
var revocable = Proxy.revocable({}, { get(target, propKey) { return propKey + '啦啦啦'; } }); var proxy = revocable.proxy; console.log(proxy.foo) // foo啦啦啦 revocable.revoke(); // 執行撤銷方法 console.log(proxy.foo); // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
總結
剛開始學proxy時,都是懵逼的狀態,阮一峰ES6入門一開始看代碼有點難度,所以我一邊看一邊查資料,裡面關於Object對象的方法居多,也順便學習了一下,知識很多,需要日常回顧加深理解,經過查閱,對於代理模式 Proxy 的作用主要體現在三個方面:1攔截和監視外部對對象的訪問,2降低函數或類的複雜度,3在複雜操作前對操作進行校驗或對所需資源進行管理,目前還沒有大量運用,最常見的應該是攔截和監聽對象的變化吧。
我把筆記放到GitHub里了,如需要可以去看看,有什麼不對的地方,歡迎指正,大家一起進步加油。
參考文獻
阮一峰ES6入門
詳解ES6中的代理模式——Proxy
MDN
[譯] 實例解析 ES6 Proxy 使用場景