阅读本文大概需要二十分钟
一直有一些刚入门js的朋友问我“什么是闭包?”,这里我就专门总结一下,下次再有人问起来,就直接把这篇文章给他看好了。
因为要想理解闭包的概念,就必须要理解js语言的几个基本特性:执行上下文
、作用域链
(与类C语言的作用域不同!)。所以闭包考察的不仅仅是这一个概念,而是考察的是对js语言基本特性的理解程度。
所以本文将从执行上下文
、作用域链
讲起,在理解了这几个概念之后,再介绍闭包
的概念。
如果要问到JS的执行顺序,想必有过JS开发经验的开发者都会有一个直观印象:顺序执行。然而,
JavaScript引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”。这里的准备工作,更专业一点的说法,叫做“执行上下文”。
我们先来讲全局环境
的“执行上下文”,再讲函数
的执行上下文。
我们分三种情况来讲全局环境的执行上下文。
我们先来看如下代码:
这里第一块直接报错,因为根本没有声明变量a,第二三块都输出了undefined,这说明a被定义了,但并没有被赋值,然而定义变量a的代码在运行时是在输出语句之后被执行到,可是变量a被提前声明了。
这说明,在js代码被一行一行执行之前,引擎已经提前做了一些准备操作
,这其中就包括对变量的声明,而不是赋值
。
还是先来看代码:
有js开发经验的朋友都知道,无论在哪个位置获取this,都可以得到值,只不过根据不同情况,this的值都不同。与第一种情况不同的是:第一种情况只对变量进行声明(并没有赋值),而这种情况直接给this赋值,这也是准备工作
做的重要事情之一。
在第三种情况中,需要注意两个概念——‘函数声明
’和‘函数表达式
’。虽然两者都很常用,但在准备工作
中却有着不同的待遇。
看以上代码,函数声明在准备工作中不但被声明,还被赋值了,而对待函数表达式就像对待变量声明一样,只是声明。
好了,准备工作
介绍完毕。
这里先看一个例子
function test() {
function a() {}
var a;
log(a); //打印出a的函数体
var b;
function b() {}
log(b); //打印出b的函数体
// !注意看,一旦变量被赋值后,将会输出变量
var c = 12
function c() {}
log(c); //12
function d() {}
var d = 12
log(d); //12
}
test();
可以看到,在准备工作
中,当变量和函数同时声明时,函数的优先级是更高的。然而,应该明确的是,变量赋值是比函数声明的优先级更高的。
我们总结一下,在准备工作
中做了哪些工作:
这些准备工作我们称之为执行上下文
。
在函数中,除了做如上准备工作,还会做其他准备工作。先来看代码:
在函数体执行前,arguments和参数x就已经被赋值。从这里可以看出,函数每调用一次,就创建一个执行上下文
。因为不同调用可能有不同的参数。
前面我们讲解了执行上下文
,下面我们来讲一下执行上下文栈
。执行全局代码时,会先创建一个执行上下文环境
;每次调用一个函数时,也会先创建一个执行上下文环境
。当函数调用完成,这个执行上下文环境及其创建的数据都会被销毁,并回到全局上下文环境,处于活动的执行上下文只有一个。
这其实是一个压栈出栈的过程——执行上下文栈。如下图:
可以根据如下代码来讲解执行上下文栈:
这是一种很理想的情况,在实际情况中可能没有办法干净利落地说销毁就销毁。这种情况就是伟大的——闭包。
在介绍闭包之前,还需要讲解下作用域
和自由变量
的概念。
这部分分为块级作用域
、立即执行函数
、作用域和执行上下文的关系
、作用域链
四个部分来讲解。
提到作用域,有一句话大家可能比较熟悉:“JavaScript没有块级作用域,只有函数作用域
”。块级作用域就是大括号“{ }”中间的代码。任何一对大括号中的语句都属于一个块,在这之中定义的变量在括号外无法访问,这叫做块级作用域。例如:
function scopeTest() {
var scope = {};
if (scope instanceof Object) {
var j = 1;
for (var i = 0; i < 10; i++) {
//console.log(i);
}
console.log(i); //输出10
}
console.log(j);//输出1
}
在JavaScript中变量的作用范围是函数级的,所以会在for循环后输出10,在if语句后输出1。
那么在JavaScript中怎么模拟一个块级作用域呢?就可以用我们接下来要讲到的立即执行函数
。
立即执行函数可以模拟块级作用域,防止变量全局污染
,同时也是为了立即去执行一个函数。
立即执行函数是指声明完便立即执行的函数,这里函数通常是一次性使用的,因此没必要给函数命名,直接让它执行就好了。
所以,立即执行函数的形式应该如下:
<script type="text/javascript">
function (){}(); // SyntaxError: Unexpected token (
//引擎在遇到关键字function时,会默认将其当做是一个函数声明,函数声明必须有一个函数名,所以在执行到第一个左括号时就报语法错误了;
(function(){…})();
//在function前面加!、+、 -、=甚至是逗号等或者把函数用()包起来都可以将函数声明转换成函数表达式;我们一般用()把函数声明包起来或者用 =
</script>
虽然立即执行函数是想在定义完函数后直接就调用,但是引擎在遇到关键字function时,会默认将其当做是一个函数声明,函数声明必须要有一个函数名,所以执行到第一个括号就报错了。
正确地定义一个立即执行函数,是应该用括号把函数声明包起来。
此外,实际应用中,立即执行函数还可用来写插件。
<script type="text/javascript">
var Person = (function(){
var _sayName = function(str){
str = str || 'shane';
return str;
}
var _sayAge = function(age){
age = age || 18;
return age;
}
return {
SayName : _sayName,
SayAge : _sayAge
}
})();
//通过插件提供的API使用插件
console.log(Person.SayName('lucy')); //lucy
console.log(Person.SayName());//shane
console.log(Person.SayAge());//18
</script>
那么在Javascript中如何模拟块级作用域呢?举个例子:
function test(){
(function (){
for(var i=0;i<4;i++){
}
})();
alert(i);
}
test();
函数执行完,弹出的是i
未定义的错误。
作用域和执行上下文是一对一的关系,如下图:
在执行全局作用域中的代码前,会先创建一个全局作用域的执行上下文;当调用函数时,也会创建一个函数作用域的上下文。
作用域是一个抽象的概念,其中没有变量,要通过作用域对应的执行上下文来获取变量的值。同一个函数,在不同的调用下,会创建不同的执行上下文,所以会产生不同的变量的值。
如果要查找一个作用域下某个变量的值,就需要找个这个作用域的执行上下文,在里面找到对应变量的值。
这个时候,要是在当前作用域的执行上下文中找不到变量该怎么办?这就涉及到了作用域链的概念。
我们先来看如下代码:
var x = 10
function fn() {
var b = 20
console.log(b + x)
}
在调用fn时,取变量b的值就可以在当前作用域中取,而取变量x的值时,就需要到另一个作用域中取。到哪个作用域中取呢?
很多人说到父作用域中取,其实这种解释会产生歧义。例如:
var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20
f()
}
show(fn)
程序执行完,输出是10,而不是20。这是因为,在当前函数中找不到变量时,要到创建
这个函数的那个作用域中去取值。
想必接下来这个程序的执行结果你应该也知道了。
var a = 'global';
var f = function(){
console.log(a); // 答案是undefined, 想想为什么
var a = 'local';
}
f();
好了,有了以上基础知识,我们接下来来讲解闭包
。
JavaScript语言的特别之处在于:函数内部可以读取全局变量,但函数外部无法访问到函数内部的变量。出于种种原因,我们有时候需要获取函数内部变量的值。正常情况下,这是办不到的!只有通过变通的方法才能实现:在函数内部,再定义一个函数。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
那么什么是闭包呢?闭包就是有权访问另一个函数内部作用域的变量的函数。
有了前文的基础,理解闭包的概念就不是那么难了。不过在现实情况中,闭包有两种典型场景一定要知道。
第一,函数作为返回值
function fn() {
var max = 10
return function bar(x) {
if(x > max) {
console.log(x)
}
}
}
var f1 = fn()
f1(15)
第二,函数作为参数被传递
var max = 10,
fn = function(x) {
if(x > max) {
console.log(x)
}
}
(function(f) {
var maxx = 100
f(15)
})(fn)
举个例子,
<script type="text/javascript">
function outer(){
var a = 1;
function inner(){
return a++;
}
return inner;
}
var abc = outer();
//outer()只要执行过,就有了引用函数内部变量的可能,然后就会被保存在内存中;
//outer()如果没有执行过,由于作用域的关系,看不到内部作用域,更不会被保存在内存中了;
console.log(abc());//1
console.log(abc());//2
//因为a已经在内存中了,所以再次执行abc()的时候,是在第一次的基础上累加的
var def = outer();
console.log(def());//1
console.log(def());//2
//再次把outer()函数赋给一个新的变量def,相当于绑定了一个新的outer实例;
//console.log(a);//ReferenceError: a is not defined
//console.log(inner);//ReferenceError: a is not defined
//由于作用域的关系我们在外部还是无法直接访问内部作用域的变量名和函数名
abc = null;
//由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,释放内存;
</script>
接下来来看一个闭包的经典陷阱——在循环中使用闭包。举个例子,要给10个span元素加上click事件监听,让每个span点击时依次输出0-9。我们可能会这样写:
for (var i = 0; i < spans.length; i++) {
spans[i].onclick = function() {
alert(i);
}
}
可实际上呢?这里每个span标签点击都输出的是10。
为什么会这样呢?
因为内部函数持有了外部函数中变量i的引用,所以i不会被销毁,每个function都返回i,而在执行上述程序时,onclick方法是没有执行的,等到for循环执行完毕,i变为10,每个onclick函数都返回i,i这时已经变为10了。
为了解决这个问题,我们应该用到立即执行函数:
for (var i = 0; i < spans.length; i++) {
(function(e){
spans[e].onclick = function() {
alert(e);
}
})(i)
}
立即执行函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。
(1 + 2) * 3 - 4
传统的写法是:
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函数式编程要求尽量使用函数,把运算过程定义为不用的函数:
var result = subtract(multiply(add(1,2), 3), 4);
此外,函数式编程把函数作为“一等公民”。函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。
function addHandle() {
var element = document.getElementById('myNode');
element.onclick = function() {
alert(element.id);
}
}
onclick保存了一个element的引用,element将不会被回收。
function addHandle() {
var element = document.getElementById('myNode');
var id = element.id;
element.onclick = function() {
alert(id);
}
element = null;
}
此处将element设为null,即解除对其引用,垃圾回收器将回收其占用内存。
http://imweb.io/topic/5665683bd91952db73b41f5e
https://www.cnblogs.com/sspeng/p/6623556.html
http://www.cnblogs.com/dolphinX/archive/2012/09/29/2708763.html
https://segmentfault.com/a/1190000003985390
https://www.jianshu.com/p/0fe03fd2d862
https://segmentfault.com/a/1190000000618597
http://www.cnblogs.com/wangfupeng1988/p/3977924.html
https://www.cnblogs.com/cxying93/p/6103375.html
https://www.cnblogs.com/ZinCode/p/5551907.html
https://blog.csdn.net/weixin_40197429/article/details/79557101
https://www.cnblogs.com/luqin/p/5164132.html