面试法宝: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, 否则绑定到全局对象。

相关推荐
Мартин.4 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。5 小时前
案例-表白墙简单实现
前端·javascript·css
数云界5 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd5 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常5 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer5 小时前
Vite:为什么选 Vite
前端
小御姐@stella5 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing5 小时前
【React】增量传输与渲染
前端·javascript·面试
GISer_Jing5 小时前
WebGL在低配置电脑的应用
javascript
eHackyd5 小时前
前端知识汇总(持续更新)
前端