JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
<details>
<summary>答案</summary>
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
</details>
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
需要我把这份指南写入文件(比如 docs/js-this-guide.md),或者补充某个章节的深入内容(如 Proxy receiver、React/Vue 中的实战案例)吗?
0 个评论
要回复文章请先登录或注册