前言
在js里面对一个js对象进行拦截和自定义操作是非常常见的场景。那么今天介绍一下如何通过Proxy对象进行创建一个对象的代理。
语法
const p = new Proxy(target, handler)
参数
- target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
handler的捕获器
handler.getPrototypeOf()
当读取代理对象的原型时,该方法就会被调用。
- 语法
const p = new Proxy(obj, {
// target 被代理的目标对象。
getPrototypeOf(target) {
// ture,当 getPrototypeOf 方法被调用时,this 指向的是它所属的处理器对象。
console.log(this === handler)
// getPrototypeOf 方法的返回值必须是一个对象 或者 null。
return null
}
});
-
以下五种方法可以触发 getPrototypeOf() 代理方法运行
-
Object.getPrototypeOf(),方法返回指定对象的原型(内部[[Prototype]]属性的值)。
-
Reflect.getPrototypeOf(),方法返回指定对象的原型 (即内部的 [[Prototype]] 属性的值) 。
-
__proto__(该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。)Object.prototype 的__proto__属性是一个访问器属性(一个getter函数和一个setter函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。 -
Object.prototype.isPrototypeOf(),用于测试一个对象是否存在于另一个对象的原型链上。
let arr = [] let obj = {} // true console.log(Array.prototype.isPrototypeOf(arr)) // false console.log(Array.prototype.isPrototypeOf(obj)) -
instanceof,用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
let arr = [] let obj = {} // true console.log(arr instanceof Array) // false console.log(obj instanceof Array)
-
-
例子
let monster1 = {
eyeCount: 4,
test: 1
};
let monsterPrototype = {
eyeCount: 20,
age: 10
};
let handler = {
getPrototypeOf(target) {
return monsterPrototype;
}
};
let proxy1 = new Proxy(monster1, handler);
monsterPrototype.age = 30
let proxy1Prototype = Object.getPrototypeOf(proxy1)
// 30
console.log(proxy1Prototype.age, 1)
// 20
console.log(proxy1Prototype.eyeCount, 2)
// undefined
console.log(proxy1Prototype.test, 3)
handler.setPrototypeOf()
handler.setPrototypeOf() 方法主要用来拦截 Object.setPrototypeOf()、Reflect.setPrototypeOf()。
- 语法
var p = new Proxy(target, {
// target, 被拦截目标对象.
// prototype, 对象新原型或为null.
setPrototypeOf: function(target, prototype) {
// 如果成功修改了[[Prototype]], setPrototypeOf 方法返回 true,否则返回 false.
return true
}
});
- 例子 如果你不想为你的对象设置一个新的原型,你的处理者的setPrototypeOf方法可以返回false,也可以抛出异常。
var handlerReturnsFalse = {
setPrototypeOf(target, newProto) {
return false;
// or
// throw new Error('custom error');
}
};
var newProto = {}, target = {};
var p1 = new Proxy(target, handlerReturnsFalse);
Object.setPrototypeOf(p1, newProto); // throws a TypeError
Reflect.setPrototypeOf(p1, newProto); // returns false
handler.isExtensible()
handler.isExtensible() 方法用于拦截对对象的Object.isExtensible()。(Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。)
- 语法
var p = new Proxy(target, {
// target,目标对象
isExtensible: function(target) {
// isExtensible方法必须返回一个 Boolean值或可转换成Boolean的值。
return true
}
});
- 注意 Object.isExtensible(proxy) 必须同Object.isExtensible(target)返回相同值。
var p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
// TypeError is thrown
Object.isExtensible(p);
var p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
Object.preventExtensions(p);
// 返回false
Object.isExtensible(p);
handler.preventExtensions()
handler.preventExtensions() 方法用于设置对Object.preventExtensions()的拦截。(Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。)
- 语法
var p = new Proxy(target, {
// target, 所要拦截的目标对象.
preventExtensions: function(target) {
// 返回布尔值, 如果目标对象是可扩展的,那么只能返回 false
return true
}
});
- 注意 handler.preventExtensions() 拦截 Object.preventExtensions()返回一个布尔值.
- 例子
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
// 返回p
console.log(Object.preventExtensions(p));
handler.getOwnPropertyDescriptor()
handler.getOwnPropertyDescriptor()用于对Object.getOwnPropertyDescriptor()方法的拦截( Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性) )
- 语法
var p = new Proxy(target, {
// target: 目标对象,prop: 返回属性名称的描述
getOwnPropertyDescriptor: function(target, prop) {
// 返回一个 object 或 undefined。
return object
}
});
- 例子
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return undefined;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'foo', baz: 'baz' };
var proxy = new Proxy(target, handler);
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'))
// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'))
// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'))
handler.defineProperty()
handler.defineProperty() 用于拦截对对象的 Object.defineProperty() 操作。(Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。)
- 语法
var p = new Proxy(target, {
// target: 目标对象,property: 待检索其描述的属性名, descriptor: 待定义或修改的属性的描述符。
defineProperty: function(target, property, descriptor) {
// 返回一个 Boolean 值,表示定义该属性的操作成功与否。
return true
}
});
- 对以下方法进行拦截
- Object.defineProperty()
- Reflect.defineProperty()
- proxy.property='value'
- 例子
var p = new Proxy({}, {
defineProperty: function(target, prop, descriptor) {
console.log('called: ' + prop);
return true;
}
});
var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "called: a"
handler.has()
handler.has()用于in操作符拦截(in操作符:如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。)
- 语法
var p = new Proxy(target, {
// target: 目标对象, prop: 需要检查是否存在的属性
has: function(target, prop) {
// 返回一个boolean值
return true
}
});
-
对以下方法进行拦截
- 属性查询: foo in proxy
- 继承属性查询: foo in Object.create(proxy)
- with 检查: with(proxy) { (foo); }
- Reflect.has()
-
例子
var obj = { a: 10 };
var p = new Proxy(obj, {
has: function(target, prop) {
return true;
}
});
'a' in p;
- 注意 如果违反了下面这些规则, proxy 将会抛出 TypeError:
- 如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏.
- 如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p; // TypeError is thrown
handler.get()
handler.get() 方法用于拦截对象的读取属性操作。
- 语法
var p = new Proxy(target, {
// target: 目标对象,property: 被获取的属性名 ,receiver:Proxy或者继承Proxy的对象
get: function(target, property, receiver) {
// get方法可以返回任何值。
return any
}
});
- 对以下方法进行拦截
- 访问属性: proxy[foo]和 proxy.bar
- 访问原型链上的属性: Object.create(proxy)[foo]
- Reflect.get()
- 例子
var p = new Proxy({
age: 10,
name: 'lili'
}, {
get: function(target, prop, receiver) {
if(prop === 'age') { return 18 }
return target[prop]
}
});
// 18
console.log(p.age)
// lili
console.log(p.name)
handler.set()
handler.set() 方法是设置属性值操作的捕获器。
- 语法
const p = new Proxy(target, {
// target: 目标对象,property: 将被设置的属性名或 Symbol, value: 新属性值, receiver: 最初被调用的对象
set: function(target, property, value, receiver) {
// 返回一个布尔值。
return true
}
});
-
对以下方法进行拦截
- 指定属性值:proxy[foo] = bar 和 proxy.foo = bar
- 指定继承者的属性值:Object.create(proxy)[foo] = bar
- Reflect.set()
-
例子
var p = new Proxy({}, {
set: function(target, prop, value, receiver) {
target[prop] = value
return true;
}
})
p.a = 102;
// 102
console.log(p.a);
handler.deleteProperty()
handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。
- 语法
var p = new Proxy(target, {
// target: 目标对象,property: 待删除的属性名
deleteProperty: function(target, property) {
// 返回一个 Boolean 类型的值,表示了该属性是否被成功删除。
return true
}
});
-
对以下方法进行拦截
- 删除属性: delete proxy[foo] 和 delete proxy.foo
- Reflect.deleteProperty()
-
例子
var p = new Proxy({a: 123}, {
deleteProperty: function(target, prop) {
return false;
}
});
delete p.a;
// 123
console.log(p.a)
handler.ownKeys()
handler.ownKeys() 方法用于拦截 Reflect.ownKeys()
- 语法
var p = new Proxy(target, {
// target: 目标对象
ownKeys: function(target) {
// 返回一个可枚举对象
return obj
}
});
- 对以下方法进行拦截
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- Reflect.ownKeys()
- 例子
var p = new Proxy({
a: 1,
b: 2
}, {
ownKeys: function(target) {
return Reflect.ownKeys(target)
}
});
// ['a', 'b']
console.log(Object.keys(p))
handler.apply()
handler.apply() 方法用于拦截函数的调用。
- 语法
var p = new Proxy(target, {
// target: 目标对象,thisArg: 被调用时的上下文对象, argumentsList: 被调用时的参数数组
apply: function(target, thisArg, argumentsList) {
// 可以返回任何值
return any
}
});
-
对以下方法进行拦截
- proxy(...args)
- Function.prototype.apply() 和 Function.prototype.call()
- Reflect.apply()
-
例子
var p = new Proxy(function() {}, {
apply: function(target, thisArg, argumentsList) {
return argumentsList[0] + argumentsList[1] + argumentsList[2];
}
});
// 6
console.log(p(1, 2, 3));
handler.construct()
handler.construct() 方法用于拦截new 操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。
- 语法
var p = new Proxy(target, {
// target: 目标对象,argumentsList: constructor的参数列表, newTarget: 最初被调用的构造函数,就这个例子而言是p。
construct: function(target, argumentsList, newTarget) {
// 必须返回一个对象
return obj
}
});
- 对以下方法进行拦截
- new proxy(...args)
- Reflect.construct()
- 例子
let p = new Proxy(function () {}, {
construct: function(target, args) {
return { value: args[0] * 10 };
}
});
// { value: 10 }
console.log(new p(1))
场景
看一下proxy的使用例子,加深理解
当对象中不存在属性名时,默认返回'该属性未被定义'
例子:
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : '该属性未被定义';
}
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
// 打印结果:1 undefined "该属性未被定义"
console.log(p.a, p.b, p.c);
// 'c' in p // 查询p上是否有'c'属性,无论属性在原型还是在实例中都会返回true
// 打印false
console.log('c' in p);
验证向一个对象的传值
例如person对象有一个age的属性,我们加个条件限制,age只能是number类型,并且不能超过100
例子:
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 > 100) {
throw new RangeError('The age seems invalid');
}
}
obj[prop] = value;
// 表示成功
return true;
}
};
let person = new Proxy({}, validator);
person.age = 17;
console.log(person.age);
// 100
person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer
person.age = 200;
// 抛出异常: Uncaught RangeError: The age seems invalid
操作DOM节点
例如设置最后选中的dom元素的aria-selected属性为true,之前选中的则全为false
- html代码
<!DOCTYPE html>
<html>
<head>
<title>文档的标题</title>
</head>
<body>
<div class="class1" id="1">aaa</div>
<div class="class2" id="2">bbb</div>
<div class="class3" id="3">ccc</div>
</body>
</html>
<style>
.class1 {
color: aqua;
}
.class2 {
color: blue;
}
.class3 {
color: pink;
}
</style>
- js代码
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('1');
let i2 = view.selected = document.getElementById('2');
let i3 = view.selected = document.getElementById('3');
- 最后执行结果
值修正及附加属性
加上products对象的browsers是存储浏览器名称的数组,browsers.latestBrowser表示最新的浏览器,我们如何实现products.browsers = xxx, xxx是string的情况,如何默认转化成数组
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'
vue3.0为何使用Proxy来代替Object.defineProperty
首先vue采用数据劫持结合发布-订阅者模式实现双向绑定
Object.defineProperty实现数据劫持的缺点
- Object.defineProperty的第一个缺陷,无法监听数组变化
- 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。
Proxy实现数据劫持的优点
- Proxy可以直接监听对象而非属性
- Proxy可以直接监听数组的变化
- Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
Proxy的缺点
Proxy的劣势就是兼容性问题,而且无法用polyfill磨平
关于这个话题
关于"vue3.0为何使用Proxy来代替Object.defineProperty"这个话题,推荐下juejin.cn/post/684490… 这篇文章,介绍的非常详细。
最后
感谢大家阅读,如有问题欢迎纠正!