美文网首页
浅谈Proxy

浅谈Proxy

作者: 朱凤丽 | 来源:发表于2022-01-07 10:38 被阅读0次

定义

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法

const p = new Proxy(target, handler)

参数

  • target:被代理的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler:可以操作代理对象的操作对象

废话不多说,使用起来。

兼容性

image.png
可以看到,IE 、 Opera Mini 和 Baidu Browser 一路扑街,这大概也是 vue3 抛弃 IE 的原因吧,毕竟,鱼和熊掌不可兼得。关于 handler 的一些方法兼容性,也可以通过caniuse查到。

简单代理

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

userP.name = '关羽';
userP.age = 14;
userP.sex = '男';

console.log(userP.name, userP.age);   // 关羽 14

而当你打印

console.log(user.name); // 关羽
console.log(user.sex);  // 男

得到的结果分别是关羽和男, 这是为什么?

proxy代理可以无操作转发代理,代理会将所有应用到它的操作转发到这个对象上。也就是说,代理对象的所有赋值,数据变更等操作,被代理对象也会做出相应的操作。

所以当你给代理对象 userP 赋值 userP.name = '关羽',被代理对象 user 也发生了同样的操作 user.name = '关羽'

handler方法

handler方法有很多,不一一介绍,感兴趣可以去MDN查阅相关文档,看下用的比较多的。

  • handler.get():拦截对象的读取属性操作。
  • handler.set():设置属性值操作的捕获器。
  • handler.apply():拦截函数的调用。
  • handler.construct():拦截 new 操作符。
  • handler.defineProperty():拦截对对象的 Object.defineProperty() 操作。
  • handler.deleteProperty():拦截对对象属性的 delete 操作。

get()

接收三个个参数

  • target:目标对象。
  • property:被获取的属性名。
  • receiverProxy 或者继承 Proxy 的对象,可选

案例:实现一个生成各种 DOM 节点的通用函数 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()

接收四个参数

  • target:目标对象。
  • property:将被设置的属性名或 Symbol。
  • value:新属性值。
  • receiver:通常是 proxy 本身,但 handlerset 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。

案例:通过监听 input 输入框,给页面给上的元素实时更新数值

let inputDom = new Proxy({
  inputValue: null,
  value: ''
}, {
  set: function(obj, key, value) {
    console.log(value);
    if ( obj.inputValue ) {
      obj.inputValue.innerHTML = value;
    }
    // 默认行为是存储被传入 setter 函数的属性值
    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:给一个数组添加数据,而不是修改,记录数组的变化历史。

let products = new Proxy({
  data: ['apple'],
  lastValue: ''  // 最后添加进来的值
},{
  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;
    // 表示设置值生效
    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: 你输入的手机号码不正确!

看了这些案例你应该能想到其他更多的用法,从此,你的业务逻辑代码又可以少些很多。

注意:

  • 如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
  • 如果目标对象自身的某个属性不可写,那么 set 方法将不起作用。

比如:

let target = Object.defineProperties({},{
  action: {
    value: '学习',
    configurable: false,
    writable: false
  }
})

let handler = {
  get(target, key) {
    return '不学习';
  }
}

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()

捕获函数的调用,接收三个参数。

  • target:目标对象(函数)。
  • thisArg:被调用时的上下文对象。
  • argumentsList:被调用时的参数数组。

注意:target 必须是函数才可被捕获,否则就会抛出 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 打印出来,就会发现是个 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 操作符,接收三个参数。

  • target:目标对象。
  • argumentsListconstructor的参数列表。
  • newTarget:最初被调用的构造函数。

其返回值必须是个对象,如果不返回或者返回的不是一个对象,则会抛出一个错误 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('参数')); // {value: '参数'}

如果代理的不是一个可调用的函数

let proxyC = new Proxy({}, {
  construct(target, arguments, newTarget) {
    return { }
  }
})
console.log(new proxyC('参数')); 
image.png

或者返回的不是一个对象

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

console.log(new proxyC('参数'));
image.png

defineProperty()

接收三个参数

  • target:目标对象。
  • 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

注意:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。

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

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

如果目标对象不可扩展(non-extensible),则defineProperty()不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty()方法不得改变这两个设置。

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

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

deleteProperty()

拦截对对象属性的 delete 操作,接收两个参数。

  • target:目标对象。
  • 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

注意:如果目标对象的属性是不可配置的,那么该属性不能被删除。

以上,关于 proxy 就写到这里,想了解更多其他函数的可以去MDN查阅相关文档。

参考文章:
Proxy MDN
阮一峰ECMAScript 6 入门

相关文章

网友评论

      本文标题:浅谈Proxy

      本文链接:https://www.haomeiwen.com/subject/anhjcrtx.html