1. 程式人生 > >ES6學習筆記10 Proxy

ES6學習筆記10 Proxy

Proxy簡介

Proxy用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“超程式設計”(meta programming),即對程式語言進行程式設計。

Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。

ES6 原生提供 Proxy 建構函式,用來生成 例項

var proxy = new Proxy(target, handler);
//其中 target表示所要攔截的目標物件,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

上述程式碼對一個空物件假設了一層攔截,重新定義了屬性的getset操作,返回一個Proxy,其中引數target為攔截物件,key為接受到的鍵名,value為接受到的鍵值,receiver為一個可選的物件。因此,當對返回的Proxy讀取和修改屬性值時,就會呼叫相應的自定義方法,在控制檯輸出字串。

同一個hander物件內可以設定多個攔截操作,對於沒有設定的操作,遵從原先的方式產生結果。注意,要使得Proxy起作用,必須針對Proxy例項進行操作,而不是針對目標物件進行操作,否則定義的攔截操作不會起作用。

Proxy支援的攔截操作包括

(1)get(target, propKey, receiver):攔截物件屬性的讀取,比如proxy.fooproxy['foo']

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 // 丟擲一個錯誤

上面程式碼表示,如果訪問目標物件不存在的屬性,會丟擲一個錯誤。如果沒有這個攔截函式,訪問不存在的屬性,只會返回undefined

get方法可以繼承

let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

第三個引數receiver表示原始操作所在的物件

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true  原始讀操作就在proxy物件

const d = Object.create(proxy);
d.a === d // true    原始讀操作在d物件

如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 物件訪問該屬性會報錯

(2) set(target, propKey, value, receiver):攔截物件屬性的設定,比如proxy.foo = vproxy['foo'] = v,返回一個布林值。

結合getset方法,就可以做到防止某些以下劃線(_)開頭,表示內部的屬性被外部讀寫。

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

上述程式碼,只要讀寫屬性的第一個字元是下劃線,則一律報錯,達到禁止讀寫內部屬性的目的

同樣與get操作類似,set操作的receiver引數代表原始操作行為所在的那個物件

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);

proxy.foo = 'bar';
proxy.foo === proxy // true

const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = 'bar';
myObj.foo === myObj // true

注意,如果目標物件自身的某個屬性,不可寫且不可配置,那麼set方法將不起作用。

(3)apply(target, object, args):攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)。apply方法可以接受三個引數,分別是目標物件、目標物件的上下文物件(this)和目標物件的引數陣列。

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 的例項,當它作為函式呼叫時(p()),就會被apply方法攔截,返回一個字串。

另外,直接呼叫Reflect.apply方法,也會被攔截。

Reflect.apply(p, null, []) // 'I am the proxy'

(4)has(target, propKey):攔截propKey in proxy的操作,返回一個布林值。判斷物件是否具有某個屬性時,這個方法會生效。典型的操作就是in運算子。

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

上面程式碼中,如果原物件的屬性名的第一個字元是下劃線,proxy.has就會返回false,從而不會被in運算子發現。

如果原物件不可配置或者禁止擴充套件,這時has攔截會報錯。has方法攔截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判斷一個屬性是物件自身的屬性,還是繼承的屬性。雖然for…in迴圈也用到了in運算子,但是has攔截對for…in迴圈不生效。

(5)construct(target,args):攔截Proxy例項作為建構函式呼叫的操作,比如new proxy(...args)。返回的必須是一個物件,否則會報錯

construct方法可以接受兩個引數:

  • target:目標物件
  • args:建構函式的引數物件
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

(6)deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布林值。如果這個方法丟擲錯誤或者返回false,當前屬性就無法被delete命令刪除。

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    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

目標物件自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯

(7)defineProperty(target,propKey,propDesc):攔截Object.defineProperty()Object.defineProperties(),返回一個布林值。如果返回false,則導致新新增的屬性總是無效

var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不會生效

注意,如果目標物件不可擴充套件(extensible),則defineProperty不能增加目標物件上不存在的屬性,否則會報錯。另外,如果目標物件的某個屬性不可寫(writable)或不可配置(configurable),則defineProperty方法不得改變這兩個設定。

(8)getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述物件或undefined

(9)getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個物件。返回值必須是物件或null,另外如果物件不可擴充套件,則必須返回目標物件的原型物件

(10)isExtensible(target):攔截Object.isExtensible(proxy),返回一個布林值。該方法有個限制,其返回值必須跟物件的isExtensible屬性保持一致,也就是如果物件可以擴充套件,你必須返回true

(11)preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布林值。該方法有個限制,只有當目標物件不可擴充套件時,才能返回true,否則會報錯。

var p = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(p) // 報錯

為了防止出現這個問題,通常要在proxy.preventExtensions方法裡面,呼叫一次Object.preventExtensions

(12)ownKeys(target):攔截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in迴圈,返回一個數組。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。

下面的例子是攔截第一個字元為下劃線的屬性名不被遍歷。

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

ownKeys方法返回的陣列成員,只能是字串或 Symbol 值。如果有其他型別的值,或者返回的根本不是陣列,就會報錯。

如果目標物件自身包含不可配置的屬性,則該屬性必須被ownKeys方法返回,否則報錯。如果目標物件是不可擴充套件的(non-extensition),這時ownKeys方法返回的陣列之中,必須包含原物件的所有屬性,且不能包含多餘的屬性,否則報錯。

(13)setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布林值。

如果目標物件不可擴充套件(extensible),setPrototypeOf方法不得改變目標物件的原型。

Proxy.revocable()

該方法返回一個可取消的Proxy例項和一個取消函式,呼叫該函式,可以取消Proxy例項,不再允許訪問

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable的一個使用場景是,目標物件不允許直接訪問,必須通過代理訪問,一旦訪問結束,就收回代理權,不允許再次訪問。

this問題

在 Proxy 代理的情況下,目標物件內部的this關鍵字會指向 Proxy 代理。將this繫結原始物件可以解決這個問題

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();   //有些原生物件的內部屬性,只有通過正確的this才能拿到
// TypeError: this is not a Date object.

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);   //繫結this至目標物件
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate() // 1