【JS基础】从零开始带你理解JavaScript闭包--我是如何彻底搞明白闭包的

在这里插入图片描述


阅读本文大概需要二十分钟

一直有一些刚入门js的朋友问我“什么是闭包?”,这里我就专门总结一下,下次再有人问起来,就直接把这篇文章给他看好了。

为什么闭包这么重要?

因为要想理解闭包的概念,就必须要理解js语言的几个基本特性:执行上下文作用域链(与类C语言的作用域不同!)。所以闭包考察的不仅仅是这一个概念,而是考察的是对js语言基本特性的理解程度。

所以本文将从执行上下文作用域链讲起,在理解了这几个概念之后,再介绍闭包的概念。

一、执行上下文

如果要问到JS的执行顺序,想必有过JS开发经验的开发者都会有一个直观印象:顺序执行。然而,

JavaScript引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”。这里的准备工作,更专业一点的说法,叫做“执行上下文”。

我们先来讲全局环境的“执行上下文”,再讲函数的执行上下文。

1. 全局环境的执行上下文

我们分三种情况来讲全局环境的执行上下文。

(1)第一种情况

我们先来看如下代码:
在这里插入图片描述
这里第一块直接报错,因为根本没有声明变量a,第二三块都输出了undefined,这说明a被定义了,但并没有被赋值,然而定义变量a的代码在运行时是在输出语句之后被执行到,可是变量a被提前声明了。
这说明,在js代码被一行一行执行之前,引擎已经提前做了一些准备操作,这其中就包括对变量的声明,而不是赋值

(2)第二种情况

还是先来看代码:
在这里插入图片描述
有js开发经验的朋友都知道,无论在哪个位置获取this,都可以得到值,只不过根据不同情况,this的值都不同。与第一种情况不同的是:第一种情况只对变量进行声明(并没有赋值),而这种情况直接给this赋值,这也是准备工作做的重要事情之一。

(3)第三种情况

在第三种情况中,需要注意两个概念——‘函数声明’和‘函数表达式’。虽然两者都很常用,但在准备工作中却有着不同的待遇。
在这里插入图片描述
看以上代码,函数声明在准备工作中不但被声明,还被赋值了,而对待函数表达式就像对待变量声明一样,只是声明。
好了,准备工作介绍完毕。

(4)注意函数声明和变量声明的优先级

这里先看一个例子

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();

可以看到,在准备工作中,当变量和函数同时声明时,函数的优先级是更高的。然而,应该明确的是,变量赋值是比函数声明的优先级更高的。


我们总结一下,在准备工作中做了哪些工作:

  • 变量、函数表达式:声明(默认值是undefined)
  • this:声明&赋值
  • 函数:声明&赋值

这些准备工作我们称之为执行上下文

2. 函数的执行上下文

在函数中,除了做如上准备工作,还会做其他准备工作。先来看代码:
在这里插入图片描述
在函数体执行前,arguments和参数x就已经被赋值。从这里可以看出,函数每调用一次,就创建一个执行上下文。因为不同调用可能有不同的参数。

3、执行上下文栈

前面我们讲解了执行上下文,下面我们来讲一下执行上下文栈。执行全局代码时,会先创建一个执行上下文环境;每次调用一个函数时,也会先创建一个执行上下文环境。当函数调用完成,这个执行上下文环境及其创建的数据都会被销毁,并回到全局上下文环境,处于活动的执行上下文只有一个
这其实是一个压栈出栈的过程——执行上下文栈。如下图:
在这里插入图片描述
可以根据如下代码来讲解执行上下文栈:
在这里插入图片描述
这是一种很理想的情况,在实际情况中可能没有办法干净利落地说销毁就销毁。这种情况就是伟大的——闭包。

在介绍闭包之前,还需要讲解下作用域自由变量的概念。



二. 作用域

这部分分为块级作用域立即执行函数作用域和执行上下文的关系作用域链四个部分来讲解。

1. 块级作用域

提到作用域,有一句话大家可能比较熟悉:“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中怎么模拟一个块级作用域呢?就可以用我们接下来要讲到的立即执行函数

2. 立即执行函数

立即执行函数可以模拟块级作用域,防止变量全局污染,同时也是为了立即去执行一个函数。

立即执行函数是指声明完便立即执行的函数,这里函数通常是一次性使用的,因此没必要给函数命名,直接让它执行就好了。

所以,立即执行函数的形式应该如下:

<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未定义的错误。

3. 作用域与执行上下文

作用域和执行上下文是一对一的关系,如下图:
在这里插入图片描述
在执行全局作用域中的代码前,会先创建一个全局作用域的执行上下文;当调用函数时,也会创建一个函数作用域的上下文。
作用域是一个抽象的概念,其中没有变量,要通过作用域对应的执行上下文来获取变量的值。同一个函数,在不同的调用下,会创建不同的执行上下文,所以会产生不同的变量的值。
如果要查找一个作用域下某个变量的值,就需要找个这个作用域的执行上下文,在里面找到对应变量的值。
这个时候,要是在当前作用域的执行上下文中找不到变量该怎么办?这就涉及到了作用域链的概念。

4. 作用域链

我们先来看如下代码:

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
    }

  }

那么什么是闭包呢?闭包就是有权访问另一个函数内部作用域的变量的函数

有了前文的基础,理解闭包的概念就不是那么难了。不过在现实情况中,闭包有两种典型场景一定要知道。

1. 闭包的两种典型应用

第一,函数作为返回值

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)
2. 如何从内存角度理解闭包?
  1. JavaScript具有自动垃圾回收机制,函数运行完之后,其内部变量就会被销毁;
  2. 闭包就是在外部可以访问此函数作用域变量的函数,JavaScript中,只要存在引用函数内部变量的可能,JavaScript就需要在内存中保留这些变量,而且JavaScript运行时需要跟踪这个内部变量的所有外部引用,直到最后一个引用被解除(置为null或者页面关闭),JavaScript垃圾收集器才释放相应的内存空间。

举个例子,

<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. 符合函数式编程规范
    什么是函数式编程?它的思想是:把运算过程尽量写成一系列嵌套的函数调用。举例来说,要想代码中实现数学表达式:
(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);

此外,函数式编程把函数作为“一等公民”。函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值

  1. 延长变量生命周期
    局部变量本来在函数执行完就被销毁,然而闭包中不是这样,局部变量生命周期被延长。不过这也容易使这些数据无法及时销毁,会占用内存,容易造成内存泄漏。如:
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,即解除对其引用,垃圾回收器将回收其占用内存。

六. tips:

  1. 如果闭包只有一个参数,这个参数可以省略,可以直接用it访问该参数。
  2. 实际中闭包常常和立即执行函数结合使用。

七. 参考

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值