一篇文章,搞懂不同场景下的this指向(含面试题)

159 阅读9分钟

this是JavaScript中的一个关键字,其会在执行上下文中绑定一个对象,但是在不同执行条件下会绑定不同的对象,那么this在不同执行条件下的绑定规则是如何的呢?

一、为什么要使用this

与Java中的this不同(this通常用在类的方法中),JavaScript中的this更加灵活,在不同的位置会代表不同的含义,但是为什么要使用this呢,又带来了怎样的意义?

  • 如果没有this
var person = {
  name: 'zs',
  eat: function() {
    console.log(person.name + 'eat');
  },
  run: function() {
    console.log(person.name + 'run')
  }
}
person.eat()

这种方式情况下,如果修改了变量person的变量名,那么所有方法中的person都得跟着修改

  • 如果有this
var person = {
  name: 'zs',
  eat: function() {
    console.log(this.name + 'eat');
  },
  run: function() {
    console.log(this.name + 'run')
  }
}
person.eat()

这种方式情况下,即使修改了变量person的变量名也不会有影响

实际上,当通过person去调取其中的方法时,this的指向就是person对象。所以可以通过this来优化。

二、两个场景下的this指向

2-1、全局作用域下的this指向

  • 浏览器中:this指向window(GO)
  • Node中:this指向空对象{}

2-2、函数中的this指向

函数在执行时会创建上下文对象,上下文中包含了VO、scopeChain(作用域链:由当前上下文的VO及父级作用域的AO组成)、this

  • this是动态绑定的,在函数执行时才回去绑定,而不是编译时确定的,所以不同的调用方式,this的指向不同:
function foo() {
  console.log(this); // window
}
foo();
var obj = {
  name: 'zs',
  foo: foo
}
obj.foo(); // obj: {name: 'zs', foo: function}
foo.apply("abc"); // String("abc")

可见:

  • this的绑定与定义位置无关
  • this的绑定与调用方式及调用位置有关
  • this是在运行时被绑定的

三、this的绑定规则

this的绑定规则有4种:

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

3-1、默认绑定

当函数被独立调用时会采用默认绑定规则(可理解为:函数没有被绑定到某个对象上进行调用)

3-1-1、案例1

function foo() {
  console.log(this); // window
}
foo();

foo被独立调用,采用默认绑定规则,this此时指向window

3-1-2、案例2

function foo1() {
  console.log(this); // window
}
function foo2() {
  console.log(this);// window
  foo1();
}
function foo3(){
  console.log(this); // window
  foo2();
}
foo3();

所有函数均是被独立调用,采用默认绑定规则,this均指向window

3-1-3、案例3

var obj = {
  name: 'zs',
  foo: function() {
    console.log(this);// window
  }
}
var bar = obj.foo;
bar();

依旧属于独立函数调用,即使bar = obj.foo,但执行bar时是独立调用的。

3-1-4、案例4

function foo(fun) {
  fun();
}
function bar() {
  console.log(this);// window
}
foo(bar);

bar最终执行也是被独立调用的,所以this也是指向window

3-1-5、案例5

function foo(fun) {
  fun();
}
var obj = {
  name: 'zs',
  foo: function() {
    console.log(this);// window
  }
}
foo(obj.foo);

注意,虽然为foo传递的为obj.foo,但传递过去后,函数仍然是被独立调用的,所以this还是指向window

3-2、隐式绑定

函数的调用是某个对象发起的,这种方式this采用的绑定规则为隐式绑定

3-2-1、案例1

function foo() {
  console.log(this);
}
var obj = {
  name: 'zs',
  foo: function() {
    console.log(this);// obj: { name: 'zs', foo: [Function: foo] }
  }
}
obj.foo();

foo的调用是obj对象发起的,this指向该对象(this会隐式绑定到obj对象上)

3-2-2、案例2

var obj1 = {
  name: 'zs',
  foo: function() {
    console.log(this); // obj2: { name: 'lisi', bar: [Function: foo] }
  }
}

var obj2 = {
  name: 'lisi',
  bar: obj1.foo
}
obj2.bar();

虽然obj2的bar是obj1的foo(bar指向foo函数),但是bar函数的调用是obj2发起的,所以this会被隐式绑定为obj2对象

3-2-3、案例3

var obj = {
  name: 'zs',
  foo: function() {
    console.log(this);// window
  }
}
var bar = obj.foo;
bar();

注意:要看最后是以什么方式调用的,虽然bar赋值为obj对象的foo,但是bar函数是被独立调用的这种情况会造成隐式绑定丢失,会采用默认绑定规则。

3-3、显示绑定

隐式绑定的前提是,对象内部必须有一个属性且属性的值为函数的引用,通过这个引用才能间接地将this绑定到这个对象上。

那如果不希望在对象内部包含这个属性值为函数的引用的属性,又希望this指向这个对象,那怎么做呢?

JavaScript中每个函数都有callapply方法,这两个方法的参数可以传递为需要绑定的对象,这样就会将this绑定到这个对象上。(显示绑定即:明确地去绑定this指向的对象

3-3-1、案例1

function foo() {
  console.log(this);
}
foo.call({name: 'zs'}); // {name: 'zs'}
foo.call(123); // Number对象

3-3-2、案例2

function foo() {
  console.log(this);
}
var fn = foo.bind({name: 'zs'}); 
fn(); // {name: 'zs'}

3-4、new绑定

在使用new关键字时,会执行如下操作:

  1. 创建一个新对象
  2. 这个新对象会被执行Prototype连接
  3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤上完成
  4. 如果函数没有返回其他对象,则返回这个新对象

3-4-1、案例

function Person(name) {
  this.name = name;
  console.log(this); // Person {name: 'zs'}
}

const p = new Person('zs');

四、内置函数的this绑定

4-1、setTimeout

setTimeout对传入的函数在内部相当于独立调用的,所以this通常是window

setTimeout(function() {
  console.log(this); // window
})

4-2、forEach、map...

数组的forEach、map等方法对于传递进去的函数默认在内部也是进行独立调用的,所以this默认指向window

var arr = [1,2,3];
arr.forEach(function(i){
  console.log(this); // window
})
  • 可通过传递参数的方式改变this指向:
var arr = [1,2,3];
var obj = {name: 'zs'};
arr.forEach(function(i) {
  console.log(this); // { name: 'zs' }
}, obj)

4-3、事件绑定相关

4-3-1、传统事件绑定

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .div{
      width: 100px;
      height: 100px;
      background-color: salmon;
    }
  </style>
</head>
<body>
  <div class="div"></div>
</body>
<script>
  var div = document.querySelector('.div');
  div.onclick = function() {
    console.log(this); // <div class="div"></div>
  }
</script>
</html>
  • 相当于给div添加了个onclick属性,底层直接通过div.onclick()的方式去调用,进行了隐式绑定

4-3-2、addEventListener

div.addEventListener('click', function() {
  console.log(this); // <div class="div"></div>
})
  • 内部进行了类似于fn.apply(div)的显式绑定

五、this绑定规则的优先级

5-1、默认绑定规则优先级最低

5-2、显示绑定优先级高于隐式绑定优先级

5-2-1、案例1

var obj = {
  name: 'zs',
  foo: function() {
    console.log(this);
  }
}
obj.foo(); // { name: 'zs', foo: [Function: foo] }
obj.foo.call('123'); // [String: '123']

var bar = obj.foo.bind('abc');
bar(); // [String: 'abc']

5-2-2、案例2

function foo() {
  console.log(this);
}
var obj = {
  name: 'zs',
  foo: foo.bind('aaa')
}
obj.foo(); // [String: 'aaa']

5-3、new绑定优先级高于隐式绑定

5-3-1、案例

var obj = {
  name: 'zs',
  foo: function() {
    console.log(this);
  }
}
new obj.foo(); // foo {}

5-4、new绑定优先级高于显式绑定

new不能和apply、call一起使用,因为apply和call是主动调用函数

5-4-1、案例

function foo() {
  console.log(this);
}
var bar = foo.bind('aaa');
new bar(); // foo {}

\

总结: new 绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定

六、this绑定的特殊场景

6-1、忽略显示绑定

当显示绑定传入nullundefined时,会自动将this绑定为全局对象

function foo() {
  console.log(this);
}
var obj = {
  name: 'zs'
}
foo.apply(obj); // {name: 'zs'}
foo.apply(null); // window
foo.apply(undefined); // window

var bar = foo.bind(null);
bar(); // window
var bza = foo.bind(undefined);
bza(); // window

6-2、间接函数引用导致使用默认绑定规则

var foo1 = {
  name: 'foo1',
  bar: function() {
    console.log(this);
  }
};
var foo2 = {
  name: 'foo2'
};
(foo2.baz = foo1.bar)(); // window

6-3、箭头函数中的this

  • 箭头函数中不绑定this和arguments
  • 所以箭头函数不使用this的四种规则,而是根据外层作用域决定this
var foo = () => {
  console.log(this);
}

var obj = {
  foo: foo
}

obj.foo(); // window
foo.call('aaa'); // window

七、相关面试题

7-1、题目1

var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName;
  sss(); // window
  person.sayName(); // person
  (person.sayName)(); // person
  (b = person.sayName)(); // window
}

sayName();
  • sss()属于独立函数调用,this为window
  • 隐式绑定,this指向person
  • 隐式绑定,this指向person
  • 间接函数引用,导致this隐式绑定失效,采用默认绑定规则,所以输出window

7-2、题目2

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
person1.foo1.call(person2); // person2

person1.foo2(); // window
person1.foo2.call(person2); // window

person1.foo3()(); // window
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2

person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1
  • 隐式绑定,输出person1
  • 显示绑定 > 隐式绑定 所以输出person2
  • 箭头函数不绑定this,其this为上层作用域(全局作用域)的this,所以是window
  • 箭头函数不绑定this,显示绑定也无效,其this为上层作用域(全局作用域)的this,所以是window
  • person1的foo3返回一个函数,再次调用函数时相当于独立函数调用,所以是window
  • 虽然最先进行了显示绑定,但绑定的是foo3,真正执行的还是返回的函数,依旧是独立调用的,所以是window
  • 这次绑定的是返回的函数,所以this指向person2
  • 执行的是foo4返回的箭头函数,不绑定this,无论怎样执行,其this为上层作用域(foo4)的this,即:person1
  • 也是执行的foo4箭头函数,但此时其上层作用域foo4被显式绑定了person2对象,所以this指向person2
  • 要显示的给箭头函数绑定person2,但箭头函数不绑定this,还是看其上层作用域的this,所以是person1

7-3、题目3

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
person1.foo1.call(person2) // person2

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

person1.foo3()() // window
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1
  • 隐式绑定,this指向person1
  • 显示绑定 > 隐式绑定,this指向person2
  • 箭头函数不绑定this,this是其上层作用域的this,this指向person1
  • 箭头函数不绑定this,this是其上层作用域的this,this指向person1
  • 相当于独立调用返回的函数,采用默认绑定规则,this指向window
  • 虽然进行了显示绑定,但绑定的为foo3函数,最终执行的依旧是返回的函数,是独立调用的,所以是window
  • 为返回的函数显示绑定了person2,所以this指向person2
  • 执行的为箭头函数,不绑定this,this为上层作用域的this,person1
  • 上层作用域this显示绑定为person2,所以为person2
  • 为箭头函数显示绑定person2无效,this依旧是其上层作用域的this,person1

7-4、题目4

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()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj
  • 最终执行的返回的函数是被独立调用的,this指向window
  • 返回的函数的上层作用域被显示绑定为了person2但函数依旧是被独立调用的,所以this还是指向window
  • 执行的函数被显示绑定为了person2,this指向person2
  • 最终执行的是箭头函数,不绑定this,this为其上层作用域(foo2)的this,foo2被隐式绑定为了obj,所以this指向obj
  • 最终执行的是箭头函数,不绑定this,this为其上层作用域(foo2)的this,foo2被显示绑定了person2,所以this指向person2(虽然foo2也被隐式绑定了obj,但显示绑定 > 隐式绑定)
  • 最终执行的是箭头函数,不绑定this,this为其上层作用域(foo2)的this,foo2被隐式绑定为了obj,所以this指向obj