目录
前言
需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程。
在ES6前,我们也是可以做到这一点的,可以通过属性描述符中的存储属性描述符,来对属性的操作进行监听。
const obj = {
bar: 123,
foo: "hello",
};
Object.keys(obj).forEach(key => {
let value = obj[key];
Object.defineProperty(obj, key, {
get: function () {
console.log(`监听到${key}属性被访问`);
return value;
},
set: function (newValue) {
console.log(`监听到${key}属性被设为${newValue}`);
value = newValue;
},
});
});
console.log(obj.bar);
obj.foo = "world";
console.log(obj.foo);
Vue 2.x的响应式就是基于这一方法,下面是一个简易版的响应式实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.box {
width: 200px;
height: 200px;
margin: 0 auto;
border: 1px solid #666;
border-radius: 10px;
background-color: aqua;
display: flex;
flex-direction: column;
justify-content: space-around;
}
p {
padding: 0 20px;
font-size: 20px;
}
.input {
width: 200px;
height: 200px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="box">
<p id="firstName" />
<p id="lastName" />
<p id="age" />
</div>
<div class="input">
<input onchange="user.name = this.value" />
<input type="date" onchange="user.birth = this.value" />
</div>
<script>
let currentFn = null;
function watchFn(fn) {
currentFn = fn;
fn();
currentFn = null;
}
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key];
const funcs = new Set();
Object.defineProperty(obj, key, {
get: function () {
if (currentFn) funcs.add(currentFn);
return value;
},
set: function (newValue) {
value = newValue;
funcs.forEach(func => func());
},
});
});
return obj;
}
var user = reactive({
name: "张三",
birth: "1998-06-08",
});
// 显示姓
function showFirstName() {
document.querySelector("#firstName").textContent =
"姓:" + user.name[0];
}
// 显示名
function showLastName() {
document.querySelector("#lastName").textContent =
"名:" + user.name.slice(1);
}
// 显示年龄
function showAge() {
var birthday = new Date(user.birth);
var today = new Date();
let age = today.getFullYear() - birthday.getFullYear();
let m = today.getMonth() - birthday.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthday.getDate())) {
age--;
}
document.querySelector("#age").textContent = "年龄:" + age;
}
watchFn(showFirstName);
watchFn(showLastName);
watchFn(showAge);
</script>
</body>
</html>
但这样做是有缺点的:
Object.defineProperty
设计的初衷,不是为了去监听一个对象中所有的属性的。我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。- 如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么
Object.defineProperty
是无能为力的。
那么有没有更好的方案呢?
当然有!ES6给我们带来了一个新的API:Proxy
Proxy - 代理
介绍
在ES6中,新增了一个Proxy
类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
如果我们希望监听一个对象(A)的相关操作,那么可以先创建一个代理对象(B)(Proxy
对象),之后对A对象的所有操作,都通过代理对象B来完成,代理对象可以监听我们想要对原对象进行哪些操作;
用法
const proxy = new Proxy(target, handler);
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时的代理行为
参数 handler 属性
如果我们想要侦听某些具体的操作,那么就可以在handler
中添加对应的捕捉器(Trap)
关于handler
拦截属性,有如下:
get(target, key, receiver)
:拦截对象属性的读取set(target, key, value, receiver)
:拦截对象属性的设置deleteProperty(target, key)
:拦截delete proxy[key]
的操作,返回一个布尔值has(target, key)
:拦截key in proxy
的操作,返回一个布尔值ownKeys(target)
:拦截Object.keys(proxy)
、for...in
等循环,返回一个数组getOwnPropertyDescriptor(target, key)
:拦截Object.getOwnPropertyDescriptor(proxy, key)
,返回属性的描述对象defineProperty(target, key, propDesc)
:拦截Object.defineProperty(proxy, key, propDesc)
,返回一个布尔值preventExtensions(target)
:拦截Object.preventExtensions(proxy)
,返回一个布尔值isExtensible(target)
:拦截Object.isExtensible(proxy)
,返回一个布尔值getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy)
,返回一个对象setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值apply(target, object, args)
:拦截Proxy
实例作为函数调用的操作construct(target, args, newTarget)
:拦截Proxy
实例作为构造函数调用的操作
优先重点关注 get
set
deleteProperty
这三个操作
取消代理
Proxy.revocable(target, handler);
案例
对象
const bar = {
a: 123,
b: "hello",
};
const barProxy = new Proxy(bar, {
get: function (target, key, receiver) {
console.log("barProxy.get", receiver);
// receiver是创建出来的代理对象
return target[key];
},
set: function (target, key, value, receiver) {
target[key] = value;
console.log("barProxy.set", receiver);
},
deleteProperty: function (target, key) {
console.log("barProxy.delete");
delete target[key];
},
});
console.log(barProxy.a);
barProxy.b = "world";
console.log(barProxy.b);
delete barProxy.b;
console.log(barProxy);
console.log(bar);
函数
function foo() {
console.log("foo函数被调用", this, arguments);
return "foo";
}
const fooProxy = new Proxy(foo, {
apply: function (target, thisArg, args) {
console.log("函数的apply侦听");
return target.apply(thisArg, args);
},
construct: function (target, argsArr, newTarget) {
console.log(target, argsArr, newTarget);
return new target(...argsArr);
},
});
fooProxy("11");
new fooProxy("22");
Reflect - 反射
介绍
Reflect
也是ES6新增的一个API,它是一个对象,字面的意思是反射。
用法
Reflect
主要提供了很多操作JS对象的方法,有点像Object
中操作对象的方法。
比如Reflect.defineProperty(target, key, attributes)
类似于Object.defineProperty()
;
比如Reflect.getPrototypeOf(target)
类似于Object.getPrototypeOf()
;
既然有
Object
可以做这些操作,为什么还需要新增Reflect
这样一个对象呢?
- 在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了
Object
上面,但是Object
作为一个构造函数,这些操作实际上放到它身上并不合适- 另外还包含一些类似于
in
、delete
操作符,让JS看起来是会有一些奇怪的- 所以在ES6中新增了
Reflect
,让我们这些操作都集中到了Reflect
对象上
Object
和Reflect
对象之间的API关系,可以参考MDN文档。
Reflect
的所有属性和方法都是静态的(就像 Math 对象)
Reflect
对象提供了以下静态方法,这些方法与 proxy handler 方法的命名相同。
方法
Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply()
功能类似。Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行new
操作,相当于执行new target(...args)
。Reflect.defineProperty(target, propertyKey, attributes)
和Object.defineProperty()
类似。如果设置成功就会返回true
Reflect.deleteProperty(target, propertyKey)
作为函数的delete
操作符,相当于执行delete target[name]
。Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于target[name]
。Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于Object.getOwnPropertyDescriptor()
。如果对象中存在该属性,则返回对应的属性描述符,否则返回undefined
。Reflect.getPrototypeOf(target)
类似于Object.getPrototypeOf()
。Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和in
运算符的功能完全相同。Reflect.isExtensible(target)
类似于Object.isExtensible().
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys()
, 但不会受enumerable
影响).Reflect.preventExtensions(target)
类似于Object.preventExtensions()
。返回一个Boolean
。Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个Boolean
,如果更新成功,则返回true
。Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数。返回一个Boolean
,如果更新成功,则返回true
。
案例
get
const person = {
name: "Guest",
};
const proxy = new Proxy(person, {
get: function (target, propKey) {
return Reflect.get(target, propKey);
// or
// return target[propKey]
},
});
console.log(proxy.name); // "Guest"
set
const obj = { name: "张三", age: 18 };
const objProxy = new Proxy(obj, {
get(target, key) {
if (key in target) {
return Reflect.get(target, key);
} else {
console.error("字段不存在");
return undefined;
}
},
set(target, key, value, receiver) {
if (key === "age") {
if (typeof value === "number") {
return Reflect.set(target, key, value, receiver);
// or
// target[propKey] = value
// return true
} else {
console.error("年龄只能输入正整数");
return false;
}
} else {
return false;
}
},
});
objProxy.age = 20;
console.log(objProxy.age); // 20
objProxy.age = "22";
console.log(objProxy.age); // 20
console.log(objProxy.test); // undefined
提醒:严格模式下,set
代理如果没有返回true
,就会报错
deleteProperty
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除
const handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
Reflect.deleteProperty(target,key)
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`无法删除私有属性`);
}
}
const target = { _prop: 'foo' };
const proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属性
console.log(proxy); // Proxy {_prop: 'foo'}
receiver的作用
案例
const obj = {
_name: "hello",
get name() {
return this._name;
},
set name(newValue) {
this._name = newValue;
},
};
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
console.log("get方法被访问-----", key, receiver);
console.log(receiver === objProxy); // true
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log("set方法被访问-----", key);
Reflect.set(target, key, value, receiver);
},
});
console.log(objProxy.name);
objProxy.name = "world";
const parent = {
get value() {
return "hello";
},
};
const proxy = new Proxy(parent, {
get(target, key, receiver) {
console.log(receiver === proxy); // log: false
console.log(receiver === child); // log:true
return target[key];
},
});
const child = {
name: "world",
};
// 设置child继承于parent的代理对象proxy
Object.setPrototypeOf(child, proxy);
child.value; // hello
Proxy
中接受的receiver
形参表示代理对象本身或者继承于代理对象的对象,本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问。
Reflect
中传递的receiver
实参表示修改执行原始操作时的this
指向。
若需要在Proxy
内部调用对象的默认行为,建议使用Reflect
,能在触发代理对象的劫持时保证正确的this
上下文指向。
面试题
如何让if
里面的代码执行,成功在控制台打印出Win
?
if (a == 1 && a == 2 && a == 3) {
console.log('Win')
}
方案一:重写toString / valueOf方法
const a = {
_a: 0,
toString: function() {
return ++a._a
},
// valueOf: function() {
// return ++a._a
// },
}
因为toString
是Object.prototype
上面默认的方法,所以这个办法相当于把正常的隐式转换中toString
方法给拦截了。
涉及原型和原型链的知识点
方案二:数组
let a = [1,2,3];
a.toString = a.shift;
方案三:Proxy
let a = new Proxy({}, {
i: 1,
get: function() {
return () => this.i++;
}
});
现在将题目简单修改一下,将双等变成三个等怎么办?
大家都知道===的话是先判断类型,再判断值。这里的toString
已经默认把对象转化为字符串了,使用toString
的话,结果就不成立了。
方案四:defineProperties
Object.defineProperties(window, {
_a: {
value: 0,
writable: true
},
a: {
get: function() {
return ++_a
}
}
})
Vue 3.x 响应式
let currentFn = null;
let dependenciesMap = new WeakMap();
function watchFn(fn) {
currentFn = fn;
fn();
currentFn = null;
}
class Dependency {
constructor() {
this.dependencies = new Set();
}
depend() {
if (currentFn) this.dependencies.add(currentFn);
}
run() {
this.dependencies.forEach(fn => fn());
}
}
function getDependency(target, key) {
let map = dependenciesMap.get(target);
if (!map) dependenciesMap.set(target, (map = new Map()));
let dependency = map.get(key);
if (!dependency) map.set(key, (dependency = new Dependency()));
return dependency;
}
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
console.log(target, `${key}被访问`);
const dependency = getDependency(target, key);
dependency.depend();
return Reflect.get(target, key, receiver);
},
set: function (target, key, newValue, receiver) {
console.log(target, `${key}被设置为${newValue}`);
Reflect.set(target, key, newValue, receiver);
const dependency = getDependency(target, key);
dependency.run();
},
has: function (target, key) {
console.log("正在查询对象", target, `上是否存在${key}属性`);
return Reflect.has(target, key);
},
});
}
const foo = reactive({
name: "foo",
age: 18,
});
// console.log(foo.name);
// console.log(foo.age);
// foo.name = "bar";
// foo.age = 20;
// console.log(foo);
// console.log("dd" in foo);
watchFn(function () {
console.log(foo.name);
console.log(foo.age);
});
watchFn(function () {
console.log(foo.name, "1111");
});
foo.name = "bar";
console.log(foo.name);