this 不再玄学:从面试套路到源码级调试,一篇彻底搞定 JS 上下文指向

139 阅读6分钟

前言

在 JavaScript 的江湖里,this 就像一把“隐形匕首”——看不见、摸不着,却总在关键时刻给你来一下。初学者以为它指向“当前对象”,结果一运行,控制台噼里啪啦全是 undefined;老鸟自信满满写箭头函数,一不留神还是踩了“丢失上下文”的坑。 今天,我们就把 this 从“玄学”拉回“科学”,带你从编译器视角拆给它“验尸”——看到底是谁偷走了你的上下文。读完这篇文章,你将获得一张“this 指向速查表”,贴在键盘边,再也不用担心“谁调的我”!

为什么要有 this

简而言之,this 提供了一种更优雅的方式来隐式的传递一个对象引用,可以让代码更简洁易于复用 ,剩下的交给代码来解释:当我们要看懂下面这一串代码时,需要在函数形参和实参中不断来回切换,作为初学者看下面代码都有可能会被绕晕,这还只是两个函数的嵌套,如果是项目开发过程中无数个函数嵌套,别说是人 ,AI 都有可能被绕晕。(注意toUpperCase 将字符串 'Tom' 转换为大写,输出 'TOM')

function identify(context) {
  return context.name.toUpperCase()
}
function speek(context) {
  var greeting = 'hello, I am ' + identify(context)
  console.log(greeting);
}
var me = {
  name: 'Tom'
}
speek(me)

为了让代码更简洁易于复用,我们就能用 this 来解决,直接省去形参(如下)

function identify() {
  return this.name.toUpperCase()
}
function speek() {
  var greeting = 'Hello, I am ' + identify.call(this)
  console.log(greeting);
}
var me = {
  name: 'Tom'
}
speek.call(me)

两段代码打印结果是一样的,但明显下面代码更加简练

this1.png

this 用在哪?以及 this 的绑定规则

this用在全局作用域和函数作用域, 在全局作用域里面 this == window ,在函数作用域里面就得看具体绑定规则(this是一个代词,用在不同的地方代指不同的值) this 的绑定规则有四种它们分别是

1. 默认绑定 --- 当函数被独立调用时,函数中的 this 指向 window

话不多说依旧一套小连招,上代码

var a = 1
function bar () {
  var a = 2
  function foo() {
    console.log(this.a);
  }
  foo()
}
bar()

如上所示,foo 里面的 this ,由于函数foo是被直接调用,所以this 指向 window ,现在会了吧,再来一段代码试试手

var a = 1
function foo() {
  console.log(this.a);
}
function bar () {
  var a = 2
  foo()
}
bar()

有没有那么一瞬间你把上面这个代码认为是指向 foo 或者 bar 函数呢,那就大错特错了,foo 里面的 this ,由于函数foo是被直接调用,所以this 就是指向 window ,在这里我们只认死理,只要是 foo() ,那 this 就是指向 window

2. 隐式绑定 --- 当函数引用有上下文对象且被该对象调用时,函数中的 this 会绑定到这个上下文对象上

依旧上代码

function foo() {
  console.log(this);
}

var a = 1
var obj = {
  foo: foo
}
obj.foo()
 // obj.foo

如上所示, thisfoo 函数里,foo 函数被引用在 obj 对象里,并且被 obj 对象调用了,this 会绑定到这个上下文对象上,在隐式绑定里要注意两点,一是得分清楚引用和调用,引用是直接把函数拿过来但是不运行它,调用则是并运行它。如上图如果把倒数第二行代码删了用最后一行被注释掉的代码,那么this 就是指向 window 。第二个就是隐式丢失 --- 当一个函数被多层对象调用时,函数的 this 指向最近的那个对象,接着上代码

function foo() {
  console.log(this.a);
}
var obj = {
  a: 1,
  foo: foo
}
var obj2 = {
  a: 2,
  foo: obj
}
obj2.foo.foo()

thisfoo 函数里面的,foo 函数被两个对象调用,函数的 this 指向最近的那个对象,也就是 obj 对象

3. 显式绑定

  • fn.call(obj, x, x) 显示的将 fn 里面的 this 绑定到 obj 这个对象上,call 负责帮 fn 接受参数
var obj = {
  a: 1
}
function foo(x, y) {    // new Function()  // foo.__proto__  === Function.prototype
  console.log(this.a, x + y);
}
foo.call(obj, 1, 2)
  • fn.apply(obj, [x, x]) 显示的将 fn 里面的 this 绑定到 obj 这个对象上,apply 负责帮 fn 接受参数,与 call 不同之处在于它是以数组形式接受参数
var obj = {
  a: 1
}
function foo(x, y) {
  console.log(this.a, x + y);
}
var arr = [1, 2]
foo.apply(obj, arr)
  • fn.bind(obj, x, x)() 显示的将 fn 里面的 this 绑定到 obj 这个对象上,bind 负责帮 fn 接受参数,与 call 不同之处在于它既可以从obj 所在括号里接受参数,也可以在后面的小括号里接收参数(前者优先级大于后者)因为他的原理是构成一个新对象你可在新对象里接受参数,因为优先级存在,x 和 y 分别被赋予了 2 和 4,而不是 3 。
var obj = {
  a: 1
}
function foo(x, y) {
  console.log(this.a, x + y);
}
const bar = foo.bind(obj, 2, 4)
bar(3)

4.new 绑定

  • new 的原理会导致 函数的 this 指向 实例对象

new 也是我这的老人了,连续三篇文章都有提到它,这个就不做过多解释,代码加注释足矣。

Person.prototype.say = 'hello'
function Person() {
  // var obj = {name: '冯总'}      1
  // Person.call(obj)   2
  this.name = '冯总'   // 3 
  // obj.__proto__ = Person.prototype  4
  // return obj  // 5
}
let p = new Person()
  • 当构造函数中存在 return ,并且 return 的是一个引用类型的数据,则 new 的返回失效 对于这一点咱还是有必要聊一聊,老规矩上代码:
function Person() {
  this.name = '冯总'
  return {a: 1} //return []  //return fn{}
}
let p = new Person()
console.log(p);

如果像第三行代码一样,在构造函数中存在 return ,并且 return 的是一个引用类型的数据,我们用的是对象做代表,那么new就不能成功

this2.png

箭头函数

最后为箭头函数单开一页,箭头函数中没有 this 这个概念,写在了箭头函数中的 this,也是它外层那个非箭头函数的 this 。看下面代码this 能最终指向 obj 对象,即它存在 foo 函数里面。

function foo() {
  // this
  var bar = () => {
    this.a = 2
  }
  bar()
}
var obj = {
  a: 1,
  baz: foo
}
obj.baz()

结语

一张速查表,把 this 从“玄学”变“显学”

  1. this 的价值 没有 this,就必须显式把对象当参数传来传去;有了 this,JavaScript 才能把“方法”做成通用组件,实现真正的复用与动态绑定。

  2. this 只认“谁最终调用我” 记住 4 条铁律,优先级从低到高:

    ① 默认绑定 → 独立调用时指向全局(严格模式 undefined)。

    ② 隐式绑定 → 被对象“点”出来调用时,指向最近的那个对象;小心“赋值给变量”造成的隐式丢失。

    ③ 显式绑定 → call / apply / bind 强行把 this 钉在指定对象上;bind 返回新函数,永久生效。

    ④ new 绑定 → 构造函数里 this 指向正在创建的那个实例;若构造函数 return 引用类型,则 new 失效。

  3. 箭头函数是“外人” 它自己没有 this,写在里面的 this 就是外层非箭头函数的词法 this,一旦定义永不改变,call / apply / new 都休想动它。

  4. 调试口诀 控制台打 console.log(this) 之前,先问自己三句话:

    • 函数是裸调的吗?→ 看默认。
    • 函数被谁“点”出来的?→ 看隐式。
    • 有没有 call / apply / bind / new?→ 按优先级覆盖。

把这张“this 四象限”脑图设成浏览器书签,下次再遇到 undefined 狂刷控制台时,3 秒内就能定位是哪条规则在“偷家”。愿你从此写 this 不猜、不蒙、不踩坑!