louzhedong / blog

前端基础,深入以及算法数据结构

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JS中的this

louzhedong opened this issue · comments

JS中的this

this关键字是JavaScript中最复杂的机制之一

它提供了一种优雅的方式来隐式传递一个对象引用

即使是非常有经验的前端程序员,也可能说不清它到底指向什么

本文的概念都基于非严格模式,严格模式中某些场景会有些许出入

this是什么

首先来理解一下this的概念:

this在函数运行时绑定,而不是在编写时,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

再深入一点:

当一个函数被调用时,会创建一个执行上下文,执行上下文包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this也是这个执行上下文的一个属性,会在函数执行的过程中用到

调用位置

要理解this的绑定过程,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是被声明的位置)

this绑定规则

找到函数的调用位置之后,我们就可以去探索this的指向了

this的指向可以通过以下四条规则去判断,分别为

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new 绑定
1.默认绑定

默认绑定发生在独立调用函数时。当其他规则无法应用时,默认应用此规则

查看下列示例:

function hello(){
  console.log('hello,' + this.name);
}
var name = 'Mike';
hello();  // hello,Mike

在这个示例中,hello函数是一个独立执行的函数,没有任何对象去调用它或者被它绑定。此时函数的this指向全局对象,所以能够找到全局对象中的name变量

2.隐式绑定

隐式绑定需要考虑调用位置是否有上下文对象,即调用函数是否被某个对象拥有或者包含

查看下列示例:

function hello(){
  console.log('hello,' + this.name);
}

var person = {
  name: 'Mike',
  hello: hello
}

// 定义全局中的name
var name = 'John'

person.hello(); // hello,Mike

这里的调用位置会使用person上下文来引用函数,因此看起来this被绑定到了person对象上

隐式丢失

初学者经常会遇到的一个问题就是隐式绑定丢失绑定对象,即应用了默认绑定,从而把this绑定到全局对象上

查看下列示例:

function hello(){
  console.log('hello,' + this.name);
}

var person = {
  name: 'Mike',
  hello: hello
}

// 定义全局中的name
var name = 'John'

// 函数别名
var myHello = person.hello; 

myHello(); // hello,John

发现此时this没有指向person,而是指向了全局对象

那为什么会发生这种情况呢?

在执行var myHello = person.hello; 这行代码时,myHello实际上是hello函数的一个引用,即他们指向同一个内存堆中的函数。调用myhello就相当于直接单独调用了hello函数,因此会使用前面所讲的默认引用规则

另一种可能会迷惑的地方是将函数当做参数进行传递

查看下列示例:

function hello(){
  console.log('hello,' + this.name);
}

var person = {
  name: 'Mike',
  hello: hello
}

// 定义全局中的name
var name = 'John'

function triggerHello(fn) {
  // fn 为hello函数的引用
  fn();
}

triggerHello(person.hello); // hello,John

出现这个情况的原因其实和上个例子一个,函数在调用时应用了默认绑定的规则

3.显示绑定

在JS中,我们可以通过apply和call来将函数绑定到某个对象中,此时函数中的this就会指向这个对象

查看下列示例:

function hello(){
  console.log('hello,' + this.name);
}

var person = {
  name: 'Mike'
}

hello.call(person); // hello,Mike

通过call函数,我们在调用hello函数时强制把它的this绑定到person上

另一种显示绑定的方式是通过bind,bind函数会返回一个新函数,并将指定给bind的参数设置为this的上下文并调用原始函数

查看下列示例:

function hello(){
  console.log('hello,' + this.name);
}

var person = {
  name: 'Mike'
}

var bindHello = hello.bind(person);
bindHello(); // hello,Mike
4.new 绑定

使用new来调用函数时,会进行this的绑定,具体在使用new时发生了什么可以查看 new原理及实现 这篇文章

查看下列示例:

function hello(name) {
  this.name = name;
}

var newHello = new hello("Mike");
console.log(newHello.name);  // Mike

规则的优先级

现在我们有了四种绑定规则,那么它们的优先级是怎么样的呢?

这里直接给出结论

  1. 函数是否通过new绑定,如果是的话this绑定的就是新创建的对象 var newHello = new hello()
  2. 函数是否通过call,apply,bind绑定,如果是的话this绑定的是指定的对象 var newHello = hello.call(person)
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话this绑定的是那个上下文对象 person.hello()
  4. 如果都不上,使用默认绑定,即在非严格模式下绑定到全局对象 hello()

箭头函数

上述四种规则应用于大部分场景,但箭头函数不包含其中。箭头函数根据外层作用域来决定this

小结

this确实是JavaScript这门语言中比较难理解但又非常重要的一个点,单单一篇文章可能也无法完全帮助我们理清楚this的问题。但对于大部分的问题,只要我们用这四种规则去匹配它们,基本上就能看透问题的本质。