高级特性与API
约 2164 字大约 7 分钟
2025-11-24
Proxy 监听
思考
Proxy 能够监听对象中的对象的引用吗?
想一想,以下代码会输出什么?为什么?
JS code example
Proxy 默认只代理对象的一层属性。如果对象的某个属性值本身是另一个对象(嵌套对象),那么对该嵌套对象 内部属性的读写操作,不会触发外层 Proxy 的拦截器(handler), 因为嵌套对象本身 不是 Proxy,而是原始引用。因此以上代码的执行流程实际上为:
// 执行 proxy.a.b = 2 时:
// 步骤1: proxy.a → 触发 get('a'),返回 target[key] 即 obj.a
// 步骤2: (返回的 obj.a).b = 2 → 直接在原始对象上设置属性
// proxy.set 不会触发,因为操作对象是 obj.a 不是 proxy如何监听嵌套对象?
必须对每个嵌套对象也创建 Proxy,即实现 深度代理:
function reactive(obj) {
if (obj === null || typeof obj !== "object") return obj;
Object.keys(obj).forEach((key) => {
obj[key] = reactive(obj[key]); // 递归代理
});
return new Proxy(obj, {
get(target, key) {
console.log("get", key);
return reactive(target[key]); // 返回也需代理
},
set(target, key, value) {
console.log("set", key, value);
target[key] = reactive(value); // 新值也需代理
return true;
},
});
}Vue 3 的 reactive() 就是基于这种深度 Proxy 实现的。Vue3 中的响应系统可阅读这里。
apply、call、bind
思考
改变 this 指向的方法有哪些?
call、apply、bind 的区别?
apply、call、、bind 都是用来改变函数执行上下文的,也就是函数运行时 this 的指向。
apply
apply 接收两个参数:function.apply(thisArg, [argsArray])。第一个参数是 this 的指向,第二个参数是函数接收的参数且以数组的形式传入。且第一个参数为 null 或 undefined 时 this 指向 window 。 apply 调用函数后会立即执行,且 this 指向只临时改变一次。
call
call 与 bind 很多相似之处:接收两个参数、调用函数后也会立即执行,且 this 指向只临时改变一次、当第一个参数为 null 或 undefined 时 this 指向 window 。它们的区别在于 call 传入的第二个参数是一个参数列表。
bind
bind 传入的参数与 call 一样,第一个为指定的 this ,第二个为 参数列表。区别在于它不是立即执行,而是返回一个永久改变 this 指向的函数,且第二个参数可以分多次传入(因为已经如同另一个函数)。
连续 bind() 时,只有第一次的 this 生效且永久锁定,后续 bind() 只能追加预设参数,无法改变 this 指向。这是 JavaScript 引擎内部 [[BoundThis]] 槽位一旦设置便不可修改的机制决定的。
对比表
| apply | call | bind | |
|---|---|---|---|
| 参数 | thisArg, [argsArray] | thisArg, arg1, arg2, ... | thisArg, arg1, arg2, ... |
| 执行 | 立即执行 | 立即执行 | 返回一个改变 this 指向的函数 |
| this | 临时改变 | 临时改变 | 永久改变 |
| 参数列表 | 数组 | 参数列表 | 参数列表 |
new 操作符
思考
new 操作符具体干了什么?
new 操作符是 JavaScript 中用于创建对象实例的关键字。当执行 new Fn(...args) 时,JavaScript 引擎会按顺序执行以下 4 个步骤:
内部过程
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log("Hi!");
};
const p = new Person("Alice");步骤 1:创建一个全新的空对象
const newInstance = {};步骤 2:将新对象的 [[Prototype]] 指向构造函数的 prototype
Object.setPrototypeOf(newInstance, Person.prototype);
// 等价于 newInstance.__proto__ = Person.prototype;这一步建立了原型链继承,使新对象能访问 Person.prototype 上的方法(如 sayHi)。
步骤 3:将构造函数内的 this 绑定到新对象,并执行构造函数
const result = Person.call(newInstance, "Alice");
// 此时构造函数内 this === newInstance
// 所以 newInstance.name = 'Alice'步骤 4:决定返回值
- 如果构造函数显式返回一个对象 → 返回该对象;
- 否则 → 返回新创建的对象(即
newInstance)。
// 情况1:构造函数无 return 或返回非对象
function A() {
this.x = 1;
}
new A(); // → { x: 1 }
// 情况2:构造函数返回对象
function B() {
return { y: 2 };
}
new B(); // → { y: 2 }(新对象被丢弃!)
// 情况3:构造函数返回原始值(无效)
function C() {
return "hello";
}
new C(); // → { }(仍返回新对象,忽略字符串返回值)手动模拟 new 的实现
new模拟
注意事项
箭头函数不能用
new
因为箭头函数没有prototype,也没有自己的this。new.target可检测是否通过new调用function Foo() { if (!new.target) throw new Error("必须用 new 调用!"); }ES6 Class 本质仍是
new+ 原型class MyClass {} // 等价于 function MyClass() {} // 但 class 默认启用严格模式,且不能直接调用(必须 new)
总结:new 做了什么?
| 步骤 | 行为 | 目的 |
|---|---|---|
| 1 | 创建空对象 {} | 准备实例容器 |
| 2 | 设置 [[Prototype]] → Fn.prototype | 实现继承 |
| 3 | 执行 Fn,this 指向新对象 | 初始化实例属性 |
| 4 | 智能返回(优先返回构造函数返回的对象) | 兼容特殊返回逻辑 |
Map 与 Set
思考
Map 与 Set 的区别?
Map 与 Set 都是集合类数据结构,用于存储唯一值。
Map 是一种存储键值对的数据结构,类似 json 对象,但 Map 的键可以是任何类型的值且唯一。
Map 可以实现比 json 对象更高效的查找修改,可以用于大量键值数据的高频增删改查,
Map 的使用
Set 类似与数组,但是用于存储唯一值。可以理解为:Map 是存储唯一的 key:value,Set 是存储唯一 value。
由于 Set 存储的值都是唯一,因此常常用于数组去重。
Set 的使用
Map 与 WeakMap
思考
WeakMap 与 Map 的区别?
WeakMap 是 ES6 新增的键值对集合。它与 Map 的区别是 WeakMap 的键只能是对象,且键值对是弱引用。
什么是弱引用?
// Map - 强引用,对象无法被回收
let mapKey = { id: 1 };
const map = new Map();
map.set(mapKey, 'value');
mapKey = null; // 虽然变量置空,但 Map 仍强引用对象
// { id: 1 } 不会被垃圾回收,造成内存泄漏
// WeakMap - 弱引用,对象可被自动回收
let weakKey = { id: 2 };
const weakMap = new WeakMap();
weakMap.set(weakKey, 'value');
weakKey = null; // 无其他引用,垃圾回收器会自动回收该对象
// WeakMap 中的对应条目也会自动消失由于 WeakMap 的键值对是弱引用,垃圾回收机制是不可预测的,因此 WeakMap 的键值映射关系随时都可能被回收,导致遍历结果不确定,因此 WeakMap 不可被遍历。