ES6(8)Proxy(攔截器、代理器)

概述:改變默認行爲,對外界的訪問進行過濾和改寫

Proxy 用於修改某些操做的默認行爲,等同於在語言層面作出修改,因此屬於一種「元編程」,即對編程語言進行編程javascript

Proxy改變默認行爲vue

Proxy 能夠理解成,在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫java

var proxy = 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);
  }
});
複製代碼

上面代碼對一個空對象架設了一層攔截重定義了屬性的讀取(get)和設置(set)行爲。對設置了攔截行爲的對象obj,去讀寫它的屬性,就會獲得下面的結果。web

proxy.count = 1
// setting count!
++proxy.count
// getting count!
// setting count!
// 2
複製代碼

上面代碼說明,Proxy 實際上重載了點運算符,即用本身的定義覆蓋了語言的原始定義編程

語法,var proxy = new Proxy(target, handler);

var proxy = new Proxy(target, handler);
複製代碼

target:參數表示所要攔截的目標對象

handler:參數也是一個對象,用來定製攔截行爲

要使得Proxy起做用,必須針對Proxy實例(上例是proxy對象)進行操做,而不是針對目標對象(上例是空對象)進行操做數組

若是handler沒有設置任何攔截,那就等同於直接通向原對象,沒有任何攔截效果,訪問proxy就等同於訪問targetmarkdown

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
複製代碼

Proxy 對象,設置到object.proxy屬性,從而能夠當作object`對象的屬性調用

var object = { proxy: new Proxy(target, handler) };
複製代碼

Proxy 實例做爲其餘對象的原型對象

必須是new之後生成的實例,纔會觸發攔截app

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

//obj對象自己並無time屬性,因此根據原型鏈,會在proxy對象上讀取該屬性,致使被攔截。
複製代碼

同一個攔截器函數,能夠設置攔截多個操做

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
複製代碼

Proxy 支持的攔截操做

  • get(target, propKey, receiver):攔截對象屬性的讀取,好比proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):攔截對象屬性的設置,好比proxy.foo = vproxy['foo'] = v,返回一個布爾值。
  • has(target, propKey):攔截propKey in proxy的操做,返回一個布爾值。
  • deleteProperty(target, propKey):攔截delete proxy[propKey]的操做,返回一個布爾值。
  • ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環,返回一個數組。該方法返回目標對象全部自身的屬性屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
  • getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
  • defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
  • preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
  • getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
  • isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。
  • setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。若是目標對象是函數,那麼還有兩種額外操做能夠攔截。
  • apply(target, object, args):攔截 Proxy 實例做爲函數調用的操做,好比proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
  • construct(target, args):攔截 Proxy 實例做爲構造函數調用的操做,好比new proxy(...args)
方法 描述
handler.apply() 攔截 Proxy 實例做爲函數調用的操做
handler.construct() 攔截 Proxy 實例做爲構造函數調用的操做
handler.defineProperty() 攔截 Object.defineProperty() 的操做
handler.deleteProperty() 攔截 Proxy 實例刪除屬性操做
handler.get() 攔截 讀取屬性的操做
handler.set() 攔截 屬性賦值的操做
handler.getOwnPropertyDescriptor() 攔截 Object.getOwnPropertyDescriptor() 的操做
handler.getPrototypeOf() 攔截 獲取原型對象的操做
handler.has() 攔截 屬性檢索操做
handler.isExtensible() 攔截 Object.isExtensible()操做
handler.ownKeys() 攔截 Object.getOwnPropertyDescriptor() 的操做
handler.preventExtension() 攔截 Object().preventExtension() 操做
handler.setPrototypeOf() 攔截Object.setPrototypeOf()操做
Proxy.revocable() 建立一個可取消的 Proxy 實例

get(target,key,receiver)

攔截某個屬性的讀取操做dom

依次三個參數:目標對象、被讀取的屬性名、proxy 實例自己

target:必選、目標對象編程語言

key:必選、被讀取的屬性名,在get內部是字符串

receiver:可選、proxy 實例自己(嚴格地說,是操做行爲所針對的對象)

須要return

若是一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,不然經過 Proxy 對象訪問該屬性會報錯

須要return

應用實例

一、訪問目標對象不存在的屬性,拋出一個錯誤而不是返回undefined

若是沒有這個攔截函數,訪問不存在的屬性,只會返回undefined

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 // 拋出一個錯誤
複製代碼
二、get方法能夠繼承(定義在Prototype對象上,攔截實例經過原型鏈獲取繼承的方法的操做)

當攔截操做定義在Prototype對象上面時,讀取obj對象繼承的屬性(自身沒有的屬性)時,攔截會生效。

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對象繼承的屬性時(自己沒有,經過原型鏈查找的屬性),攔截會生效。

三、使用get攔截,實現數組讀取負數的索引
function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c
複製代碼
//簡略版
var arr=new Proxy([0,1,2,3,4],{
    get(target, p, receiver) {
        if(p<0){
            var n=eval(target.length-1+p);
            return target[n];
        }
        return target[p];
    }
})

console.log(arr[-1]);   //3
複製代碼
四、將讀取屬性的操做(get),轉變爲執行某個函數【vue3.0】有點像發佈訂閱

利用Proxy get攔截之後仍然返回Proxy實例的特性,達到了將函數名鏈式使用的效果

var pipe = (function () {
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (pipeObject, fnName) {
        if (fnName === 'get') {
	        //若是獲取的是get 對數組funcStack中的函數一次調用
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        //若是不是get 向數組funcStack中添加函數
        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

//有點像發佈訂閱
複製代碼
五、利用get攔截,實現一個生成各類 DOM 節點的通用函數dom【vue3.0】
const dom=new Proxy({},{
    get(target, p, receiver) {
        //(attrs={},...children) 第一個參數爲attrs 剩餘的都是children
        return function (attrs={},...children) {
            //get屬性名 就是要建立的 元素名稱
            const el=document.createElement(p);
            //循環 attrs設置元素屬性
            for (let prop of Object.keys(attrs)){
                el.setAttribute(prop,attrs[prop]);
            }
            for (let child of children){
                if(typeof child === 'string'){
                    child = document.createTextNode(child);
                }
                el.append(child)
            }
            return el;
        }
    }
})


const el = dom.div(
    {},
    'Hello, my name is ',
    dom.a({href: '//example.com'}, 'Mark'),
    '. I like:',
    dom.ul({},
        dom.li({}, 'The web'),
        dom.li({}, 'Food'),
        dom.li({}, '…actually that\'s it')
    )
);

//相對與div :{}爲 attrs ,剩餘的都是子級
console.log(el);
document.body.appendChild(el);
複製代碼

image.png

第三個參數的例子,通常狀況下就是 Proxy 實例

老是指向原始的讀操做所在的那個對象,通常狀況下就是 Proxy 實例。

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true
複製代碼
const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});

const d = Object.create(proxy);
d.a === d // true
複製代碼

上面代碼中,d對象自己沒有a屬性,因此讀取d.a的時候,會去d的原型proxy對象找。這時,receiver就指向d,表明原始的讀操做所在的那個對象。

set(obj, prop, value,receiver)

攔截某個屬性的賦值操做

依次四個參數:目標對象,屬性名,屬性值,Proxy 實例自己

target:目標對象

prop:屬性名,在get內部是字符串

value:屬性值

receiver:可選,Proxy 實例自己

若是目標對象自身的某個屬性,不可寫且不可配置,那麼set方法將不起做用。

在賦值操做發生時進行本身想要的操做,還能夠進行數據綁定,即每當對象發生變化時,會自動更新 DOM(vue3.0)

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 // 報錯

複製代碼

設置一些內部屬性不被外部讀寫(假定開頭是_的爲內部屬性)

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
複製代碼

第四個參數的例子

set方法的第四個參數receiver,指的是原始的操做行爲所在的那個對象,通常狀況下是proxy實例自己,跟get的第三個參數的運用相同

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true
複製代碼

apply(target, context, args),攔截函數的調用、call和apply操做

依次三個形參:目標對象,上下文對象(this),參數數組

target:目標對象

context:目標對象的上下文對象this

args:目標對象的參數數組

設置apply攔截後 函數自身內部的代碼再也不執行

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments);
  }
};
複製代碼

示例

var fn=function () {
    //不執行
    console.log(this,'fn')
    return 'I am fn'
}

var p=new Proxy(fn,{
    apply(target, thisArg, argArray) {
        console.log(thisArg,'p')   //undefined 'p'
        return 'I am p'
    }
})

console.log(p());   //I am p
複製代碼
var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2;
  }
};
function sum (left, right) {
  return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

//直接調用Reflect.apply方法,也會被攔截。

Reflect.apply(proxy, null, [9, 10]) // 38
複製代碼

construct(target, args, newTarget)

用於攔截new命令

依次三個形參:目標對象,參數對象,new 後面的函數

target:目標對象

args:構造函數的參數對象

newTarget:可選,創造實例對象時,new命令做用的構造函數(new 後面的函數)

construct方法返回的必須是一個對象

var handler = {
  construct (target, args, newTarget) {
    return new 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
複製代碼

has(target, key),攔截HasProperty操做,即判斷對象是否具備某個屬性(in運算符)

攔截HasProperty操做,即判斷對象是否具備某個屬性時,這個方法會生效。典型的操做就是in運算符

依次兩個形參

target:目標對象

key:需查詢的屬性名

下面的例子使用has方法隱藏某些屬性,不被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
複製代碼

deleteProperty(target, key),用於攔截delete操做

用於攔截delete操做

經過拋出錯誤或者返回false,阻止delete命令刪除。

依次兩個形參:目標對象、需查刪除的屬性名

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    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
複製代碼

Proxy.revocable() ,返回一個可取消的 Proxy 實例

Proxy.revocable方法返回一個對象,該對象的proxy屬性是Proxy實例,revoke屬性是一個函數,能夠取消Proxy實例。

當執行revoke函數以後,再訪問Proxy實例,就會拋出一個錯誤

使用場景是,目標對象不容許直接訪問,必須經過代理訪問,一旦訪問結束,就收回代理權,不容許再次訪問。

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

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

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

revoke();
proxy.foo // TypeError: Revoked
複製代碼

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 _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxy = new Proxy(jane, {});
proxy.name // undefined

複製代碼

上面代碼中,目標對象janename屬性,實際保存在外部WeakMap對象_name上面,經過this鍵區分。因爲經過proxy.name訪問時,this指向proxy,致使沒法取到值,因此返回undefined

原生對象的內部屬性,只有經過正確的this才能拿到

也就是說this必須是對應類的實例才能拿到的內部屬性

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

proxy.getDate();
// TypeError: this is not a Date object.
複製代碼

用bind將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
複製代碼
相關文章
相關標籤/搜索