背景
需求
比如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, undefined
console.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 value
obj[prop] = value;
// 表示成功
return true;
}
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age);
// 100
person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer
person.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 获取 product
if (result) {
return result;
}
// 通过 type 获取 products
if (prop in types) {
return types[prop];
}
// 获取 product type
if (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']); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3