this指向总结

326 阅读5分钟

this的指向问题是我们工作中经常用到,面试中也会频繁问到的一个基础必考题。

这篇文章来对this的指向做一个全面的总结。若有不对的地方,欢迎指正。

一、全局执行

1. 非严格模式

浏览器下this指向window

console.log(this); // window

2. 严格模式

严格模式下this指向undefined

"use strict";
console.log(this); // undefined

当然,面试官可能会这样迷惑一下你:

var a = 1
function fn() {
  var a = 2
  console.log(this.a) // console what ?
}
fn()

// -------------- 变 -----------------
// 1. 把最外层 var a = 1 -> let a = 1,输出结果是?

// -------------- 变 -----------------
var b = 1
function outer () {
  var b = 2
  function inner () { 
    console.log(this.b) // console what ?
  }
  inner()
}

outer()

// --------------- 变 -----------------
const obj = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

obj.fn() // console what ?
const f = obj.fn
f() // console what ?

二、作为对象方法执行

  1. 调用方式为 对象.方法() 时,该方法中的this指向这个对象。即谁调用指向谁。
var obj = {
    name:'aaa',
    hello3(){
        console.log('我是hello3 this:',this); // obj
    }
}

obj.hello3(); // obj
  1. 若将对象的方法赋值给一个变量,然后使用变量调用该方法时,this指向window
var obj = {
    name:'aaa',
    hello3(){
        console.log('我是hello3 this:',this); // obj
    }
}

var func = obj.hello3;

func(); // window

练习题

function fn () {
 console.log(this.a)
}

const obj = {
 a: 1
}

obj.fn = fn
obj.fn() // console what ?
function fn () {
 console.log(this.a)
}

const obj1 = {
 a: 1,
 fn
}

const obj2 = {
 a: 2,
 obj1
}

obj2.obj1.fn() // console what ?

有时面试官一般问的是一些边界 case,比如隐式绑定失效(列举部分):

// 第一种 是前面提过的情况
const obj1 = {
 a: 1,
 fn: function() {
   console.log(this.a)
 }
}

const fn1 = obj1.fn // 将引用给了 fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境

// 第二种 setTimeout
setTimeout(obj1.fn, 1000) // 这里执行的环境同样是全局

// 第三种 函数作为参数传递
function run(fn) {
 fn()
}
run(obj1.fn) // 这里传进去的是一个引用

// 第四种 一般匿名函数也是会指向全局的
var name = 'The Window';
var obj = {
   name: 'My obj',
   getName: function() {
       return function() { // 这是一个匿名函数
           console.log(this.name)
       };
   }
}
obj.getName()()

// 第五种 函数赋值也会改变 this 指向,下边练习题会有 case,react 中事件处理函数为啥要 bind 一下的原因
// 第六种 IIFE

三、定时器内部执行

定时器setTimeOutsetInterVal内部的this都指向window

setTimeout(()=>{
    console.log(this); // window
},2000)

四、构造函数内部执行

构造函数的调用要使用new,所以,构造函数中的this,指向该构造函数实例出的对象。

function Person(name){
    this.name = name
    console.log(this)
}

var p1 = new Person('yunxi');  // Person

若构造函数return了一个对象,则this指向这个对象。

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

const f = new foo(2)
f.a // console what?

// ------------------------- 变 ---------------------------
function bar(a) {
  this.a = a
  return {
    a: 100
  }
}
const b = new bar(3)
b.a // console what ?

五、箭头函数

箭头函数的this是在定义时根据上下文函数决定的。

1.当箭头函数的上下文没有函数环境时,即直接调用时,this指向window/undefined

var obj = {
    a: 100,
    hello5:()=>{
        console.log(this.a)
    }
}
obj.hello5(); // undefined

2.当箭头函数有上下文函数环境时,this指向上下文函数的this。(因为是obj2调用的hello6,所以箭头函数的this,也就是hello6this)。

var obj2 = {
    a: 100,
    hello6:function(){
        var a = 200;
        setTimeOut(()=>{
            console.log(this.a)
        },2000)
    }
}
obj2.hello6(); // 100

箭头函数本身是没有 this 的,继承的是外层的

function fn() {
  return {
    b: () => {
      console.log(this)
    }
  }
}

fn().b() // console what?
fn().b.bind(1)() // console what?
fn.bind(2)().b.bind(3)() // console what?

六、call、apply、bind

使用call、apply、bind可以修改this的指向。(箭头函数的this不会被修改)

1. call 的用法

(1)call的第一个参数就是this,若第一个参数传的是null,则不会改变this指向。

(2)call 可以传多个参数

function fn(){
    console.log(this)
}
var obj = {
    a: 100
}
fn.call(obj); // {a:100}
fn.call(obj,[1,2],true,{},false)

2. apply 的用法

apply只接受2个参数:

(1)第一个参数就是this,若第一个参数传的是null,则不会改变this指向。

(2)第二个参数必须是数组。

function fn(){
    console.log(this)
}
var obj = {
    a: 100
}
fn.apply(obj); // {a:100}
fn.apply(obj,[1,2,4])

3.bind 的用法

(1)bind不会立即执行。

(2)bind的第一个参数就是this,若第一个参数传的是null,则不会改变this指向。

(3)多bind时,只和第一次bind的值有关。

(4)bind 可以传多个参数。

function fn(){
    console.log(this)
}
var obj = {
    a: 100
}
fn.bind(obj)(); // {a:100}
fn.bind(1).bind(2)(); // 1
总结:

相同点
bind、call、apply都可以改变this的指向,其中接受的第一个参数就是this。若第一个参数传的null,都不会改变this的指向。

不同点
1.bind不会立即执行,而applycall会立即执行。
2.applycall的区别是: apply只能接受两个参数并且第二个参数必须是数组,而call可以接受很多参数并且不限制类型。

七、优先级

这上面的各种方式一定是有先后顺序的,同时作用于一个函数的时候,以哪一个为准呢?这取决于优先级

// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
  console.log(this)
}

const obj = {
  fn
}

obj.fn() // what ?

// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // what ?

// new vs 显式 -> 结论:new > 显式
function foo (a) {
    this.a = a
}

const obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // what ?

// new
var baz = new bar(3)
console.log( obj1.a ) // what ?
console.log( baz.a ) // what ?

// 箭头函数没有 this,比较没有意义

优先级「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」

八、练习题

// 1.
function foo() {
  console.log( this.a ) // console what
}
var a = 2;
(function(){
  "use strict" // 迷惑大家的
  foo();
})();

// 2.
var name="the window"

var object={
  name:"My Object", 
  getName: function(){ 
    return this.name
  } 
}
object.getName() // console what ?
(object.getName)() // console what ?
(object.getName = object.getName)() // console what ?
(object.getName, object.getName)() // console what ?

// 3.
var x = 3
var obj3 = {
  x: 1,
  getX: function() {
    var x = 5
    return function() {
      return this.x
    }(); // ⚠️
  }
}
console.log(obj3.getX()) // console what?

// 4. 
function a(x){
  this.x = x
  return this
}
var x = a(5) // 替换为 let 再试试
var y = a(6) // 替换为 let 再试试 // 再换回 var,但是去掉 y 的情况,再试试

console.log(x.x) // console what ?
console.log(y.x) // console what ?

// 等价于
window.x = 5;
window.x = window;

window.x = 6;
window.y = window;

console.log(x.x) // void 0 其实执行的是 Number(6).x
console.log(y.x) // 6