1. 闭包
闭包就是一个函数,捕获作用域内的外部绑定。
-
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包” ----《JavaScript权威指南》
-
闭包是指有权访问另一个函数作用域中的变量的函数。 ---- 《Javascript高级程序设计》
-
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。— 《你不知道的JavaScript(上)》
2. 常见闭包两种形式
2.1 函数作为返回值
function connectTest() {
var a = 'OK'
function childFun(childParam) {
return "haha~~~" + a + childParam
}
return childFun
}
var report = connectTest()
console.log(report(''))
函数childFun()的词法作用域能够 访问 \color{#FF0000}{访问} 访问connectTest()的内部作用域。然后将 函数 c h i l d F u n 当做一个值类型 \color{#FF0000}{函数childFun当做一个值类型} 函数childFun当做一个值类型进行传递。
在函数connectTest执行后,其返回值(函数childFun)赋值给report 对象。
当打印执行report()的时候,实际是通过不同的标识符引用调用了内部函数childFun 函数 c h i l d F u n 在自己定义的词法作用域以外的地方执行 \color{#FF0000}{函数childFun在自己定义的词法作用域以外的地方执行} 函数childFun在自己定义的词法作用域以外的地方执行,
在代码最下方延时一下还会打印么?
setTimeout(() => {
console.log(report('!!!'))
}, 3000)
js中有垃圾回收器,函数等执行完之后会释放不再使用的内存空间。当执行完connectTest(),按道理会被销毁,但是由于函数childFun所在,使得该作用域能够一直存活,以供childFun之后任何时间进行引用。 相当于把局部变量驻留在内存中,避免使用了全局变量 \color{#FF0000}{相当于把局部变量驻留在内存中,避免使用了全局变量} 相当于把局部变量驻留在内存中,避免使用了全局变量。但每一个新的闭包都会捕获不一样的值!
2.2 函数作为参数传递
function connectTest() {
var a = 'OK'
function childFun(childParam) {
return "haha~~~" + a + childParam
}
return childFun
}
var report = connectTest()
function test(fn) {
var a = 'test'
fn('')
console.log(fn('!!!test')) // haha~~~OK!!!test
}
test(report)
3. 闭包优缺点
优点:
- 避免全局变量的污染【阻止父作用域被销毁,没有被垃圾回收器释放内存】
- 变量尽量私有化
缺点:
- 使用不佳内存泄露 ,降低程序的性能【驻留在内存中,增大内存使用率】
- 解决方案将保存内层函数对象的变量赋值为null
4. 常见闭包的场景
4.1 回调函数
在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
4.2 模块化
为了最大限度减少暴露捕获变量的风险,JS经常用下面这种模式
var dateYear = (function () {
var year = 2019;
return {
lastYear: function (n) {
return year -= n;
},
futureYear: function (n) {
return year += n;
}
}
})()
var num = 3;
console.log(dateYear.year) // undefined
console.log('2019的前面' + num + '年是:' + dateYear.lastYear(num))
// console.log('2019的后面' + num + '年是:' + dateYear.futureYear(num))
此时year是私有变量,只有这两个函数可以调用,其他任何手段都是无法进行访问,闭包模式提过强大的访问保护。
4.3 循环中的问题
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000);
};
输出的结果是// 5 5 5 5 5.
解释一下为什么5个5,延迟函数的会调会在循环结束时才执行,此时循环已经在5了。
如何需要修改
for (var i = 0; i < 5; i++) {
(function () {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})();
// 修改完善一下
// (function (j) {
// setTimeout(function timer() {
// console.log(j);
// }, j * 1000);
// })(i);
}
此时打印// 0 1 2 3 4
方案二,当然将var换成let,就不存在这回事儿了,【所以说让let代替var】
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000);
};
到此差不多对闭包已经认识了,可以在自己开发的项目中看看有那些闭包~~~
总结:只要希望给给一个函数,保存一个即可反复使用,又不会被外界污染的专属局部变量时,就用闭包
- 闭包也是一个对象
- 闭包就是每次调用外层函数时,临时创建的函数作用域对象。
- 为什么外层函数作用域对象能留下来?因为被内层函数对象的作用域链引用着,
无法释放。
5 递归
5.1 递归的定义
递归通常涉及函数调用自身。
递归常见的两种方式
a. 自身直接调用自身的方法或函数
function searchSelf(someProperty) {
searchSelf(someProperty)
}
b. 间接调用自身的函数
function searchSelf(someProperty) {
myself(someProperty)
}
function myself(someProperty) {
searchSelf(someProperty)
}
每个递归函数都必须要有边界条件,即一个停止的条件,以防止无限递归(同时浏览器
会抛出栈溢出错误 stack overflow error)
递归并不比普通版本更快,反而更慢。但递归容易理解,而且它所需的代面量少。
ES6中使用了尾掉用优化,让递归不会更慢。
(相关尾掉用的参考)