ECMAScript 6 笔记(8)- 函数的扩展(2)

原创 乘风逐月 随笔 ES6 109阅读 2018-08-29 16:28:05 举报

阅读原文: 阮一峰:ECMAScript 6 入门

一、箭头函数

ES6 允许使用 “箭头” (=>)定义函数。

1.基本用法

=> 前面的部分是函数的参数,=> 后面的部分是函数的代码块。

(1)如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

(2) 如果箭头函数的代码块部分多余一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回

(3)由于大括号会被解释为代码块,所以如果箭头函数直接返回一个对象,就必须在对象外面加上括号,否则会报错。

(4)箭头函数内部,可以嵌套箭头函数

2.箭头函数与变量解构结合使用
3.rest 参数与箭头函数结合
4.箭头函数的优点

(1)简化代码

(2)简化回调函数

5.箭头函数使用注意点

ES5中 this 的指向是可变的,但是箭头函数中 this 指向固定化。
例如:

上例中, this 总是指向 handler 对象。

this 指向的固定化,并不是因为箭头函数内部有绑定 this 的机制,实际原因是因为箭头函数内部根本没有自己的this,导致内部的 this 就是外层代码块的 this 。

所以有一下几点需要注意:

(1)函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当构造函数,即不可以使用 new 命令,因为它没有 this,否则会抛出一个错误。
(3)箭头函数没有自己的 this,所以不能使用 call()、apply()、bind() 这些方法去改变 this 指向。
(4)不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以使用 rest 参数代替。

二、双冒号运算符

箭头函数可以绑定 this 对象,大大减少了显示绑定 this 对象的写法。但是箭头函数不能适用于所有场合。所以提案 “函数绑定” 运算符,用来取代 call、apply、bind调用。

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。 该运算符会自动将左边的对象,作为上下文环境(即 this 对象),绑定到右边的函数上面。

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

如果双冒号运算符的运算结果,还是一个对象,还可以采用链式写法。

三、尾调用优化

1.尾调用

尾调用是函数式编程的一个重要概念,是指某个函数的最后一步是调用另一个函数。
例如:

以下情况不属于尾调用:

上例中,情况1是调用函数g之后,还有赋值操作,所以不属于尾调用;情况2也是调用之后还有加法运算,即使写在了一行内也不属于尾调用;情况算是没有return 返回则相当于:

尾调用不一定要出现在函数尾部,只要是最后一步操作即可。

上例中,函数m和n 都属于尾调用,因为它们都是函数f的最后一步。

2.尾调用优化

函数调用会在内存形成一个“调用记录”,又称为“调用帧”,保存调用位置和内部变量等信息。

如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等B运行结束,将结果返回给A,B的调用帧才会消失。如果函数B的内部调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧就会形成一个“调用栈”

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧即可。
只要不在用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾部调用优化”。

“尾调用优化”就是只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。

上例中,函数不会进行“尾调用优化”,因为内层函数inner用到了外层函数addOne的内部变量one。

注:ES6的尾调用优化只在严格模式下开启,正常模式下是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
--a. func.arguments: 返回调用时函数的参数
--b. func.caller: 返回调用当前函数的那个函数
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

四、尾递归

1.尾递归

函数调用自身,称为递归。如果尾调用自身,就称尾递归。

递归调用非常耗费内存,因为需要同时保存成百上千调用帧,很容易发生“栈溢出”错误。但对于尾递归来说,由于只存在一个调用帧,所有以永远不会发生“栈溢出”错误。

上例中,计算n的阶乘,最多需要保存n个调用记录,复杂度O(n)。如果改成尾递归,只保留一个调用记录,复杂度O(1)。

2.尾递归的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。例如上面的阶乘函数factorial需要用到中间变量total。但是这样看起来不直观,可以使用函数默认值解决这个问题:

递归的本质是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现。

3.尾递归优化的实现

尾递归需要优化,是因为调用栈太多,造成溢出,只要减少调用栈,就不会溢出。如何减少调用栈,就是采用“循环”替换“递归”。

改写:

上例中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一乱递归sum都返回undefined,就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总会执行。这样就将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

五、函数参数的尾逗号

ES2017运行函数的最后一个参数有尾逗号。这在此之前,在函数定义和调用时,都不允许最后一个参数后面出现逗号,否则回报错。
如果函数参数写成每个参数占一行,以后修改代码的时候,想为函数添加参数或者跳转参数的次序,就需要在原来的最后一个参数后面添加一个逗号。这对版本管理系统来说,就会显示添加逗号的那一行也发生了改变。这样看上去有点冗余,所以新语法允许定义和调用时,尾部直接有一个逗号。

这样规定使得,函数参数与数组和对象的尾逗号规则保持一致了。

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复