JavaScript 中 this 的那些事儿:从“指哪打哪”到“我到底是谁?”

58 阅读5分钟

大家好!今天我们来聊一聊 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

🔒 严格模式下,普通函数调用的 thisundefined,规避了没必要的this指向,防止意外的全局绑定,提高了代码的安全性


四、手动指定 thiscall / 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 引擎会:

  1. 创建一个空对象 {}
  2. 把这个对象的 __proto__ 指向构造函数的 prototype
  3. this 绑定到这个新对象
  4. 执行函数体;
  5. 返回这个对象(除非你显式 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 的“相爱相杀”故事 😄