在 JavaScript 编程中,this 是一个看似简单却极易引发困惑的关键字。它不像其他语言中的 this 那样总是指向类的实例,而是由函数的调用方式动态决定,这使得它的行为常常出人意料。
本文将结合你提供的代码示例和核心概念(如自由变量查找、作用域链、Lexical Scope、执行上下文等),系统性地讲解 JavaScript 中 this 的设计原理、常见陷阱以及最佳实践,帮助开发者真正掌握这一关键机制。
一、前置知识:自由变量查找与词法作用域(Lexical Scope) 🧠
在讨论 this 之前,我们需要先明确另一个重要概念 —— 变量的查找机制。
1. 编译阶段与作用域链
JavaScript 虽然是解释型语言,但它有“编译阶段”的概念。在代码执行前,引擎会进行词法分析,确定变量的作用域。
- 词法作用域(Lexical Scope) :函数的作用域是在其声明时就确定的,而不是运行时。
- 作用域链(Scope Chain) :当访问一个变量时,JS 引擎首先在当前作用域查找,若未找到,则沿着外层作用域逐级向上查找,直到全局作用域为止。
- 自由变量(Free Variable) :指在函数内部使用,但既不是参数也不是局部变量,而是来自外层作用域的变量。
let myName = '极客时间';
function foo() {
console.log(myName); // myName 是自由变量,从外层作用域获取
}
这里的 myName 就是 foo 函数中的自由变量,它的值通过词法作用域链找到。
✅ 注意:变量查找依赖的是“声明位置”,而
this的指向则完全取决于“调用方式”——这是二者本质区别!
二、this 到底是什么?为何如此特殊? 🤔
1. this 的定义
this 是一个关键字,表示“当前执行上下文中的上下文对象”。可以通俗理解为:
“谁调用了这个函数,
this就指向谁。”
但这只是一个粗略的说法,实际情况更为复杂。
这是执行上下文中this的邻居
红色代表在执行阶段动态确认,蓝色代表编译阶段静态确定
2. this 的设计问题:由调用方式决定
与其他语言不同,JavaScript 的 this 不是在函数定义或类构造时绑定的,而是在运行时根据调用方式动态绑定。
这种设计源于早期 JS 没有 class 关键字,需要借助函数模拟面向对象编程。为了支持方法能访问所属对象的属性,引入了 this。
然而这也带来了混乱:
- 同一个函数,被不同方式调用,
this可能完全不同。 - 在普通函数中使用
this往往没有意义,但却默认指向全局对象(window),容易造成污染。
三、this 的五种典型指向情况 🔍
我们可以通过以下五种场景来全面掌握 this 的指向规则。
场景 1:作为对象的方法被调用 → 指向该对象 🎯
var bar = {
myName: "time.geekbang.com",
printName: function () {
console.log(this); // bar 对象
console.log(this.myName); // "time.geekbang.com"
}
};
bar.printName(); // 正常调用,this 指向 bar
✅ 结论:当函数作为某个对象的属性被调用时,this 指向该对象。
场景 2:作为普通函数调用 → 指向全局对象 / undefined(严格模式) ⚠️
var myName = "极客帮";
function foo() {
console.log(this); // 非严格模式下为 window;严格模式下为 undefined
console.log(this.myName); // 非严格模式可能输出 "极客帮"(挂载在 window 上)
}
foo(); // 相当于 window.foo()
⚠️ 问题所在:
- 在非严格模式下,
this自动指向window。 - 所有
var声明的全局变量都会成为window的属性,导致潜在的全局污染。 - 使用
let/const声明的变量不会挂载到window,因此即使this指向window也无法访问它们。
💡 解决方案:使用 'use strict';
'use strict';
function strictFoo() {
console.log(this); // undefined
}
strictFoo(); // 不再指向 window,避免误操作
✅ 建议:始终启用严格模式以防止意外的全局 this 绑定。
场景 3:作为构造函数被调用 → 指向新创建的实例 🏗️
function CreateObj() {
this.name = "极客时间";
console.log(this); // 新创建的对象实例
}
var obj = new CreateObj(); // this 指向 obj
console.log(obj.name); // "极客时间"
🔧 构造函数的工作流程:
- 创建一个空对象;
- 将构造函数的
prototype赋给该对象的__proto__; - 将
this指向这个新对象并执行构造函数体; - 返回该对象(除非显式返回其他对象)。
手动模拟 new 操作符:
function CreateObj() {
this.name = "极客时间";
}
// 手动实现 new 的效果
var temObj = {};
CreateObj.call(temObj); // 显式绑定 this
temObj.__proto__ = CreateObj.prototype;
var myObj = temObj;
console.log(myObj.name); // "极客时间"
场景 4:通过 call / apply / bind 显式指定 → 指向传入的对象 🎛️
这三个方法允许我们手动控制 this 的指向。
call 和 apply
立即执行函数,并指定 this。
var bar = {
myName: '极客帮',
test1: 1
};
function setMyName() {
this.myName = '极客时间';
}
setMyName.call(bar); // this 指向 bar
console.log(bar.myName); // "极客时间"
// apply 参数传递方式不同(第二个参数是数组)
setMyName.apply(bar, []);
bind
返回一个新的函数,永久绑定 this。
var boundFunc = setMyName.bind(bar);
boundFunc();
console.log(bar.myName); // "极客时间"
💡 bind 常用于解决回调函数中 this 丢失的问题。
场景 5:事件处理函数 → 指向绑定的 DOM 元素 🖱️
<a href="#" id="link">点击我</a>
<script>
document.getElementById("link").addEventListener("click", function() {
console.log(this); // <a> 元素本身
});
</script>
✅ 浏览器自动将事件处理函数的 this 绑定为触发事件的 DOM 元素。
⚠️ 注意闭包中的陷阱:
document.getElementById("link").addEventListener("click", () => {
console.log(this); // window(箭头函数无自己的 this)
});
所以事件监听推荐使用普通函数而非箭头函数,除非你明确知道后果。
四、经典陷阱:方法赋值后 this 丢失 💥
var myObj = {
name: "极客时间",
showThis: function() {
console.log(this); // 期望是 myObj
}
};
var foo = myObj.showThis; // 只是引用函数,脱离了对象
foo(); // 输出 window(非严格模式)或 undefined(严格模式)
🔍 分析:
foo()是作为普通函数调用,不是myObj.foo()。- 即便函数原本属于某个对象,一旦独立调用,
this就不再指向原对象。
✅ 解决方案:
-
使用
call/apply/bind显式绑定:foo.call(myObj); // 正确绑定 -
或者提前绑定:
var boundShow = myObj.showThis.bind(myObj); boundShow(); // 始终指向 myObj
五、深入理解:执行上下文(Execution Context)视角下的 this 🧩
JavaScript 执行函数时会创建一个执行上下文(Execution Context) ,包含三个部分:
- 变量环境(Variable Environment)
- 词法环境(Lexical Environment)
- this 绑定(This Binding)
其中,this 的绑定发生在函数被调用时,依据如下优先级顺序:
| 调用方式 | this 指向 |
|---|---|
new Func() | 新创建的实例 |
func.call(obj) / func.apply(obj) / func.bind(obj) | 显式指定的对象 |
obj.method() | obj |
func() | 全局对象(非严格)或 undefined(严格) |
| 箭头函数 | 继承外层作用域的 this |
📌 this 绑定优先级(高 → 低):
newbind/call/apply- 对象调用(隐式绑定)
- 默认绑定(普通函数调用)
- 箭头函数(词法继承)
六、箭头函数对 this 的影响 🏹
ES6 引入的箭头函数改变了游戏规则:
- 箭头函数没有自己的
this。 - 它的
this继承自外层函数或全局作用域的this。 - 不能用
call/apply/bind改变其this。
var name = 'window';
var obj = {
name: 'obj',
normalFunc: function() {
console.log(this.name); // 'obj'
setTimeout(function() {
console.log(this.name); // 'window'(普通函数)
}, 100);
setTimeout(() => {
console.log(this.name); // 'obj'(箭头函数继承外层 this)
}, 100);
}
};
obj.normalFunc();
✅ 箭头函数非常适合用于回调,避免 this 丢失。
七、如何优雅地拿到 time.geekbang.com? 🎯
回顾最初的问题:
如何在函数内正确访问
bar.myName(即"time.geekbang.com")?
看这段代码:
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(myName); // ❌ 报错!找不到 myName
console.log(bar.myName); // ✅ 正确:直接访问对象属性
console.log(this.myName); // ✅ 只有当 this 指向 bar 时才有效
}
};
❗ 错误做法:console.log(myName)
myName并非局部变量或参数,也不是外层作用域变量(全局没有myName),所以会报错。- 这里的
myName并不是自由变量,而是试图访问不存在的标识符。
✅ 正确做法:
- 使用
this.myName—— 当且仅当函数作为bar的方法调用时有效。 - 使用
bar.myName—— 更稳定,但耦合性强。 - 使用闭包保存引用(更高级技巧):
var bar = (function() {
const myName = "time.geekbang.com";
return {
printName: function() {
console.log(myName); // 通过闭包访问
}
};
})();
八、总结:this 设计的利与弊 ⚖️
| 优点 | 缺点 |
|---|---|
| 支持灵活的函数复用(如借用方法) | 行为难以预测,学习成本高 |
| 支持动态上下文切换(OOP 模拟) | 普通函数中 this 指向全局易造成污染 |
可通过 call/apply/bind 精确控制 | 方法赋值后 this 丢失常见 bug |
| 事件处理天然绑定 DOM 元素 | 箭头函数虽好,但也改变了传统逻辑 |
🧠 最佳实践建议:
- 使用
'use strict'避免无意中修改window。 - 多用
bind或箭头函数防止this丢失。 - 尽量避免依赖
this,可改用参数传递数据。 - 使用 ES6 Class 替代构造函数,语义更清晰。
- 调试时打印
this,确认当前上下文。
结语 🌟
JavaScript 的 this 虽然设计上存在争议,但它也是 JS 灵活性的体现之一。理解 this 的核心在于记住一句话:
this不看函数怎么写,只看函数怎么调!
结合词法作用域、执行上下文和调用方式,我们才能真正驾驭 this,写出健壮、可维护的代码。
掌握
this,是迈向高级 JavaScript 开发者的必经之路。🚀