JS高级之彻底搞懂this关键字

601 阅读6分钟

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

写在前面

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清楚他到底指向什么。

实际上,JavaScript中this的机制并没有那么先进,但是开发者往往吧理解过程复杂化。this在实际开发中有着非常重要的作用。

this都有一个共同的点,它总是返回一个对象。简单的说,this就是属性或方法“当前”所在的对象。

this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用,随着以后开发的场景越来越复杂,显式传递上下文对象会使代码变得越来越复杂和混乱,使用this则不会这样。

想要了解this的绑定过程,首先要理解调用位置就是函数在代码中被调用的位置(而不是声明的位置)。通常的说,寻找调用位置就是寻找“函数被调用的位置”。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数。)

绑定规则

默认绑定

在一个函数中使用this,当函数被独立调用,我们可以将这条规则看做是无法应用其他规则的而应用默认规则。示例代码如下所示:

var v = 100
function fun() {
  console.log(this.v)
}
fun()

在浏览器环境下,这个函数的中的this指代window对象,所以会输入100;在Node环境下,得到的结果为undefined这是以为Node中的v并不是挂在全局对象上的。

还有就是在严格模式下这个this会指向undefiend

隐式绑定

隐式绑定的规则需要考虑的是调用的位置是否有上下文对象,或者说是否被某个对象拥有或者包含。当然这种说法并不是很准确,只是为了帮助我们自己理解。

示例代码如下所示:

var v = 200
var obj = {
  v: 100,
  fun: function () {
    console.log(this.v)
  },
}
obj.fun() //100

隐式绑定最常见的问题就是绑定丢失,指的是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定,从而把this绑定到全局对象。

示例代码如下所示:

var obj = {
  v: 100,
  fun: function () {
    console.log(this.v)
  },
}
var fun = obj.fun
fun() //undefined

我们将obj对象的fun方法赋值给全局对象的fun方法,将其引入到了全局作用域,使其应用了默认绑定。

除了上面这一种,绑定丢失也会发生在回调函数中,示例代码如下:

var person = {
  name: '一碗周',
  sayMe: function () {
    console.log(this.name)
  },
}
setTimeout(person.sayMe) // undefined

按照我们的思维,这里的this应该指向的是person对象,实际上这里应用的默认绑定规则,相当于将person.sayMe赋值给一个变量,然后执行。

显式绑定

所谓的显式绑定,就是明确在调用对象时,this所绑定的对象。JavaScript中的Function提供的apply()方法和call()方法实现。这两个方法的第一个参数接受的是一个对象,会将这个对象绑定到this,接着调用函数是指向这个this

除了apply()方法和call()方法,Function提供bind()方法也可以实现显式绑定,不同的是,前者会执行应用的函数,而bind()只是创建一个绑定了新this指向的函数。

示例代码如下:

var obj = {
  v: 100,
  fun: function () {
    console.log(this.v)
  },
}
var fun = obj.fun

// 通过 call() 方法
fun.call(obj) // 100

// 通过 apply() 方法
fun.apply(obj) // 100

// 两者的区别只是接受参数的方式不同

// 使用bind
newFun = fun.bind(obj)
newFun() // 100

new绑定

在JavaScript中,构造函数只是使用new操作符是被调用的函数。包括内置对象函数在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。

使用new来调用函数,会自动执行下面的操作:

  1. 创建一个全新的空对象,构造函数中的this指向这个空对象
  2. 将对象的的原型进行连接
  3. 执行构造函数方法,属性和方法被添加到this引用的对象中
  4. 如果函数没有返回其他对象那么new表达式中的函数调用会自动返回这个新对象。

示例代码如下:

function Fun() {
  this.v = 100
}
var fun = newFun()
console.log(fun.v) //100

绑定规则的例外

被忽略的this

如果吧null或者undefined作为this的绑定对象传入callapply或者bind方法,这些值在调用是会被忽略,实际应用的是默认绑定规则。

示例代码如下所示:

var v = 200
var obj = {
  v: 100,
  fun: function () {
    console.log(this.v)
  },
}
obj.fun.call(null) //200

间接引用

我们在实际的开发中,有可能无意间创建了一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。简洁引用最有可能发生在赋值时,示例代码如下所示:

var v = 100

function fun() {
  console.log(this.v)
}
var obj = {
  v: 200,
  fun: fun,
}
var ob = {
  v: 300,
}
;(ob.fun = obj.fun)() //100

最后一行的赋值操作等于以下这个自调函数

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

这也就导致了this指向了全局对象。

注意事项

避免多层this

多层函数或方法嵌套可能导致不同层次的this绑定对象不同,如下示例代码所指示

var obj = {
  f1: function () {
    console.log(this) //指向obj
    var f2 = (function () {
      console.log(this) //指向全局
    })()
  },
}
obj.f1()

这就有可能导致我们并不知道this的最终指向,从而导致一些异常。

避免数组方法中的this

在数组的mapforEach方法中,允许提供一个函数作为参数,这个函数内部不应该使用this

示例代码如下所示:

var arr = [1, 2, 3, 4, 5]
var v = 100
arr.forEach(function () {
  console.log(this.v)
})

此时的this.v是指向全局对象的

避免回调函数中的this

回调函数中的this经常会改变绑定的对象,最好的解决方案就是避免这样使用this。

示例代码如下所示:

var v = 100

var obj = {
  v: 200,
  f: function () {
    console.log(this.v)
  },
}

function fn(f) {
  f()
}
fn(obj.f) //100

上述代码中,fn()方法中的this其实是指向全局对象的。

写在最后

本篇文章介绍了this的概念、this在各种位置的一个指向以及使用this关键字的一些注意事项。

这是【从头学前端】系列文章的第五十五篇-《错误与异常》,如果你喜欢这个专栏,可以给我或者专栏一个关注~

本系列文章在掘金首发,编写不易转载请获得允许

往期推荐