1.尾调用
函数 f1 调用函数 f2,如果调用 f2 就是 f1 的最后一步操作的话,那么这就叫做尾调用。
下面二种情况不属于尾调用:
第一种
function f1(x){
f2(x);
}
因为 f1 的最后一步其实是 return undefined;
第二种
function f1(x){
return f2(x) + 1;
}
只有其最后一句是return f2()才算尾调用:
function f1(x){
return f2(x);
}
2.尾调用优化:
因为只有代码执行完毕,其执行期上下文才会被移出执行栈。f1 调用函数 f2,那么执行栈中就会保存这两个执行期上下文。(即使f1、f2是同一个函数)但是,f1 的执行期上下文已经没有任何意义留着了,如果执行引擎聪明一点,它就该把f1的执行期上下文移出,这就叫做尾调用优化。
执行栈和调用栈是同一个东西。参考 wiki、stackoverflow
不幸的是,虽然ES6已经把尾调用优化写进了规范里(需要使用严格模式)。但是时至今日(2020-7-5),真正实现了尾调用优化的执行引擎并不多。比如桌面浏览器里,只有safari13及以上版本实现了。chrome实现了但是又有bug,所以默认不开启。

测试:分别使用safari 13.0或以上和chrome执行下面两段代码
递归求和,未使用尾调用
"use strict";
function sum(n) {
if (n === 0) { return 0}
return n + sum(n-1)
}
console.log(sum(100000))
使用尾调用
"use strict";
function sum(n) {
if (n === 0) { return 0}
return n + sum(n-1)
}
console.log(sum(100000))
3.闭包
从理论上说:闭包指能够访问(即引用了)自由变量的函数,所以一切函数都是闭包。
从实践上说:能够访问已经被销毁的执行期上下文的活动对象的函数。或者直接高程上说的“有权访问另一个函数作用域中的变量的函数。”
自由变量是指在函数中使用的,但既不是此函数参数也不是此函数的局部变量的变量。
function f1() {
var x = 1;
function f2() {
console.log(x)
}
return f2;
}
var f = fn();
f();
f2就是一个闭包。当return f2后,函数f1执行完毕,其上下文f1Context出栈(被销毁),但是由于f2中引用了f1Context.AO中的变量,所以引擎会把f1Context.AO继续留在内存中。这就是闭包访问已经被销毁的执行期上下文的活动对象的原因。
闭包在实际开发中,主要用来提供较好的封装,不污染环境。不然,就为了访问一个内部变量而已,直接把它赋值到全局最简单。比如防抖函数:
不使用闭包
let timer;
function debounced_handleClick() {
clearTimeout(timer)
timer = setTimeout(function() {
console.log('clicked')
}, 1000)
}
btn.addEventListener('click', handleClick)
使用闭包
function debounce(f) {
let timer;
return function() {
clearTimeout(timer)
timer = setTimeout(f, 1000)
}
}
btn.addEventListener('click', debounce(handleClick))
参考
阮一峰 - 尾调用优化
Tail call optimization
How to use Tail Call Optimizations in JavaScript
冴羽 - JavaScript深入之闭包
网友评论