[译]理解JavaScrip箭头函数中的“This”

1,041 阅读5分钟

原文链接:

www.codementor.io/dariogarcia…

这是理解Javascript中的"This"的第二篇文章([译] 理解Javascript中的"This"),可以对箭头函数有进一步的理解。

之前在使用箭头函数的时候也犯过文中的错误,那时候还不清楚this的指向还有箭头函数并没有属于自己的this这一点。

这篇文章可以作为“理解JavaScript中的"This"”的第二篇文章。

我们将会通过同样的例子,但是我们将会使用箭头函数代替普通函数来对比输出结果。

关于第二篇文章的动机是:尽管箭头函数是ES6中新增的强大功能,但是一定不能滥用它们。

“this”默认的上下文

箭头函数不会绑定它们自己的this,相反,它们会从父级作用域中继承,被称为“词法作用域(静态作用域)”。这让箭头函数成为了一些场景中的最佳选择,但同时有些场景却是非常糟糕。

译者注:

lexical scoping:词法作用域/静态作用域

既函数的作用域在函数定义的时候就决定好了。

与其相反的是动态作用域:函数的作用域在函数调用的时候才决定。

来看一个例子:

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();
// 结果是 ???

假设JavaScript采用静态作用域,分析过程如下:

执行foo函数,首先从foo函数内部查找是否有变量value;

如果没有,就根据书写的位置查找上一层的代码,我们发现value等于1,所以结果打印为1。

假设JavaScript采用动态作用域,分析过程如下:

执行foo函数,依然是从foo函数内部查找是否有局部变量value。

如果没有,就从调用函数的作用域,也就是bar函数内部查找value变量,所以结果打印为2。

JavaScript采用的是静态作用域,所以这个例子的结果是1。

我们看看第一个例子改写为箭头函数后:

// define a function
const myFunction = () => {
  console.log(this);
};

// call it
myFunction();

我们可以期待this是什么呢?...没错,这和普通函数一样,window/全局对象。同样的结果但并不是同样的原因。在普通函数下,作用域默认绑定为全局作用域;但像我前面说的,箭头函数并没有属于它们自己的this,但它们继承于父级作用域,在这个例子中,它就是全局作用域。

如果我们使用严格模式(use strict)会怎样呢?并没有什么不一样的,会是一样的结果,因为作用域来自父级。

箭头函数作为方法

const myObject = {
  myMethod: () => {
    console.log(this);
  }
};

现在这样呢?

在这个例子中,有的人会说,这取决于方法被谁调用,和普通函数一样。但事实并不是这样的,让我们一起看看...

myObject.myMethod() // this === window or global object

const myMethod = myObject.myMethod;
myMethod() // this === window or global object

很奇怪,对吧?请记住:箭头函数并不会绑定它们自己的作用域,而是继承于父级。而在这个例子中就是window/全局对象。

让我们改变列子一点点。

const myObject = {
  myArrowFunction: null,
  myMethod: function () {
    this.myArrowFunction = () => { console.log(this) };
  }
};

我们需要调用myObject.myMethod()去初始化myObject.myArrowFunction,然后让我们看看输出结果是怎么样的。

myObject.myMethod() // this === myObject

myObject.myArrowFunction() // this === myObject

const myArrowFunction = myObject.myArrowFunction;
myArrowFunction() // this === myObject

现在清楚了吗?当我们调用myObject.myMethod(),我们使用myMethod内部的箭头函数初始化myObject.myArrowFunction,所以它将继承其作用域。我们可以清楚的看到一个完美的应用例子:闭包。

显式、Hard 和 New 绑定

如果我们尝试通过这些手段绑定作用域会发生什么呢?

让我们看看...

const myMethod = () => {
  console.log(this);
};

const myObject = {};

显式绑定

myMethod.call(myObject, args1, args2, ...) // this === window or global object
myMethod.apply(myObject, [array of args]) // this === window or global object

Hard绑定

const myMethodBound = myMethod.bind(myObject);
myMethodBound(); // this === window or global object

New绑定

new myMethod(); // Uncaught TypeError: myMethod is not a constructor

正如你看到的,不管我们尝试用哪种方式绑定作用域都不会生效。同样,箭头函数没有构造函数,所以你不能使用new。

API回调

这一部分非常的有趣。对于接口回调(异步代码)而言,箭头函数是一个非常好的选择,仅当我们使用闭包时,让我们看看下面的代码...

myObject = {
  myMethod: function () {
    helperObject.doSomethingAsync('superCool', () => {
      console.log(this); // this === myObject
    });
  },
};

这是一个完美的例子,我们需要在异步过程中做一些事情,我们等待一些结果去执行一些操作,然而我们不需要担心正在操作的作用域。

但是如果我们需要提取和重构一些代码用以复用,这又会发生什么呢?例如:

const reusabledCallback = () => {
  console.log(this); // this === window or global object
};

myObject = {
  myMethod: function () {
    helperObject.doSomethingAsync('superCool', reusabledCallback);
  },
};

如果我们这样做了,我们会破环当前工作的代码,还有请记住:不管我们尝试怎么绑定作用域都将会失效。所以如果你决定这样做,你必须使用普通函数并手动绑定作用域,例如:

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

myObject = {
  myMethod: function () {
    helperObject.doSomethingAsync('superCool', reusabledCallback.bind(myObject));
  },
};

结论

箭头函数是ES6中新增的强大功能,但当我们使用的时候必须保持谨慎和明智的判断。我不断的寻找那些不适用箭头函数的地方,它会导致一些错误难以追踪,尤其是当我们并不理解它的原理时。

我的建议是,在闭包或者回调的情况下,箭头函数是一个很好的选择。但在class/object、构造函数的方法中,它并不是一个好的选择。

PS:

箭头函数还有很多有趣的特点,例如:arguments或者prototype,但这篇文章的主题是this的作用域。更多信息可以查看这个文档**mozilla web docs**。


如果你有任何问题或意见,请给我留言!