在 JavaScript 开发中,this 是一个既基础又容易让人困惑的核心概念。它不像其他语言那样固定指向当前对象,而是由函数的调用方式决定 。这种灵活性带来了强大的能力,也埋下了不少"坑"。本文系统梳理 this 的行为规则、常见误区,并结合底层机制与实战代码,帮助你彻底掌握这一关键知识点。
一、为什么 this 如此特殊?
1.1 自由变量 vs this:作用域与执行上下文的区别 ✅⭐️
JavaScript 中有两种变量查找机制:
- 自由变量(Lexical Scope) :在编译阶段确定,沿着词法作用域链向上查找。
this绑定 :在运行时 根据函数调用方式动态决定,属于执行上下文的一部分。
✅ 关键区别 :
自由变量看"写在哪"(声明位置),
this看"怎么调"(调用方式)。
javascript
js
编辑
var myName = '极客邦'; // 全局变量,挂载到 window
var bar = {
myName: 'time.geekbang.com',
printName: function () {
console.log(myName); // 自由变量 → '极客邦'(全局)
console.log(this.myName); // this 绑定 → 取决于调用方式
}
};
💡 面试重要性 :⭐️⭐️⭐️⭐️
大厂常考:作用域链 vs 执行上下文、闭包与 this 的区别。
补充:什么是自由变量?
自由变量(Free Variable) 是指在当前作用域中未被声明,但被引用的变量。JavaScript 引擎会在词法作用域链中向上查找其定义。
javascript
js
编辑
function outer() {
var x = 10;
return function inner() {
console.log(x); // x 是 inner 的自由变量
};
}
x在inner中未声明,但在外层outer中存在 → 是自由变量;- 查找发生在编译期(静态),与运行时无关;
- 这就是闭包的基础:内部函数保留对外层自由变量的引用。
🔍 对比 this:
- 自由变量:静态绑定(词法作用域)→ 编译期确定;
this:动态绑定(执行上下文)→ 运行期确定。
二、this 的五种绑定规则(按优先级排序)✅⭐️⭐️⭐️⭐️⭐️
2.1 默认绑定:普通函数调用
当函数作为独立函数被调用时:
- 非严格模式:
this指向全局对象(浏览器中是window) - 严格模式:
this为undefined
scss
js
编辑
function foo() {
console.log(this); // 非严格 → window;严格 → undefined
}
foo(); // 普通调用
⚠️ 问题:这是 JS 的"历史包袱",易导致意外污染全局变量。
2.2 隐式绑定:作为对象方法调用
当函数通过对象属性调用时,this 指向该对象:
javascript
js
编辑
var myObj = {
name: '极客时间',
showThis: function () {
console.log(this.name); // '极客时间'
}
};
myObj.showThis(); // this → myObj
但注意"隐式丢失"问题:
ini
js
编辑
var foo = myObj.showThis;
foo(); // this → window(非严格)或 undefined(严格)
💡 原因 :
foo是普通函数引用,调用时已脱离对象上下文。
💡 面试重要性 :⭐️⭐️⭐️⭐️高频考点:方法解构后 this 丢失、如何修复。
2.3 显式绑定:call / apply / bind ✅⭐️⭐️⭐️⭐️⭐️
通过这些方法可以强制指定 this:
ini
js
编辑
function updateInfo() {
this.myName = '极客时间';
this.age = 18;
}
let bar = { myName: '极客邦' };
updateInfo.call(bar); // 或 apply(bar)
console.log(bar); // { myName: '极客时间', age: 18 }
核心原理:apply(bar) 让 this 与 bar 指向同一内存地址
- 对
this.xxx = ...的操作,等价于直接操作bar.xxx - 属性存在 → 覆盖值;不存在 → 新增属性
示例验证:
javascript
js
编辑
let bar = {
myName: '极客邦',
text1: 1
};
function foo() {
this.myName = '极客时间'; // 覆盖
this.text2 = 2; // 新增
this.fn = () => console.log('新增函数');
}
foo.apply(bar);
console.log(bar);
// {
// myName: '极客时间',
// text1: 1,
// text2: 2,
// fn: [Function]
// }
🔍 本质 :对象是引用类型,
apply只是让this成为bar的替身。
引用类型同样适用:
ini
js
编辑
let bar = { info: { age: 18 } };
function foo() {
this.info = { gender: 'male' }; // 覆盖整个引用
this.list = [1, 2, 3]; // 新增
}
foo.apply(bar);
💡 面试重要性 :⭐️⭐️⭐️⭐️⭐️
必考:call/apply/bind 区别、手写 bind、apply 如何影响对象属性。
2.4 构造函数调用:new 关键字 ✅⭐️⭐️⭐️⭐️⭐️
使用 new 调用函数时,this 指向新创建的实例对象。
底层四步流程(引擎自动完成):
-
创建临时空对象 :
var tempObj = {}(隐式,不可见) -
绑定原型链 :
tempObj.__proto__ = Constructor.prototype -
执行构造函数,绑定 this :
Constructor.call(tempObj) -
返回对象:
- 若无 return 或 return 基本类型 → 返回
tempObj - 若 return 对象类型 → 返回该对象
- 若无 return 或 return 基本类型 → 返回
示例代码:
javascript
js
编辑
function CreateObj() {
console.log(this); // 输出临时对象
this.name = '极客时间';
}
var myObj = new CreateObj(); // myObj = { name: '极客时间' }
手动模拟 new:
ini
js
编辑
function myNew(constructor) {
var tempObj = {};
tempObj.__proto__ = constructor.prototype;
constructor.call(tempObj);
return tempObj;
}
💡 关键细节:
- 临时对象是引擎内部创建的,你无法直接访问;
__proto__是非标准属性,规范中为[[Prototype]];- 构造函数不应返回基本类型(会被忽略)。
💡 面试重要性 :⭐️⭐️⭐️⭐️⭐️高频大题:手写 new、解释 new 的执行过程、原型链构建。
2.5 事件处理函数中的 this
在 DOM 事件监听器中,this 指向触发事件的元素:
javascript
js
编辑
document.getElementById('link').addEventListener('click', function() {
console.log(this); // <a id="link">...</a>
});
✅ 浏览器环境特殊约定,符合直觉。
💡 面试重要性 :⭐️⭐️前端岗可能涉及,但不如前几项核心。
三、箭头函数的 this:词法绑定,永不改变 ✅⭐️⭐️⭐️⭐️⭐️
箭头函数的 this 是 JavaScript 中极易混淆的知识点,核心要抓住:箭头函数没有自己的 this ------ 它的 this 完全继承自「定义时的外层词法作用域」,且永久固定、无法修改。
3.1 核心规则(一句话记住)
箭头函数的
this= 定义箭头函数时,其外层最近的非箭头函数 / 全局作用域的this;且一旦确定,无论以何种方式调用(普通调用、方法调用、call/apply/bind),都不会改变。
3.2 关键特性(和普通函数的核心差异)
| 特性 | 箭头函数 | 普通函数 |
|---|---|---|
this 绑定时机 |
定义时绑定(词法作用域) | 执行时绑定(调用方式决定) |
this 能否修改 |
不能 (call/apply/bind 无效) |
能(调用方式 / 绑定可改变) |
自身是否有 this |
无(继承外层) | 有(执行时动态绑定) |
构造函数调用(new) |
不支持(报错) | 支持 (this 指向新实例) |
💡 本质 :箭头函数的
this是静态的 ,普通函数的this是动态的。
3.3 实战场景(结合代码理解)
场景 1:箭头函数定义在全局作用域 → this 继承全局 window
ini
js
编辑
const arrowFn = () => {
console.log(this); // window(浏览器)
};
arrowFn(); // 普通调用 → this 仍为 window
const obj = { fn: arrowFn };
obj.fn(); // 方法调用 → this 还是 window(不会指向 obj)
arrowFn.call({ name: '测试' }); // call 绑定 → this 依然是 window
✅ 结论 :全局定义的箭头函数,this 永久指向 window,调用方式不影响。
场景 2:箭头函数定义在普通函数内部 → 继承外层函数的 this
这是箭头函数最常用的场景(解决普通函数内层 this 指向混乱的问题):
javascript
js
编辑
const obj = {
name: '极客时间',
outerFn: function() {
console.log('外层普通函数 this:', this); // obj(方法调用)
// 箭头函数定义在 outerFn 内部,继承 outerFn 的 this
const arrowFn = () => {
console.log('箭头函数 this:', this); // obj(继承外层)
};
arrowFn(); // 普通调用 → this 仍为 obj
arrowFn.call(window); // call 尝试改 this → 无效,还是 obj
}
};
obj.outerFn();
执行结果:
kotlin
text
编辑
外层普通函数 this: {name: '极客时间', outerFn: ƒ}
箭头函数 this: {name: '极客时间', outerFn: ƒ}
✅ 结论 :箭头函数完美解决了 "普通函数内层函数 this 指向全局" 的问题。
🔍 关键澄清 :
箭头函数的
this不是指向"外层函数本身" ,而是指向 "外层函数执行时的this" 。
outerFn作为函数对象确实没有.name属性,但outerFn执行时的this是obj,而obj.name = '极客时间',所以箭头函数能正确访问。
场景 3:箭头函数定义在对象属性 → 警惕 "外层是全局"
易错点:对象的属性不是 "作用域",箭头函数定义在对象里,外层作用域仍是全局:
javascript
js
编辑
const obj = {
name: '极客时间',
arrowFn: () => {
console.log(this); // window(外层是全局作用域)
}
};
obj.arrowFn(); // 方法调用 → this 还是 window
❌ 误区 :不要以为箭头函数作为对象方法,this 就指向对象 ------ 对象本身不形成作用域,箭头函数的外层仍是全局。
💡 正确做法:对象方法应使用普通函数。
场景 4:箭头函数不能作为构造函数(new 调用报错)
ini
js
编辑
const ArrowConstructor = () => {
this.name = '极客时间';
};
// 报错:ArrowConstructor is not a constructor
new ArrowConstructor();
✅ 原因 :箭头函数没有自己的 this,也没有 prototype,无法作为构造函数。
场景 5:箭头函数在回调函数中的应用(解决 this 丢失)
经典场景 :定时器 / 事件回调中保留外层 this:
javascript
js
编辑
const obj = {
name: '极客时间',
delayLog: function() {
// 普通函数回调:this 指向 window
setTimeout(function() {
console.log('普通函数回调 this:', this.name); // undefined
}, 100);
// 箭头函数回调:继承外层 delayLog 的 this(obj)
setTimeout(() => {
console.log('箭头函数回调 this:', this.name); // 极客时间
}, 100);
}
};
obj.delayLog();
✅ 结论 :箭头函数是解决回调函数 this 丢失的最优方案(无需手动 bind 或存 that = this)。
🔍 原理拆解:
obj.delayLog()→delayLog是普通函数,作为方法调用 →this = obj;- 箭头函数定义在
delayLog内部 → 继承delayLog执行时的this(即obj);- 即使
setTimeout在 100ms 后调用,箭头函数的this依然锁定为obj。
3.4 避坑指南(常见错误)
| 错误用法 | 正确做法 |
|---|---|
把箭头函数作为对象的方法(想让 this 指向对象)→ 实际指向全局 |
对象方法用普通函数 |
用 call/apply/bind 改变箭头函数的 this → 完全无效 |
不要尝试修改箭头函数的 this |
| 用箭头函数作为构造函数 → 直接报错 | 构造函数必须用普通函数 |
在需要动态 this 的场景使用箭头函数 |
动态 this → 普通函数;保留外层 this → 箭头函数 |
✅ 使用原则:
- 箭头函数适合 :回调函数、内层函数,需要保留外层
this的场景; - 普通函数适合 :对象方法、构造函数,需要动态
this的场景。
四、隐式丢失 vs 显式绑定:实战对比 ✅⭐️⭐️⭐️⭐️
4.1 隐式丢失示例
ini
js
编辑
var bar = {
myName: 'time.geekbang.com',
printName: function () {
console.log(this.myName);
}
};
let _printName = bar.printName;
_printName(); // this → window / undefined ❌
4.2 显式绑定修复
ini
js
编辑
let boundPrintName = bar.printName.bind(bar);
boundPrintName(); // ✅ 正确输出
4.3 对比总结
| 场景 | this 指向 |
结果 |
|---|---|---|
bar.printName() |
bar |
✅ |
f = bar.printName; f() |
window / undefined |
❌ |
f.bind(bar)() |
bar |
✅ |
💡 经验法则 :只要函数可能被提取单独调用,就必须提前绑定
this。
💡 面试重要性 :⭐️⭐️⭐️⭐️React 类组件中常见问题,大厂必问。
五、如何正确拿到 bar.myName?方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
直接调用 bar.printName() |
简单 | 无法传递引用 |
| 箭头函数 | 避免 this 问题 | 紧耦合,失去通用性 |
.bind(bar) |
安全可靠 | 需额外步骤 |
闭包保存 self |
兼容旧环境 | 代码冗余 |
✅ 推荐 :使用
.bind()。
六、关于 var 与 let/const 在全局作用域的行为差异(重要澄清)✅⭐️⭐️⭐️⭐️
🚨 常见误解纠正 :
let不挂载window不是因为 TDZ ,而是因为 ES6 引入了词法绑定模型。
6.1 核心结论
| 特性 | var 全局变量 |
let/const 全局变量 |
|---|---|---|
自动挂载 window |
✅ 是 | ❌ 否(词法绑定) |
| 暂时性死区(TDZ) | ❌ 无 | ✅ 有 |
| 块级作用域 | ❌ 无 | ✅ 有 |
6.2 原理说明
var:全局变量 ≡window属性(历史遗留)let/const:存储在「全局词法环境」的声明式记录中,与window解耦
ini
js
编辑
var a = 1;
let b = 2;
console.log(window.a); // 1
console.log(window.b); // undefined
💡 面试重要性 :⭐️⭐️⭐️⭐️
大厂常考:全局变量存储机制、TDZ 与挂载无关。
七、严格模式:规避 this 的陷阱 ✅⭐️⭐️
在 'use strict' 下:
- 普通函数调用
this === undefined - 避免意外创建全局变量
javascript
js
编辑
'use strict';
function bad() {
this.x = 1; // TypeError
}
✅ 建议:所有现代项目启用严格模式。
八、总结要点(面试速记版)✅⭐️⭐️⭐️⭐️⭐️
| 绑定方式 | 触发条件 | this 指向 |
优先级 |
|---|---|---|---|
| 默认绑定 | 独立函数调用 | window / undefined |
最低 |
| 隐式绑定 | obj.method() |
obj |
↑ |
| 显式绑定 | call/apply/bind |
指定对象 | ↑ |
| new 绑定 | new Fn() |
新实例 | 最高 |
| 箭头函数 | 定义时继承 | 外层非箭头函数的 this |
静态,不受调用影响 |
🔔 口诀 :
谁调用,this就是谁;没谁调,默认全局走;new出来,实例有;call/apply/bind,我说了算;箭头函数,看定义,外层this永不变!
九、高频面试题清单(按重要性打星)⭐️⭐️⭐️⭐️⭐️
| 问题 | 重要性 | 考察点 |
|---|---|---|
解释 this 的绑定规则及优先级 |
⭐️⭐️⭐️⭐️⭐️ | 基础核心 |
手写 bind / new |
⭐️⭐️⭐️⭐️⭐️ | 实现能力 |
为什么 let 不挂载 window? |
⭐️⭐️⭐️⭐️ | 作用域模型理解 |
方法解构后 this 丢失如何解决? |
⭐️⭐️⭐️⭐️ | 实战经验 |
apply(obj) 对 obj 属性的影响? |
⭐️⭐️⭐️⭐️ | 引用类型理解 |
箭头函数的 this 为何不能被 call 修改? |
⭐️⭐️⭐️⭐️⭐️ | 词法绑定 vs 动态绑定 |
| 箭头函数能否作为对象方法?为什么? | ⭐️⭐️⭐️⭐️ | 作用域与 this 理解 |
箭头函数与普通函数 this 差异? |
⭐️⭐️⭐️ | 语法特性 |
严格模式对 this 的影响? |
⭐️⭐️ | 安全编程意识 |
🌟 结语 :
this并非"玄学",而是 JavaScript 执行模型的核心部分。掌握其规则,不仅能避开陷阱,还能灵活构建对象系统。理解底层机制(如 new 创建临时对象、apply 绑定引用、箭头函数词法绑定)是大厂面试脱颖而出的关键 。同时,自由变量(词法作用域)与
this(动态绑定)的对比,是区分初级与高级开发者的重要分水岭。