背景
需求

比如Vue,如下,一个标签里面有个插值的变量name,Vue的响应式需要监听这个变量的变化,如果发生变化,就执行对应的操作,更新到页面上
<template><div>{{name}}</div></template>
Object.defineProperty

Vue2 就是使用这种方式。
https://www.yuque.com/yejielin/mypn47/mkkfc6#y0Swp
通过设置对象属性特性的方式,添加设置值时执行(set)和读取值时执行(get),让变量改变时自动调用对应的方法
Proxy

Proxy的原理是,用一个类(中介)去代理对象,然后用户可以直接操作这个类,就相当于操作原对象,同时可以设定,什么操作触发会什么函数
Vue3 开始采用 Proxy 的方式监听对象属性的变化
===================
操作
1、创建代理

第二个参数是捕获器对象,就是你希望什么操作,给你执行什么函数,见下
2、操作代理
源对象被修改
3、创建并设置捕获器(Trap)
捕获器(Trap)会监听目标对象(target)的某些操作(get读取时、set设置值时,等等),执行一些方法(handler)

编写捕获器,推荐使用Reflect反射,反射的方法正好与捕获器的方法一一对应。
4、receiver 参数

(原本对象里面的this,指的是对象本身)
当我定义好一个Proxy代理,用户访问objProxy.name时,会正常触发get代理操作1次,而不是访问name触发1次同时_name触发1次,一共2次。
此时如果用到了 receiver 参数,这个参数是指代理对象(例子中的objProxy)本身,把obj里面的this改成了代理对象,配合Reflect反射,这样访问 objProxy.name 时,会触发2次get。
==================
Proxy的13种捕获器
编写捕获器,推荐使用Reflect反射,反射的方法正好与捕获器的方法一一对应。
| 说明 | new Proxy( obj , { 捕获器名 }) | 对代理后对象 objProxy 的操作 | |
|---|---|---|---|
| 1 | 获取值 | get( target , key ) | objProxy.属性名 |
| 2 | 设置值 或 新增属性 | set( target , key , newValue ) | objProxy.属性名 = 新值 objProxy.新属性名 = 新值 |
| 3 | 是否有某个属性 | has( target , key ) | 属性名 in objProxy,返回布尔值 |
| 4 | 删除属性 | deleteProperty( target, key ) | delete objProxy.属性名 |
| 5 | 获取对象的key | ownKeys( target ) | Object.getOwnPropertyNames() Object.getOwnPropertySymbols() Object.keys( objProxy ) Reflect.ownKeys( ) |
| 6 | 函数调用操作 | apply( target , 被调用时的 this 对象 , 被调用时的参数数组 ) | functionProxy( ) Function.prototype.apply( “functionProxy”, [ 参数1 , 参数2 ] ) Function.prototype.call( “functionProxy”, 参数1 , 参数2 ) Reflect.apply( functionProxy , this对象 , [ 参数1 , 参数2 ] ) |
| 7 | 函数被 new 时 | construct( target, 参数列表 ) | new functionProxy( 参数1 , 参数2 ) |
| 8 | 获取对象的原型 proto | getPrototypeOf( target ) | Object.getPrototypeOf( objProxy ) |
| 9 | 设置对象的原型 proto | setPrototypeOf( target , newPrototype ) | Object.setPrototypeOf( objProxy ) |
| 10 | 是否可以添加新属性 | isExtensible( target ) | Object.isExtensible( objProxy ),返回布尔值 |
| 11 | 禁止对象添加新属性 | preventExtensions( target ) | Object.preventExtensions( objProxy ) |
| 12 | 获取对象属性的描述符(属性特性) | getOwnPropertyDescriptor( target , 属性的描述 ) | Object.getOwnPropertyDescriptor( objProxy ) |
| 13 | 设置对象属性的描述符(属性特性) | defineProperty( target , 属性的描述 ) | Object.defineProperty( objProxy , prop , descriptor) |
==================
示例
设置默认值
const handler = {get: function(obj, prop) {return prop in obj ? obj[prop] : 37;}};const p = new Proxy({}, handler);p.a = 1;p.b = undefined;console.log(p.a, p.b); // 1, undefinedconsole.log('c' in p, p.c); // false, 37
无操作转发代理
let target = {};let p = new Proxy(target, {});p.a = 37; // 操作转发到目标console.log(target.a); // 37. 操作已经被正确地转发
验证属性的值
//设置验证器对象,定义一个set属性,相当于让proxy拦截设置属性的方法,然后判断设置的属性有没有问题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');}}// The default behavior to store the valueobj[prop] = value;// 表示成功return true;}};let person = new Proxy({}, validator);person.age = 100;console.log(person.age);// 100person.age = 'young';// 抛出异常: Uncaught TypeError: The age is not an integerperson.age = 300;// 抛出异常: Uncaught RangeError: The age seems invalid
扩展构造函数
function extend(sup, base) {var descriptor = Object.getOwnPropertyDescriptor(base.prototype, "constructor");base.prototype = Object.create(sup.prototype);var handler = {construct: function(target, args) {var obj = Object.create(base.prototype);this.apply(target, obj, args);return obj;},apply: function(target, that, args) {sup.apply(that, args);base.apply(that, args);}};var proxy = new Proxy(base, handler);descriptor.value = proxy;Object.defineProperty(base.prototype, "constructor", descriptor);return proxy;}var Person = function (name) {this.name = name};var Boy = extend(Person, function (name, age) {this.age = age;});Boy.prototype.sex = "M";var Peter = new Boy("Peter", 13);console.log(Peter.sex); // "M"console.log(Peter.name); // "Peter"console.log(Peter.age); // 13
操作 DOM 节点
let view = new Proxy({selected: null}, {set: function(obj, prop, newval) {let oldval = obj[prop];if (prop === 'selected') {if (oldval) {oldval.setAttribute('aria-selected', 'false');}if (newval) {newval.setAttribute('aria-selected', 'true');}}// 默认行为是存储被传入 setter 函数的属性值obj[prop] = newval;// 表示操作成功return true;}});let i1 = view.selected = document.getElementById('item-1');console.log(i1.getAttribute('aria-selected')); // 'true'let i2 = view.selected = document.getElementById('item-2');console.log(i1.getAttribute('aria-selected')); // 'false'console.log(i2.getAttribute('aria-selected')); // 'true'
值修正及附加属性
以下products代理会计算传值并根据需要转换为数组。
这个代理对象同时支持一个叫做 latestBrowser的附加属性,这个属性可以同时作为 getter 和 setter。
let products = new Proxy({browsers: ['Internet Explorer', 'Netscape']}, {get: function(obj, prop) {// 附加一个属性if (prop === 'latestBrowser') {return obj.browsers[obj.browsers.length - 1];}// 默认行为是返回属性值return obj[prop];},set: function(obj, prop, value) {// 附加属性if (prop === 'latestBrowser') {obj.browsers.push(value);return;}// 如果不是数组,则进行转换if (typeof value === 'string') {value = [value];}// 默认行为是保存属性值obj[prop] = value;// 表示成功return true;}});console.log(products.browsers); // ['Internet Explorer', 'Netscape']products.browsers = 'Firefox'; // 如果不小心传入了一个字符串console.log(products.browsers); // ['Firefox'] <- 也没问题, 得到的依旧是一个数组products.latestBrowser = 'Chrome';console.log(products.browsers); // ['Firefox', 'Chrome']console.log(products.latestBrowser); // 'Chrome'
通过属性查找数组中的特定对象
let products = new Proxy([{ name: 'Firefox' , type: 'browser' },{ name: 'SeaMonkey' , type: 'browser' },{ name: 'Thunderbird', type: 'mailer' }], {get: function(obj, prop) {// 默认行为是返回属性值, prop ?通常是一个整数if (prop in obj) {return obj[prop];}// 获取 products 的 number; 它是 products.length 的别名if (prop === 'number') {return obj.length;}let result, types = {};for (let product of obj) {if (product.name === prop) {result = product;}if (types[product.type]) {types[product.type].push(product);} else {types[product.type] = [product];}}// 通过 name 获取 productif (result) {return result;}// 通过 type 获取 productsif (prop in types) {return types[prop];}// 获取 product typeif (prop === 'types') {return Object.keys(types);}return undefined;}});console.log(products[0]); // { name: 'Firefox', type: 'browser' }console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }console.log(products['Chrome']); // undefinedconsole.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]console.log(products.types); // ['browser', 'mailer']console.log(products.number); // 3
拦截函数执行

