JavaScript 关键字this的学习

104 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情

前言

1.this 关键字

什么是 this?this 是 JavaScript 的一个关键字,一般指向调用它的对象。

这句话其实有两层意思,首先 this 指向的应该是一个对象,更具体地说是函数执行的“上下文对象”。其次这个对象指向的是“调用它”的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。

下面举几个例子来进行说明:

  • 当代码 1 执行 fn() 函数时,实际上就是通过对象 o 来调用的,所以 this 指向对象 o。
  • 代码 2 也是同样的道理,通过实例 a 来调用,this 指向类实例 a。
  • 代码 3 则可以看成是通过全局对象来调用,this 会指向全局对象(需要注意的是,严格模式下会是 undefined)。
// 代码 1

var o = {

  fn() {

    console.log(this)

  }

}

o.fn() // o

// 代码 2

class A {

  fn() {

    console.log(this)

  }

}

var a = new A() 

a.fn()// a

// 代码 3

function fn() {

  console.log(this)

}

fn() // 浏览器:Window;Node.js:global

2.是不是觉得 this 的用法很简单?别着急,我们再来看看其他例子以加深理解。

2.1 this的用法1

如果在函数 fn2() 中调用函数 fn(),那么当调用函数 fn2() 的时候,函数 fn() 的 this 指向哪里呢?

function fn() {console.log(this)}

function fn2() {fn()}

fn2() // ?

  • 由于没有找到调用 fn 的对象,所以 this 会指向全局对象,答案就是 window(Node.js 下是 global)。

2.2 this的用法2

再把这段代码稍稍改变一下,让函数 fn2() 作为对象 obj 的属性,通过 obj 属性来调用 fn2,此时函数 fn() 的 this 指向哪里呢?

function fn() {console.log(this)}

function fn2() {fn()}

var obj = {fn2}

obj.fn2() // ?
  • 这里需要注意,调用函数 fn() 的是函数 fn2() 而不是 obj。虽然 fn2() 作为 obj 的属性调用,但 fn2()中的 this 指向并不会传递给函数 fn(),  所以答案也是 window(Node.js 下是 global)。

2.3 this的用法3

对象 dx 拥有数组属性 arr,在属性 arr 的 forEach 回调函数中输出 this,指向的是什么呢?

var dx = {

  arr: [1]

}

dx.arr.forEach(function() {console.log(this)}) // ?
  • 按照之前的说法,很多同学可能会觉得输出的应该是对象 dx 的属性 arr 数组。但其实仍然是全局对象。

  • 如果你看过 forEach 的说明文档便会知道,它有两个参数,第一个是回调函数,第二个是 this 指向的对象,这里只传入了回调函数,第二个参数没有传入,默认为 undefined,所以正确答案应该是输出全局对象。

  • 类似的,需要传入 this 指向的函数还有:every()、find()、findIndex()、map()、some(),在使用的时候需要特别注意。

2.4 this的用法4

前面提到通过类实例来调用函数时,this 会指向实例。那么如果像下面的代码,创建一个 fun 变量来引用实例 b 的 fn() 函数,当调用 fun() 的时候 this 会指向什么呢?

class B {

  fn() {

    console.log(this)

  }

}

var b = new B()

var fun = b.fn

fun() // ?

这道题你可能会很容易回答出来:fun 是在全局下调用的,所以 this 应该指向的是全局对象。这个思路没有没问题,但是这里有个隐藏的知识点。那就是 ES6 下的 class 内部默认采用的是严格模式,实际上面代码的类定义部分可以理解为下面的形式。

class B {

  'use strict';

  fn() {

    console.log(this)

  }

}

而严格模式下不会指定全局对象为默认调用对象,所以答案是 undefined。

2.5 this的用法5

ES6 新加入的箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。可以简单地理解为箭头函数的 this 继承自上层的 this,但在全局环境下定义仍会指向全局对象。

var arrow = {fn: () => {

  console.log(this)

}}

arrow.fn() // ?

所以虽然通过对象 arrow 来调用箭头函数 fn(),那么 this 指向不是 arrow 对象,而是全局对象。如果要让 fn() 箭头函数指向 arrow 对象,我们还需要再加一层函数,让箭头函数的上层 this 指向 arrow 对象。

var arrow = {

  fn() {

    const a = () => console.log(this)

    a()

  }

}

arrow.fn()  // arrow

2.6 this的用法6

前面提到 this 指向的要么是调用它的对象,要么是 undefined,那么如果将 this 指向一个基础类型的数据会发生什么呢?

比如下面的代码将 this 指向数字 0,打印出的 this 是什么呢?

[0].forEach(function() {console.log(this)}, 0) // ? 结合上一讲关于数据类型的知识,我们知道基础类型也可以转换成对应的引用对象。所以这里 this 指向的是一个值为 0 的 Number 类型对象。

2.7 this的用法7

改变 this 指向的常见 3 种方式有 bind、call 和 apply。call 和 apply 用法功能基本类似,都是通过传入 this 指向的对象以及参数来调用函数。区别在于传参方式,前者为逐个参数传递,后者将参数放入一个数组,以数组的形式传递。bind 有些特殊,它不但可以绑定 this 指向也可以绑定函数参数并返回一个新的函数,当 c 调用新的函数时,绑定之后的 this 或参数将无法再被改变

function getName() {console.log(this.name)}

var b = getName.bind({name: 'bind'})

b()

getName.call({name: 'call'})

getName.apply({name: 'apply'})

由于 this 指向的不确定性,所以很容易在调用时发生意想不到的情况。在编写代码时,应尽量避免使用 this,比如可以写成纯函数的形式,也可以通过参数来传递上下文对象。实在要使用 this 的话,可以考虑使用 bind 等方式将其绑定。