es6 proxy 個人理解和應用場景示例

一,基礎

1. 概念

對外界的訪問進行過濾和改寫

2. 基本用法

// 用法1
var object = { proxy: new Proxy(target, handler) };
// 用法2
var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

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

3. 攔截操作

  1. get(target, propKey, receiver):攔截對象屬性的讀取,比如proxy.foo和proxy[‘foo’]。

應用1:訪問不存在的屬性返回undefined, 設置報錯機制

var person = {
  name: "張三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (Reflect.has(target, prokey)) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
    }
  }
});

proxy.name // "張三"
proxy.age // 拋出一個錯誤

應用2: 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"

應用3: 實現屬性的鏈式操作。

var pipe = function(value) {
	var funcStacks = [] // 函數棧 
    var oproxy = new Proxy({}, {
        get: function(pipeObject, fnName) {// pipeObject={},代理對象爲空,fnName爲設置的屬性值double,pow,reverseInt,get
            if( fnName == 'get') { // 不到最後的get不執行調用函數
                return funcStacks.reduce(function(val, fn){
                  console.log(typeof fn)
                  console.log('coloe:'+fn)
                  if(typeof fn === 'function') {
                    // 調用函數並返回結果
                    return fn(val)
                  }
                }, value) // 傳入外部的值
            }
            // 每次調用都將該函數放到函數棧裏面待機。
            funcStacks.push(window[fnName]) 
            console.log(funcStacks) 
            // 返回代理權繼續操作
            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

  1. set(target, propKey, value, receiver):攔截對象屬性的設置,比如proxy.foo = v或proxy[‘foo’] = v,返回一個布爾值。
  1. apply(target, object, args):攔截 Proxy 實例作爲函數調用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。

apply方法可以接受三個參數,分別是目標對象、目標對象的上下文對象(this)和目標對象的參數數組。

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments);
  }
};
  1. has(target, propKey):攔截propKey in proxy的操作,返回一個布爾值。

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

has方法可以接受兩個參數,分別是目標對象、需查詢的屬性名。

  • 使用has方法隱藏某些屬性,不被in運算符發現。

  • has方法攔截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判斷一個屬性是對象自身的屬性,還是繼承的屬性。

  • for...in循環不生效

  • for...inhasOwnProperty使用ownKeys攔截

  1. construct(target, args):攔截 Proxy 實例作爲構造函數調用的操作,比如new proxy(…args)。

construct方法用於攔截new命令,下面是攔截對象的寫法。

var handler = {
  construct (target, args, newTarget) {
    return new target(...args);
  }
};

construct方法可以接受三個參數。

  • target:目標對象
  • args:構造函數的參數對象
  • newTarget:創造實例對象時,new命令作用的構造函數(下面例子的p

返回值:返回的必須是一個對象,否則會報錯

  1. deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布爾值。

攔截delete操作,如果這個方法拋出錯誤或者返回false,當前屬性就無法被delete命令刪除。

  1. defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
  1. getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。

返回一個屬性描述對象或者undefined

  1. getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。

攔截獲取對象原型

返回值必須是對象或者null,否則報錯

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof
  1. isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。

只能返回布爾值,否則返回值會被自動轉爲布爾值

**強限制:**它的返回值必須與目標對象的isExtensible屬性保持一致,否則就會拋出錯誤。

  1. ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。

攔截對象自身屬性的讀取操作

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in循環

有三類屬性會被ownKeys()方法自動過濾,不會返回。

  • 目標對象上不存在的屬性
  • 屬性名爲 Symbol 值
  • 不可遍歷(enumerable)的屬性
  1. preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。

只有目標對象不可擴展時(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true,否則會報錯。

  1. setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。如果目標對象是函數,那麼還有兩種額外操作可以攔截。

只能返回布爾值,否則會被自動轉爲布爾值

  1. revocable(target, handler): 取消proxy實例

示例看下面的撤銷代理

通用情景整理

  1. 第三個參數receiver總是指向原始的讀操作所在的那個對象,一般情況下就是 Proxy 實例
const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  },
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
});
// get
proxy.getReceiver === proxy // true
const d = Object.create(proxy);
d.a === d // true
// set
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true

  1. 如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則會報錯
const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});
// 不可擴展
Object.preventExtensions(target);

const handler = {
  // 如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 對象訪問該屬性會報錯
  get(target, propKey) {
    return 'abc';
  },
  // 如果目標對象自身的某個屬性,不可寫且不可配置,那麼set方法將不起作用。
  set: function(obj, prop, value, receiver) {
    // 嚴格模式下,set代理如果沒有返回true,就會報錯。
    obj[prop] = 'baz';
  }// 如果原對象不可配置或者禁止擴展,這時`has`攔截會報錯。
  has: function(target, prop) {
    return false;
  },
  // 目標對象自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯。
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    return true;
  },
  // 如果目標對象不可擴展(non-extensible),則不能增加目標對象上不存在的屬性,否則會報錯。
  // 如果目標對象的某個屬性不可寫(writable)或不可配置(configurable),則不得改變這兩個設置。
  defineProperty (target, key, descriptor) {
    return false; // false只是用來提示操作失敗,本身並不能阻止添加新屬性。
  },
   // 如果目標對象不可擴展(non-extensible), getPrototypeOf()方法必須返回目標對象的原型對象。
  getPrototypeOf(target) {
    return target.prototype;
  },
  // 如果目標對象自身包含不可配置的屬性,則該屬性必須被`ownKeys()`方法返回,否則報錯。 
  // 如果目標對象是不可擴展的(non-extensible),這時ownKeys()方法返回的數組之中,必須包含原對象的所有屬性,且不能包含多餘的屬性,否則報錯。
  ownKeys: function(target) {
    return ['foo'];
  },
  // 只有目標對象不可擴展時(即Object.isExtensible(proxy)爲false),proxy.preventExtensions才能返回true,否則會報錯。
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  },
  // 如果目標對象不可擴展(non-extensible),setPrototypeOf()方法不得改變目標對象的原型。
   setPrototypeOf (target, proto) {
    	throw new Error('Changing the prototype is forbidden');
  }
};

const proxy = new Proxy(target, handler);
// get
proxy.foo
// set
proxy.foo = 'baz';
proxy.foo // 123
// has
'a' in p // TypeError is thrown
// ownKeys
Object.getOwnPropertyNames(p)
// preventExtensions
Object.preventExtensions(proxy)
// "called"
// Proxy {}
// setprototypeOf 不可修改對象原型
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
  1. 防止內部屬性被外部讀寫。
const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  },
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    return true;
  },
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  },
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
var target = {};
var 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

// getOwnPropertyDescriptor
target = { _foo: 'bar', baz: 'tar' };
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 }

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

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

  1. this指向問題

目標對象內部的this關鍵字會指向 Proxy 代理。

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      // 給原來的對象綁定this,不然找不到原對象屬性
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate() // 1

二,應用

1. 私有化api

let api = { 
  _apiKey: '123abc456def',
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};
console.log(api._apiKey); // 此處可以使用
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, { 
  get(target, key, proxy) {
    if(RESTRICTED.indexOf(key) > -1) {
      throw Error(`${key} is restricted. Please see api documentation for further info.`);
    }
    return Reflect.get(target, key, proxy);
  },
  set(target, key, value, proxy) {
    if(RESTRICTED.indexOf(key) > -1) {
      throw Error(`${key} is restricted. Please see api documentation for further info.`);
    }
    return Reflect.get(target, key, value, proxy);
  }
});
 
// 以下操作都會拋出錯誤
console.log(api._apiKey);
api._apiKey = '987654321';

2. 生成隨機編碼,只讀

class Component {
  constructor () {
    this.proxy = new Proxy({
      id: Math.random().toString(36).slice(-8)
    },{})
  }
  get id() {
    return this.proxy.id
  }
}
let com = new Component()
let com2 = new Component()
for(let i = 0; i < 10; i++){
  console.log(com.id,com2.id)
}
com.id = '123' // 設置無效,只讀
console.log(com.id,com2.id)

3. 校驗表單

簡單校驗

 let numericDataStore = { 
  count: 0,
  amount: 1234,
  total: 14
};

numericDataStore = new Proxy(numericDataStore, { 
  set(target, key, value, proxy) {
    if (typeof value !== 'number') { // 不是數值拋出錯誤
      throw Error("Properties in numericDataStore can only be numbers");
    }
    return Reflect.set(target, key, value, proxy);
  }
});

// 拋出錯誤,因爲 "foo" 不是數值
numericDataStore.count = "foo";

// 賦值成功
numericDataStore.count = 333;

抽離邏輯校驗

function createValidator(target, validator) { 
  return new Proxy(target, {
    _validator: validator,
    set(target, key, value, proxy) {
      if (Reflect.has(target, key)) {
        let validator = this._validator[key];
        if (!!validator(value)) {
          return Reflect.set(target, key, value, proxy);
        } else {
          throw Error(`Cannot set ${key} to ${value}. Invalid.`);
        }
      } else {
        throw Error(`${key} is not a valid property`)
      }
    }
  });
}

const personValidators = { 
  name(val) {
    return typeof val === 'string';
  },
  age(val) {
    return typeof age === 'number' && age > 18;
  }
}
class Person { 
  constructor(name, age) {
    this.name = name;
    this.age = age;
    return createValidator(this, personValidators);
  }
}

const bill = new Person('Bill', 25);

// 以下操作都會報錯
bill.name = 0; 
bill.age = 'Bill'; 
bill.age = 15; 

擴展校驗器,增加校驗類型

let obj = { 
  pickyMethodOne: function(obj, str, num) { /* ... */ },
  pickyMethodTwo: function(num, obj) { /*... */ }
};

const argTypes = { 
  pickyMethodOne: ["object", "string", "number"],
  pickyMethodTwo: ["number", "object"]
};

obj = new Proxy(obj, { 
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...args) {
      var checkArgs = argChecker(key, args, argTypes[key]);
      return Reflect.apply(value, target, args);
    };
  }
});

function argChecker(name, args, checkers) { 
  for (var idx = 0; idx < args.length; idx++) {
    var arg = args[idx];
    var type = checkers[idx];
    if (!arg || typeof arg !== type) {
      console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
    }
  }
}

obj.pickyMethodOne(); 
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3

obj.pickyMethodTwo("wopdopadoo", {}); 
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1

// No warnings logged
obj.pickyMethodOne({}, "a little string", 123); 
obj.pickyMethodOne(123, {});

4. 請求時間日誌,分析性能

let api = { 
  _apiKey: '123abc456def',
  getUsers: function() { /* ... */ },
  getUser: function(userId) { /* ... */ },
  setUser: function(userId, config) { /* ... */ }
};

function logMethodAsync(timestamp, method) { 
  setTimeout(function() {
    console.log(`${timestamp} - Logging ${method} request asynchronously.`);
  }, 0)
}

api = new Proxy(api, { 
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...arguments) {
      logMethodAsync(new Date(), key);
      return Reflect.apply(value, target, arguments);
    };
  }
});
api.getUsers();

5. 預警和攔截

let dataStore = { 
  noDelete: 1235,
  oldMethod: function() {/*...*/ },
  doNotChange: "tried and true"
};

const NODELETE = ['noDelete']; 
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod']; 

dataStore = new Proxy(dataStore, { 
  set(target, key, value, proxy) {
    if (NOCHANGE.includes(key)) {
      throw Error(`Error! ${key} is immutable.`);
    }
    return Reflect.set(target, key, value, proxy);
  },
  deleteProperty(target, key) {
    if (NODELETE.includes(key)) {
      throw Error(`Error! ${key} cannot be deleted.`);
    }
    return Reflect.deleteProperty(target, key);

  },
  get(target, key, proxy) {
    if (DEPRECATED.includes(key)) {
      console.warn(`Warning! ${key} is deprecated.`);
    }
    var val = target[key];

    return typeof val === 'function' ?
      function(...args) {
        Reflect.apply(target[key], target, args);
      } :
      val;
  }
});

// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo"; 
delete dataStore.noDelete; 
dataStore.oldMethod();

6. 過濾操作

某些操作會非常佔用資源,比如傳輸大文件,這個時候如果文件已經在分塊發送了,就不需要在對新的請求作出相應(非絕對),這個時候就可以使用 Proxy 對當請求進行特徵檢測,並根據特徵過濾出哪些是不需要響應的,哪些是需要響應的

let obj = { 
  getGiantFile: function(fileId) {/*...*/ }
};
 
obj = new Proxy(obj, { 
  get(target, key, proxy) {
    return function(...args) {
      const id = args[0];
      let isEnroute = checkEnroute(id);
      let isDownloading = checkStatus(id);   
      let cached = getCached(id);
 
      if (isEnroute || isDownloading) {
        return false;
      }
      if (cached) {
        return cached;
      }
      return Reflect.apply(target[key], target, args);
    }
  }
});

7. 撤銷代理

let o = {
   name: 'xiaoming',
   price : 190
}
let d = Proxy.revocable(o, {
  get (target, key) {
    if(key === 'price') {
      return target[key] + 20
    }else {
      return target[key]
    }
  }
})
console.log(d.proxy.price, d)
setTimeout(function () {
  d.revoke()
  setTimeout(function  () {
    console.log(d.proxy.price)
  },100)
},1000)

8. 實現觀察者模式

const queuedObservers = new Set() // 觀察者隊列
const observe = fn => queuedObservers.add(fn) // 添加觀察者函數
const observable = obj => new Proxy(obj, {set, get}) // 代理觀察者
function set(target, key, value, receiver) {
   	const result = Reflect.set(target, key, value, receiver) // 重置原屬性,已經替換了原來的屬性值
    queuedObservers.forEach(observer => observer()) // 執行觀察者函數
	// return Reflect.set(target, key, value, receiver) // 如果是後面賦值,最後也會改變屬性值,但是在觀察者函數中,該屬性值還是原來的屬性值。我們需要對新的值處理,所以是之前就要賦值再進行觀察處理。
   	return result
}
function get(target, key, value, receiver) {
    return Reflect.get(target, key, value, receiver)
}
let user = {
    name: '張三',
    age: 28
}
const person = observable(user)
let gcUser = () => {
    // 打印新屬性
    console.log(`${person.name}, ${person.age}`)
}
observe(gcUser)
person.name = '李四' // 李四, 28 

9. 服務端代理,跨域,取消請求等

function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      return () => httpGet(baseUrl + '/' + propKey);
    }
     ... // 各種情況處理
  });
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章