1. 程式人生 > 其它 >JavaScript 中 Proxy 的理解

JavaScript 中 Proxy 的理解

介紹

Proxy 是由 ES6 提供的一種機制,可以對外界的訪問進行過濾和改寫;可以理解成在目標物件之前架設一層 "攔截", 外界對這個物件的訪問,都必須先通過這層攔截;
Proxy 這個詞的原意是代理,用在這裡表示由它來“代理”某些操作,可以譯為“代理器”;

用法

Proxy 為建構函式,用來生成 Proxy 例項

var proxy = new Proxy(target, handler);

上面的程式碼,new Proxy() 生成一個 Proxy 例項,target 引數表示所要攔截的目標物件,handler 引數也是一個物件,用來指定攔截行為;
一個簡單的示例:

const person = {
	name: "趙雲",
	age: 21
}

const proxy = new Proxy(person, {
	get: function(target, propKey) {
		console.log(target);
		console.log(propKey);
		return target[propKey];
	}
});

// {name: '趙雲', age: 21}
// name
// 趙雲
console.log(proxy.name);

上面的程式碼,建立了一個 person 物件,它有兩個屬性,分別為 name 和 age; 例項化了一個 Proxy ,將 person 作為第一個引數(所要攔截的目標物件),第二個傳入的引數是一個物件,它有一個 get 屬性,值是一個回撥函式;

Proxy 支援的攔截操作

例項化 Proxy 的第二個引數是一個物件,其中可以指定 Proxy 支援的攔截操作;對幾種常見的攔截操作進行舉例說明;

  1. get(target, propKey, receiver)
    get 方法用於攔截某個屬性的讀取操作;
    get 方法接收三個引數,目標物件、屬性名、proxy 例項本身(操作行為所針對的物件);最後一個引數是可選的;
    示例:
    const person = {
    	name: "趙雲"
    }
    
    const proxy = new Proxy(person, {
    	get: function(target, propKey) {
    		if (propKey in target) {
    			return target[propKey];
    		} else {
    			throw new ReferenceError(`屬性名${propKey}不存在`);
    		}
    	}
    });
    
    console.log(proxy.name);    // 趙雲
    console.log(proxy.age);     // Uncaught ReferenceError: 屬性名age不存在
    
  2. set(target,propKey,value,receiver)
    set 方法用來攔截某個屬性的賦值操作;
    set 方法接收四個引數,目標物件、屬性名、屬性值和 Proxy 例項本身;最後一個引數是可選的;
    示例:
    假定 Person 物件有一個 age 屬性,該屬性應該是一個不大於 200 的整數,那麼可以使用 Proxy 保證 age 的屬性值符合要求;
    const person = {
    	age: 20
    }
    
    // 年齡大於 200 報錯
    const proxy = new Proxy(person, {
    	set: function(target, propKey, propValue) {
    		if (propKey === 'age') {
    			if (!Number.isInteger(propValue)) {
    				throw new ReferenceError(`age 不是一個整數`);
    			}else if (propValue > 200) {
    				throw new ReferenceError(`age 不能超過200`);
    			}
    		}
    
    		// 滿足條件的 age 及其他屬性,直接儲存
    		target[propKey] = propValue;
    		return true;
    	}
    });
    proxy.age = 28;
    
    console.log(proxy.age);         // 28
    console.log(proxy.age = "28");  // Uncaught ReferenceError: age 不是一個整數
    console.log(proxy.age = 288);   // Uncaught ReferenceError: age 不能超過200
    
  3. has(target,propKey)
    has() 方法用來攔截 HasProperty 操作,即判斷物件是否具有某個屬性時,這個方法會生效; 典型的操作就是in運算子;
    has() 方法可以接受兩個引數,分別是目標物件、需查詢的屬性名;
  4. deleteProperty(target,propKey)
    deleteProperty() 方法用於攔截 delete 操作,如果這個方法丟擲錯誤或者返回 false,當前屬性就無法被 delete 命令刪除;
  5. ownKeys(target)
    ownKeys() 方法用來攔截物件自身屬性的讀取操作; 具體來說,攔截以下操作;
    • Object.getOwnPropertyNames()
    • Object.getOwnPropertySymbols()
    • Object.keys()
    • for...in迴圈
  6. getOwnPropertyDescriptor(target, propKey)
    getOwnPropertyDescriptor() 方法攔截 Object.getOwnPropertyDescriptor(),返回一個屬性描述物件或者 undefined;
  7. defineProperty(target, propKey, propDesc)
    defineProperty() 方法攔截了 Object.defineProperty() 操作;
  8. preventExtensions(target)
    preventExtensions() 方法攔截 Object.preventExtensions(); 該方法必須返回一個布林值,否則會被自動轉為布林值
    注意:
    這個方法有一個限制,只有目標物件不可擴充套件時(即 Object.isExtensible(proxy) 為 false ),proxy.preventExtensions 才能返回 true,否則會報錯;
  9. getPrototypeOf(target)
    getPrototypeOf() 方法主要用來攔截獲取物件原型; 具體來說,攔截下面這些操作;
    • Object.prototype.proto
    • Object.prototype.isPrototypeOf()
    • Object.getPrototypeOf()
    • Reflect.getPrototypeOf()
    • instanceof
  10. isExtensible(target)
    isExtensible() 方法攔截 Object.isExtensible() 操作;
  11. setPrototypeOf(target, proto)
    setPrototypeOf() 方法主要用來攔截 Object.setPrototypeOf() 方法;
  12. apply(target, object, args)
    apply 方法攔截函式的呼叫、callapply 操作;
    apply 方法接受三個引數,目標物件、目標物件的上下文物件(this)、目標物件的引數陣列;
  13. construct(target, args)
    construct() 方法用於攔截 new 命令,下面是攔截物件的寫法
    const proxy = new Proxy({}, {
    	construct: function (target, args, newTarget) {
    		return new target(...args);
    	}
    });
    
    construct() 方法可以接收三個引數
    1. target:目標物件;
    2. args:建構函式的引數陣列;
    3. newTarget:創造例項物件時,new命令作用的建構函式

取消代理

Proxy.revocable(target, handler);

使用場景

  1. 資料校驗(校驗表單)
    let data = {
    	count: 8
    };
    
    data = new Proxy(data, {
    	set: function(target, propKey, propValue, proxy) {
    		if (propKey === "count" && typeof propValue !== 'number') {
    			// 不是 number 型別丟擲錯誤
    			throw Error(`count 屬性只能設定 number 型別資料`);
    		}
    		return Reflect.set(target, propKey, propValue, proxy);
    	}
    });
    data.count = 88;
    console.log(data.count);        // 88
    console.log(data.count = "99"); // Uncaught Error: count 屬性只能設定 number 型別資料
    
  2. 私有化 api, 防止內部屬性被外部讀寫
    let api = {
    	_apiKey: "123abc456def",
    	getUser: function(userId) {},
    	setUser: function(userId, config) {}
    }
    
    console.log(api._apiKey);   // 此處可用
    const restricted = ['_apiKey'];
    api = new Proxy(api, {
    	set: function(target, propKey, propsValue, proxy) {
    		if (restricted.indexOf(propKey) > -1) {
    			throw Error(`${propKey} 不可訪問`);
    		}
    		return Reflect.set(target, propKey, propsValue, proxy);
    	},
    
    	get: function(target, propKey, proxy) {
    		if (restricted.indexOf(propKey) > -1) {
    			throw Error(`${propKey} 不可訪問`);
    		}
    		return Reflect.get(target, propKey, proxy);
    	}
    });
    
    // 一下操作都會丟擲錯誤
    api._apiKey;
    api._apiKey = "123";
    
  3. 預警,攔截,過濾
  4. 實現觀察者模式
  5. 服務端代理,跨域,取消請求等