四、this的绑定规则

239 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

一、概述

在js中,this可以帮我们提供很多的便利。但也是js中比较难掌握的知识,下面我们一起来看了解一下this

二、this在不同环境中的指向

我们都知道js的运行环境大多情况下可以分为两种:

  • 浏览器
  • node

👶 浏览器环境中

我们在浏览器的控制台打印一下当前的this,可以发现this指向window对象

🤵在node环境中

同样,我们在node环境下打印一下this

发现,在node环境中this指向全局对象global Object

三、同一个函数this不同

我们来看一下下面的代码

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

// 1. 直接调用
foo()

// 2. 间接调用
var obj = {
	message: 'wkb',
  foo: foo
}
obj.foo()

// 3.call,apply调用
foo.call({message: 'obj'})

我们可以发现,三种情况调用的同一个函数,但是打印出的结果确实不同的。第一种打印window,第二种打印obj对象,第三种情况打印出传入的对象。

更据情况我们可以总结出以下规律:

  1. this的指向与函数定义的位置没有关系
  2. this的指向与调用对象有关系
  3. this是在运行时被绑定的

四、四种this绑定规则

🙍默认绑定

首先什么是默认绑定呢?

函数在调用时,js会默认给this绑定一个值,例如

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

foo() // window

在全局状态下执行foo函数会被默认绑定window对象

难度升级

function fn1() {
  console.log(this)
  fn2()
}

function fn2() {
  console.log(this)
  fn3()
}

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


fn1()

大家猜一猜最终打印的三个this是啥。。。

都是window,其实我们只要看是谁调用的函数就行了。在这个代码中,fn1,2,3前面都没有调用的对象,则会默认绑定window对象

👮隐式绑定

隐式绑定比较难理解一点,先来看一段代码吧

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

var obj = {
	message: 'wkb',
  foo: foo
}
obj.foo()

同样也是执行的foo函数,但是打印出的结果却不相同,这就是隐式绑定规则。

与默认绑定不同的是,foo有调用对象,谁调用this就指向谁。

我们再看一个案例

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

var obj1 = {
  name:'obj1',
  foo: foo
}

var obj2 = {
  name:'obj2',
  obj1: obj1
}

obj2.obj1.foo()

很绕哈,但是用上面的结论很容易就能得到答案,结果就是this指向obj1对象。

我们只需要看最后的调用对象就行,最后的调用对象是obj1。

💆显示绑定

在js中有三个方法可以显示的改变this,这三个函数分别是call,apply,bind

我们来看一下call,apply,bind的区别

function foo(name, age) {
  console.log(this, name, age)
}

foo.call({ message: 'call' }, 'wkb', 18)
foo.apply({ message: 'apply' }, ['wkb', 18])

var fn = foo.bind({ message: 'bind' })
fn('wkb', 18)
  1. call,apply,bind的第一个参数都是要指向的this对象。
  2. call和apply会立即执行,bind会返回一个函数,这个函数的this指向是调用bind时传入的第一个参数
  3. call之后的传参和bind返回函数的传参方式一致,以剩余参数的方式传递
  4. apply之后的传参则是数组类型

如果call,apply,bind传入的this是null或者undefined,则会使用默认绑定规则

👳new绑定

在了解new绑定之前,我们先了解一下,new 一个构造函数的时候,都做了什么?

  1. 创建一个空的简单 JavaScript 对象(即 {});
  2. 为步骤 1 新创建的对象添加属性 proto,将该属性链接至构造函数的原型对象;
  3. 将步骤 1 新创建的对象作为 this 的上下文;
  4. 如果该函数没有返回对象,则返回 this。

上代码

function person(name, age) {
  this.name = name
  this.age = age
  console.log(this)
}

var p = new person('wkb', 18)
console.log(p)

根据打印结果,我们可以发现this的指向不再是window,而是指向了一个新对象

五、绑定规则的优先级

好,我们已经了解了四种this的绑定规则,但是有想过如果不同的规则一起使用的时候会发生什么呢?下面介绍一下绑定规则的优先级

1. 默认规则优先级最低

毫无疑问,其他三种规则都能改变this指向,则默认规则优先级最低

2. 显示绑定 > 隐式绑定

举个🌰,上代码

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

var obj = {
  message: 'obj',
  foo: foo,
}

obj.foo.call({ message: 'call' })

打印出{message: 'call'},虽然是obj调用的foo,但最终还是被call改变了this指向

3. new绑定 > 隐式绑定

举个🌰,我们稍微把案例2的代码改亿点点

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

var obj = {
  message: 'obj',
  foo: foo,
}
 
let p = new obj.foo()

结果是this指向了一个空对象,并且空对象的类型是构造函数foo,显然new绑定的优先级高于隐式绑定

4. new绑定 > 显示绑定

因为call和apply都是立即执行的,而new需要一个构造函数则new不能与call,apply一起使用。别担心,还记得bind吗。bind不会立即执行函数,我们可以用bind返回后的函数与new来比较。

废话不多说,福利来一波。。。。

错了,错了,上代码

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

var obj = {
  message: 'obj',
  foo: foo,
}
var fn = obj.foo.bind({ message: 'bind' })
let p = new fn()

最终this还是指向空对象

六、箭头函数不绑定this

在es6中引出了非常好的函数简写方法,箭头函数。在使用箭头函数的时候,我们要注意箭头函数是不绑定this的,this会指向上一层作用域,例如

var obj = {
  data: [],
  foo: () => {
    console.log(this)
  },
}

obj.foo()

如果是foo定义的是普通的函数,则obj.foo()打印出的应该是obj对象,但foo定义的是箭头函数,箭头函数不绑定this,则往上层找,打印出window

七、面试题

我们来看一道tihs面试题

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)

首先来看

  1. person1.obj.foo1()(),先是执行foo1函数,foo1返回一个普通函数,然后全局调用,则打印window
  2. person1.obj.foo1.call(person2)(),改变了foo1的this执像,但没有改变返回函数的this执行,则打印window
  3. person1.obj.foo1().call(person2),改变foo1返回函数的this指向,call绑定大于默认绑定,则打印person2
  4. person1.obj.foo2()(),执行foo2函数,隐式绑定,当前foo2函数中的this为obj对象。然后foo2返回箭头函数,我们知道箭头函数不绑定this,会从上级作用域中找this,foo1的上级作用域是foo2,则打印obj
  5. person1.obj.foo2.call(person2)(),同样,改变foo2中的this,foo2返回的箭头函数不绑定this,打印person2
  6. person1.obj.foo2().call(person2),foo2中this为obj,foo2返回箭头函数不绑定this,则打印obj