开头:一个让无数人困惑的问题
js
const obj = {
name: '张三',
sayHi() {
console.log(`你好,我是 ${this.name}`);
}
};
obj.sayHi(); // "你好,我是 张三" ✅ 符合直觉
const fn = obj.sayHi;
fn(); // "你好,我是 undefined" ❌ 怎么回事?
同样的函数,换一种调用方式,结果完全不同。这就是 JavaScript 的 this------一个让初学者抓狂、让面试官兴奋的话题。
本文用一个统一的心智模型,把 this 的所有场景串起来。读完你会发现,this 其实很简单。
一、核心心法:一句话记住所有场景
this是运行时绑定。谁调用函数,this就指向谁。函数定义在哪里不重要,重要的是谁在"点"它。
记住这句话,你已经理解了 this 的 80%。剩下的 20% 是五个具体场景的细节。
二、五种场景,一张优先级表搞定
this 的所有行为可以归纳为五条规则,而且它们的优先级是确定的:
sql
new 绑定 > call/apply/bind > 对象方法调用 > 普通调用
| 优先级 | 场景 | this 指向 |
怎么写 |
|---|---|---|---|
| 🔴 最高 | new 构造调用 |
新创建的实例 | new Foo() |
| 🟠 | 显式绑定 | 你指定的对象 | fn.call(obj) |
| 🟡 | 方法调用 | 点号前面的对象 | obj.fn() |
| 🟢 最低 | 普通调用 | window / undefined |
fn() |
遇到任何 this 问题,从上到下对号入座,优先级最高的那条规则生效。
三、逐条拆解
🔴 规则一:new 绑定(最高优先级)
js
function Person(name) {
this.name = name; // this 是谁?别急,往下看
}
const p = new Person('张三');
console.log(p.name); // "张三"
new 操作符在背后做了四件事:
markdown
1. 创建一个空对象 → {}
2. 空对象挂上原型链 → {} 的 __proto__ 指向 Person.prototype
3. 执行 Person,this 绑定空对象 → Person.apply({}, args) ← 这是关键
4. 返回这个对象
手动还原一下,你会看得更清楚:
js
function myNew(Constructor, ...args) {
const obj = {}; // 步骤 1
Object.setPrototypeOf(obj, Constructor.prototype); // 步骤 2
const result = Constructor.apply(obj, args); // 步骤 3 ← this 在这里绑定
return result instanceof Object ? result : obj; // 步骤 4
}
第三步就是答案:new 本质上也是用 apply 把 this 强行指到新对象上。
🟠 规则二:显式绑定(call / apply / bind)
当你想让 this 指向一个特定的对象,用这三兄弟:
js
function sayHi() {
console.log(`你好,我是 ${this.name}`);
}
const a = { name: '张三' };
const b = { name: '李四' };
sayHi.call(a); // "你好,我是 张三" --- 逐个传参,立刻执行
sayHi.apply(b); // "你好,我是 李四" --- 数组传参,立刻执行
const fn = sayHi.bind(a);
fn(); // "你好,我是 张三" --- 返回新函数,不立即执行
call 和 apply 的区别只是传参方式不同,bind 的区别是不立即执行 ,返回一个新函数------这就是为什么事件监听场景只能用 bind:
js
// ❌ call 立即执行了,不行
element.addEventListener('click', obj.handle.call(obj));
// ✅ bind 返回新函数,事件触发时才执行,可以
element.addEventListener('click', obj.handle.bind(obj));
方法借用的威力:
同一个函数,被不同的对象"借用",表现出完全不同的行为------不需要任何继承关系,这是 JavaScript 独有的灵活性:
js
function introduce() {
console.log(`我叫 ${this.name},来自 ${this.city}`);
}
const p1 = { name: '张三', city: '北京' };
const p2 = { name: '李四', city: '上海' };
introduce.call(p1); // "我叫 张三,来自 北京"
introduce.call(p2); // "我叫 李四,来自 上海"
🟡 规则三:方法调用(隐式绑定)
对象的方法被调用,this 指向调用它的对象:
js
const obj = {
name: '张三',
sayHi() {
console.log(this.name);
}
};
obj.sayHi(); // this → obj,输出 "张三"
这个看起来最简单,但藏了一个高频面试题------隐式丢失:
js
// 把方法赋值给一个变量
const fn = obj.sayHi;
fn(); // this → window(严格模式 undefined),输出 undefined
为什么?因为你把函数从这个对象里"掏出来"了,fn() 是裸调用,没有 obj. 前缀。函数本质上是独立的值 ,obj.sayHi 只是把那个函数值取出来,取出来之后它就跟 obj 没有任何关系了。
一句话:this 看的是调用时有没有点号,不是定义时在不在对象里。
🟢 规则四:普通调用(默认绑定)
没有任何修饰的裸函数调用,this 指向全局对象:
js
function foo() {
console.log(this);
}
foo(); // 浏览器 → window | Node.js → global | 严格模式 → undefined
var 声明的变量会挂到 window 上,这是 JavaScript 早期的一个设计缺陷。let 和 const 修复了这个问题,所以现在很少有人用 var 了。
🔵 规则五:事件处理函数
js
button.addEventListener('click', function(e) {
console.log(this); // this → 触发事件的 button 元素
});
事件监听器里,this 默认指向触发事件的 DOM 元素。但这经常不是你想要的------你需要 this 指向你自己的数据对象:
js
const user = {
name: '张三',
score: 0,
handleClick() {
this.score += 10;
console.log(`${this.name} 当前分数:${this.score}`);
}
};
// ❌ this → button 元素,this.score = undefined,this.name = undefined
button.addEventListener('click', user.handleClick);
// ✅ bind 锁死 this,this → user,正常累加分数
button.addEventListener('click', user.handleClick.bind(user));
这是 bind 在实际开发中最高频的应用场景。
四、箭头函数:规则之外的特殊存在
上面的规则都适用普通 function。箭头函数跳出这个体系 ------它根本没有自己的 this:
js
const obj = {
name: '张三',
// 普通函数
regular() {
setTimeout(function () {
console.log(this.name); // this → window ❌
}, 100);
},
// 箭头函数
arrow() {
setTimeout(() => {
console.log(this.name); // this → obj ✅
}, 100);
}
};
普通 function |
箭头函数 () => {} |
|
|---|---|---|
自己的 this |
✅ 有,运行时动态绑定 | ❌ 没有,从外层捕获 |
| 适用场景 | 构造函数、需要动态 this | 回调、需要固定 this |
能被 new 调用? |
✅ 可以 | ❌ 不行 |
设计初衷 :在箭头函数出现之前,每次写回调都要 var self = this 或者 .bind(this),非常繁琐。箭头函数就是为这个痛点而生。
五、终极速查:遇到 this 问题怎么排查?
按这个顺序排查,三步定位:
kotlin
1. 是箭头函数吗?
→ 是:this = 定义时外层作用域的 this,不用往下看了
→ 否:继续
2. 是 new 调用的吗?(前面有 new 关键字)
→ 是:this = 新创建的实例对象
→ 否:继续
3. 函数怎么被调用的?
→ fn.call(obj) / fn.apply(obj) / fn.bind(obj)() → this = obj
→ obj.fn() → this = obj(点号前面的那个)
→ fn() → this = window / undefined
六、经典陷阱一览
| 陷阱 | 代码 | 原因 | 解法 |
|---|---|---|---|
| 隐式丢失 | const f = obj.m; f(); |
脱离了对象,变成普通调用 | .bind(obj) 或用箭头函数包裹 |
| 定时器丢 this | setTimeout(obj.m, 100) |
回调被独立调用 | () => obj.m() 或 obj.m.bind(obj) |
| 事件监听丢 this | el.addEventListener('click', obj.m) |
this 变成了 DOM 元素 | obj.m.bind(obj) |
| class 方法提取 | const h = new Btn().click; h(); |
class 默认严格模式 | constructor 中 bind 或用箭头字段 |
| 箭头函数冒充构造函数 | new ArrowFn() |
箭头函数没自己的 this | 换普通 function |
结尾
this 表面看起来有很多特殊情况,但底层逻辑只有一条:谁调用,就指向谁。
这种设计是 JavaScript 的"基因"决定的------它不是基于类的语言,函数是一等公民,方法可以被任意对象借用。这种灵活性是代价,也是威力。
把那张优先级表记在心里,以后遇到任何 this 的问题,从上往下对号入座就好。
如果这篇文章对你有帮助,欢迎分享给同样被 this 困扰的朋友。