大家好!今天我们来聊一聊 JavaScript 中那个让人又爱又恨、经常让人抓狂的关键词 —— this。
你是不是也曾经在控制台看到 undefined 或者意外地修改了全局变量,然后一脸懵:“这 this 到底指向谁啊?”别急,今天我们就结合几个小例子,轻松愉快地把 this 的各种行为搞清楚!
一、this 由调用方式决定
先说一个关键点:this 的值不是在函数定义时确定的,而是在函数被调用时才确定的。
换句话说,this 是个“执行时变量”,和作用域链(词法作用域)无关。而像 myName 这样的变量,是通过词法作用域查找的,属于“编译阶段就定好的”。
来看第一个例子:
'use strict'
var bar = {
myName: 'time.geekbang.com',
printName: function(){
console.log(myName) // 自由变量,查词法作用域 → 全局 var myName
console.log(this.myName) // this 指向谁?看怎么调用!
console.log(this)
}
}
var myName = '极客邦'
var _printName = foo() // 返回 bar.printName 函数引用
_printName() // 普通函数调用!
关键解析:
-
printName虽然写在bar对象里,但它本质上是在全局作用域中定义的函数(JS 对象不创建新作用域)。 -
所以
console.log(myName)中的myName是自由变量,引擎会沿着词法作用域向外找 → 找到全局的var myName = '极客邦'。 -
但
this.myName就不一样了!this取决于调用方式:_printName()是普通函数调用 → 非严格模式下this === window,严格模式下this === undefined。- 所以
this.myName在严格模式下会报错(因为undefined.myName);非严格模式下会输出window.myName,而var声明的变量会挂载到window上,所以能拿到'极客邦'。
✅ 小贴士:
let声明的变量不会挂载到window,所以如果你把var myName改成let myName,即使非严格模式下this.myName也会是undefined!
二、作为对象方法调用:这才是 this 的“本职工作”
当你这样调用:
bar.printName() // 作为对象的方法调用
这时,this 就会正确指向 bar 对象,于是 this.myName 输出 'time.geekbang.com'。
这就是 this 最常见的用途:在面向对象编程中,让方法能访问所属对象的属性。
可惜的是,JavaScript 的设计有点“灵活过头”——函数可以脱离对象单独调用,这时候 this 就“迷失自我”了。
三、严格模式:给 this 加个“安全锁”
早期 JavaScript 有个“偷懒”设计:普通函数调用时,this 默认指向全局对象(浏览器中是 window),虽然说此时this是没有必要的,但是它总得有要指向的东西,作者直接让this指向全局了。这很容易造成意外污染全局变量。
比如:
function foo() {
this.x = 100; // 如果不小心这么写,x 就挂到 window 上了!
}
foo(); // 普通调用 → this = window
为了解决这个问题, 引入了 严格模式('use strict') :
'use strict'
function foo() {
console.log(this); // undefined!
}
foo(); // 报错 if you access this.property
🔒 严格模式下,普通函数调用的
this是undefined,规避了没必要的this指向,防止意外的全局绑定,提高了代码的安全性
四、手动指定 this:call / apply
有时候我们想“强行”让某个函数的 this 指向特定对象,怎么办?
JavaScript 提供了两大法宝(其实还有bind,我们下次再聊它):
func.call(obj, arg1, arg2)func.apply(obj, [arg1, arg2])
看看这个例子:
let bar = { myName: '极客邦', test1: 1 }
function foo() {
this.myName = '极客时间'
}
foo.call(bar) // 强制 this 指向 bar
console.log(bar) // { myName: '极客时间', test1: 1 }
foo.call(bar)这段代码是关键,强制执行foo函数,并将其内部this强行指向bar
五、构造函数中的 this:指向新实例
再看:
function CreateObj() {
console.log(this) // 指向新创建的实例
this.name = '极客时间'
}
var myObj = new CreateObj()
当使用 new 调用函数时,JavaScript 引擎会:
- 创建一个空对象
{}; - 把这个对象的
__proto__指向构造函数的prototype; - 把
this绑定到这个新对象; - 执行函数体;
- 返回这个对象(除非你显式 return 一个对象)。
所以,构造函数里的 this 永远指向即将被创建的实例。
六、事件处理函数中的 this
在 DOM 事件中,this 也有特殊规则。
<a href="#" id="link">点击我</a>
<script>
document.getElementById('link').addEventListener('click', function() {
console.log(this) // 指向 <a> 元素!
})
</script>
普通函数作为事件处理器时,this 指向触发事件的 DOM 元素。
但如果换成箭头函数:
addEventListener('click', () => {
console.log(this) // 指向外层作用域的 this(通常是 window 或 undefined)
})
因为箭头函数没有自己的 this,它会继承外层作用域的 this。这也是为什么在 React 等框架中,类方法常用箭头函数避免 this 丢失。
七、常见误区澄清
❌ 误区1:“返回函数就形成闭包”
看这段代码:
function foo() {
let myName = '极客时间'
return bar.printName
}
很多人以为这里形成了闭包。其实没有!因为 printName 并没有引用 foo 内部的任何变量。它的词法作用域仍然是全局,所以 console.log(myName) 打印的是全局的 '极客邦',而不是 '极客时间'。
✅ 闭包 = 函数 + 引用了外层变量。缺一不可!
❌ 误区2:“对象内部的函数有自己的作用域”
JS 中,只有函数能创建作用域,对象 {} 不会!所以 bar.printName 的作用域就是它被定义的地方 —— 全局。
八、总结:this 的五大绑定规则
| 调用方式 | this 指向 |
|---|---|
| 普通函数调用 | 全局对象(非严格模式)/ undefined(严格模式) |
| 对象方法调用 | 该对象 |
call / apply | 第一个参数指定的对象 |
new 构造函数调用 | 新创建的实例 |
| DOM 事件处理函数 | 触发事件的元素 |
记住一句话:this 指向“调用者”,而不是“定义者” 。
结语:和 this 和解吧!
this 看似混乱,其实规则很清晰。只要记住它的核心原则 —— 由调用方式决定,再配合严格模式、箭头函数、apply 等工具,你就能完全掌控它。
下次再遇到 this 问题,不妨问自己一句:
“这个函数,到底是谁在调用它?”
答案,就在调用的那一行代码里。
希望这篇文章能帮你彻底理清 this 的迷思!如果觉得有用,欢迎点赞、收藏、转发~也欢迎在评论区分享你和 this 的“相爱相杀”故事 😄