this 全面解析

571 阅读10分钟

写在前面

this 指向可能是新手经常会碰到的问题, 记得之前在一些博客上面看到这么一句话

ES5 function里面的this谁调用它就指向谁,ES6箭头函数的this是在哪里定义就指向哪里

暂且先不讨论这句话的正确与否,先往下面看

1.普通函数和箭头函数的this差别

普通函数:

function say(){
    console.log(this.a)
}
var a='window'
var obj={
    a:'inside obj',
    say:say
}
obj.say() //inside obj 

箭头函数:

var say=()=>{
    console.log(this.a)
}
var a='window'
var obj={
    a:'inside obj',
    say:say
}
obj.say() // window

普通函数下:因为obj.say()是obj去调用say方法,所以say里面的this绑定在obj

箭头函数下: 因为say方法是定义在window全局环境,因此它的this永远指向window,值得一提的是,箭头函数的this无法通过call bind apply方法改变this的绑定对象,一经定义,无法改变

我们可以做个尝试:

var say=()=>{
    console.log(this.a)
}
var a='window'
var obj={
    a:'inside obj',
    say:say
}
say.call(obj) // window

发现他还是指向了window

另外还有一种情况,比较绕:

function say(){
    return ()=>{
        console.log(this.a)
    }
}
var a = 'a in window'
var obj1 ={
    a:'a in obj1'
}
var obj2 ={
    a:'a in obj2'
}
var d = say.call(obj1)
d.call(obj2) // 'a in obj1' 

发现打印出的 'a in obj1',说明this绑定在obj1上面。

一般情况下, 内部创建的箭头函数会捕获调用时 say() 的 this,也就是window,这时如果我们调用 var s= say(); s() ;就会打印出 'a in window'

但是当say函数的this绑定在obj1上时,箭头函数的this也会跟着绑定在obj1。

总之牢记一句话:箭头函数会捕获其所在的上下文的this值,作为自己的this值,无法改变指向

2.隐式丢失和隐式赋值

  • 隐式丢失就是this绑定丢失,多见于赋值操作
  • 隐式赋值就是this绑定默认的全局对象window或者undefiend,取决于是否严格模式

我们把上述普通函数例子进行一个改变:

function say(fn){
   fn() //把say函数改成通过接受一个函数名称,并且在say函数体内执行这个传入进来的函数
}
function getWord(){
     console.log(this.a) //定义一个打印出this.a的函数
}
var a='window'
var obj={
    a:'inside obj',
    getWord:getWord //把getWord作为obj的一个属性
}

say(obj.getWord) //window

obj.getWord作为函数名放到say方法里面执行,其实say就是相当于回掉函数,这时候发现他居然指向了window?

别急,我们在看一个比较简单例子:

var a='a in window'
var obj={
    a:'a in obj',
    say:function(){
        console.log(this.a)
    }
}

var s = obj.say //赋值操作的时候,this绑定丢失
s() // 'a in window' a指向了window,因为这是使用了默认this绑定,也就是隐式赋值

this在它的隐式绑定函数丢失了绑定对象,也就是this隐式丢失,这时候它会应用默认绑定,也就是隐式赋值,从而把this绑定在window全局对象或者undefiend上,这取决于是否是严格模式下

在函数say(obj.getWord)的时候,obj.getWord相当进行了个赋值操作,因此丢失了this的绑定 相当于

function say(fn){
    fn = obj.getWord //这么看是不是瞬间就明白了
}

这种隐式丢失多见于回调函数里面,比如 setTimeout(obj.getWord,1000) 也会造成隐式丢失问题

3.显示绑定

分为两种情况:

  1. 硬绑定
  2. api上下文的绑定

1,硬绑定

其实说白了就是调用apply call方法对this进行指向绑定, 比如上述的例子,只要我们把say函数方法改成

function say(fn){
    fn.call(obj) //显示绑定,把this绑定在obj
}
function getWord(){
     console.log(this.a) //定义一个打印出this.a的函数
}
var obj ={
    a:'a in obj',
    getWord:getWord
}

say(obj.getWord) //  理所当然打印出 'a in obj'

另外一提,就是通过call apply 在函数题内部改变this绑定的函数方法,在调用时不能够再次改变

看例子:

function say() {
  console.log(this.a)
}
function doSay() {
  say.call(obj)
}
var a = 'window'
var obj = { a: 'a inside obj' }

doSay.call(window) // 还是打印出 'a inside obj' ,并没有指向window全局环境

2,API的上下文

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this。 ---《你不知道的JAvascript》

比如forEach这个数组方法,查了下mdn,他有两个参数:

  • 1,callback:forEach的回调函数,也就是forEach(function(item,index)...)
  • 2,thisArg:可选参数。当执行回调函数时用作 this 的值(参考对象)。

看下例子:

function say(item){
console.log(item,this.a)
}
var obj={
a:'this a is inside obj'
};
[4,2,21].forEach(say,obj) 
//4 "this a is inside obj"
//2 "this a is inside obj"
//21 "this a is inside obj"

这些内置函数其实就是通过callapply实现了显示绑定,我们可以自己实现一个


Array.prototype.myForEach=function(fn,thisArg){
  var arr = this // 这个是myForEach的this绑定在调用他的数组
  for (var i = 0; i < arr.length;i++){
    fn.apply(thisArg, [arr[i], i, arr]) //这个是fn的this,指向thisArg
  }
}
var obj ={
  a:'this is in obj'
}
function say(item,i,arr){
  console.log(
    '当前item:'+item,
    '当前i:' + i,
    '当前arr:' + arr,
    'this.a:' + this.a,
  )
}
[4,2,1,5].myForEach(say,obj)
// 当前item: 4 当前i: 0 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 当前item: 2 当前i: 1 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 当前item: 1 当前i: 2 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 当前item: 5 当前i: 3 当前arr: (4)[4, 2, 1, 5] this.a: this is in obj

4,软绑定

其实软绑定也算是显示绑定的一种,单独拿出来讲是因为它属于通过骚操作实现对this绑定丢失情况的容错处理。

比如在赋值的时候可能造成this绑定丢失情况,这时候我希望它this绑定丢失的时候不要绑定在全局对象上(非严格模式下),而是能够把它的this绑定在某个位置上,所以我们就要用到软绑定:

Function.prototype.softBind = function (obj) {
  var fn = this; //调用方法
  var curried = Array.prototype.slice.call(arguments, 1);  // 获取除了绑定对象参数外的其余参数

  function bound () {
      var thisArgs = !this||this===(window||global)?obj:this // 如果指向window则指向obj本身
    var args = Array.prototype.slice.call(arguments)
    return fn.apply(thisArgs, curried.concat(args)) //参数合并
  };
  bound.prototype = Object.create(fn.prototype); // 这个是继承fn,作为他的子类
  return bound;
};

来试验下

function say(){
    console.log(this.a)
}
var a = 'a in window'
var obj1={
    a:'a in obj1,默认绑定'
}
var obj2={
    a:'a in obj2'
}
var obj3={
    a:'a in obj3'
}
var handleSay = say.softBind(obj1) // 默认绑定obj1
handleSay() // 'a in obj1,默认绑定'
obj2.handleSay = say.softBind(obj1) // 默认绑定obj1
obj2.handleSay()//'a in obj2'
handleSay.call(obj3) // 'a in obj3'
 

解释下,可能这里有点绕,或许你们会觉得这个跟bind没啥区别,但是仔细看下,

比如obj2.handleSay = say.softBind(obj1)我这里给他了一个默认绑定对象,但是我调用obj2.handleSayde的时候,它的this还是绑定在了obj2上,其作用代码就是:

var thisArgs = !this||this===(window||global)?obj:this 这一行代码 ,

这个功能bind是无法实现的,因此这就softBind软绑定的作用

5,new 绑定

new 的调用也能改变this的指向,先说一说 new 做了什么事就明白了

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

用代码来表示就是

function myNew(ClassFn){
  var obj ={} //1,创建(或者说构造)一个全新的对象。
  obj.__proto__ = ClassFn.prototype //2,这个新对象会被执行[[原型]]连接。
  ClassFn.call(obj) //3,这个新对象会绑定到函数调用的this。
  return obj //4,如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象,意思就是说,如果构造函数ClassFn本身就有返回的东西则返回它,否则返回实例化的这个obj
}

补充说明第4点,意思是:

function Test(){
    this.a='text'
    return {}
}
var s = new Text()// s为空对象,因为Test构造函数已经返回了空对象

另外补充说明一个知识点,就是我们之前没有提及到的bind,其实bind内部也是通过applycall实现,借助bind我们可以实现函数柯里化等骚操作:

Function.prototype.myBind=function(){
    var self =this //谁调用指向谁
    var args = Array.prototype.slice.call(arguments)
    var thisArgs = args[0] //获取传进来的绑定对象,也就是第一个参数,比如 .bind(obj) 
    args = args.slice(1) //获取除了绑定对象外的其余参数
    return function(){
        return self.apply(thisArgs,args.concat(Array.prototype.slice.call(arguments)))
    }
}

尝试一下,发现可以正常工作:

function say(p){
    this.p = p
}
var obj={}
var s= say.bind(obj)
s(2)
obj//{p:2} 

但是,这只是实现bind的部分功能,还有另外一部分是当bind绑定后返回的函数作为构造函数时,会有不同的表现,有兴趣可以自行了解developer.mozilla.org/zh-CN/docs/…

7,判断this

判断this绑定对象(this指向)可以按照下面5步来:

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。 如:var bar = new foo()

  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。 var bar = foo.call(obj2)

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()

  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。 var bar = foo()

  5. 除此之外还要记住当有赋值情况的时候会造成this绑定丢失情况function go(fn){fn=obj.say()}//具体可以查看上述2隐式丢失的内容

另外,ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。

写在最后

回到文章开头那句话

ES5 function里面的this谁调用它就指向谁,ES6箭头函数的this是在哪里定义就指向哪里

虽然看起来不太严谨,但是它的确是正确的,在我们日常开发中,比如是刚入门不久的前端开发者,不会频繁接触到函数柯里化这种写法,更多的是在使用框架的时候分辨this的指向,比如vue框架+iview,使用Table组建的时候,如果想自定义表格内容,其中一个方法就得借助render函数,写成普通函数的话,这一行 onClick={this.viewItem.bind(this, params)}就会报错,因为this指向错误,被绑定到了调用render函数的对象,也就是Table组件上, 需要手动赋值this才能解决,但是使用箭头函数就能完全规避这个问题

<template>
  <div>
        <Table
          :columns="tableHead"
          :data="dataList"
        ></Table>
  </div>
</template>
export default {
  data() {
    return {
      dataList: [],
      tableHead: [
        {
          title: '操作',
          render: (h, params) => {
          // 这里的this 永远指向 VueComponent对象,其实就是data(){}的this绑定对象
            return (
              <div>
                <i-button
                  type="primary"
                  size="small"
                  style=" marginRight:5px"
                  onClick={this.viewItem.bind(this, params)}
                >
                  查看
                </i-button>
              </div>
            )
          }
        }
      ]
    }
  }



总而言之,希望读到这篇文章的人能有所收获

如有不正确的地方欢迎斧正,谢谢各位