面试法宝:this指向终极指南

前言

我相信大多数掘友在平时开发时都和我一样有着相同的困惑,就是始终搞不懂 this 这个关键词在 JavaScript 中的使用方法,也就是 this 指向问题。虽然有时候能碰巧或者自以为的感觉是这样而得出正确的结果,但是还是不知道其背后的原因。

那么接下来我们就好好讨论下 this 指向问题,因为这不仅在面试中会考到,而且在工作中使用的频率也非常高,只有彻底弄懂 this 指向问题,我们才能在开发的道路上一马平川!

什么是 this

《你不知道的JavaScript(上卷)》 这本书中说到,this 关键字是 JavaScript 中最复杂的机制之一。 它是一个很特别的关键字,被自动定义在所有函数的作用域中。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、 函数的调用方法、 传入的参数等信息。 this 就是记录的其中一个属性, 会在函数执行的过程中用到。

我们不妨举一个例子来具体的分析一下:

js 复制代码
// 首先定义函数
function foo() {
  console.log(this)
}

// 我们可以直接调用函数
foo() // Window

// 也可以通过对象调用函数
var obj = {
  foo: foo
}

obj.foo() // obj对象

// 也可以通过call/apply调用函数
foo.call("bar") // String {"bar"}

从上面的代码中,我们可以得出一个结论:

1. 当函数被调用时,JavaScript 会默认给 this 绑定一个值

2. this 是在运行时进行绑定的,而不是在编写时进行绑定的

3. this 的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式,也就是函数在代码中被调用的位置

this 的绑定规则

默认绑定

独立函数调用是最常用的函数调用类型,我们可以把这条规则看作是无法应用其他规则时的默认规则,那么什么是独立函数调用呢?举例:

当我们定义一个普通的函数的时候,并且调用它,就是独立函数调用。

js 复制代码
// 定义函数
function foo() {
    console.log("foo:", this)
}
// 调用函数
foo() // foo: Window

从上面的代码我们可以看出,其运行的结果是 Window,因为独立函数调用时,this 指向全局对象 Window

当我们把函数定义在对象中时,独立的去调用它,也是独立函数调用。

js 复制代码
var obj = {
  name: "lisi",
  bar: function() {
    console.log("bar:", this)
  }
}

var baz = obj.bar
baz() // bar: Window

从上面的代码我们可以看出,其运行结果与函数定义的位置没有关系,而是看它是在什么时候被调用的,obj 是一个对象,obj 对象中 bar 属性的引用被赋值给 baz 变量,baz 独立的去调用函数,所以结果同样指向 Window

当我们定义一个高阶函数时,并对它传入一个参数调用,也是独立函数调用。

js 复制代码
var obj = {
  name: "lisi",
  bar: function() {
    console.log("bar:", this)
  }
}

// 定义一个高阶函数
function foo(fn) {
  fn()
}
// 调用函数
foo(obj.bar) // bar: Window

从以上的代码分析中我们可以看出,独立函数调用,也称为默认规则,可以理解为函数没有被绑定到某个对象上进行调用 。其结果都是指向 Window。我们可以把它当作一个小的结论记住,只要遇到类似的场景,就能够明确 this 的指向。注意:如果函数体处于严格模式, this 会被绑定到 undefined, 否则 this 会被绑定到全局对象。

隐式绑定

隐式绑定要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。那么什么又是隐式绑定呢?

当我们定义一个函数,把这个函数放入对象中作为其属性值,然后再通过对象调用,这就是隐式绑定。

js 复制代码
function foo() {               
  console.log("foo函数:", this)
}

var obj = {
  bar: foo
}

obj.bar()

从上面的代码我们可以看出,foo 函数被调用时, obj 对象拥有或者包含它,其落脚点确实是指向 obj 对象的,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象上,即调用 foo()this 被绑定到 obj 上。

显式绑定

其实隐式绑定是有前提条件的,那就是必须在调用的对象内部有一个对函数的引用,否则在进行调用时会因找不到函数而报错,也正是因为这个引用,才间接的将 this 绑定到这个对象上。但是我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

我们可以使用 call(..)apply(..) 方法,它们的第一个参数是一个对象,会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象, 因此我们称之为显式绑定。

js 复制代码
function foo() {
  console.log(this.a)
}

var obj = {
  a: 2
}

foo.call(obj) // 2
foo.apply(obj) // 2

从上面的代码我们可以看出,通过 foo.call(..) 函数调用,我们可以在调用 foo 函数时强制把它的 this 绑定到 obj 上。

其实,bind(..) 也是一种显示绑定的方法,它返回一个新的函数,当调用该新函数时,会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数。

js 复制代码
function foo(name, age, height, address) {
    console.log("foo:", this) // foo: { name: "zhangsan" }
    console.log("参数:", name, age, height, address) // 参数: "lisi" 24 1.8 地址
}

var obj = { name: "zhangsan" }

var bar = foo.bind(obj, "lisi", 24, 1.8)
bar("地址")

new 绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

1. 创建一个全新的对象

2. 这个新对象会被执行 prototype 连接

3. 这个新对象会绑定到函数调用的 this 上(this 的绑定在这个步骤完成)

4. 如果函数没有返回其他对象,表达式会返回这个新对象

js 复制代码
function foo() {
  this.name = "wangwu"
  console.log("foo函数:", this)
}

new foo() // foo函数: foo { name: 'wangwu' }

从上面的代码我们可以看出,使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。

内置函数的绑定

在平时的开发中,我们有时候会调用一些 JavaScript 的内置函数,或者第三方库中的内置函数,这些内置函数可能会让我们传入另外一个函数,并通过 JavaScript 内部或者第三方库内部帮助我们执行,那么这些函数中的 this 又是如何进行绑定的?

比如我们常用的定时器函数:

js 复制代码
setTimeout(function() {
  console.log("定时器函数:", this) // 定时器函数: Windows
}, 1000)

从上面的代码我们可以看出,当我们将一个函数传入定时器函数时,其 this 指向的是全局对象 Windows

又比如我们常用的 forEach 函数,其实它还有第二个参数,就是执行回调函数时用作 this 的值。

js 复制代码
var names = ["123", "456", "789"]
names.forEach(function(item) {
  console.log("forEach:", this)
}, "000")

从上面的代码我们可以看出,其运行结果则会遍历返回 forEach: String {'000'},即通过 forEach 的第二个参数,指定 this

所以针对一些特殊的内置函数,要具体问题具体分析,它们都有一套属于自己的绑定规则。

绑定规则的优先级

既然绑定的规则有这么多种,那么如果某个调用位置可以应用多条规则该怎么办?这时我们就要抛出绑定规则优先级这个概念,应用多条规则时,一定是有区分的,接下来我们就来分析下它们的优先级。

情况一:apply/call 的优先级高于默认绑定

js 复制代码
function foo() {
  console.log( this.a )
}

var obj1 = {
  a: 2,
  foo: foo
}

var obj2 = {
  a: 3,
  foo: foo
}

obj1.foo()// 2
obj2.foo() // 3
obj1.foo.apply( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

从上面的代码我们可以看出,apply/call 修改了函数中的 this 指向,由此可以得出 apply/call 是高于默认绑定的。

情况二:bind 的优先级高于默认绑定

js 复制代码
function foo() {
  console.log("foo:", this)
}

var bar = foo.bind("aaa")

var obj = {
  baz: bar
}

obj.baz() // foo: String {'aaa'}

从上面的代码我们可以看出,其运行结果是 foo: String {'aaa'},显然 bind 同样高于默认绑定。

情况三:bind 的优先级高于 apply/call

js 复制代码
function foo() {
  console.log("foo:", this)
}
var bindFn = foo.bind("aaa")
bindFn.call("bbb") // foo: String {'aaa'}

从上面的代码我们可以看出,其运行结果是 foo: String {'aaa'},显然 bind 的优先级高于 apply/call

情况四:new 绑定优先级高于隐式绑定

js 复制代码
function foo() {
  console.log("foo:", this)
}
var obj = {
  foo: function () {
    console.log('foo:', this) // foo: foo {}
    console.log('foo:', this === obj) // foo: false
  }
}
new obj.foo()

从上面的代码我们可以看出,其运行结果分别是 foo: foo {}foo: false,显然 new 绑定优先级高于隐式绑定。

情况五:new 优先级高于 bind

js 复制代码
function foo(something) {
  this.a = something
}

var obj1 = {}

var bar = foo.bind(obj1)

bar(2)

console.log(obj1.a) // 2

var baz = new bar(3)

console.log(obj1.a) // 2

console.log(baz.a) // 3

从上面的代码我们可以看出,bar 被硬绑定到 obj1 上, 但是 new bar(3) 并没有像我们预计的那样把 obj1.a 修改为 3。 相反,new 修改了硬绑定从而调用 bar(..) 中的 this

因为使用了 new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。 由此可以得出 new 优先级高于 bind

注意:new 绑定不可以和 apply/call 一起使用,否则会报错。

根据以上示例我们可以得出优先级的结论:new -> bind -> apply/call -> 隐式 -> 默认

this 绑定之外的情况

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

情况一:显式绑定 null/undefined,那么使用的规则是默认绑定

js 复制代码
function foo() {
  console.log("foo:", this)
}

foo.apply("abc") // foo: String {'abc'}
foo.apply(null) // foo: Window
foo.apply(undefined) // foo: Window

情况二:间接引用

当创建一个函数的"间接引用"时,调用这个函数会应用默认绑定规则,间接引用通常在赋值时发生。

js 复制代码
var obj1 = {
  name: "obj1",
  foo: function() {
    console.log("foo:", this)
  }
}

var obj2 = {
  name: "obj2"
}

obj2.foo = obj1.foo
obj2.foo()

// 间接引用
(obj2.foo = obj1.foo)()

上述代码中,赋值表达式 obj2.foo = obj1.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 obj2.foo() 或者 obj1.foo()

箭头函数中的 this

在箭头函数中,无论 this 绑定到什么上,箭头函数都会继承外层函数调用的 this 绑定。这其实和 ES6 之前代码中的 self = this 机制一样。

普通函数中是有 this 的标识符,但是在箭头函数中是没有的,通过 apply 调用时, 也是没有 this 的。

js 复制代码
var bar = () => {
  console.log("bar:", this)
}

bar() // bar: Window

bar.apply("aaaa") // bar: Window

this 在箭头函数中的查找规则,根据外层(函数或者全局) 作用域来决定 this

js 复制代码
var obj = {
  name: 'obj',
  foo: () => {
    var bar = () => {
      console.log('bar:', this)
    }
    return bar
  }
}
var fn = obj.foo()

fn() // bar: Window

fn.apply('bbb') // bar: Window

this 指向面试题

面试题一:

js 复制代码
var name = 'window'

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }


// 面试题目:
person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

面试题二:

js 复制代码
var name = 'window'

function Person(name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')


// 面试题目:
person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

面试题三:

js 复制代码
var name = 'window'

function Person(name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

// 面试题目
person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)

上述就是典型的三道 this 指向面试题,感兴趣的掘友们可以先试着练习下,可以把代码复制到浏览器中,看看结果是否和预期的一样,也可以在下方评论区我们一起讨论。

总结

this 指向一直是我们老生常谈的问题,尤其是在面试中,考察的频率非常高。我们在平时的工作中或者学习中用到它的地方也很多,甚至在一些框架的源码中,都能见到 this 的身影,可谓学好 this 有多么的重要。

在这里我们做一个总结,如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置,然后我们可以根据下面的规则进行判定 this 指向:

首先,函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。

然后,函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。

其次,函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。

最后,如果都不是的话,使用默认绑定。 如果在严格模式下,就绑定到 undefined, 否则绑定到全局对象。

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪8 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪8 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试