mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ES6 系列之箭头函数

mqyqingfeng opened this issue · comments

回顾

我们先来回顾下箭头函数的基本语法。

ES6 增加了箭头函数:

let func = value => value;

相当于:

let func = function (value) {
    return value;
};

如果需要给函数传入多个参数:

let func = (value, num) => value * num;

如果函数的代码块需要多条语句:

let func = (value, num) => {
    return value * num
};

如果需要直接返回一个对象:

let func = (value, num) => ({total: value * num});

与变量解构结合:

let func = ({value, num}) => ({total: value * num})

// 使用
var result = func({
    value: 10,
    num: 10
})

console.log(result); // {total: 100}

很多时候,你可能想不到要这样用,所以再来举个例子,比如在 React 与 Immutable 的技术选型中,我们处理一个事件会这样做:

handleEvent = () => {
  this.setState({
    data: this.state.data.set("key", "value")
  })
};

其实就可以简化为:

handleEvent = () => {
  this.setState(({data}) => ({
    data: data.set("key", "value")
  }))
};

比较

本篇我们重点比较一下箭头函数与普通函数。

主要区别包括:

1.没有 this

箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。

这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。

模拟一个实际开发中的例子:

我们的需求是点击一个按钮,改变该按钮的背景色。

为了方便开发,我们抽离一个 Button 组件,当需要使用的时候,直接:

// 传入元素 id 值即可绑定该元素点击时改变背景色的事件
new Button("button")

HTML 代码如下:

<button id="button">点击变色</button>

JavaScript 代码如下:

function Button(id) {
    this.element = document.querySelector("#" + id);
    this.bindEvent();
}

Button.prototype.bindEvent = function() {
    this.element.addEventListener("click", this.setBgColor, false);
};

Button.prototype.setBgColor = function() {
    this.element.style.backgroundColor = '#1abc9c'
};

var button = new Button("button");

看着好像没有问题,结果却是报错 Uncaught TypeError: Cannot read property 'style' of undefined

这是因为当使用 addEventListener() 为一个元素注册事件的时候,事件函数里的 this 值是该元素的引用。

所以如果我们在 setBgColor 中 console.log(this),this 指向的是按钮元素,那 this.element 就是 undefined,报错自然就理所当然了。

也许你会问,既然 this 都指向了按钮元素,那我们直接修改 setBgColor 函数为:

Button.prototype.setBgColor = function() {
    this.style.backgroundColor = '#1abc9c'
};

不就可以解决这个问题了?

确实可以这样做,但是在实际的开发中,我们可能会在 setBgColor 中还调用其他的函数,比如写成这种:

Button.prototype.setBgColor = function() {
    this.setElementColor();
    this.setOtherElementColor();
};

所以我们还是希望 setBgColor 中的 this 是指向实例对象的,这样就可以调用其他的函数。

利用 ES5,我们一般会这样做:

Button.prototype.bindEvent = function() {
    this.element.addEventListener("click", this.setBgColor.bind(this), false);
};

为避免 addEventListener 的影响,使用 bind 强制绑定 setBgColor() 的 this 为实例对象

使用 ES6,我们可以更好的解决这个问题:

Button.prototype.bindEvent = function() {
    this.element.addEventListener("click", event => this.setBgColor(event), false);
};

由于箭头函数没有 this,所以会向外层查找 this 的值,即 bindEvent 中的 this,此时 this 指向实例对象,所以可以正确的调用 this.setBgColor 方法, 而 this.setBgColor 中的 this 也会正确指向实例对象。

在这里再额外提一点,就是注意 bindEvent 和 setBgColor 在这里使用的是普通函数的形式,而非箭头函数,如果我们改成箭头函数,会导致函数里的 this 指向 window 对象 (非严格模式下)。

最后,因为箭头函数没有 this,所以也不能用 call()、apply()、bind() 这些方法改变 this 的指向,可以看一个例子:

var value = 1;
var result = (() => this.value).bind({value: 2})();
console.log(result); // 1

2. 没有 arguments

箭头函数没有自己的 arguments 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 arguments 对象:

function constant() {
    return () => arguments[0]
}

var result = constant(1);
console.log(result()); // 1

那如果我们就是要访问箭头函数的参数呢?

你可以通过命名参数或者 rest 参数的形式访问参数:

let nums = (...nums) => nums;

3. 不能通过 new 关键字调用

JavaScript 函数有两个内部方法:[[Call]] 和 [[Construct]]。

当通过 new 调用函数时,执行 [[Construct]] 方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。

当直接调用的时候,执行 [[Call]] 方法,直接执行函数体。

箭头函数并没有 [[Construct]] 方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

4. 没有 new.target

因为不能使用 new 调用,所以也没有 new.target 值。

关于 new.target,可以参考 http://es6.ruanyifeng.com/#docs/class#new-target-%E5%B1%9E%E6%80%A7

5. 没有原型

由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

6. 没有 super

连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。

总结

最后,关于箭头函数,引用 MDN 的介绍就是:

An arrow function expression has a shorter syntax than a function expression and does not have its own this, arguments, super, or new.target. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

翻译过来就是:

箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数(non-method functions),并且它们不能用作构造函数。

那么什么是 non-method functions 呢?

我们先来看看 method 的定义:

A method is a function which is a property of an object.

对象属性中的函数就被称之为 method,那么 non-mehtod 就是指不被用作对象属性中的函数了,可是为什么说箭头函数更适合 non-method 呢?

让我们来看一个例子就明白了:

var obj = {
  i: 10,
  b: () => console.log(this.i, this),
  c: function() {
    console.log( this.i, this)
  }
}
obj.b();
// undefined Window
obj.c();
// 10, Object {...}

自执行函数

自执行函数的形式为:

(function(){
    console.log(1)
})()

或者

(function(){
    console.log(1)
}())

利用箭头简化自执行函数的写法:

(() => {
    console.log(1)
})()

但是注意:使用以下这种写法却会报错:

(() => {
    console.log(1)
}())

为什么会报错呢?嘿嘿,如果你知道,可以告诉我~

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

跟着大佬学习,少走弯路

this 指向的是按钮元素,那 this.element 就是 undefined

这里说 this 就是 this.element 会更好一点

大佬大佬,产出那么恐怖的吗?才刚刚看完一篇

commented

鼓掌

我要学习 我渴望学习 我爱学习!!!

一如前端深似海,文章何时能写完……

default

关于文章最后的疑问,感谢 SF 的 happy007:

default

箭头函数还是很方便的。

发现好多问题从规范的角度理解,一下就解释通了 😸

小哥哥确实想的更深一层

但是注意:使用以下这种写法却会报错:

(() => {
console.log(1)
}())
为什么会报错呢?嘿嘿,如果你知道,可以告诉我~

等价于这样子的:
(function f(){console.log(1)}())

function f(){
console.log(1)
}()

为什么会报错呢 你得上个括号:

((() => {
    console.log(1)
})())

关于Assignment Expression和Call Expression,可以看这里的介绍

image
有个疑问,对于总结这一块标红的区域。首先还是没太明白methodnon-method的区别。作者给的例子很容易看懂,但看完后并不是很清楚:为什么箭头函数更适合 non-method 呢?

image
有个疑问,对于总结这一块标红的区域。首先还是没太明白methodnon-method的区别。作者给的例子很容易看懂,但看完后并不是很清楚:为什么箭头函数更适合 non-method 呢?

因为它内部this的指向原因,当使用obj.b()的时候,很明显我们希望b方法里面的this指向obj,但是它却指向了obj所在上下文中的this(即window),违背了我们的意愿,所以箭头函数不适合作为对象的方法。这也是为什么vue组件里面方法不允许使用箭头函数的原因。

感觉最后一个问题就是符号运算优先级的问题. 函数的定义应该是个整体, 然后再自运行这个函数. 否则就会是语法错误

image
有个疑问,对于总结这一块标红的区域。首先还是没太明白methodnon-method的区别。作者给的例子很容易看懂,但看完后并不是很清楚:为什么箭头函数更适合 non-method 呢?

因为它内部this的指向原因,当使用obj.b()的时候,很明显我们希望b方法里面的this指向obj,但是它却指向了obj所在上下文中的this(即window),违背了我们的意愿,所以箭头函数不适合作为对象的方法。这也是为什么vue组件里面方法不允许使用箭头函数的原因。

很感谢你的回答,懂啦!

commented

关于文章最后的疑问,感谢 SF 的 happy007:

default

还是不太明白 楼主

受益匪浅

可以这样理解吗,我自己来捋下、
1.js中的函数遵守词法作用域,及定义时
2.普通函数中的this有比较特殊,遵守四大绑定规则,及调用时绑定this指向(有点类似动态作用域,‘’你不知道的JavaScirpt‘’也是这么描述的)
3.箭头函数有不一样,不创建局部this对象,通过词法作用域(及箭头函数定义时)寻找上级作用域(最近一层普通函数)的this

关于文章最后的疑问,感谢 SF 的 happy007:
default

还是不太明白 楼主

我想你得疑问应该是不清楚 表达式(expression) 的定义,可以看看规范的这一章,详细的说明了不同的语法分别属于什么 表达式
https://262.ecma-international.org/6.0/#sec-left-hand-side-expressions

坚持每日进步 ✊ 感谢大佬🙏

commented

关于文章最后的疑问,感谢 SF 的 happy007:
default

还是不太明白 楼主

函数调用是 CallExpression,CallExpression 要求左侧必须是 MemberExpression 或 CallExpression,比如 obj.fn()fn()(),而箭头函数是 AssignmentExpression,不符合要求所以报错。

但是我又不太理解为什么匿名函数就可以 😂

(function(){
    console.log(1)
}())