Javascript的噩梦之 this

夙愿已清 提交于 2020-12-24 15:10:17

看过我以往文章的朋友知道我是个人推崇JavaScript 函数而尽量避免使用class

FreewheelLee:应该在JavaScript中使用Class吗?zhuanlan.zhihu.com

但是,人在江湖飘哪能不被社会毒打。当你所在的团队的代码是以class为核心的时候,你还是不得不去熟悉、了解甚至精通 JavaScript this / class / prototype。


this 是面向对象编程中非常常见的一个概念,本文将由浅入深,探讨JavaScript this 的相关知识。



函数 中 的 this 和 bind

当我们用字面量的形式创建一个JavaScript object

const cat = {
  sound: '喵',
  talk(){
    console.log(this.sound); // this 指向当前对象  }}cat.talk(); // 输出: 喵

这个行为大家应该都不觉得奇怪,尤其是从Java过来的朋友。

场景一:

JavaScript最特别的特性之一是函数是一等公民,可以被当做变量、参数返回值等。于是我们尝试把 cat.talk 拿出来玩玩

const catTalkFunc = cat.talk; // 将函数赋值给变量
catTalkFunc(); // 输出:undefined

be2384a698fa7944d74c1da40e360557.jpeg

原因:

执行 cat.talk() 时,talk 更准确地说 是个属于cat这个对象的方法,它的this此时指向的是 cat 这个对象,所以this.soud指向的是 '喵'

而当 cat.talk 赋值给 catTalkFunc 时,就类似于

const catTalkFunc = function(){
  console.log(this.sound);
}

执行 catTalkFunc() 时 this 不再指向 cat 对象。

划重点:JavaScript 函数 中的 this 指向的不是该函数定义的时候的上下文而是调用该函数的上下文

如果我们希望输出跟之前一样的结果,有以下三种选择

// 直接指定this并调用
catTalkFunc.call(cat); 
catTalkFunc.apply(cat);

// 绑定this后产生一个新的函数
const catTalkFunc2 = catTalkFunc.bind(cat);
catTalkFunc2(); // 调用新函数

call 和 apply 都是 Function 的方法,第一个参数是打算指定的 this 上下文,后面的参数是传递给 catTalkFunc 的参数,但是本例中 catTalkFunc 不需要任何参数。这两个方法的更多细节可以参考官方文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call 和 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply


我们重点看一下 bind 方法,bind 方法会产生一个新的函数 catTalkFunc2 ,直接调用 catTalkFunc2 就能输出: ”喵“

这是怎么做到的呢?这是不是违背了上面说的 ”JavaScript 函数 中的 this 指向的不是该函数定义的时候的上下文而是调用该函数的上下文“ —— 当然没有

bind 方法可以近似理解为这么实现(仅仅是模拟实现)

Function.prototype.newBind = function (thisArg) {
  const func = this; // func 变量指向当前这个函数
  return function () { // 返回新函数
    const args = Array.prototype.slice.call(arguments);
    func.apply(thisArg, args); // 使用 thisArg 作为 this上下文
  }
}

即 利用高阶函数和 apply 方法,绑定好 this 并返回一个新的函数,而新的函数里并不依赖执行时的任何 this 上下文。


场景二:

接上面的例子

const dog = {
  sound: '汪',
  talk: cat.talk
}

dog.talk();

预测一下最后一行会输出什么? 是 ’喵‘,’汪‘ 还是 undefined ?

答案是:’汪‘ ,因为talk被调用时,this 上下文是 dog 对象

这边要强调的一点是 无论是场景一中 的 cat.talk 和 catTalkFunc 还是 场景二里的 dog.talk 其实并没有什么特别的处理 都只是同一个函数的引用 即

function(){
  console.log(this.sound);
}



关于 this 和 bind 先小结一下

  1. 在JavaScript 中 函数 是类似于 string、number、boolean 的值存在的,它们可以被赋给变量、可以做参数、可以做返回值。由于这种灵活性,在函数中的 this 只会在函数运行时才确定

    可以使用一个口语例子作为类比更好理解:
    当我说”这里好热!“的时候,你并不知道”这里“是哪里。
    假如我在车里这么说,我指的是车里热;当我在房间里这么说,我指的是房间很热;当我在户外这么说,我指的是当地的天气很热 —— ”这里“的具体指向只取决于我在什么地方说出这句话。




  2. bind 方法
    有时我们想避免这种歧义,就可以使用 bind 显示地指定这个函数的 this 上下文。bind 会返回一个已经绑定好 this 的新函数。



fb887cd20769b6b0dc951d21ba4169ad.jpeg道理我都懂,但为什么鸽子这么大


this 到底指向了什么


当 函数在对象中定义且作为对象方法运行时,函数中的 this 自然指向了对象,如上面的例子

const cat = {
  sound: '喵',
  talk(){
    console.log(this.sound); // this 指向当前对象
  }
}

cat.talk(); // 输出: 喵


当函数在没有对象上下文的时候被直接执行,函数中的 this 指向的是全局对象 —— 在 Node 环境中对应的就是 global 对象,在 浏览器环境中对应的就是 window 对象

可以做以下测试验证这个说法

const cat = {
  myThis(){
    return this;
  }
}

console.log(cat.myThis() === cat); // true

const theThis = cat.myThis;
console.log(theThis() === cat); // false
console.log(theThis() === global); // 在 Node 环境中为 true
console.log(theThis() === window); // 在 浏览器 环境中为 true


常规函数中的 this 没有作用域链

const foo = {
  sound: 'fffff',
  say(){
    console.log(this.sound);
  } ,
  bar(){
    function inner(){
      console.log("this.sound inner " + this.sound);
    }

    inner();
  }
}

foo.say(); // fffff
foo.bar(); // 输出什么?

上面的最后一行代码会输出什么呢?

如果你认为是 'this.sound inner fffff' ,那么你的思路应该是 inner() 在被执行时,虽然外层是 bar() 函数不是对象,但再往外一层就是 foo 对象了,所以 this.sound 指向的是 foo.sound

这种思路是典型的变量作用域链的思路 —— 对于其他 JavaScript 变量而言,的确是一层层函数往外这么找的,如下面这个例子

function aFunc() {
  const aVar = 1;

  function bFunc() {
    const bVar = 2;

    function cFunc() {
      const cVar = 3;

      console.log(aVar);
    }

    cFunc();
  }

  bFunc();
}

aFunc(); // 输出 1 —— 从 cFunc 到 bFunc 到 aFunc 一层层寻找 aVar 的定义

但是,this 比较特殊,它不遵循这种链式的规则 —— 如果函数执行时不处于对象上下文中,那么this就直接指向全局对象

可以对代码做以下修改来打印出 this.sound inner fffff 的结果

const foo = {
  sound: 'fffff',
  say() {
    console.log(this.sound);
  },
  bar() {
    const _this = this; // 使用一个临时变量存放 this 引用
    function inner() {
      console.log("this.sound inner " + _this.sound); // 使用 _this 变量
    }

    inner();
  }
}

foo.say(); // fffff
foo.bar(); // this.sound inner fffff


箭头函数的 this

到目前为止,本文提到的函数都是常规函数

在 ES2015(即ES6)之后,JavaScript引入一个新特性叫 箭头函数 (arrow function),它内部的 this 实现跟常规函数有很大的不同:

箭头函数没有自己的 this,箭头函数中的 this 直接绑定了创建(定义)该箭头函数时的 this

因此上面的代码也可以这么改

const foo = {
  sound: 'fffff',
  say() {
    console.log(this.sound);
  },
  bar() {
    const inner = () => {  // 箭头函数
      console.log("this.sound inner " + this.sound);
    }

    inner();
  }
}

foo.say(); // fffff
foo.bar(); // this.sound inner fffff

由于 inner 函数是个箭头函数,且在定义 inner 函数时,this 指向的是 foo 对象,因此inner的this 就绑定了 foo 对象


补充:

关于 箭头函数的 this,有一位读者有困惑跟我讨论了一下,他提出了以下的例子

const cap = {
  sound: 'capcap',
  bar: foo.bar,
}

cap.bar(); // 输出 this.sound inner capcap

这样是不是推翻了 “箭头函数中的 this 直接绑定了创建(定义)该箭头函数时的 this” 呢?

当然不是。上面的代码等同于

const cap = {
  sound: 'capcap',
  bar: function () {
    const inner = () => {
      console.log("this.sound inner " + this.sound);
    }

    inner();
  },
}

那箭头函数 inner 是什么时候被创建的呢? —— 是在 bar() 方法执行的时候创建的,因此 inner 的 this 指向的就是 bar() 方法上下文中的 this —— 也就是 cap 对象


再看一个特别的例子:

const bird = {
  color: 'white',
  describe: () => {
    console.log("bird is " + this.color);
  }
}

bird.describe(); // 输出 bird is undefined

看到这里,你是豁然开朗了还是更加困惑了?

解释: 创建 describe 箭头函数时,bird 还在构建中,因此箭头函数指向的是全局对象而不是 bird 对象。

const birdGarden = {
  location: 'Shanghai',
  bird: {
    color: 'white',
    describe: () => {
      console.log("bird is " + this.color + " in " + this.location);
    }
  }
}


birdGarden.bird.describe(); // 输出 bird is undefined in undefined

跟上例同理


另外,上面提到的 Function 的 apply, call 和 bind 方法都对箭头函数无效!

bird.describe.call(bird);  // bird is undefined
bird.describe.apply(bird);  // bird is undefined
bird.describe.bind(bird)(); // bird is undefined

因此,如果你想给箭头函数赋 this 的值,一种方法是利用高阶函数 —— 在目标this上下文中定义一个函数,在函数中创建箭头函数

const bird = {
  color: 'white',
  describe() {
    return () => {
      console.log("bird is " + this.color);
    }
  }
}

bird.describe()(); //bird is white

解析: bird.describe 方法的this指向的是 bird 对象,bird.describe 方法执行时会创建一个匿名的箭头函数,这个箭头函数的 this 也因此指向了 bird 对象,所以这个箭头函数的执行结果是 "bird is white"

另一种方法是在构造器函数中创建箭头函数

function Bird() {
  this.color = 'white';
  this.describe = () => {
    console.log("bird is " + this.color);
  }
}

const b = new Bird();
b.describe(); // bird is white

Bird 函数作为构造器,创建 describe 箭头函数时,上下文的this就是即将构建的 Bird 实例(构造器特性),因此能输出正确结果。


如果你已经开始晕头转向了,那么恭喜你 品尝到了 JavaScript的噩梦之 this 的滋味!也初步领略到了 JavaScript 诡异的面向对象机制。



本文先从这些简单的例子介绍 JavaScript this 的一些基础知识,在之后的文章会结合prototype、构造器函数、class关键字 进一步阐释。


关于 JavaScript 的 this 基础知识今天就分享到这,如果对你有帮助,欢迎点赞、喜欢、收藏三连!

有任何问题和建议可以留言或私信我。


推荐阅读:

FreewheelLee:应该在JavaScript中使用Class吗?zhuanlan.zhihu.com


FreewheelLee:什么?JavaScript不用class也能实现设计模式!zhuanlan.zhihu.com


参考链接:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

https://www.youtube.com/watch?v=PIkA60I0dKU&list=PL0zVEGEvSaeHBZFy6Q8731rcwk0Gtuxub&index=2&ab_channel=FunFunFunction


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!