一些连篇累牍的概念对于初学者实在太劝退了。但是相信我,“书读百遍其义自见”是真的有道理的,也许在生产环境中无法直接用到那些怪怪的知识,但这些内容就像英语单词一样,能构建起对知识理解的框架。
废话不多说了,书接上文,今天我们来看看闭包。
理解闭包
闭包的本质
实际来说,闭包的本质就是函数。只不过这个函数绑定了上下文环境(即:函数内部引用变量)。
举个红宝书上的 🌰
var a = 10;
add.onClick = function() {
a++;
span.innerHTML = a;
};
此时如果点击表单中的 add 按钮,那么 span 中的数字便会加一。那如果我改成:
add.onClick = function() {
var a = 10;
a++;
span.innerHTML = a;
};
相信结果你已经猜到了,无论怎么点击这个按钮,span 中的数字都不会变。
其中原因就是js 垃圾回收机制规定:第一次定义全局变量 a 只有当页面关闭时才会被回收,而第二次定义的 a 在函数内部,函数每次执行完毕后都会被回收。因此每次的 a 都会被初始化为 10。
如果将第二个函数改成:
(
function() {
var a = 10;
add.onClick = function() {
a++;
span.innerHTML = a;
};
}
)()
为什么就又可以了呢?其实这里就利用立即执行函数形成了一个闭包环境。闭包最大的特性翻译成白话就是:函数局部的变量享受和全局变量一样的特权。因此可以说:函数和内部能访问到的变量(也叫环境)的总和,就是一个闭包。
闭包的特点
在 js 的垃圾回收机制规定:函数执行后作用域就会被清理,随之回收内存。但由于闭包是建立在函数内部的子函数,即存在上级作用域引用,因此不会随之销毁,将一直存在与内存中。总结一下,闭包的特点为以下:
- 让外部访问函数内部变量成为可能;
- 局部变量会常驻在内存中;
- 可以避免使用全局变量,防止全局变量污染;
- 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)这一点存疑,有兴趣的同学自行去了解下不同浏览器下垃圾回收的机制。
从柯理化看闭包的应用场景
什么是柯理化
柯理化是一个编程语言中的通用概念,是指将接收多个参数的函数变换成接受单一参数的函数,嵌套返回直到所有参数都被使用并返回最终结果。即从 f(a, b, c)
变成 f(a)(b)(c)
的过程。
举个🌰
function add(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
让我们依次调用并查看返回值:
柯理化后的 add 函数返回值会保留之前传入的所有参数并在获得最后一个参数后进行计算。这种方式正是闭包应用的体现:因为存在上级作用域的引用因此一直存在在内存中,直到函数调用最终完成值的计算。
柯理化应用
因为柯理化后的函数会在接收到最后一个参数才进行最后的计算,因此比起一次性接收参数完成所有计算,柯理化延迟了计算,并且,之前传入的参数还能被重复利用,实现参数复用。
例如,当我们需要计算一个圆柱体体积,可通过公式:
function volume(l, w, h) {
return l * w * h;
}
而这时碰巧有一批圆柱体的高度都为 100m,那么按照上方的方法调用时,则每次都需写 100 这个重复的高度:valume(1, 2, 100)
,如果用柯理化改编一下这个方法,就可以省去重复参数带来的重复劳动:
function volume(h) {
return function (l, w) {
return l * w * h;
}
}
// 调用
const height = volume(100);
const v1 = height(1, 2); // 200
const v2 = height(1, 3); // 300
总结
闭包使得柯理化在 js 中得以实现,它能够保持已执行过的函数状态。柯理化不是什么洪水猛兽,它是一种思维方式亦或是方法论,就像我们能够通过柯理化创建添加特定参数的函数,或是重构一些之前不够优雅的函数,是一种解决问题的方法。
10 道面试原题
一起在浏览器里敲敲看,结果会与想法一致吗?
function funA(){
var a = 10;
return function(){
alert(a);
}
}
var b = funA();
b();
每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址。↓
function outerFn(){
var i = 0;
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn();
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2();
函数调用顺序不影响全局变量 i 的值。↓
var i = 0;
function outerFn(){
function innnerFn(){
i++;
console.log(i);
}
return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2();
两个立即执行函数,两个不相关的作用域。↓
function fn(){
var a = 3;
return function(){
return ++a;
}
}
alert(fn()());
alert(fn()());
(function() {
var m = 0;
function getM() { return m; }
function seta(val) { m = val; }
window.g = getM;
window.f = seta;
})();
f(100);
console.info(g());
柯理化:↓
var add = function(x) {
var sum = 1;
var tmp = function(x) {
sum = sum + x;
return tmp;
}
tmp.toString = function() {
return sum;
}
return tmp;
}
alert(add(1)(2)(3));
alert(add(3)(2)(3));
事件处理函数中闭包的写法:↓
var lis = document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++){
(function(i){
lis[i].onclick = function(){
console.log(i);
};
})(i);
}
m1()() 创建了三个作用域,因此,每次的输出都为 1,相当于var mx = m1(); var my = m1(); var mz = m1();
在分别做输出;m2 只声明了一个作用域,因此会累加。↓
function m1(){
var x = 1;
return function(){
console.log(++x);
}
}
m1()();
m1()();
m1()();
var m2 = m1();
m2();
m2();
m2();
最后两道脑经急转弯:↓
function love1(){
var num = 223;
var me1 = function() {
console.log(num);
}
num++;
return me1;
}
var loveme1 = love1();
loveme1();
function fun(n,o) {
console.log(o);
return {
fun:function(m) {
return fun(m,n);
}
};
}
var a = fun(0);
a.fun(1);
a.fun(2);
a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
结论
闭包找到的是同一地址中父级函数中对应变量最终的值。在此结论依据之上,上面的 10 个🌰具体输出什么内容,你有数了吗?留言一起讨论一下吧~
参考:
闭包,看这一篇就够了:https://blog.csdn.net/weixin_43586120/article/details/89456183