引言
在 JavaScript 编程的世界里,this 是一个既基础又令人困惑的概念。它看似简单,却常常在不经意间"背叛"我们的预期;它灵活多变,却又遵循着一套严格的规则。尤其当与 call、apply、bind 以及 ES6 引入的箭头函数 结合时,this 的行为变得更加微妙而强大。
本文将结合代码,对 this 的机制进行一次全面、深入且生动的剖析。我们将逐字引用原始代码片段,还原其技术含义,并通过大量示例揭示背后的原理。无论你是初学者还是资深开发者,相信都能从中获得新的洞见。
一、核心内容
我们可以将this问题拆解为几个核心命题:
this可以被覆盖bind用于定时器中绑定this,延迟执行call/apply可指定this指向,并立即运行that = this利用作用域链保存this- 箭头函数没有自己的
this,而是继承父级作用域的this
接下来,我们将围绕这五点展开详细论述。
源代码链接:lesson_zp/batjtmd/this: AI + 全栈学习仓库
二、this 的默认行为:谁调用,就属于谁
在深入高级技巧前,必须先理解 this 的默认绑定规则。
1. 全局上下文中的 this
在非严格模式下,全局作用域中的 this 指向全局对象(浏览器中是 window,Node.js 中是 global):
javascript
console.log(this === window); // true (浏览器)
在严格模式下,全局函数中的 this 为 undefined:
javascript
'use strict';
function f() { console.log(this); }
f(); // undefined
2. 对象方法中的 this
当函数作为对象的方法被调用时,this 指向该对象:
javascript
const obj = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
obj.greet(); // "Alice"
但注意:函数一旦脱离对象调用,this 就会丢失:
ini
const fn = obj.greet;
fn(); // 非严格模式下输出 undefined(this 指向 window)
这就是为什么我们需要 call、bind 等工具来"拯救"迷失的 this。
三、call 与 apply:强制指定 this,立即执行!
原理与语法
func.call(thisArg, arg1, arg2, ...)func.apply(thisArg, [arg1, arg2, ...])
两者功能相同,区别仅在于参数传递方式。
"call apply 指定this 指向 立即运行"
call 和 apply 的核心作用就是在调用函数的同时 ,显式指定 this 的值,并立刻执行该函数。
示例演示
javascript
function introduce(age) {
console.log(`I'm ${this.name}, ${age} years old.`);
}
const person = { name: 'Bob' };
introduce.call(person, 25); // I'm Bob, 25 years old.
introduce.apply(person, [30]); // I'm Bob, 30 years old.
即使 introduce 不是 person 的方法,我们也能让它"假装"是------这就是 this 的灵活性。
实际应用场景
-
数组方法借用(如将类数组转为真数组):
javascriptconst args = Array.prototype.slice.call(arguments); -
通用工具函数适配不同上下文。
四、bind:永久绑定 this,延迟执行
原理与语法
func.bind(thisArg, arg1, arg2, ...) 返回一个新函数 ,该函数的 this 被永久绑定到 thisArg,且可预设部分参数(柯里化)。
"bind 定时器this绑定(a) 以后再执行"
这句话精准指出了 bind 的两大特点:
- 常用于定时器等异步回调中绑定
this - 不立即执行,而是返回一个可稍后调用的函数
经典问题:setTimeout 中的 this 丢失
javascript
const timer = {
seconds: 0,
start() {
setInterval(function() {
this.seconds++; // ❌ this 指向 global/window
console.log(this.seconds);
}, 1000);
}
};
timer.start(); // 输出 NaN(因为 window.seconds 未定义)
解决方案一:使用 bind
javascript
const timer = {
seconds: 0,
start() {
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000); // ✅ 绑定外层 this
}
};
timer.start(); // 1, 2, 3...
这里,.bind(this) 创建了一个新函数,其 this 永久指向 timer 对象,无论何时被调用都不会改变。
解决方案二:that = this(闭包保存)
"that = this; 作用域链保存this且指向"
这是 ES5 时代的经典写法:
javascript
const timer = {
seconds: 0,
start() {
const that = this; // 保存 this 到变量
setInterval(function() {
that.seconds++; // 通过闭包访问
console.log(that.seconds);
}, 1000);
}
};
虽然有效,但需要额外变量,且在深层嵌套中容易混乱。bind 更加声明式和安全。
五、箭头函数:没有 this 的"佛系"函数
核心特性
箭头函数(Arrow Function)没有自己的 this 。它不会创建新的 this 上下文,而是词法地继承外层作用域的 this。
"Cherry箭头函数放弃了自己的this,没有this 指向指向 父级作用域的this"
这句话堪称对箭头函数 this 行为的完美概括!
- "放弃了自己的 this" → 箭头函数内部不存在
this绑定。 - "指向父级作用域的 this" → 它的
this由定义时的外层作用域决定,与调用方式无关。
示例对比
普通函数(this 丢失)
javascript
const obj = {
name: 'David',
delayedLog() {
setTimeout(function() {
console.log(this.name); // ❌ undefined
}, 100);
}
};
obj.delayedLog();
箭头函数(自动继承)
javascript
const obj = {
name: 'Cherry', // 注意:原文特意用了 "Cherry"
delayedLog() {
setTimeout(() => {
console.log(this.name); // ✅ "Cherry"
}, 100);
}
};
obj.delayedLog();
为什么能成功?因为箭头函数的 this 继承自 delayedLog 方法,而 delayedLog 的 this 指向 obj。
箭头函数的不可变性
由于箭头函数没有 this,所以以下操作无效:
ini
const fn = () => console.log(this.name);
const obj = { name: 'Eve' };
fn.call(obj); // 仍输出全局作用域的 name(或 undefined)
fn.bind(obj)(); // 同上
call、apply、bind 对箭头函数的 this 毫无影响。
适用场景 vs 陷阱
✅ 适合:
- 回调函数(如
map,filter,setTimeout) - 避免
that = this的样板代码
❌ 不适合:
- 对象方法(会继承外层 this,可能不是对象本身)
- 构造函数(箭头函数不能用
new调用) - 需要动态
this的场景
六、综合案例:四种方式对比
假设我们有一个计数器对象,需要在 1 秒后打印当前值:
javascript
const counter = {
count: 0,
// 方式1:普通函数 + that = this
method1() {
const self = this;
setTimeout(function() {
console.log('method1:', self.count);
}, 1000);
},
// 方式2:bind
method2() {
setTimeout(function() {
console.log('method2:', this.count);
}.bind(this), 1000);
},
// 方式3:箭头函数
method3() {
setTimeout(() => {
console.log('method3:', this.count);
}, 1000);
},
// 方式4:call(需立即执行,不适合定时器,但可模拟)
method4() {
const fn = function() { console.log('method4:', this.count); };
setTimeout(() => fn.call(this), 1000); // 结合箭头+call
}
};
counter.method1(); // 0
counter.method2(); // 0
counter.method3(); // 0
counter.method4(); // 0
四种方式都能正确输出,但箭头函数最简洁 ,bind 最显式 ,that = this 最传统。
七、this 绑定优先级总结
当多个规则同时存在时,JavaScript 按以下优先级确定 this:
- new 绑定(构造函数)→ 最高
- 显式绑定 (
call/apply/bind) - 隐式绑定 (对象方法调用,如
obj.fn()) - 默认绑定(独立函数调用)
箭头函数不参与此规则,因为它根本没有
this。
八、常见误区与最佳实践
误区1:认为箭头函数总是更好
错!箭头函数在对象方法中会导致 this 指向外层(可能是全局):
javascript
const badObj = {
name: 'Bad',
getName: () => this.name // ❌ this 指向全局
};
console.log(badObj.getName()); // undefined
应使用普通函数:
javascript
const goodObj = {
name: 'Good',
getName() { return this.name; } // ✅
};
误区2:bind 后还能被 call 覆盖
不能!bind 创建的函数其 this 是永久锁定的:
javascript
function f() { console.log(this.x); }
const bound = f.bind({x: 1});
bound.call({x: 2}); // 输出 1,不是 2!
最佳实践
- 在类方法或对象方法中,使用普通函数。
- 在回调、事件监听、定时器中,优先考虑箭头函数。
- 若需兼容旧环境或需要预设参数,使用
bind。 - 避免过度使用
that = this,除非在不支持箭头函数的环境中。
九、结语:掌握 this,就是掌握 JavaScript 的灵魂
现在,我们不仅读懂了它,更理解了其背后的技术哲学:
call/apply是"命令式"的干预------我要你现在就用这个this!bind是"防御式"的设计------无论何时调用,都必须用这个this!that = this是"妥协式"的智慧------既然你靠不住,我就自己存一份!- 箭头函数 是"声明式"的优雅------我不需要
this,我信任我的上下文!
JavaScript 的魅力,正在于这种灵活性与规则性的统一。当你能自如地在这些工具之间切换,你就不再是 this 的奴隶,而是它的主人。
愿你在未来的代码中,不再对 this 感到迷茫,而是微笑着说:
"我知道你是谁,也知道你该去哪。"