JavaScript:从此不再怕闭包

1,249 阅读5分钟

闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能到达那里。——《你不知道的JavaScript 上卷》

1、起源

js闭包很长一段时间里都是让我头疼的一个东西。工作中遇到类似这样的代码就很怕:

需求

页面内三个按钮,点击按钮控制台输出按钮在所有按钮中的序号,序号从1开始

说明

当然,实际的应用中我们一般不会有这么单纯的需求,也不会写这么刻意的代码,这里我们为了学习,强行挖个坑,自己再填坑。

上代码

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>闭包</title>
</head>
<body>
    点击显示按钮序号<br>
    <button>click</button>
    <button>click</button>
    <button>click</button>
    <script>
        var btns = document.querySelectorAll("button");
        for(var i=0;i<btns.length;i++){
            btns[i].onclick = function(){
                console.log(i+1);
            }
        }
    </script>
</body>
</html>

不懂闭包前,我就觉得,这很优雅啊,按钮在集合btns中的索引+1正好就是满足需求的。兴奋地赶紧自测,咔咔咔连点三下。

结果

点击按钮.png

why.jpg

当时内心表情大概就像上面这个哥们。但还是在工位上故作镇定地赶紧百度了下。

修正

百度和修改后的for循环就变成了这个样子,用一个闭包存一下数组索引i的值传给闭包内的函数。

for(var i=0;i<btns.length;i++){
    btns[i].onclick = (function(tmpI){
       return function(){
            console.log(tmpI+1);
       } 
    })(i);
}

click,click,click。大功告成!

点击按钮修正.png

2、原理

工作中满足了当时的需求也就立马沉迷代码撸下一个功能去了。但对闭包的详细原理知之甚少,相关问题稍微发生点变化,就又可能让自己云里雾里。近来专门找了几本书,刻意攻克了下,才算开始了解了闭包。

如上问题的解析

代码再贴一下:

var btns = document.querySelectorAll("button");
for(var i=0;i<btns.length;i++){
    btns[i].onclick = function(){
        console.log(i+1);
    }
}

上面代码不能按需实现功能的问题在于: 1、for循环内只是将匿名函数引用赋给onclick方法,在函数被调用时才会去实时取i的值 2、i为全局变量,在onclick方法触发时,i的值已被i++的操作变成了4

for(var i=0;i<btns.length;i++){
    btns[i].onclick = (function(tmpI){
       return function(){
            console.log(tmpI+1);
       } 
    })(i);
}

上述代码,onclick方法获得的匿名函数通过立即执行函数返回,立即执行函数内又通过传参的方式,将i的值传入,用tmpI变量保存起来。确保了外部的i++操作不会影响函数内的tmpI的值的变化,就解决了问题。

闭包定义

"闭包(closure)是一个函数在创建时允许自身函数访问并操作该自身函数之外的变量时所创建的作用域。"——《JavaScript 忍者秘籍》

这是我觉得相对来说比较易懂的对闭包的解释。关键要素有两个: 1、函数创建时产生。 2、允许自身访问并操作函数之外的变量。 上述代码return的匿名函数中访问了其函数之外的tmpI变量,形成了闭包。

按书中定义,如下我们大家每天都在写的代码也属于闭包。

var outerValue = "外部变量";

function outerFunction(){
    console.log("outerValue",outerValue);
}
outerFunction();

按定义,outerFunction确实访问了其外的outerValue值,属于闭包。但因为函数定义和外部变量都处在全局作用域中,该作用域从未消失过,所以我们也没觉得这有什么特殊的地方。通过实际的chrome调试发现,chrome也不会标记此类的闭包。以下,我们和chrome保持一致,不再特别说明全局作用域闭包。

再举个栗子

var outerValue ="外部变量";
var later;

function outerFunction(){
	var innerValue = "内部值";

	function innerFunction(paramValue){
		console.log(outerValue,"Inner can see the outerValue.");
		console.log(innerValue,"Inner can see the innerValue.");
		console.log(paramValue,"Inner can see the paramValue.");
		console.log(tooLate,"Inner can see the tooLate.");
	}
	later = innerFunction;
}

console.log(tooLate,"Outer can't see the tooLate.");
var tooLate = "outerFunction之后声明的变量";

outerFunction();
later("later参数");

输出结果如下:

ninja.png

上述例子引用自《JavaScript 忍者秘籍》,笔者做了部分修改。这里innerFunction函数创建时形成了闭包,其访问了outerFunction中的innerValue。 其余部分的代码书中是为了说明闭包的三种更有趣的性质。
1、内部函数的参数是包含在闭包中的。(这是显而易见的)
2、作用域之外的所有变量,即便是函数声明之后的那些声明,也都包含在闭包中。(在调用later时可以访问到tooLate变量)
3、相同的作用域内,尚未声明的变量不能进行提前引用。(如代码中先打印的tooLate为undefined,我觉得这也是显而易见的。)

chrome中对outerFunction闭包的标识如下:

outerFunction闭包.png

在调用outerFunction,定义innerFunction时,访问了innerValue,形成了outerFunction闭包。

3、延伸

按我的理解,js有闭包的概念是因为js设计之初没有块级作用域,只能通过函数来限制变量的有效范围。当有了块级作用域,其实也就不再需要写闭包。如开头例子如果用ES6 let改写,也可实现需求功能。

var btns = document.querySelectorAll("button");
for(let i=0;i<btns.length;i++){
    btns[i].onclick = function(){
        console.log(i+1);
    }
}

读者可以试下,这也是满足需求的。这是因为使用let定义i,ES6做了处理,每次循环的i都有各自的作用域,其值不会互相影响,函数调用时,其仍然保持了原值。

文中内容有错误,或读者对此仍有疑惑的,欢迎大家在评论中留言或者加微信一起探讨学习。

主要参考资料

《你不知道的JavaScript 上卷》——[美]Kyle Simpson
《JavaScript 忍者秘籍》——[美]John Resig,Bear Bibeault