淺談Proxy

定義

Proxy 對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。

語法

const p = new Proxy(target, handler)

參數(shù)

  • target:被代理的目標(biāo)對象(可以是任何類型的對象,包括原生數(shù)組,函數(shù),甚至另一個代理)
  • handler:可以操作代理對象的操作對象

廢話不多說,使用起來。

兼容性

image.png

可以看到,IE 、 Opera Mini 和 Baidu Browser 一路撲街,這大概也是 vue3 拋棄 IE 的原因吧,畢竟,魚和熊掌不可兼得。關(guān)于 handler 的一些方法兼容性,也可以通過caniuse查到。

簡單代理

let user = {
  name: '劉備',
  age: 18
}
let userP = new Proxy(user, {
  get(obj, key) {
    return obj[key]
  }
});

userP.name = '關(guān)羽';
userP.age = 14;
userP.sex = '男';

console.log(userP.name, userP.age);   // 關(guān)羽 14

而當(dāng)你打印

console.log(user.name); // 關(guān)羽
console.log(user.sex);  // 男

得到的結(jié)果分別是關(guān)羽和男, 這是為什么?

proxy代理可以無操作轉(zhuǎn)發(fā)代理,代理會將所有應(yīng)用到它的操作轉(zhuǎn)發(fā)到這個對象上。也就是說,代理對象的所有賦值,數(shù)據(jù)變更等操作,被代理對象也會做出相應(yīng)的操作。

所以當(dāng)你給代理對象 userP 賦值 userP.name = '關(guān)羽',被代理對象 user 也發(fā)生了同樣的操作 user.name = '關(guān)羽'

handler方法

handler方法有很多,不一一介紹,感興趣可以去MDN查閱相關(guān)文檔,看下用的比較多的。

  • handler.get():攔截對象的讀取屬性操作。
  • handler.set():設(shè)置屬性值操作的捕獲器。
  • handler.apply():攔截函數(shù)的調(diào)用。
  • handler.construct():攔截 new 操作符。
  • handler.defineProperty():攔截對對象的 Object.defineProperty() 操作。
  • handler.deleteProperty():攔截對對象屬性的 delete 操作。

get()

接收三個個參數(shù)

  • target:目標(biāo)對象。
  • property:被獲取的屬性名。
  • receiverProxy 或者繼承 Proxy 的對象,可選

案例:實現(xiàn)一個生成各種 DOM 節(jié)點的通用函數(shù) dom。(來自阮一峰ECMAScript 6 入門

const dom = new Proxy({}, {
  get(target, property) {
    return function(attrs = {}, ...children) {
      const el = document.createElement(property);
      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.appendChild(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')
  )
);

document.body.appendChild(el);

效果

image.png

set()

接收四個參數(shù)

  • target:目標(biāo)對象。
  • property:將被設(shè)置的屬性名或 Symbol。
  • value:新屬性值。
  • receiver:通常是 proxy 本身,但 handlerset 方法也有可能在原型鏈上,或以其他方式被間接地調(diào)用(因此不一定是 proxy 本身)。

案例:通過監(jiān)聽 input 輸入框,給頁面給上的元素實時更新數(shù)值

let inputDom = new Proxy({
  inputValue: null,
  value: ''
}, {
  set: function(obj, key, value) {
    console.log(value);
    if ( obj.inputValue ) {
      obj.inputValue.innerHTML = value;
    }
    // 默認(rèn)行為是存儲被傳入 setter 函數(shù)的屬性值
    obj[key] = value;
    // 表示操作成功
    return true;
  }
});

inputDom.inputValue = document.getElementById('input_value');
document.getElementById('input').addEventListener('input', (e)=>{
  inputDom.value = e.target.value;
})
dom_proxy.gif

綜合案例1:給一個數(shù)組添加數(shù)據(jù),而不是修改,記錄數(shù)組的變化歷史。

let products = new Proxy({
  data: ['apple'],
  lastValue: ''  // 最后添加進(jìn)來的值
},{
  get(obj, key) {
    if(key === 'lastValue') {
      return obj['data'][obj['data'].length -1]
    }
    return obj[key]
  },
  set (obj, key, value) {
    if(value in obj[key]) {
      return false
    }
    obj[key][obj[key].length] = value
    return true;
  }
})

分別做以下操作

console.log(products.lastValue); // apple
products.data = 'car';
console.log(products.lastValue); // car
products.data = 'ice cream';
console.log(products.lastValue); // ice cream
console.log(products.data);      // ['apple', 'car', 'ice cream']

綜合案例2:正則匹配校驗傳入的 phoneNumber 是否合法

定義一個驗證的 handler

let phoneValidator = {
  set(obj, key, value) {
    // 先判斷 key 是否為 phoneNumber
    if(key === 'phoneNumber') {
      let reg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/
      if(!reg.test(value)) {
        throw new TypeError('你輸入的手機號碼不正確!')
      }
    }
    obj[key] = value;
    // 表示設(shè)置值生效
    return true;
  }
}

代理驗證

let person = new Proxy({
  phoneNumber: ''
}, phoneValidator);
person.phoneNumber = '13299998888';
console.log(person.phoneNumber);  // 13299998888

person.phoneNumber = '231';
console.log(person.phoneNumber);  // Uncaught TypeError: 你輸入的手機號碼不正確!

看了這些案例你應(yīng)該能想到其他更多的用法,從此,你的業(yè)務(wù)邏輯代碼又可以少些很多。

注意:

  • 如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 對象訪問該屬性會報錯。
  • 如果目標(biāo)對象自身的某個屬性不可寫,那么 set 方法將不起作用。

比如:

let target = Object.defineProperties({},{
  action: {
    value: '學(xué)習(xí)',
    configurable: false,
    writable: false
  }
})

let handler = {
  get(target, key) {
    return '不學(xué)習(xí)';
  }
}

let proxy = new Proxy(target, handler);
console.log(proxy.action);  // Uncaught TypeError
image.png
let target = {};
Object.defineProperty(target, 'action', {
  value: '睡覺',
  writable: false
})

let handler = {
  set(target, key, value, receiver) {
    target[key] = 'value';
    return true
  }
}

let proxy = new Proxy(target, handler);
proxy.action = '后悔';
console.log(proxy.action);  // Uncaught TypeError
image.png

apply()

捕獲函數(shù)的調(diào)用,接收三個參數(shù)。

  • target:目標(biāo)對象(函數(shù))。
  • thisArg:被調(diào)用時的上下文對象。
  • argumentsList:被調(diào)用時的參數(shù)數(shù)組。

注意:target 必須是函數(shù)才可被捕獲,否則就會拋出 TypeError 錯誤。

該方法攔截以下操作:

  • proxy(...args)
  • Function.prototype.apply()Function.prototype.call()
  • Reflect.apply()
function parent(num1, num2) {
    console.log(num1 + num2);
};
var parentProxy = new Proxy(parent, {
  apply(target, thisArg, argumentsList) {
    console.log(argumentsList[0] + argumentsList[1]);
  }
})

parent(4, 5);  // 9
parentProxy(40, 50);  // 90

如果這時候把 thisArg 打印出來,就會發(fā)現(xiàn)是個 undefined

我們把代碼改一下

function parent(name) {
  return '這是' + name;
};
var parentProxy = new Proxy(parent, {
  apply(target, thisArg, argumentsList) {
    // 有thisArg上下文的時候就用name
    let name = (thisArg && thisArg.name) || '不知道誰人'
    return '這是' + name +'的兒子' +  argumentsList[0] + '和' + argumentsList[1]
  }
})

// 定義一個全局變量name
var name = 'window';
console.log(parent('曹操'));;  // 這是曹操
console.log(parentProxy('張三', '李四'));  // 這是不知道誰人的兒子張三和李四 thisArg是個undefined
console.log(parentProxy.call(null, '王五', '老六'));  // 這是不知道誰人的兒子王五和老六 thisArg是個null
console.log(parentProxy.apply(this, ['location', 'history']));  // 這是window的兒子location和history thisArg是window
console.log(Reflect.apply(parentProxy,null,['雜七', '雜八'])); // 這是不知道誰人的兒子雜七和雜八 thisArg是個null

let otherParent = function() {
  this.name = '劉備';
  console.log(parentProxy.call(this, '劉嬋','劉永'));   // 這是劉備的兒子劉嬋和劉永 thisArg是 callParent {name: '劉備'}
}
new otherParent();

construct()

攔截 new 操作符,接收三個參數(shù)。

  • target:目標(biāo)對象。
  • argumentsListconstructor的參數(shù)列表。
  • newTarget:最初被調(diào)用的構(gòu)造函數(shù)。

其返回值必須是個對象,如果不返回或者返回的不是一個對象,則會拋出一個錯誤 Uncaught TypeError

let proxyC = new Proxy(function() {}, {
  construct(target, arguments, newTarget) {
    console.log(target);
    console.log(arguments);
    console.log(newTarget);
    return {
      value:arguments[0]
    }
  }
})

console.log(new proxyC('參數(shù)')); // {value: '參數(shù)'}

如果代理的不是一個可調(diào)用的函數(shù)

let proxyC = new Proxy({}, {
  construct(target, arguments, newTarget) {
    return { }
  }
})
console.log(new proxyC('參數(shù)')); 
image.png

或者返回的不是一個對象

let proxyC = new Proxy(function () {}, {
  construct(target, arguments, newTarget) {
    return arguments[0]
  }
})

console.log(new proxyC('參數(shù)'));
image.png

defineProperty()

接收三個參數(shù)

  • target:目標(biāo)對象。
  • property:待檢索其描述的屬性名。
  • descriptor:待定義或修改的屬性的描述符。
var numbers = new Proxy({}, {
  defineProperty: function(target, prop, descriptor) {
    console.log('prop: ' + prop + '--' + descriptor.value);
    return true;
  }
});

var desc = { 
  value: 10,
  configurable: true, 
  enumerable: true
};
Object.defineProperty(numbers, 'a', desc); // prop: a--10

注意:當(dāng)且僅當(dāng)該屬性的 enumerable 鍵值為 true 時,該屬性才會出現(xiàn)在對象的枚舉屬性中。

Object.defineProperty(numbers, 'b', {
  value: 20,
  configurable: true, 
  enumerable: false
});  // prop: b--20

numbers.b = 45;   // prop: b--45
console.log(numbers.b);   // undefined 因為不可枚舉 

如果目標(biāo)對象不可擴展(non-extensible),則defineProperty()不能增加目標(biāo)對象上不存在的屬性,否則會報錯。另外,如果目標(biāo)對象的某個屬性不可寫(writable)或不可配置(configurable),則defineProperty()方法不得改變這兩個設(shè)置。

Object.defineProperty(numbers, 'b', {
  value: 20,
  configurable: false
}); // prop: b--20

numbers.b = 45;   // Uncaught TypeError
image.png

deleteProperty()

攔截對對象屬性的 delete 操作,接收兩個參數(shù)。

  • target:目標(biāo)對象。
  • property:待刪除的屬性名。
var userinfo = new Proxy({
  name: '朱鳳麗',
  age: 19
}, {
  deleteProperty(target, prop) {
    if(prop === 'name') {
      return false
    }
    delete target[prop]
    return true;
  }
});

delete userinfo.name;
delete userinfo.age;
console.log(userinfo.name); // 朱鳳麗
console.log(userinfo.age);  // undefined

注意:如果目標(biāo)對象的屬性是不可配置的,那么該屬性不能被刪除。

以上,關(guān)于 proxy 就寫到這里,想了解更多其他函數(shù)的可以去MDN查閱相關(guān)文檔。

參考文章:
Proxy MDN
阮一峰ECMAScript 6 入門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容