前言:
闭包和作用域,非常重要的一个点。
闭包的官方定义:
在计算机科学中,闭包(也称词法闭包或函数闭包)是指一个函数或函数的引用,与一个引用环境绑定在一起。这个引用环境是一个存储该函数每个非局部变量(也叫自由变量)的表。
闭包不同于一般的函数,它允许一个函数在立即词法作用域外调用时,仍可访问本地变量。
另外本文还有一个大boss是ES3的执行上下文,我会放在最后说。
闭包
一下子不知道怎么用通俗的语言解释,先写一个🌰:
1 | function outer() { |
这就是一个一般的函数。
调用结束之后,局部变量就可以被释放了。
再看一个:
1 | function outer() { |
我们把函数作为了一个返回值,对于这种情况localVal不能被释放。
我们返回的函数仍然可以访问outer函数的局部变量。
这种情况就可以被理解为闭包。
闭包的常见错误: 循环闭包
我们会在很多的闭包文章里面看到这样的代码:
1 | document.body.innerHTML = "<div id='div1'>aaa</div>" + |
你的本意是,为每一个divn元素加上一个点击事件的监听函数,然后第一个点上去输出1,第二个点上输出2,第三个点上输出3.
然而事实是,它们全部输出了4.
这是因为,闭包的特点是,只有在你实际调用它的时候,它才会去寻找要用到的变量的值。
在这个例子里面,当我们去调用监听函数的时候,变量i的值已经变成4了啊。
所以这点要注意,如果我们希望实现这样的效果,最好还是把每个值用不同的变量保存一下,或者说,让函数立即执行。
比如这样:
1 | document.body.innerHTML = "<div id='div1'>aaa</div>" + |
通过这种每次都单独调用的方法,我们使每一个i的值都能够顺利地保存下来。
闭包和封装
闭包可以帮助我们去封装私有变量:
🌰:
1 | (function(){ |
如此,其实多少有一点构造函数的意思,和构造函数不同的是,我们可以自己决定暴露给用户的接口是什么,私有的属性和方法,只要我们不暴露,那么用户除非去看源码,不然是一定找不到的~~。
闭包的局限性
不合理的闭包使用会导致:
- 空间浪费
- 内存泄漏
- 性能消耗
其实这些都是因为你在使用闭包的时候,维持了自由变量的不释放,从而占据了内存导致的~~
Javascript的作用域
Javascript的作用域有以下几种:
- 全局作用域
- 函数作用域
- eval作用域
注意ES5以及之前版本的Js都是没有块级作用域的。
作用域链
作用域链决定了哪些数据能被函数访问。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。
举个例子来说说作用域链:
1 | function outer2() { |
在outer1中,我们可以访问到局部变量local1,全局变量global3和自由变量local2。
这个访问有优先顺序是: local1 -> local2 -> global3。
而new出来的函数,函数体中是无权访问自由变量的。
ES3执行上下文(Execution Context)
简单回顾,js作用域有三种:全局,函数,eval。
说是上下文,其实就是做的一些准备工作。
在“准备工作”中完成了这些工作:
变量、函数表达式——变量声明,默认赋值为undefined;
this——赋值;
函数声明——赋值;
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
事实上,每次函数调用的时候,都会有一个对应的执行环境,就是执行上下文。
对于全局作用域,在进入第一行代码之前,首先也会有一个全局上下文。
举个🌰来理解:
1 | console.log("EC0"); |
这段代码里面,首先我们进入一个全局的EC0,此刻创建出了一个全局上下文EC1,当我们进入EC1的时候,又会进入一个新的上下文EC1,然后我们进入EC2,EC3,当EC3执行完之后,我们会到EC2上下文,EC2执行问之后,再回到EC1上下文,最后回到全局上下文。
就像图里面这样:
变量对象
Js解释器是如何找到我们定义的函数和变量呢?
就是从变量对象中啦!
变量对象(Variable Object)缩写VO,是一个抽象概念中的对象,它用于存储执行上下文中的:
- 变量
- 函数声明
- 函数参数
总结
要理解函数,必须理解作用域,为了理解作用域,本文引出了执行上下文。
理解执行上下文,会对函数各种机制的理解都起到很大的帮助~~