JavaScript:最麻烦的this判定简单搞定,用一张图三个判断就可以避开this陷阱

简介: 什么是this陷阱?

什么是this陷阱?


看一段代码:


var name = "global"
var obj = {
  name: "LIYI",
  getName: function () {
    function fn () {
        return this.name // 期望返回 LIYI
    }
    return fn()
  }
}
obj.getName() // 实际返回 global


在这段代码中,第6行,我们期待返回“LIYI”,因为我们认为此时this等于obj,但事实并非如此,最终返回的是“global”,是我们在obj外面定义的name变量。为什么会这样?


我们知道,JS是一门动态语言,JS中的this是一个运行时动态对象,在程序没有运行的时候谁也不知道代码中的this具体指向哪里,只有运行以后,JS宿主的上下文执行环境才会给this赋予一个具体的对象,this指向哪里,与它在编写时所处的代码位置有关,但也取决于运行时它是在什么位置被调用的。 很多程序员依赖静态编译型语言的直观经验判断,或者依据this所处的代码位置进行简单判断,难免会产生错误,仿佛掉入了陷阱,这便是this陷阱说法的由来。


如何避免this陷阱?


怎么避免掉入这样的this陷阱呢?


在JavaScript中,箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的 this 关键字所指向的对象 是 由 箭头函数所在的父作用域决定的,并不是箭头函数本身,这是箭头函数除了格式简洁之外最大的用途之一。


对于以下代码:


1. const obj = {
2.   name: "LY",
3.   run: function (n) {
4.     const fn = function (n) {
5.       // 这里的 this 指向全局对象window或GameGlobal,并不指向obj
6.       return `${this.name}${n}`
7.     }
8.     return fn
9.   }
10. }
11. obj.run()(1) // Output:1


示例A


在上述代码的第6行中,我们想让this.name取到第2行的属性值,然而实际情况是,this 并不能如愿以偿地取到 name 的值。


this在这个示例中具体是指什么,取决于所在的宿主环境,在浏览器中是window,在小游戏中是GameGlobal。由于全局对象上并不存在一个名为name的值,因此this.name未定义,所以最终输出为1(this.name输出为空,1来自参数)。


注意:第6行中的this为什么返回全局对象?在JS中,普通函数中的this是一个特殊的动态变量,它在函数定义的时候是确定不了的,只有函数执行的时候才能确定。关于如何确定this,下面有详细的规则讲解。


如果我们想取到obj.name,怎么办?在没有箭头函数之前,可以这样修改:


const obj = {
  name: "LY",
  run: function (n) {
    const that = this
    const fn = function (n) {
      // 这里的 that 指向 obj
      return `${that.name}${n}`
    }
    return fn
  }
}
obj.run()(2) // Output:LY2


示例B


在上面的代码中,第4行声明一个临时常量that,使其等于this,这时第9行返回的fn实际上是一个裹挟了临时常量that的闭包。that等于obj,第7行that.name等于LY,所以最终输出LY2。

在有了箭头函数以后,可以这样改写:


const obj = {
  name: "LY",
  run: function (n) {
    const fn = n =>  {
      // 这里的 this 指向 obj
      return `${this.name}${n}`
    }
    return fn
  }
}
obj.run()(3) // Output:LY3


示例C


与上一个示例相比,上述代码没有声明临时常量that,且第4行的fn是用箭头函数声明的。

为什么在这里的箭头函数可以帮助我们找到obj这个对象呢?


箭头函数没有自己的this,箭头函数中的this指向运行时父级作用域下的this对象。我们可以将箭头函数看作是一个lambda表达式,一个表达式是可以视为没有自己的作用域的(但其实箭头函数有花括号,它是有自己的作用域的),箭头函数内部函数体中的this就是它所在作用域中的this。


在上面的代码中,第6行中的this,其实是第3行至第9行run函数所在的作用域——这个作用域里的this,和上一示例(示例B)中第4行中的that是等同的,是同一个对象引用。


示例C和示例A中都使用了this,但示例C中的this是从run函数所在的作用域绑定的,示例A中的this却是从全局作用域绑定的,所以示例C中的this可以找到obj对象,而示例A中的this不可以。


简单的三条判定规则


考察代码中的this具体指向哪个对象,我们要看三个问题,而这其中又涉及三条规则:


❑  一、 是不是顶级函数? 这要看函数是不是全局作用域下的顶级函数,如果是,this等于全局对象;


❑  二、 是不是箭头函数? 这要看是不是箭头函数,如果是,将箭头函数看成lamda表达式,以其父函数重新作为考察对象,回到第1条规则继续;


❑  三、 有没有执行者? 如果不是箭头函数,看被执行的函数有没有执行者,如果有,this等于执行者;如果没有执行者,this等于全局对象。


为了更好地理解这三条规则,下面用一张示意图来展示这三条判断规则,如图5-6所示。


image.png

图5-6 this关键字判定规则图


14个小练习


针对上述判定规则,我们再看一些具体示例,这是一套关于this对象判定的思维体操,共有14个小练习,它们基于一个示例演绎,几乎涵盖了所有变化情况,你可以根据自己的情况,选择练习全部或其中几个。


示例1:


function foo(){
  this.name = "LY1"
  return () =>  {
    console.log("name", this.name)
  }
}
foo()() // Output:name LY1
console.log(name)  // Output:LY1


在上面的代码中,第4行想通过this.name取到第2行赋值的LY1,这里取到了,但是我们要小心,因为此时this指向全局对象,this.name随时可能被其他代码污染,这个写法是不安全的。这个示例代码的初衷,可能是想将name限定在foo函数之内,但这是行不通的。第8行打印name,仍然有值,这个值便是在第2行写入的。


为什么第4行的this指向全局对象?


我们应用上面的三条规则判定一下,判定过程如下:


this所在的函数不是顶级函数,是箭头函数,向上跃升一个作用域,相当于取foo函数下的this,foo函数是一个处在全局作用域下的顶级函数,所以this等于全局对象。


注意:示例1及以下各示例,都可以在chrome浏览器的Console面板中执行,在这个环境中全局对象是window。注意每次执行后要换一个Tab页面或刷新Tab页面,避免受上一次测试代码的污染。

既然示例1不行,我们换个写法,在foo函数内创建一个内部对象,在这个对象上声明属性name,下面来看示例2:


function foo(){
  const country = {
    name: "LY2"
  }
  country.bar = () =>  {
    console.log("name", this.name)
  }
  return country
}
foo().bar() // Output:name undefined


运行后发现,很遗憾,第6行的this并不有指向country,而是指向了全局对象,为什么?


按判断规则,如果this所在的函数不是顶级函数,而是箭头函数,则向上跃升一个作用域,相当于取foo函数下的this,foo函数是一个处在全局作用域下的顶级函数,所以this等于全局对象。


可能有读者会想,country是函数作用域下的对象,如果将它变成一个全局对象,情况会不会改变?我们可以通过示例3试一下:


const country = {
  name: "LY3"
}
function foo() {
  country.bar = () =>  {
    console.log("name", this.name)
  }
  return country
}
foo().bar() // Output:name undefined


现在country已经是一个foo函数外部的全局对象了,但仍然没有用,第6行中的this仍然指向全局对象,为什么?


按判断规则,如果this所在的函数不是顶级函数,而是箭头函数,则向上跃升一个作用域,而 foo函数是一个处在全局作用域下的顶级函数,所以this等于全局对象。

可能有读者会想,我不仅将country提升全局作用域,还将包含this的箭头函数也提升,又会怎样?来看示例4:


const country = {
  name: "LY4"
}
country.bar = () =>  {
  console.log("name", this.name)
}
function foo() {
  return country
}
foo().bar() // Output:name undefined
country.bar() // Output:name undefined

其中第10行、第11行,无论是通过foo()返回的country调用,还是通过全局常量country直接调用bar函数,结果都是一样的,第5行中的this仍然指向全局对象,为什么?


按规则,如果this所在的函数是顶级函数,那么this等于全局对象。


如果我们不用箭头函数,将this所在的函数改为普通函数呢?来看示例5:


function foo() {
  const country = {
    name: "LY5"
  }
  country.bar = function () {
    console.log("name", this.name)
  }
  return country
}
foo().bar() // Output:name LY5


从打印结果看,第6行中的this指向第2行声明的对象country,this.name成功取到了值LY5,为什么?

按规则,如果this所在的函数不是顶级函数,而是普通函数,且它有执行者,其执行者是第10行第一步调用foo()返回的country,那么this对象等于country。


示例5的第5行是通过赋值的方法声明了普通函数,如果将函数直接写在对象的键值对属性里又会怎样?来看示例6:


function foo() {
  const country = {
    name: "LY6",
    bar: function () {
      console.log("name", this.name)
    }
  }
  return country
}
foo().bar() // Output:name LY6


与上一个示例类似,只是bar函数声明的方式不同,测试结果是一样的。


这一次我们不让foo函数返回对象,让它返回一个函数,来看示例7:


function foo() {
  const country = {
    name: "LY7",
    bar: function () {
      console.log("name", this.name)
    }
  }
  return country.bar
}
foo()() // Output:name undefined

这里第5行中的this.name取不到LY7了,this指向全局对象,为什么?


按规则,如果this所在的函数不是顶级函数,而是普通函数,但它没有执行者,且第10行第一步调用foo()返回的是函数bar,不是一个执行者,那么this等于全局对象。


再来看示例8,这次我们将foo也放在一个对象里面:


const obj = {
  name: "LY8-1",
  foo: function () {
    const country = {
      name: "LY8-2",
      bar: function () {
        console.log("name", this.name)
      }
    }
    return country
  }
}
obj.foo().bar() // Output:name LY8-2


这一次,第7行中的this.name指向第5行定义的name,this指向country,为什么?


按规则,如果this所在的函数不是顶级函数,而是普通函数,且它有执行者,其执行者是第10行第一步调用obj.foo()返回的country,那么this对象等于country。


我们将this关键字所在的普通函数改为箭头函数试一下,具体如示例9所示:

const obj = {
  name: "LY9-1",
  foo: function () {
    const country = {
      name: "LY9-2",
      bar: () =>  {
        console.log("name", this.name)
      }
    }
    return country
  }
}
obj.foo().bar() // Output:name LY9-1

这次第7行中的this指向了第1行的obj,而非第4行的country,为什么?


按规则,如果this所在的函数不是顶级函数,而是箭头函数,向上跃升一个作用域,相当于取函数foo作用域下的this,以函数foo的拥有者obj为执行者,函数foo是普通函数,它有执行者obj,那么this对象等于obj。


上一示例函数foo返回的是一个对象,我们让它返回一个函数再试一下,具体如示例10所示:


const obj = {
  name: "LY10-1",
  foo: function () {
    const country = {
      name: "LY10-2",
      bar: () =>  {
        console.log("name", this.name)
      }
    }
    return country.bar
  }
}
obj.foo()() // Output:name LY10-1

在这个示例中,我们很期望第7行中的this.name返回第5行写下的LY10-2,但事实上它返回了第2行写下的LY10-1,为什么?


按规则,this所在的函数不是顶级函数,是箭头函数,向上跃升一个作用域,相当于取函数foo作用域下的this,以函数foo的拥有者obj作为执行者,函数foo是普通函数,它有执行者obj,所以this对象等于obj。判断路径与上一示例是一样的。


该示例与示例7有点像,第一步调用同样是返回一个函数,而不是一个对象。为什么示例7中的this指向全局对象,这次示例中的this就等于obj了呢?根本原因在于,本示例判断时发生了作用域跃升,在父级作用域中找到了执行者。


假设作用域不跃升,我们再看一个示例,具体如示例11所示:


const obj = {
  name: "LY11-1",
  foo: function () {
    const country = {
      name: "LY11-2",
      bar: function () {
        console.log("name", this.name)
      }
    }
    return country.bar
  }
}
obj.foo()() // Output:name undefined

这个示例与示例10很像,只是第6行声明函数的方式不同,上一个示例bar函数是箭头函数,本示例中是普通函数。


如何判断呢?


按规则,this所在的函数不是顶级函数,是普通函数,且第13行第一步调用obj.foo()返回的是一个函数,它没有执行者,所以this等于全局对象。


以上示例都没有涉及类,接下来看一个在一个对象上调用其方法的示例,具体如示例12所示:


class User {
  name = "LY12"
  foo() {
    console.log("name", this.name)
  }
}
const u = new User()
u.foo() // Output:name LY12
const f = u.foo
f()  // TypeError: Cannot read properties of undefined (reading 'name')

第8行输出name LY12,说明第4行中的this指向了User类的实例。第10行报错了,错误大意是“类型错误:无法读取未定义的属性name”,说明此时第4行中的this又不指向User类的实例了。


注意:为什么第10行调用f()会报错,却不会打印name undefined呢?这是因为类是ES6语法,在class内部,默认开启了JS的use strict,即开启了严格模式。在严格模式下,未定义的属性不能访问,否则报错。


从以上12个示例的练习可以看出,同样一份类代码,调用方法不一样,this的指向就不同,这也从侧面说明了this纯粹是一个动态关键字,它具体指向谁,完全取决于运行时。


那么,现在我们再想一下,为什么第10行调用f(),取不到正确的this呢?


按规则,this所在的函数不是顶级函数,是普通函数(第3行的foo函数只是简写,它并不是箭头函数),第9行返回的f是一个函数,而不是一个对象,它没有执行者,所以this等于全局对象。


再想一下,为什么第8行调用u.foo(),this又取到了正确的对象了呢?


按规则,如果this所在的函数不是顶级函数,而是普通函数,第9行调用u.foo()时,foo有执行者,执行者即u,那么this等于类User的实例。

也不一定返回一个函数,就取不到正确的t

his对象,看示例13:


class User {
  name = "LY13"
  foo() {
    return () =>  {
      console.log("name", this.name)
    }
  }
}
const u = new User()
const f = u.foo()
f() // Output:name LY13

这个示例是在示例12的基础上修改的,在foo函数内,使用箭头函数将代码包裹了一下。第10行仍是返回了一个函数,不是对象,但this所在的函数是箭头函数,发生作用域跃升了,相当于取函数foo作用域下的this对象,foo的拥有者(User的实例u)是执行者,所以this指向了类User的实例u。


JS的这个动态关键字this,很容易将程序员搞得晕头转向,一不小心也很容易写出有Bug的代码,大多数时候程序员在使用this的时候,都会经过本地测试,在发现苗头不对时,马上修改。不过有一个简单的方法可以避免在普通函数中使用this关键字时产生错误,这个方法就是使用Function.bind或Function.call。


bind允许开发者在运行时动态改变代码执行上下文环境中的this,call则是既改变又执行,来看示例14:

class User {
  name = "LY14"
  foo() {
    console.log("name", this.name)
  }
}
const u = new User()
u.foo() // Output:name LY14
const f = u.foo
f.call(u) // Output:name LY14
f.bind(u)() // Output:name LY14

这个示例是从示例12修改过来的,类代码没有修改,只是修改了调用方式。第10行使用call将u绑定为函数的执行者,并执行函数,this等于实例u;第11行,先用bind绑定u为函数的执行者,再执行,this等于实例u。


注意:本节在讲解this关键字时,没有特意区分函数和方法。一般情况下,属于某个对象的是方法,不属于任何对象的是函数,但从根本上讲它们都是Function,要么是普通函数,要么是箭头函数。


小结


以上就是在普通函数和箭头函数中判定this关键字所指的具体内容,如仍有疑问,对照示意图多分析几遍示例代码就明白了。判断规则记住三句话就可以了:


❑  是不是顶级函数?

❑  是不是箭头函数?

❑  有没有执行者?


最后留一道思考题给你:箭头函数中的this指向哪里?如果是面试官问你这个问题,你会如何回答呢?(思考与练习5-5,附录有参考答案)


以上内容摘自机工出版的《微信小游戏开发》,李艺著,该书已在京东上架,内容为适合网络发表有少量修改。

目录
相关文章
|
6月前
|
JavaScript
JS中改变this指向的六种方法
JS中改变this指向的六种方法
|
5月前
|
自然语言处理 JavaScript 前端开发
在JavaScript中,this关键字的行为可能会因函数的调用方式而异
【6月更文挑战第15天】JavaScript的`this`根据调用方式变化:非严格模式下直接调用时指向全局对象(浏览器为window),严格模式下为undefined。作为对象方法时,`this`指对象本身。用`new`调用构造函数时,`this`指新实例。`call`,`apply`,`bind`可显式设定`this`值。箭头函数和绑定方法有助于管理复杂场景中的`this`行为。
60 3
|
4月前
|
JavaScript
js 【详解】函数中的 this 指向
js 【详解】函数中的 this 指向
40 0
|
4月前
|
JavaScript 前端开发
|
6月前
|
JavaScript 前端开发
js中改变this指向、动态指定函数 this 值的方法
js中改变this指向、动态指定函数 this 值的方法
|
5月前
|
JavaScript
js -- 函数总结篇,函数提升、动态参数、剩余参数、箭头函数、this指向......
js -- 函数总结篇,函数提升、动态参数、剩余参数、箭头函数、this指向......
|
5月前
|
JavaScript 前端开发
JS中如何使用this方法
JS中如何使用this方法
19 0
|
6月前
|
自然语言处理 JavaScript 前端开发
在JavaScript中,this关键字的行为可能会因函数的调用方式而异
【5月更文挑战第9天】JavaScript中的`this`关键字行为取决于函数调用方式。在非严格模式下,直接调用函数时`this`指全局对象,严格模式下为`undefined`。作为对象方法调用时,`this`指向该对象。用`new`调用构造函数时,`this`指向新实例。通过`call`、`apply`、`bind`可手动设置`this`值。在回调和事件处理中,`this`可能不直观,箭头函数和绑定方法可帮助管理`this`的行为。
43 1
|
6月前
|
JavaScript 前端开发
深入探索JavaScript:如何改变this的指向
深入探索JavaScript:如何改变this的指向
51 2
|
6月前
|
JavaScript 前端开发
【专栏】`Function.prototype.apply` 在JavaScript中用于动态设定函数上下文(`this`)和参数列表
【4月更文挑战第29天】`Function.prototype.apply` 在JavaScript中用于动态设定函数上下文(`this`)和参数列表。它接受两个参数:上下文对象和参数数组。理解`apply`有助于深入JS运行机制。文章分三部分探讨其原理:基本概念和用法、工作原理详解、实际应用与注意事项。在应用中要注意性能、参数类型和兼容性问题。`apply`可用于动态改变上下文、传递参数数组,甚至模拟其他语言的调用方式。通过深入理解`apply`,能提升代码质量和效率。
41 3