8
头图
大家好,我是归思君

一、引言

this可以说是前端开发中比较常见的一个关键字,由于其指向是在运行时才确定,所以大家在开发中判断其方向时也会很模糊,今天就把this的指向问题拆开了,揉碎了,好好讲一讲。
先来看一个场景,看看该处的 this 应该指向哪:首先在 request.js 中定义一个 getAction 函数

export function getAction(url,parameter) {
  let headers = {}
  if (this && this.realReferer && this.realReferer !== '') {
    headers.realReferer = window.location.origin + this.realReferer
  }
  return axios({
    url: url,
    method: 'get',
    params: parameter,
    headers
  });
}

然后在 test.vue 文件中引用该 getAction函数并使用

import { getAction } from '@api/request'
export default {
  methods: {
    getTableData() {
      getAction(this.url.requestUrl).then(res => {
        //1.这个时候getAction中的this将打印出什么
        //2.在该处打印this,会输出什么
        console.log(this);
      })
    },
  }
}

现在有两个问题:

  1. test.vue中调用 getAction()时,此时其内部,也就是request.js 中的 this 指向什么?
  2. getAction() then 后的箭头函数中的 this 指向什么?

思考一下能判断出这两个this的指向吗?先卖个关子,等咱们再讲完this的相关原理后再来解答这两个问题。这篇文章会从这几个方面讲解:

  • 什么是this,this和执行上下文的关系
  • this中的默认、隐式、显式和new的绑定规则
  • 箭头函数中的this指向问题

    二、什么是this?

    this 其实就是一个JavaScript语言中的一个关键字,  它的值是某个对象引用值,其指代的是当前执行上下文中的对象。那么为何需要this?我们先来看看一个例子:

    var testObj = {
      name: "testObj",
      print: function () {
          console.log(name)
      }
    } 
    var name = "global name";
    //想通过调用print()来调用testObj中的name
    testObj.print();//global name

    从结果可知,最后print() 输出"global name", 而不是 testObj 中的 name。为何出现这种情况?
    这是因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的:

  • testObj.print()执行时,这段代码的词法作用域是全局作用域,所以这个时候 js 引擎会去全局作用域中寻找 name,最后打印出“global name”。
  • 因此为了避免这种情况,JavaScript 设计者引入了 this 机制,来调用对象的内部属性,如下代码:

    var testObj = {
      name: "testObj",
      print: function () {
          console.log(this.name)
      }
    } 
    var name = "global name";
    testObj.print();//testObj

    最后就能够通过testObj.print() 来调用对象内部的属性了。
    不同于词法作用域链,this的指向是在运行时才能确定,实际上当执行上下文创建后,会生成一个this引用值,指向当前执行上下文对象,如下图所示:

    而 js 引擎在执行代码时的运行时上下文主要有三种:全局执行上下文、函数执行上下文和 eval 执行上下文。不同场景的this指向如下:

    //全局执行上下文,当前对象是window
    console.log(this);//window
    //函数执行上下文外部对象是全局对象,所以指向全局对象window
    function test(){
    console.log(this);//window
    }
    //函数执行上下文外部对象是test,因此指向当前的对象test
    var test = {
    test: function(){
      console.log(this);//test{...}对象
    }
    }
    //eval执行上下文,根据默认绑定规则,指向全局对象window
    eval(`console.log(this); `) //window

    正是因为this在运行中才得以确定其指向的上下文对象,所以为了规范和标准化this的指向方式,规定了一系列的绑定规则,来决定什么情况下this会绑定到哪个对象。下面就来聊聊this的绑定规则

    三、this 绑定规则

    this的绑定大致可以划分为默认、隐式、显式和new四种场景下的绑定规则:

    1. 默认绑定

    当函数被独立调用时,会将this绑定到全局对象。浏览器环境下是window, 严格模式是undefined主要有以下几种场景:

    //1. 定义在全局对象下的函数被独立调用
    function test(){
      console.log("global:", this);
    }
    test();//window
    //2. 定义在某个对象下的函数被独立调用
    var testObj = {
    test: function(){
      console.log("testObj:", this);
    }
    }
    var testfun = testObj.test;
    testfun();//window
    //3. 定义在某个函数下的函数被独立调用
    function testFun(fn){
      fn();
    }
    testFun(testObj.test); //window

    2. 隐式绑定

    当函数作为对象的方法被调用时,隐式绑定规则会将this绑定到调用该方法的对象,也就是"谁调用,就指向谁"。

    const obj = {
    name: 'innerObj',
    fn:function(){
      return this.name;
    }
    }
    //调用者是obj, this指向obj
    console.log(obj.fn());//innerObj
    
    const obj2 = {
    name: 'innerObj2',
    fn: function() {
      return obj.fn(); //此时是obj调用fn,所以此时this指向obj
    }
    }
    //调用者是obj, this指向obj
    console.log(obj2.fn())//innerObj

    现在我们可以回答引言中的问题1:在request.jsgetAction() 中this指向test.vue 中的全局vue对象,因为import {getAction} from '@api/request' 后,相当于vue对象调用了getAction(),因此其内部的this方向符合隐式绑定规则,所以指向调用者——test.vue 中的全局vue对象

    3. 显式绑定

    显式绑定主要指通过call、apply和bind方法可以显式地绑定this的值:

    call 方法

    语法: function.call(thisArg, arg1, arg2, ...) :
    参数: thisArg 表示 this 指向的上下文对象, arg1...argn  表示一系列参数
    功能:  无返回值立即调用 function 函数

    var test = {
    }
    function test2(){
      console.log(this);
    }
    //此时是独立函数,因此指向全局对象
    test2();//window
    //call显式绑定,将函数内部的this绑定至call中指定的引用对象
    test2.call(test);//test

    apply 方法

    语法: function.apply(thisArg, [argsArray]) :
    参数: thisArg 表示 this 指向的上下文对象, argsArray  表示参数数组
    功能: 没有返回值, 立即调用函数
    apply 和 call 的区别在于传参,call 传的是一系列参数,apply 传的是参数数组

    var test = {
    }
    function test2(name){
      console.log(this);
      console.log(name);
    }
    //此时是独立函数,因此指向全局对象
    test2();//window
    //call显式绑定,将函数内部的this绑定至call中指定的引用对象
    test2.apply(test, ["name"]);//test, name
    test2.call(test, "name"); //test

    bind 方法

    语法:function.bind(thisArg[, arg1[, arg2[, ...]]])
    参数: thisArg 表示 this 指向的上下文对象; arg1, arg2, ...表示 要传递给函数的参数。这些参数将按照顺序传递给函数,并在调用函数时作为函数参数使用
    功能: 返回原函数 function 的拷贝, 这个拷贝的 this 指向 thisArg

    var test = {
      fun: function(){
          console.log(this);
          var test = function(){
              console.log("test", this);
          }
          //1. 因为test.fun()在全局作用域中,所以属于独立函数调用,默认绑定规则指向全局对象
          test(); //window
          //2. bind方法会创建一个原函数的拷贝,并将拷贝中的this指向bind参数中的上下文对象
          var test2 = test.bind(this);
          test2();//test
          //3. apply方法会将this指向参数中的上下文,并立即执行函数
          test.apply(this);//test
          
      }
    }
    test.fun();

    4. new 绑定

    主要是在使用构造函数创建对象时,new 绑定规则会将 this 绑定到新创建的实例对象,因此构造函数中用 this 指向的属性值和参数也会被赋给实例对象:

    function funtest(){
      this.name = "funtest"
    }
    var tete = new funtest();
    console.log(tete.name); //"funtest"

    new 操作符实际上的操作步骤:

  • 创建一个新的对象 {}
  • 将构造函数中的 this 指向这个新创建的对象
  • 为这个新对象添加属性、方法等
  • 返回这个新对象

等价于如下代码:

var obj = {}
obj._proto_ = funtest.prototype
funtest.call(obj)

5. 绑定规则的优先级

上述的绑定规则有时会一起出现,因此需要判断不同规则之间的优先级,然后再来确定其 this 指向:
a. 首先是默认绑定和隐式绑定,执行以下代码:

function testFun(){
    console.log(this);
}
var testobj = {
    name:"testobj",
    fun:testFun
}
//若输出window,则证明优先级默认绑定大于隐式绑定;
//若输出testobj,则证明优先级隐式绑定大于默认绑定;
testobj.fun()//testobj

输出为 testobj 对象,所以隐式绑定的优先级高于默认绑定
b. 下面来看一下隐式绑定和显式绑定,执行以下代码:

function testFun(){
    console.log(this);
}
var testobj = {
    name:"testobj",
    fun:testFun
}
//若输出testobj,则证明优先级隐式绑定大于显式绑定
//若输出{}, 则证明优先级显式绑定大于隐式绑定
testobj.fun.call({})//{}

结果输出 { },说明显式绑定优先级大于隐式绑定
c. 显式绑定的 call, apply,bind 的优先级相同,与先后顺序有关,看以下代码:

function testFun(){
    console.log(this);
}
var testobj = {
    name:"testobj",
    fun:testFun
}
//若输出testobj,则证明优先级隐式绑定大于显式绑定
//若输出{}, 则证明优先级显式绑定大于隐式绑定
testobj.fun.call({})//{}
testobj.fun.call(testobj)

d. 最后来看看显式绑定和 new 绑定的优先级,执行以下代码:

function testFun(){
    console.log(this.name);
}
var testobj = {
    name:"testobj",
}
testFun.call(testobj);//testobj
//new 操作符创建了一个新的对象,并将this重新指向新对象
//覆盖了testFun原来绑定的testobj对象
var instance = new testFun();
console.log(instance.name) //undefined

从结果可知,new 绑定的优先级大于显式绑定
最后总结一下 this 绑定的 优先级是:

fn()(全局环境)(默认绑定)< obj.fn()(隐式绑定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(显式绑定)< new fn()

6. 绑定的丢失

有时 this 绑定可能会在某些情况下丢失,导致 this 值的指向变得不确定:

赋值给变量后调用

当使用一个变量作为函数的引用值,并使用变量名执行函数时,会发生绑定丢失,此时 this 会默认绑定到全局对象或变成 undefined(严格模式下)

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

var lostfun = lostObj.fun;
lostfun();//window
lostObj.fun();//lostObj

从结果发现,lostfun 虽然指向对象中的方法,但是在调用时发生了 this 绑定丢失。因为当赋值给变量时,对象中的 fun就失去了与对象的关联,变成了一个独立函数,所以此时执行 lostfun也就相当于执行独立函数,默认绑定到全局对象。
那如果通过对象来执行呢?看如下代码:

var lostObj = {
  name: "lostObj",
  fun: function(){
    console.log(this);
    }
}
var lostObj2 = {
  name: "lostObj2",
  fun: lostObj.fun
}
var lostfun = lostObj.fun;
lostfun();//window
lostObj.fun();//lostObj
lostObj2.fun();//lostObj2

同样,一旦将方法赋值给变量后,其内部与对象的关联就此丢失,默认绑定到全局对象。但是将变量放到对象中后,就与该对象进行关联。所以该方法执行后的 this 执行了 lostObj2对象。

函数作为参数传递

将函数作为参数传递到新函数中,并在新函数中执行该参数函数:

var lostObj3 = {
  name: "lostObj3",
  fun: function(){
    console.log(this.name);
    }
}
var name = "global"
function doFun(fn){
  fn();
}
doFun(lostObj3.fun);//global

从结果可知,当函数作为参数传递后,其形参 fn 被赋值为 lostObj3.fun。实际上也相当于赋值给变量后调用这种情况,而且 doFun()作为独立函数调用,所以其 this 也就指向全局对象了

回调函数

如果将对象方法作为回调函数传递给其他函数,this 绑定也可能丢失

var lostObj4 = {
  name: 'lostObj4',
  fun: function() {
    setTimeout(function() {
      console.log(`Hello, ${this.name}!`);
    });
  }
};
lostObj4.fun(); // Hello, undefined!

因为 setTimeout 的回调函数最后会以普通函数的形式调用,所以其 this 指向的是全局对象,所以即便是 lostObj4调用 fun(),最后其内部的 this 仍然会丢失。

嵌套函数

当某个函数是嵌套在另一个函数内部的函数时,内部函数中的 this 绑定会丢失,并且会绑定到全局对象或 undefined(严格模式下):

var lostObj5 = {
  name: 'lostObj5',
  fun: function() {
    function innerFun() {
      console.log(`Hello, ${this.name}!`);
    };
    innerFun();
  }
};
lostObj5.fun();// Hello, undefined!

从结果可以发现,嵌套函数 innerFun()中的 this 此时是指向全局环境。所以从这个案例可以说明作用域链和 this 没有关系,作用域链不影响 this 的绑定。
原因是当innerFun()被调用时,是作为普通函数调用,不像 fun()属于对象 lostObj5的内部方法而调用,因此最后其内部的 this 指向全局对象。
其实 this 丢失可以通过箭头函数来解决,下面就来聊聊箭头函数

四、箭头函数中的 this

箭头函数是 ES6 增加的一种编写函数的方法,它用简洁的方式来表达函数
语法:()=>{}
参数:(): 函数的参数,{}: 函数的执行体

1. 箭头函数中的 this 指向

箭头函数中的this是在定义时确定的,它是继承自外层词法作用域。而不是在运行时才确定,如以下代码:

var testObj2 = {
    name: "testObj2", 
    fun: function(){
        setTimeout(()=>{
            console.log(this);
        })
    }
}
var testObj3 = {
    name: "testObj3", 
    fun: function(){
        setTimeout(function(){
            console.log(this);
        })
    }
}
//即使独立调用函数,箭头函数内的this指向是在定义时就已经确定
testObj2.fun();//testObj
testObj3.fun();//window

实际上箭头函数中没有 this 绑定,它是继承自外层作用域的 this 值。因此在许多情况下,箭头函数能解决 this 在运行时函数的绑定问题。

2. 箭头函数与普通函数中的 this 差异

从 上面的例子可以看出箭头函数和普通函数在 this 的处理上存在很大的差异,主要有:

this 绑定方式

普通函数的 this 是在运行时确定的;箭头函数的 this 值是函数定义好后就已经确定,它继承自包含箭头函数的外层作用域

作用域

普通函数是具有动态作用域,其 this 值在运行时基于函数的调用方式动态确定。箭头函数具有词法作用域,其 this 值在定义时就已经确定,并继承外部作用域

绑定 this 的对象

普通函数中 this 可以通过函数的调用方式(如对象方法、构造函数、函数调用等)来绑定到不同的对象,而箭头函数没有自己的 this 绑定;箭头函数没有自己的 this 绑定,它只能继承外部作用域的 this 值,无法在运行时改变绑定对象,而且也无法通过显式绑定来改变 this 的指向。

var testObj4 = {
  arrowFun: ()=>{
    console.log(this);
  },
  normalFun: function(){
    console.log(this);
  }
}
//此时箭头函数的this继承全局上下文的this,显式绑定无法修改箭头函数中的this值
testObj4.arrowFun();//window
testObj4.arrowFun.apply({});//window
testObj4.normalFun();//testObj4
testObj4.normalFun.apply({});//{}

下面我们就可以解答引言中的问题 2 了。箭头函数中的 this 指向其上层的作用域,也就是 getAction() 中的 this 值,而从隐式绑定调用规则,当前是 vue 实例调用 getTableData()然后再调用 getAction(),因此 this 值指向当前 vue 实例。

五、 this 中的面试题

手写实现一个 bind 函数

通过分析 bind 函数的语法和参数来:function.bind(thisArg[, arg1[, arg2[, ...]]])

  • 返回值是一个函数
  • 参数 thisArg 指向

我们暂时不考虑原型问题,实现如下代码:

Function.prototype.mybind = function (thisArg) {
  //1.隐式绑定,当前的this指向目标函数
  var targetFn = this;
  //将参数列表转换为数组,并删除第一个参数
  var args = Array.prototype.slice.call(arguments, 1);
  //2.返回值一个函数
  return function bound() {
     var innerArgs = Array.prototype.slice.call(arguments);
     var finalArgs = args.concat(innerArgs);
    //解决返回函数使用new后,绑定this忽略问题
     var _this = targetFn instanceof this ? this: thisArg;
     return targetFn.apply(thisArg, finalArgs)
   }
 }
}

总结

文章回顾 this 的概念和 this 指向的判断绑定规则,

  1. 首先是绑定规则:
  • 独立函数调用执行时,使用默认绑定规则,this 指向 window
  • 当函数作为对象方法被调用,使用隐式绑定规则,this 指向这个对象
  • 当函数作为构造方法时,使用 new 绑定规则,this 指向返回的对象
  • apply/call/bind 要注意参数的传递和返回值不同
  • 箭头函数要看该箭头函数在哪个作用域下,this 就指向谁
  1. 绑定规则的优先级:

    fn()(全局环境)(默认绑定)< obj.fn()(隐式绑定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(显式绑定)< new fn()
  2. 此外要注意绑定失效的情况,善用箭头函数来保证 this 的指向稳定

归思君
1.2k 声望209 粉丝

阿里云社区专家博主,华为云社区云享专家,一个会点前端的java工程师。