JS中this的指向

132 阅读6分钟

1. this的理解

this关键字是javascript中最复杂的机制之一,无论是一个JavaScript初学者还是已经有一些使用经验的老鸟来说,清晰的理解this的指向也是很有必要的.

1.1 关于this的误解

1.1.1 指向函数自身

这个理解从英语语法角度来说是可以说得通的.然后通过this来存储一些函数自身的状态属性.然而我们可以通过一个很简单例子来说明这个的错误,比如:

    function foo() {
	console.log(this.count)
    }
    foo.count = 1
    foo() // undefined

但是从上面的例子中我们可以看到,this实际上并没有指向foo本身, 至于为什么打印结果是undefined,我们后面会讲到.

1.2 this的指向

实际上,this的指向并不是确定的,它不像函数的作用域一样在定义的时候就已经确定,它更像动态作用域一样,它只关心函数的调用位置,以及是被谁调用的.

2. this的绑定规则

下面让我们来看看,具体的情况下,函数的this到底是指向谁的

2.1 默认绑定

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

foo() // window


function foo() {
    "use strict"
    console.log(this)
}

foo() // undefined

上面的例子可以看出来, 独立函数调用时, 该函数的this指向在非严格模式下是window的, 在严格模式下则是undefined.

2.2 隐式绑定

2.2.1.一般情况
function foo() {
  this.a = 3
  console.log(this.a)
}

let obj1 = {
  foo,
  a: 2,
}

obj1.foo() // a
console.lgo(obj1) // {foo:Fcuntion,a:3}

作为对象的属性,被对象调用时,该函数的this指向了他的调用者.

2.2.2.特殊情况
  1. 隐式丢失
var a = 2
function foo() {
  console.log(this.a)
}

let obj = {
  a: 1,
  foo,
}

let bar = obj.foo
bar()//2

bar是obj.foo的一个引用,但实际上它引用的是foo函数本身,因此此时的bar()实际上是一个不带任何修饰的函数调用,因此采用了默认绑定.

  1. 作为回调函数
function foo() {
	console.log(this.a)
}

function doFn(fn) {
	fn()
}
let obj = {
	a: 1,
	foo,
}

doFn(obj.foo) // fn = obj.foo undefined

和上面的情况基本相同,doFn函数中的参数fn实际上就是对obj.foo的引用,也会采用默认绑定.

2.3 显式绑定

上面的隐式绑定,我们必须在一个对象内部包含一个指向函数的属性, 通过这个属性来间接调用函数,那么我们不想在对象内部包含函数,也想通过这个对象来调用函数,有没有办法呢?
答案是有的, 那就是通过显示绑定来实现.

2.3.1 一般情况下,我们就是通过函数原型上的call,apply方法来实现的,我们也称这种方式为显式绑定
function foo() {
    this.c = 3
    console.log(this)
    console.log(this.a)
}

let obj = {
    a: 1,
    b: 2,
}
foo.call(obj) // {a:1,b:2,c:3} 1
foo.apply(obj) // {a:1,b:2,c:3} 1

通过call和apply方法, 我们在调用foo函数时,强制将它的this绑定到obj上.
从函数绑定的调度来说,call,apply是没有区别的,它们主要是在给函数传递参数上有一定的区别.

2.3.2 实际上,像上面这两种方法,也无法解决我们之前的提出的绑定丢失的情况.我们就可以使用第三种方法了, 硬绑定(另类的显示绑定),利用函数原型上的bind方法.
var a = 2
function foo() {
    console.log(this.a)
}
let obj = { a: 1, foo }
let bar = obj.foo.bind(obj)
bar() //1

通过bind方法返回了一个被绑定了this的函数, 并且这个函数的this不会再被改变,通过这种方法,就可以解决上面提到的绑定丢失的情况.

2.4 new关键字绑定

利用new关键字,调用函数,也可以实现this的绑定, 我们就需要知道,在使用new关键字调用函数的时候,到底发生了什么?

1. 创建一个新的对象
2. 将这个对象的[[prototype]]赋值为函数的prototype
3. 将函数的this绑定为创建的对象
4. 如果这个函数没有返回一个对象的话,就会把创建的对象返回.

function Foo() {
  this.a = 1
}

let f = new Foo()
console.log(f.a)//1

在使用new调用foo函数时, 将创建出的f对象, 绑定到了foo种的this上.

2.5 优先级比较

我们再来看看,以上几种方法的优先级.通过上面的例子,就能看出了,默认绑定的优先级是最低的.我们再来看看另外几种的优先级呢?

2.5.1 显示-隐式
function foo() {
    console.log(this.a)
}

let obj1 = {
    a: 1,
    foo,
}

let obj2 = {
    a: 2,
    foo,
}

obj1.foo() // 1
obj2.foo() // 2

obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1

上面两个,采用了隐式绑定,正常打印出了1,2,下面两种,先采用了隐式绑定,再通过call方法绑定了this,同时修改了函数的this,可以看出,显示绑定优先级是大于隐式绑定的.

2.6.2 显示-new

由于, call,apply方法是无法和new关键字一起使用的.因此,我们可以比较一下bind和new.

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

let obj1 = {
    a: 1,
}

let bar = foo.bind(obj1)
bar(2) // 2

let newB = new bar(3)
console.log(newB.a) // 3, 3

通过上面的结果,我们也可以看出来,通过bind绑定了的函数, 最后也被new关键字改变了this的指向, 因此, new关键调用的函数的优先级是要高于显示绑定的.

2.6.特殊情况

3.1 apply,call,bind

如果,我们在使用apply,call方法时,传入了undefined或者null时,它们在函数调用的时候会被忽略,函数实际采用了默认绑定规则.

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

let obj = {
    a: 1,
}
foo.call(obj) // {a:1}
foo.call(null) //undefined
foo.call(undefined) //undefined
3.2 箭头函数

箭头函数无法通过call,apply,bind,new等方式绑定this,它没有属于自己this,它的this来自于它的上层作用域.

3. 面试题

经过上面的学习,我们来一道面试题,检验一下吧.

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)() 
// person1.foo1() // 隐式调用 :person1
// person1.foo1.call(person2) // 显式调用优先级大于隐式调用: person2

// person1.foo2() // 箭头函数不绑定this,找上层作用域 window
// person1.foo2.call(person2) // 箭头函数无法通过call/apply/bind进行this绑定 window

// person1.foo3()() // 独立函数调用: window
// person1.foo3().call(person2) // 显式调用: person2
// person1.foo3.call(person2)() //独立函数调用 :window

// person1.foo4()() // 上层函数作用域 :person1
// person1.foo4().call(person2) // 上层函数作用域: person1
// person1.foo4.call(person2)() // 上层函数作用域: person2

作为一名前端开发者,关于this是绕不开的,我们必须要掌握它.不仅仅是学习以上关于this的内容,更重要的是懂得灵活的使用它.

感谢大家的耐心观看,希望能对你有帮助!