严格模式
从 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),只需知道递归可以代替循环 即可(虽然可以代替,但实际情况下,还是那种简单用那种,无须强求),而一旦使用递归,就最好使用尾递归。