JS 题目
约 2616 字大约 9 分钟
2026-01-23
this + 箭头函数
const obj = {
a: 1,
foo() {
return () => this.a;
}
};
const f = obj.foo();
console.log(f());const f = obj.foo(); 这一步调用位置是 obj.foo(),所以 foo 里的 this 确实绑定到 obj。
foo 返回的是 箭头函数,而箭头函数的 this 不是运行时绑定的,它会词法捕获创建时外层的 this(也就是 foo 执行时的 this)。
所以 f() 里用的 this 其实仍然是 obj,等价于读 obj.a, f() 应该输出 1。
const obj = { a: 1, foo() { return function(){ return this.a } } };
const f = obj.foo();
console.log(f()); // undefined(严格模式下是 TypeError)箭头函数没有自己的 this,或者说 箭头函数不创建自己的 this,它的 this 在“定义时”就从外层词法作用域捕获好了
也就是说:
foo() {
// 这里的 this 是 obj
return () => this.a;
}foo执行时,this === obj- 箭头函数在这里被创建
- 所以箭头函数里的
this永远是obj
所以 f() 执行时,this 不会再发生变化
const f = obj.foo();
f(); // this 仍然是 obj不是因为“f 运行在 obj 作用域”,而是因为 箭头函数早就把 this 记死了
不能用 call / apply / bind 改变箭头函数的 this
const f = () => this.a;
f.call({ a: 100 }); // 还是原来的 this原因不是“不能用”,而是:call/apply/bind 只能影响“函数调用时的 this”,而箭头函数 压根没有“调用时 this”这个概念
注意一个细节:
const g = f.bind(obj);
g(); // 仍然没用bind 会返回新函数,但箭头函数内部的 this 依然不会变。
准确来说:
bind确实会返回一个新的包装函数,但这个新函数调用时,依然会执行原箭头函数,而原箭头函数的this早已捕获,所以bind传递的this会被忽略(不是 “没用”,而是箭头函数不接收)。
如果 foo 是箭头函数,外层 this 会变成全局,而非 obj,比如:
const obj = {
a: 1,
foo: () => { // foo 本身是箭头函数
return () => this.a;
}
};
const f = obj.foo();
console.log(f()); // undefined(浏览器环境)原因:obj.foo() 调用时,foo 作为箭头函数,其 this 捕获的是定义时的外层(全局)this,而非 obj,所以内部箭头函数的 this 也是全局,最终输出 undefined。这个例子能进一步区分 “箭头函数定义位置” 和 “调用位置” 的差异。
原型链
function A() {}
A.prototype.x = 1;
const a = new A();
A.prototype = { x: 2 };
console.log(a.x);输出是:1
const a = new A(); 创建一个新对象 a, 设置内部属性:a.__proto__ === 当时的 A.prototype,注意关键词:当时的
function A() {} => A.prototype.constructor===A
当 A.prototype.x = 1; 后,A.prototype 是 { x: 1, constructor: A }
在const a = new A(); 的时候,已经 a.__proto__ === A.prototype 了,已经绑定了
A.prototype = { x: 2 }; 这一行不会 修改 A.prototype ,更不会影响已经创建好的 a
它只是做了一件事:A.prototype → 指向一个全新的对象(proto2)
一个非常高级但常见的错觉,可能在调试时看到了类似:
a.__proto__.constructor.prototype然后发现那里是 { x: 2 }
但注意这条链:
a.__proto__ // proto1
a.__proto__.constructor // A
a.__proto__.constructor.prototype // A.prototype(proto2)⚠️ 这是你“手动绕了一圈”,不是 JS 自动查找路径!
如果下面是 A.prototype.x = 2,那答案就是 2
总结:new 绑定的是“当时的 prototype 对象”,以后你怎么改 A.prototype,都不会影响已经创建的实例。
标准答案:是因为执行 A.prototype = { x: 2 } 后,A.prototype 已经不是原来的那个 A.prototype 了,也不是 new A() 时用的那个 A.prototype
第一段代码
function A() {}
A.prototype.x = 1;
const a = new A();
console.log(a.__proto__ === A.prototype); // truenew A() 时:
a.__proto__ = A.prototype此时 两边指向的是同一个对象,所以 true
第二段代码(关键差别)
function A() {}
A.prototype.x = 1;
const a = new A();
A.prototype = { x: 2 };
console.log(a.__proto__ === A.prototype); // false重点只看这一行:
A.prototype = { x: 2 };⚠️ 这不是“改原型对象”
⚠️ 而是让 A.prototype 这个变量,指向了一个全新的对象
就像:
let p1 = { x: 1 };
let p2 = p1;
p1 = { x: 2 };
p2 === p1 // false终极总结:实例对象的 [[Prototype]] 在 new 的那一刻就绑定到“当时的 A.prototype 所指向的对象”,之后无论你怎么重新赋值 A.prototype,都不会影响已经创建的实例。
补充:
p1.x = 2 是改同一个对象(地址不变)
p1 = { x: 2 } 是换了一个对象(地址变了,p1 这根引用改指向)
用这个段类比,直接映射回原型题
这种写法:改内容(同一个原型对象):
A.prototype.x = 2;等价于:
p1.x = 2;所以:
a.__proto__还是那个对象A.prototype还是那个对象- 只是对象里的
x从 1 变成 2 a.x查到的就是 2
这种写法:换引用(原型对象换了):
A.prototype = { x: 2 };等价于:
p1 = { x: 2 };所以:
A.prototype现在指向新对象- 但
a.__proto__仍指向旧对象(new 的那一刻绑定的) a.x还是从旧对象上拿到 1
再补一个“浅拷贝/引用”的点(顺手清掉混淆)
- 改对象本体的内容(如
obj.x = ...)→ 所有指向它的引用都能看到变化 - 改某个变量的指向(如
obj = newObj)→ 只影响这个变量,不影响别人
速记:点号(.)通常是“改内容”,等号(=)常常是“换引用”。
补充:只要 a.__proto__ 还指向那个旧原型对象,它就一定不能被 GC 回收。
现代 JS 引擎的 GC(V8 / SpiderMonkey / JSC)本质上都是 可达性(reachability)分析:从 GC Roots 出发,能不能沿着引用链“走到”这个对象
把原型例子放进 GC 模型里
function A() {}
A.prototype.x = 1;
const a = new A();
A.prototype = { x: 2 };此时内存关系是:
(global)
│
├── a ──▶ oldProto { x: 1 }
│
└── A.prototype ──▶ newProto { x: 2 }关键点:
a是全局变量(GC Root)a.__proto__→oldProto- 所以
oldProto是可达的 - GC 绝对不会回收它
只有在 所有引用都断掉 的时候,旧原型对象才“有资格”被 GC
let a = new A();
A.prototype = { x: 2 };
// 断掉最后一条引用
a = null;现在:
oldProto没有任何变量能再访问到- 从 GC Roots 出发走不到它
- 下一轮 GC 才可能回收它
一个非常重要、但很少人意识到的点:
原型链本身,就是一条“隐式引用链”
也就是说:
- 就算你代码里“没写变量保存它”
- 只要有对象的
[[Prototype]]指向它 - 它就是活着的
这也是为什么:
- 原型污染
- 原型上挂大对象
- 滥用共享原型
都会带来 内存风险
“频繁替换 prototype”是危险操作
A.prototype = { bigData: new Array(10_000_000) };新实例 → 用新 prototype
老实例 → 仍然引用旧 prototype
如果你长期存活大量老实例
- 多个巨大 prototype 全都被“挂住”
- GC 无法回收
- 隐性内存泄漏
GC 回不回收对象,和“你还需不需要它”无关,只和“还有没有人能引用到它”有关。
原型链+accessor
function A() {}
Object.defineProperty(A.prototype, "x", {
get() { return this._x ?? 1 },
set(v) { this._x = v },
enumerable: true
});
const a1 = new A();
A.prototype.x = 10; // 注意:这是“赋值”,不是 defineProperty
const a2 = new A();
A.prototype = { x: 100 }; // 换引用
const a3 = new A();
a1.x = 7;
console.log(a1.x, a2.x, a3.x);
console.log(Object.keys(a1), Object.keys(a2), Object.keys(a3));答案:
7 10 100
["_x"] [] []用 defineProperty 在 A.prototype(旧原型对象) 上定义了 x:
x不是一个普通值属性(data property)- 它有
get/set - 对
x的读取/写入会触发函数逻辑
而 _x 只是 getter/setter 里访问的“普通属性名”,最初不存在。
const a1 = new A() 创建实例 a1,并把:a1.[[Prototype]] 绑定到 当时的 A.prototype(旧原型对象),a1 自身没有 x、也没有 _x,a1 的原型(旧原型)有 accessor:x
A.prototype.x = 10 这一步是赋值,不是重新 defineProperty,因为旧原型上已经存在 accessor x(有 setter),所以这句会走:[[Set]] 发现原型链上命中了 setter → 直接调用 setter,不会把 x 覆盖成普通值属性
此时 setter 执行:set(v) { this._x = v }
这里的 this 是:赋值表达式是 A.prototype.x = 10,“接收者(receiver)”就是 A.prototype(旧原型对象),所以 this === 旧原型对象
于是这句等价于:旧原型对象._x = 10
结果:旧原型对象身上新增了一个自有属性 _x: 10,但 x 仍然是 getter/setter(没有被覆盖)
const a2 = new A() 此时 A.prototype 还没被换引用,所以 a2.[[Prototype]] 仍指向旧原型对象,也就是说,a2 的原型对象身上有 _x = 10,原型对象身上还有 accessor x
A.prototype = { x: 100 } ,这是一个关键点,这句是替换 A.prototype 的引用,旧原型对象仍然存在(因为还被 a1.__proto__、a2.__proto__ 引用着)
新的 A.prototype 是一个全新的对象 { x: 100 },这里的 x 是普通值属性(data property),值是 100,没有 getter/setter
const a3 = new A() 现在 new A() 会把实例的 [[Prototype]] 绑定到新的 A.prototype,也就是 a3.__proto__ === 新原型对象({x:100})
a1.x = 7 又是一个赋值,但这次接收者是 a1,赋值查找流程大概是:
- 看
a1自己有没有自有属性x(没有) - 沿原型链找
x:在旧原型对象上找到了 accessorx,且它有 setter - 命中 setter → 调用 setter,并且 this 绑定为 receiver(也就是 a1)
因此 setter 内部执行的是:
a1._x = 7结果:_x 被写到了实例 a1 自己身上,并没有在 a1 身上创建 x(仍然走原型上的 accessor)
为什么第一行是 7 10 100?
a1.x
a1自己没有x- 原型上
x是 getter → 调 getter,this === a1 - getter 返回:
this._x ?? 1 a1._x是 7 → 返回 7
a2.x
a2自己没有x- 原型上
x是 getter → 调 getter,this === a2 - getter 返回:
this._x ?? 1 - 关键点:读
this._x会沿原型链找a2自己没有_x- 但
a2.__proto__(旧原型对象)有_x = 10
- 所以
this._x读到 10 → 返回 10
a3.x
a3原型是新原型对象{ x: 100 }x是普通值属性,直接得到 100
为什么第二行是 ["_x"] [] []?
Object.keys(obj) 只返回:
- 自身(own)
- 且 enumerable: true 的字符串键
a1自身有_x(setter 写入的普通赋值创建,默认 enumerable 为 true)→["_x"]a2自身没有任何属性 →[]a3自身也没有属性(x 在原型上)→[]
变体代码(只改 getter)
function A() {}
Object.defineProperty(A.prototype, "x", {
get() {
return Object.prototype.hasOwnProperty.call(this, "_x")
? this._x
: 1
},
set(v) { this._x = v },
enumerable: true
});
// 下面完全不变
const a1 = new A();
A.prototype.x = 10;
const a2 = new A();
A.prototype = { x: 100 };
const a3 = new A();
a1.x = 7;
console.log(a1.x, a2.x, a3.x);
console.log(Object.keys(a1), Object.keys(a2), Object.keys(a3));变体输出
7 1 100
["_x"] [] []return hasOwnProperty.call(this, "_x") ? this._x : 1 这里强行规定:只要 _x 不是 this 自身的自有属性,就当作不存在,a2 自己没有 _x → hasOwnProperty 为 false → 返回 1,即使原型上有 _x=10,也不算数