别再用self=this、that=this了,代码太难读懂!

1. 前言

JavaScript中的this可能是当年设计的时候存在着设计缺陷,在ES6中能使用()=>这个高端的箭头函数就尽量使用箭头函数,箭头函数在其他语言中还有个高大上的名字Lambda表达式。

PS:nodejs环境中没有window对象

2. this指向

对于this的指向,我的理解就是记住一句话,**如果没有使用apply和call还有箭头函数的情况下,this指向最后一次调用它的对象,如果最后一次调用它的是函数,则在严格模式指向undefined非严格模式下指向全局变量window**。

下面的例子全部为非严格模式。

var a = 1;
var obj = {
  a: 2,
  b: function() {
    return this.a;
  }
};
console.log(obj.b()); //2

上面的例子应该很清晰,因为最后一次调用b方法的是obj这个对象,而对象obj里面a的值为2,所以这个例子会输出2。

那么我们加大一下难度看下面的例子。

var a = 1;
var obj = {
  a: 2,
  b: function() {
    return this.a;
  }
};
var t = obj.b;
console.log(t());
console.log(obj.b());
console.log(t() === obj.b()); 
console.log(t === obj.b);

这个时候我们新加入一个变量t,将b方法赋值给t,大家思考一下这个时候函数t会输出什么。

神奇的事情发生了!

console.log(t()); //1
console.log(obj.b()); //2
console.log(t() === obj.b()); //false
console.log(t === obj.b); //true

函数t输出的结果是1。

因为这次调用方法的是函数t而他的对象为window,因为对象window上的a值为1,故t的最后输出结果为1。

下面这个例子也同样。

var a = 1;
var obj = {
  a: 2,
  b: function() {
    function fun() {
      return this.a;
    }
    console.log(fun());
  }
};
obj.b(); //1

因为调用fun方法的是方法b,这时最后一次调用fun的不为对象,则this指向全局变量window

3. 箭头函数

  • 箭头函数会捕获其所在上下文的 this 值,作为自己的 this 值。
  • 箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined

3.1 解决的痛点

ES5语法中使用this有时候会很难以预测this的指向,从而产生bug,这个问题到了现在框架时代大量通过类进行封装时变得尤为明显。如果学习过react,就会发现将父组件的函数传递给子组件使用时,this指向总是个头疼的问题。

虽然使用call,apply,bind可以解决这部分痛点,但是需要额外的代码,不仅增大了工作量,而且增加了维护成本。

3.2 简单题目

var a = 1;
var obj = {
  a: 2,
  b: () =>{
    return this.a;
  }
};
console.log(obj.b()); //1

因为b是通过箭头函数进行声明,则它的this就指向了obj的this,而obj的this就是全局变量window

3.3 中等题目

var a = 1;
var obj = {
  a: 2,
  b: function () {
    var fun = () => {
      return this.a;
    };
    console.log(fun());
  },
};
obj.b(); //2

因为箭头函数会找上下文的this,所以这里找到的上文就是方法b的this,即为对象obj

var a = 1;
var obj = {
  a: 2,
  b: () => {
    var fun = () => {
      return this.a;
    };
    console.log(fun());
  },
};
obj.b(); //1

箭头函数是没有this的,这个时候方法fun就逐层寻找this,最终找到的是对象obj的this。

4. call,apply,bind

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

  1. apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。

  2. call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。

  3. bind:除了返回是函数以外,它的参数和call一样。

4.1 箭头函数

this是不能改变的,所以对箭头函数使用call,apply,bind是没有效果的

var a = 1;
var obj = {
  a: 2,
  b: function () {
    var fun = () => {
      return this.a;
    };
    console.log(fun.call(a));
  },
};
obj.b(); //2

4.2 特殊情况

var a = 1;
var obj = {
  a: 2,
  b: function () {
    var fun = () => {
      return this.a;
    };
    console.log(fun());
  },
};
var obj1 = {
  a: 3,
};
obj.b.call(obj1); //3

箭头函数的this虽然不能被改变,但是可以改变它上下文的this。

5. 一大堆题

既然上面都讲了那么多,那么我们来看一下下面的题:

var age = 10;
var person = {
  age: 20,
  getAge() {
    var age = 30;
    return this.age; //20
  }
};
console.log(age, age * 2); //10 20
console.log(person.getAge()); //20
var b = person.getAge;
console.log(b()); //10
console.log(person.getAge()); //20
console.log((1, person.getAge)()); //10
console.log((1, person.getAge.bind(person))()); //20
console.log((person.getAge, person.getAge)()); //10
console.log((person.getAge = person.getAge)()); //10
console.log(person.getAge.call()); //10
console.log(person.getAge.call(person)); //20
function getAge2() {
  this.age = 40;
  console.log(person.getAge());
}
getAge2(); //20
console.log(age); //node环境和浏览器环境不一样,因为浏览器环境中有window对象
function getAge3() {
  this.age = 50;
  this.getAge4 = () => {
    console.log(person.getAge.call(this)); //50
  };
}
console.log(new getAge3().getAge4());
console.log(age); //10
function getAge4() {
  this.age = 60;
  this.getAge5 = () => {
    console.log(person.getAge.call(this)); //60
  };
}
console.log(new getAge4().getAge5()); //undefined
console.log(age); //10
var age2 = 10;
var person2 = {
  age2: 20,
  getAge2: () => {
    var age2 = 30;
    return this.age2;
  }
};
console.log(person2.getAge2.call()); //10
console.log(person2.getAge2.call(person2)); //10

6. 别再用self=this、that=this

文章到这里就应该完了,但是!现在已经进入新时代了啊!可以使用箭头函数尽量的避免使用const self = this这种代码,之前我不太明白为什么React和Vue要进入函数式编程时代,直到我阅读到一个代码中有const self = this这种代码,光说无凭,我们直接来看一眼下面的代码:

<template>
  <div>
    <button @click="click">点我</button>
    <div>{{ center }}</div>
  </div>
</template>
<script>
export default {
  name: "test",
  data() {
    // 万恶之源 const self = this
    const self = this;
    return {
      self: 100,
      center: 50,
      test: {
        click: () => {
          console.log(self.self);
          // 这里的this指的什么?
          this.center = 10;
          console.log(this.center);
        },
      },
    };
  },
  methods: {
    click() {
      this.test.click();
    },
  },
};
</script>

<style scoped></style>

可以看到这段代码,又是self又是this的,这仅仅是我从项目代码中截取后简化的一段,原代码更加复杂,下面我们来看一眼原项目中test那个位置是怎么写的:

amapEvents: {
  // 这里用了箭头函数
  click: (e) => {
    let { lng, lat } = e.lnglat;
    // 这里的self其实就是this
    console.log(self.plugin);
    self.plugin[1];
    let address;
    // 这里没有用箭头函数
    self.plugin[1].getAddress([lng, lat], function (status, result) {
      if (status === "complete" && result.info === "OK") {
        if (result && result.regeocode) {
          // 这里的self就不是this了,因为上面没有使用箭头函数
          self.address = result.regeocode.formattedAddress;
          address = self.address;
          console.log("地址:" + self.address);
          self.$nextTick();
        }
      }
    });
    this.center = [lng, lat];
    this.address = address;
  },
},

眼花嘛?头晕嘛?这项目上又是this又是self的,又是用了箭头函数又是不用箭头函数的,错综复杂的调用关系,最关键的是它还将函数放在data里面!太不守代码基本法了,所以在项目中最好都使用箭头函数,因为箭头函数的this会逐层查找。反正我用了箭头函数后,再也不用纠结this的问题了!