基础篇 | this & 箭头函数

140 阅读5分钟

ES6 中介绍了一种无法使用这些规则的特殊函数类型——箭头函数。我们先来简单介绍一下我们的箭头函数 (Arrow Function)

上一篇文章说到 this 动态作用域以及四个绑定规则,基本上已经将 this 的四个规则都讲了一遍,复习一下:

  • 默认绑定:独立函数调用
  • 隐式绑定:被某个对象调用,例如 obj.foo()
  • 显式绑定: call or apply or bind
  • New 操作符绑定

上面这几种规则基本上可以包含所有的正常函数。 但是 ES6 中介绍了一种无法使用这些规则的特殊函数类型——箭头函数。我们先来简单介绍一下我们的箭头函数 (Arrow Function)

何为箭头函数

通常而言,箭头函数实例化函数对象的方式可以和函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数,例如

var sum = function (a, b) {
  return a + b
}

var sumByArrow = (a, b) => {
  return a + b
}

而,箭头函数的简洁通常会受到一些嵌入函数场景的青睐,如:

const arr = [1, 2, 3]

arr.map(function (item) {
  console.log(item)
})

arr.map(item => console.log(item))

参数括号在箭头函数中也不是必须的,只有当没有参数,或者存在多个参数时,才需要参数括号;当加上花括号 {} 时,会将 {} 中内容作为函数体;当只有一条语句或者表达式时,可以不加 {} ,箭头函数会隐式将箭头后面的内容作为返回值 return 处理,而无需通过 return + {} 的方式再冗余的写一遍。

箭头函数的 this?

箭头函数是不适用 this 的四种绑定规则的,箭头函数是根据外层作用域(函数或者是全局对象)来定义 this ,或者叫做词法作用域。 箭头函数的 this 不是在执行的时候确定,而是在定义的时候确定

在 YDKJS 中有这么一个例子:

function foo() {
	// 自动继承了 foo 里面的 this
  return (a) => {
    console.log(this.a)
  }
}

var obj1 = {
  a: 2
}

var obj2 = {
  a: 3
}

var bar = foo.call(obj1)
bar.call(obj2) // 2

在上面的案例中,foo 内部创建的箭头函数会捕获调用时 foo 所绑定的 this ,由于 foothis 绑定到了 obj1 上,所以这里 this 也是在 obj1 上,箭头函数的绑定无法被修改。

箭头函数最常用的场景是定时器或者其他函数的回调函数中,箭头函数可以确保函数的 this 绑定在指定的对象中,例如:

function foo() {
  setTimeout(() => {
    console.log(this.a) // 捕获词法作用域中 this 指向
  })
}

var obj1 = {
  a: 2,
  foo: foo
}

obj1.foo() // 2 

箭头函数局限性

箭头函数虽然简洁,但是也有其局限性,箭头函数不能使用 argumentssupernew.target,也不能被当作构造函数来使用。此外,箭头函数也没有 prototype 属性。

函数参数 arguments

如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问。

// 函数声明
function foo(a) {
  console.log('foo')
  console.log(arguments[0])
}

foo(5) // it works

// 函数表达式
var bar = function (b) {
  console.log('bar')
  console.log(arguments[0])
}

bar(5) // it works

// 箭头函数
var baz = (c) => {
  console.log('baz')
  console.log(arguments[0])
}

baz(5) // undefined 访问到全局对象上

箭头函数无法修改 this 指向

由于箭头函数并没有自己的 this 对象,它是其词法作用域的 this 对象。所以,自然地,我们无法通过 call or apply or bind 去改变它的 this, 毕竟巧妇难为无米之炊。

对象方法 or 原型方法

首先,我们知道在普通函数中, this 指向当前的调用对象;而箭头函数中,this 确实执行当前定义时所在对象,在定义时就已经确定下来,继承自父级作用域的 this

在下面的例子中, obj.getName() 通过隐式绑定指向的当前的上下文对象,或者叫做函数调用者,即 obj ,但是 obj.getName1() 执行时,对应的上下文对象是调用 obj.getName1() 函数的对象,即全局对象。原型方法也是同理。

const obj = {
  name: "obj",
  getName: function () {
    return this.name
  },
  getName1: () => {
    return this.name
  }
}

console.log(obj.getName()) // obj
console.log(obj.getName1())  // undefined

构造函数

由于通过 new 操作符来对函数进行构造调用时,生成的对象实例也是通过给实例绑定 this 对象,但是箭头函数并没有自己的 this ,因此箭头函数无法被当做构造函数实现,无法通过 new 操作符来进行构造调用。

new.target 是 ES6 中新增的方法,用于检测函数是否使用 new 关键字调用,如果函数是通过 new 关键字调用的话,则 new.target 将引用被调用的构造函数。

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。听完 super 关键字的用法,应该就可以理解为什么箭头函数无法使用 super 关键字了吧。

似曾相识的箭头函数

看起来箭头函数是一种新鲜事物,但是如果之前经常会和 this 打交道的话,我们应该会感觉有点似曾相识。在以前,为了 this 指向的正确或者确定性,我们常常用一个额外的变量去保留当前函数调用的 this 指向,如 var that = this or var self = this 。从本质上来说,这种方式实际上和箭头函数有着异曲同工之处,都是保留着外部作用域的 this 指向。

ES6 的箭头函数会根据当前词法作用域来决定 this 的指向,具体而言,箭头函数会继承外层函数调用的 this 绑定。这个其实和在箭头函数出现之前,我们用 var self = this 的机制是一样的。

Reference:

MDN-Arrow Function 箭头函数

You Don't Know JS: this & Object Prototypes —— Chapter 2: this All Makes Sense Now!

红宝书 - chap8、chap10 箭头函数相关内容