JS中的this指向问题|青训营笔记

97 阅读9分钟

这是我参与「第四届青训营 」笔记创作活动的的第5天。为了应对面试八股,也顺便梳理一下基础知识,做出一点关于JS中this指向方面的个人总结心得。

反思

经常会在笔试中遇到闭包、普通函数、箭头函数中this指向的问题,包括后面改变this指向的apply和call,各种条件容易使人迷惑,老司机也不得不小心呐,所以总结回顾了下面this指针指向的问题。

执行上下文

执行上下文中,包含了变量环境,词法环境,外部环境,this

this 是和执行上下文绑定的,也就是说每个执行上下文都有一个this。

执行上下文的概念

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念,当 JavaSciprt 代码在运行时,其运行在执行上下文中。JavaScript 中有三种执行上下文类型:

  • 全局执行上下文: 任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
  • 函数执行上下文: 当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
  • eval函数执行上下文: 执行在eval函数中的代码会有属于它自己的执行上下文。

由于eval函数执行上下文用的不多,所以这里只介绍全局执行上下文和函数执行上下文:

  • 在全局执行上下文中,this 是指向 window 对象的;
  • 在函数执行上下文中,默认情况下调用一个函数,其执行上下文的 this 也是指向 window 的。

执行上下文栈

浏览器中的JS解释器为单线程,也就是说浏览器同⼀时间只能做⼀个事情。代码中只有⼀个全局执⾏上下⽂和⽆数个函数执⾏上下⽂,这些组成了执⾏上下⽂栈(Execution Stack)。 ⼀个函数的执⾏上下⽂,在函数执⾏完毕后,会被移出执⾏上下⽂栈。

比如:

function c(){
    console.log('ok');
}
function a(){
  function b(){
    c();
  }
  b();
}
a();

执行上下文的栈是这样:

函数的this指向

this是JS中的关键字,多数情况下this指向调用它的对象

首先 this 指向的应该是一个对象(函数执行上下文对象)。其次,这个对象指向的是调用它的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。

其实,this 是在函数被调用时确定的,它的指向取决于函数调用的地方,而不是它被声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this 就是这个记录的一个属性,它会在函数执行的过程中被用到。

this 在函数的指向绑定形式有四种:默认绑定、隐式绑定、显式绑定、new绑定

默认绑定(全局情况)

函数在浏览器全局环境中直接使用不带任何修饰的函数引用进行调用,非严格模式下 this 指向 window;在 use strict 指明严格模式的情况下就是 undefined(严格模式不允许 this 指向全局对象)。

function fn1 () {
    console.log(this)
}
function fn2 () {
    'use strict'
    console.log(this)
}
fn1() // window
fn2() // undefined
var num = 1
var foo = {
    num: 10,
    fn: function() {
       console.log(this)
       console.log(this.num)
    }
}
var fn1 = foo.fn
fn1()//window,1

输出window和1这里 this 仍然指向 window。虽然 fn 函数在 foo 对象中作为方法被引用,但是在赋值给 fn1 之后,fn1 的执行仍然是在 window 全局环境中。因此输出 window1,它们相当于:

console.log(window)
console.log(window.num)

隐式绑定(上下文对象)

如果函数在某个上下文对象中调用,那么 this 绑定的是那个上下文对象。

var a = 'hello'var obj = {
    a: 'world',
    fn: function() {
        console.log(this.a)
    }
}
​
obj.fn()

最后输出为world这里fn方法是作为对象的属性调用的,此时fn方法执行时,this会指向obj对象。也就是说,此时this指向的是调用这个方法的对象。

嵌套多层对象时

const obj1 = {
    text: 1,
    fn: function() {
        return this.text
    }
}
const obj2 = {
    text: 2,
    fn: function() {
        return obj1.fn()
    }
}
const obj3 = {
    text: 3,
    fn: function() {
        var fn = obj1.fn
        return fn()
    }
}
console.log(obj1.fn())
console.log(obj2.fn())
console.log(obj3.fn())

第一个 console 输出 1 ,这时 this 指向了调用 fn 方法的对象 obj1,所以会输出obj1中的属性 text 的值 1

第二个 console 输出 1,这里调用了 obj2.fn(),最终还是调用 o1.fn(),因此仍然会输出 1

第二个 console 输出 undefined,在进行 var fn = o1.fn 赋值之后,是直接调用的,因此这里的 this 指向 window,答案是 undefined

使最后的console.log(obj2.fn())输出2:

const obj1 = {
    text: 1,
    fn: function() {
        return this.text
    }
}
const obj2 = {
    text: 2,
    fn: o1.fn
}
console.log(obj2.fn())

虽然调用了obj1.fn,但是最后还是调用了obj2,相当于提前进行了赋值

显示绑定(apply,bind,call)

显式绑定是指需要引用一个对象时进行强制绑定调用,显式绑定可以使用apply、call、bind方法来绑定this值,使其指向我们指定的对象。

call、apply 和 bind三个方法都可以改变函数 this 指向,但是 call 和 apply 是直接进行函数调用;bind 不会执行函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this 指向,需要我们手动调用。call 和 apply 的区别: call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。

三个方法的使用:

const target = {}
fn.call(target, 'arg1', 'arg2')
fn.apply(target, ['arg1', 'arg2'])
fn.bind(target, 'arg1', 'arg2')()

如果把 null 或 undefined 作为 this 的绑定对象传入 call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

var a = 'hello'

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

fn.call(null)   

输出的是"hello"因为传入了null使用的是默认的绑定规则,(this.a)相当于window.a所以输出hello

const foo = {
    name: 'hello',
    logName: function() {
        console.log(this.name)
    }
}
const bar = {
    name: 'world'
}
console.log(foo.logName.call(bar))

最后输出的是"hello",call中把bar传入,相当于this=bar,bar.name为world

多次bind

let a = {}
let fn = function () { 
  console.log(this) 
}
fn.bind().bind(a)() 

输出为window而不是a

其中fn.bind().bind(a)相当于

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
  return function() {
    return fn.apply()
  }.apply(a)
}
fn2()

不管给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

new绑定(构造函数)

函数作为构造函数使用 new 调用时, this 绑定的是新创建的构造函数的实例:

function Person(name,age){
  this.name = name;
  this.age = age;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
var person = new Person("CUGGZ",18);
console.log(person.name); // CUGGZ
console.log(person.age);  // 18
person.say(); // CUGGZ:18

this 就指向了构造函数 Person 的新对象person,所以使用 this 可以获取到 person 对象的属性和方法。

在使用new调用构造函数的时候,执行这些操作:

  1. 创建一个新对象;
  2. 构造函数的 prototype 被赋值给这个新对象的 proto
  3. 将新对象赋给当前的 this;
  4. 执行构造函数;

this优先级

使用this的优先级:new绑定>显式绑定>隐式绑定>默认绑定

function foo (a) {
    console.log(this.a)
}
const obj1 = {
    a: 1,
    foo: foo
}
const obj2 = {
    a: 2,
    foo: foo
}
obj1.foo.call(obj2)//2
obj2.foo.call(obj1)//1

根据显式绑定>隐式绑定,所以指向的this分别为call中传入的obj2和obj1。

function foo (a) {
    this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)//2

先显式绑定了obj1,然后bar(2)给它赋值。

this指向的整个流程图

特殊的this指向

箭头函数

箭头函数根据声明的位置来指向this

const foo = {  
    fn: function () {  
        setTimeout(function() {  
            console.log(this)
        })
    }  
}  
console.log(foo.fn())
// window

这里,this 出现在 setTimeout() 中的回调函数里,因此 this 指向 window 对象。如果需要 this 指向 foo 这个 object 对象,可以使用箭头函数解决:

const foo = {  
    fn: function () {  
        setTimeout(() => {  
            console.log(this)
        })
    }  
} 
console.log(foo.fn())
// {fn: ƒ}

因为setTimeout在fn中声明,所以this指向fn

由于箭头函数没有 this ,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 thiswindow

需要注意,箭头函数的 this 绑定是无法通过 call、apply、bind 方法修改的。且因为箭头函数没有构造函数 constructor,所以也不可以使用 new 调用,即不能作为构造函数,否则会报错。

数组方法

在数组arr的forEach回调函数中的this

var obj = {
  arr: [1]
}
obj.arr.forEach(function() {
  console.log(this)
}) //window

this指向window

forEach()语法:

array.forEach(function(currentValue, index, arr), thisValue)

1)function(currentValue, index, arr) :必需。 数组中每个元素需要调用的函数。

  • currentValue:必需,当前元素
  • index:可选,当前元素的索引值
  • arr:可选,当前元素所属的数组对象

2)thisValue:可选,传递给函数的值一般用 "this" 值。如果这个参数为空, "undefined" 会传递给 "this" 值。

可以看到,forEach方法有两个参数,第一个是回调函数,第二个是 this 指向的对象,这里只传入了回调函数,第二个参数没有传入,默认为 undefined,所以会输出全局对象。

除了forEach方法,需要传入 this 指向的函数还有:every()、find()、findIndex()、map()、some(),在使用的时候需要注意。

立即执行函数

立即执行函数就是定义后立刻调用的匿名函数:

var name = 'hello'
var obj = {
  name: 'world',
  sayHello: function() {
    console.log(this.name)
  },
  hello: function() {
    (function(cb) {
      cb()
    })(this.sayHello)
  }
}
obj.hello() // hello

执行结果是 hello,是 window.name 的值。立即执行函数作为一个匿名函数,通常就是直接调用,而不会通过属性访问器(obj.fn)的形式来给它指定一个所在对象,所以它的 this 是确定的,就是默认的全局对象 window。

setTimeOut和setInterval

var name = 'hello'
var obj = {
  name: 'world',
  hello: function() {
    setTimeout(function() {
      console.log(this.name)
    })
  }
}
obj.hello() // hello

this.name 是在 obj.hello () 里被调用的,结果却输出了 window.name。其实,延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window。