ES6 - 基础学习(15): 函数的扩展 补充

严格模式

从 ES5开始,函数内部可以设定为严格模式。ES2016做了一些修改,规定只要函数参数使用了默认值解构赋值、或者扩展运算符,则该函数内部就不能显式设定为严格模式,否则会报错。

// ES5下
function doSomething(x, y) {
    'use strict';
    console.log(x, y);
}
doSomething(1, 2);                // 1 2

// ES6下 下列各种函数声明都会报错
// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
function doSomething(x, y = x) {
    'use strict';
    console.log(x, y);
}

// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
var doSomething = function ({x, y}) {
    'use strict';
    console.log(x, y);
};

// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
var doSomething = (...x) => {
    'use strict';
    console.log(x);
};

// Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
var tempObj = {
    doSomething({x, y}) {
        'use strict';
        console.log(x, y);
    }
};

这样规定是因为函数内部的严格模式,同时适用于函数体和函数参数,但 函数在执行的时候,是 先执行函数参数,然后再执行函数体。从而就造成了一个不合理的地方,因为只有执行函数体时,才能知道参数是否应该以严格模式执行,但以上几种情况 参数却先于函数体执行了,所以要报错。

虽然可以先解析函数体代码,再执行参数代码,但这样无疑增加了代码的复杂性。因此,ES6标准索性禁止这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式设定为严格模式。

以下两种方式 可以规避这种限制,但实际上没多大用。

// 1、设定全局严格模式
'use strict';

function doSomething(x, y = x) {
    console.log(x, y);
}
doSomething(1);                    // 1 1


// 2、把函数封装在一个无参数的自执行函数内。
var doSomething = (function () {
    'use strict';
    return function (x = 1) {
        console.log(x);
    };
}());
doSomething();                    // 1

尾调用 及 尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数。尾调用不一定出现在函数尾部,只要是最后一步操作即可。

function tailCallFunc(x) {
    console.log(x);                    // 123
}

function testFunc(x){
    return tailCallFunc(x);            // 尾调用
}
testFunc(123);
尾调用优化

尾调用之所以与其他调用不同,区别在于它特殊的调用位置。

函数调用时会在内存中形成一个“调用记录”,又称“调用帧”(call frame),保存调用函数的 调用位置和内部变量等信息。如果函数A调用了函数B,则在函数A的调用帧上方,会形成一个函数B的调用帧。系统要等到函数B运行结束后,将结果返回到函数A,函数B的调用帧才会消失。

同样,如果函数B内又调用了函数C,则还有一个函数C的调用帧,以此类推。所有的调用帧,就形成了一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不再需要保留外层函数的调用帧,因为调用位置、内部变量等信息都用不到了,直接用内层函数的调用帧,取代外层函数的调用帧就行了。

function g(par) {
    console.log(par);                    // 3
}

function f() {
    let m = 1;
    let n = 2;
    return g(m + n);
}
f();

// 等同于
function f() {
    return g(3);
}
f();

// 等同于
g(3);
// 如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f()的调用帧,只保留g(3)的调用帧。

这就是“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。

如果所有函数都是尾调用,则完全可以在每次尾调用函数执行完毕后,只保留一项调用帧(即内层函数的调用帧),这将很大程度上节约内存,减少调用帧的查询时间 (注:只有不再用到外层函数的内部变量时,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”)。

尾递归优化过的 Fibonacci数列

function fibonacci(n, ac1 = 1, ac2 = 1) {
    return n >1 ? fibonacci(n - 1, ac2, ac1 + ac2) : ac2;
}

console.log(fibonacci(100));                // 573147844013817200000
console.log(fibonacci(1000));               // 7.0330367711422765e+208
console.log(fibonacci(10000));               // Uncaught RangeError: Maximum call stack size exceeded
// "尾调用优化"对递归操作意义重大,因此一些函数式编程语言将其写入到语言规格中。
// ES6亦是如此,第一次明确规定,所有 ECMAScript的实现,都必须部署"尾调用优化"。也就是说 ES6中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),即节约了内存,又维护了系统的稳定性。
递归函数的改写

尾调用 及 尾调用优化的实现,往往需要改写已有的递归函数,确保最后一步操作是调用自身。实现这一点的方法,就是把所有要用到的内部变量全部改写成函数的参数,以参数传入的方式,记住上一次操作的结果。

function factorial(n, total = 1) {
    return n > 1 ? factorial(n - 1, n * total) : total;
}
console.log(factorial(5));                  // 120

递归在本质上也是一种循环操作,纯粹的函数式编程语言没有循环操作命令,所有的循环都是用递归实现的,这也就是为啥 尾递归对这些语言尤为重要的原因。对于其他支持“尾调用优化”的语言(比如Lua,ES6),只需知道递归可以代替循环 即可(虽然可以代替,但实际情况下,还是那种简单用那种,无须强求),而一旦使用递归,就最好使用尾递归。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值