一, 箭头函数
ES6 允许使用 "箭头" (=>) 定义函数.
1. 基本用法
- var f = v => v;
- // 等同于
- var f = function (v) {
- return v;
- }
=> 前面的部分是函数的参数,=> 后面的部分是函数的代码块.
(1) 如果箭头函数不需要参数或需要多个参数, 就使用一个圆括号代表参数部分.
- var f = () => 5;
- // 等同于
- var f = function (v) {
- return 5
- };
- var sum = (num1, num2) => num1 + num2;
- // 等同于
- var sum = function(num1, num2){
- return num1 + num2;
- }
(2) 如果箭头函数的代码块部分多余一条语句, 就要使用大括号将它们括起来, 并且使用 return 语句返回.
var sum = (num1,num2) => { return num1 + num2; }
(3) 由于大括号会被解释为代码块, 所以如果箭头函数直接返回一个对象, 就必须在对象外面加上括号, 否则会报错.
- let getTempItem = id => ({ id: id, name: "Temp" });
- getTempItem(1);//{id: 1, name: "Temp"}
- let getTempItem = id => {
- return { id: id, name: "Temp" };
- }
- getTempItem(1);// {id: 1, name: "Temp"}
(4) 箭头函数内部, 可以嵌套箭头函数
- function insert (value) {
- return {into: function (array){
- return {after: function (afterValue) {
- array.splice(array.indexOf(ahterValue)+1, 0, value);
- return array;
- }
- }
- }
- }
- }
- insert(2).into([1, 3]).after(1); //[1, 2, 3]
- // 使用箭头函数改写
- let insert = (value) => ({into: (array) => ({after: (afterValue) => {
- array.splice(array.indexOf(afterValue)+1, 0, value);
- return array;
- }
- })
- });
2. 箭头函数与变量解构结合使用
- const full = ({ first, last }) => first + ' ' + last;
- full({first: 1, last: 2});// "1 2"
- // 等同于
- function full(person) {
- return person.first + ' ' + person.last;
- }
- full({first: 1, last: 2});// "1 2"
3.rest 参数与箭头函数结合
- const numbers = (...nums) => nums;
- numbers(1,2,3,4);// [1, 2, 3, 4]
- const headAndTail = (head, ...tail) => [head, tail];
- headAndTail(1,2,3,4);// [1,[2,3,4]]
4. 箭头函数的优点
(1) 简化代码
- const isEven = n => n%2 == 0;
- isEven(3);// false
- isEven(4);// true
(2) 简化回调函数
- [1, 2, 3].map( x => x*x ); //[1, 4, 9]
- [3,2,5].sort((a, b) => a - b); // [2, 3, 5]
5. 箭头函数使用注意点
ES5 中 this 的指向是可变的, 但是箭头函数中 this 指向固定化. 例如:
- var handler = {
- id: '123',
- init: function () {
- document.addEventListener('click',
- event => this.doSomethisng(event.type), false);
- },
- doSomethisng: function (type) {
- console.log('Handling' + type + 'for' + this.id);
- }
- };
- handler.init();
- //Handling click for 123
上例中, 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 对象), 绑定到右边的函数上面.
- foo::bar;
- // 等同于
- bar.bind(foo);
- foo::bar(...arguments);
- // 等同于
- bar.apply(foo, arguments);
如果双冒号左边为空, 右边是一个对象的方法, 则等于将该方法绑定在该对象上面.
- var method = obj::obj.foo;
- // 等同于
- var method = ::obj.foo;
- let log = ::console.log;
- // 等同于
- var log = console.log.bind(console);
如果双冒号运算符的运算结果, 还是一个对象, 还可以采用链式写法.
三, 尾调用优化
1. 尾调用
尾调用是函数式编程的一个重要概念, 是指某个函数的最后一步是调用另一个函数. 例如:
- function f(x) {
- return g(x);
- }
以下情况不属于尾调用:
- // 情况 1
- function f(x) {
- let y = g(x);
- return y;
- }
- // 情况 2
- function f(x) {
- return g(x) + 1;
- }
- // 情况 3
- function f(x) {
- g(x);
- }
上例中, 情况 1 是调用函数 g 之后, 还有赋值操作, 所以不属于尾调用; 情况 2 也是调用之后还有加法运算, 即使写在了一行内也不属于尾调用; 情况算是没有 return 返回则相当于:
- function f(x) {
- g(x);
- return undefined;
- }
尾调用不一定要出现在函数尾部, 只要是最后一步操作即可.
- function f(x) {
- if(x> 0){
- return m(x);
- }
- return n(x);
- }
上例中, 函数 m 和 n 都属于尾调用, 因为它们都是函数 f 的最后一步.
2. 尾调用优化
函数调用会在内存形成一个 "调用记录", 又称为 "调用帧", 保存调用位置和内部变量等信息.
如果在函数 A 的内部调用函数 B, 那么在 A 的调用帧上方, 还会形成一个 B 的调用帧. 等 B 运行结束, 将结果返回给 A,B 的调用帧才会消失. 如果函数 B 的内部调用函数 C, 那就还有一个 C 的调用帧, 以此类推. 所有的调用帧就会形成一个 "调用栈"
尾调用由于是函数的最后一步操作, 所以不需要保留外层函数的调用帧, 因为调用位置, 内部变量等信息都不会用到了, 只要直接用内层函数的调用帧, 取代外层函数的调用帧即可. 只要不在用到外层函数的内部变量, 内层函数的调用帧才会取代外层函数的调用帧, 否则无法进行 "尾部调用优化".
"尾调用优化" 就是只保留内层函数的调用帧. 如果所有函数都是尾调用, 那么完全可以做到每次执行时, 调用帧只有一项, 这将大大节省内存.
- function addOne(a){
- var one = 1;
- function inner(b){
- return b + one;
- }
- return inner(a);
- }
上例中, 函数不会进行 "尾调用优化", 因为内层函数 inner 用到了外层函数 addOne 的内部变量 one.
注: ES6 的尾调用优化只在严格模式下开启, 正常模式下是无效的.
这是因为在正常模式下, 函数内部有两个变量, 可以跟踪函数的调用栈. --a. func.arguments: 返回调用时函数的参数 --b. func.caller: 返回调用当前函数的那个函数 尾调用优化发生时, 函数的调用栈会改写, 因此上面两个变量就会失真. 严格模式禁用这两个变量, 所以尾调用模式仅在严格模式下生效.
四, 尾递归
1. 尾递归
函数调用自身, 称为递归. 如果尾调用自身, 就称尾递归.
递归调用非常耗费内存, 因为需要同时保存成百上千调用帧, 很容易发生 "栈溢出" 错误. 但对于尾递归来说, 由于只存在一个调用帧, 所有以永远不会发生 "栈溢出" 错误.
- function factorial(n) {
- if(n === 1) return 1;
- return n*factorial(n-1);
- }
- factorial(5);//120
上例中, 计算 n 的阶乘, 最多需要保存 n 个调用记录, 复杂度 O(n). 如果改成尾递归, 只保留一个调用记录, 复杂度 O(1).
- function factorial(n, total){
- if(n === 1) return total;
- return factorial(n - 1, n*total)
- }
- factorial(5, 1);//120
2. 尾递归的改写
尾递归的实现, 往往需要改写递归函数, 确保最后一步只调用自身. 做到这一点的方法, 就是把所有用到的内部变量改写成函数的参数. 例如上面的阶乘函数 factorial 需要用到中间变量 total. 但是这样看起来不直观, 可以使用函数默认值解决这个问题:
- function factorial(n, total = 1){
- if(n === 1) return total;
- return factorial(n - 1, n * total);
- }
- factorial(5);//120
递归的本质是一种循环操作. 纯粹的函数式编程语言没有循环操作命令, 所有的循环都用递归实现.
3. 尾递归优化的实现
尾递归需要优化, 是因为调用栈太多, 造成溢出, 只要减少调用栈, 就不会溢出. 如何减少调用栈, 就是采用 "循环" 替换 "递归".
- // 正常的递归
- function sum(x, y){
- if(y> 0){
- return sum(x + 1, y - 1);
- }else{
- return x;
- }
- }
- sum(1, 100000);// 报错 提示超出调用栈的最大次数.
改写:
- function tco(f){
- var value;
- var active = false;
- var accumulated = [];
- return function accumulator() {
- accumulated.push(arguments);
- if(!active) {
- active = true;
- while (accumulated.length) {
- value = f.apply(this, accumulated.shift())
- }
- active = false;
- return value;
- }
- }
- }
- var sum = tco(function(x, y){
- if(y> 0){
- return sum( x + 1, y - 1 );
- }else{
- return x;
- }
- });
- sum(1, 100000);//100001
上例中, tco 函数是尾递归优化的实现, 它的奥妙就在于状态变量 active. 默认情况下, 这个变量是不激活的. 一旦进入尾递归优化的过程, 这个变量就激活了. 然后, 每一乱递归 sum 都返回 undefined, 就避免了递归执行; 而 accumulated 数组存放每一轮 sum 执行的参数, 总是有值的, 这就保证了 accumulator 函数内部的 while 循环总会执行. 这样就将 "递归" 改成了 "循环", 而后一轮的参数会取代前一轮的参数, 保证了调用栈只有一层.
五, 函数参数的尾逗号
ES2017 运行函数的最后一个参数有尾逗号. 这在此之前, 在函数定义和调用时, 都不允许最后一个参数后面出现逗号, 否则回报错. 如果函数参数写成每个参数占一行, 以后修改代码的时候, 想为函数添加参数或者跳转参数的次序, 就需要在原来的最后一个参数后面添加一个逗号. 这对版本管理系统来说, 就会显示添加逗号的那一行也发生了改变. 这样看上去有点冗余, 所以新语法允许定义和调用时, 尾部直接有一个逗号.
- function f(
- param1,
- param2,
- ){}
这样规定使得, 函数参数与数组和对象的尾逗号规则保持一致了.
来源: http://www.qdfuns.com/article/46690/901914788a2863d498dd411e4800bfa9.html