天天看點

ES6_Proxy詳解

Proxy 是 ES6 為了操作對象引入的 API 。

Proxy 可以對目标對象的讀取、函數調用等操作進行攔截,然後進行操作處理。它不直接操作對象,而是像代理模式,通過對象的代理對象進行操作,在進行這些操作時,可以添加一些需要的額外操作。

基本用法

一個 Proxy 對象由兩個部分組成: target 、 handler 。在通過 Proxy 構造函數生成執行個體對象時,需要提供這兩個參數。 target 即目标對象, handler 是一個對象,聲明了代理 target 的指定行為。

let target = {
    name: 'Tom',
    age: 24
}
let handler = {
    get: function(target, key) {
        console.log('getting '+key);
        return target[key]; // 不是target.key
    },
    set: function(target, key, value) {
        console.log('setting '+key);
        target[key] = value;
    }
}
let proxy = new Proxy(target, handler)
proxy.name     // 實際執行 handler.get
proxy.age = 25 // 實際執行 handler.set
// getting name
// setting age
// 25
 
           

target也可以為空對象

let targetEpt = {}
let proxyEpt = new Proxy(targetEpt, handler)
// 調用 get 方法,此時目标對象為空,沒有 name 屬性
proxyEpt.name // getting name
// 調用 set 方法,向目标對象中添加了 name 屬性
proxyEpt.name = 'Tom'
// setting name
// "Tom"
// 再次調用 get ,此時已經存在 name 屬性
proxyEpt.name
// getting name
// "Tom"
 
// 通過構造函數建立執行個體時其實是對目标對象進行了淺拷貝,是以目标對象與代理對象會互相
// 影響
targetEpt)
// {name: "Tom"}
 
// handler 對象也可以為空,相當于不設定攔截操作,直接通路目标對象
let targetEmpty = {}
let proxyEmpty = new Proxy(targetEmpty,{})
proxyEmpty.name = "Tom"
targetEmpty) // {name: "Tom"}
           

Proxy執行個體方法:

get( )

get方法用于攔截某個屬性的讀取操作,可以接收三個參數,依次為目标對象,屬性名和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 // 抛出一個錯誤
           

上面代碼表示如果有這個屬性則傳回屬性值,如沒有就會報錯,如果沒有這個攔截,沒有屬性會傳回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"
           

上面代碼中,攔截操作定義在prototype對象上面,是以如果讀取obj對象繼承的屬性時,攔截會生效。

利用proxy,可以将讀取某個屬性的操作(get),轉變為執行某個函數,進而實作屬性的鍊式操作。

var pipe = (function () {
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (pipeObject, fnName) {
        if (fnName === 'get') {
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        funcStack.push(window[fnName]);
        return oproxy;
      }
    });

    return oproxy;
  }
}());

var double = n => n * 2;
var pow    = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63
           

如果一個屬性不可配置(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
           

set()

set方法用來攔截某個屬性的指派操作,可以接受4個參數,依次為目标對象,屬性名,屬性值和Proxy執行個體本身,其中最後一個參數可選。

假設Person對象有個age屬性,該屬性是一個不大于200的整數,那麼可以使用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');
      }
    }

    // 對于滿足條件的 age 屬性以及其他屬性,直接儲存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
           

上面代碼中,由于設定了存值函數set,任何不符合要求的age屬性指派都會報錯,這是資料驗證的一種實作方法。利用set方法,還可以資料綁定,每當對象發生變化時,會自動更新DOM。

有時我們可以對一個對象設定内部屬性,屬性名的第一個字元使用下劃線開頭,表示這些屬性不應被外部使用,如:

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
           

上面代碼中,隻要讀寫的屬性名的第一個字元是下劃線,一律報錯,進而禁止讀寫内部屬性的目的。