浅谈Proxy

定义

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 入门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352