01
什么是块级作用域
在例如Java 、C++等语言中,一个 for循环语句中定义了一个变量 i
,那么该变量就只属于这个 for循环语句块中,即循环结束后,自动销毁该语句块中定义的变量。这个 for循环语句形成的一个作用域就叫做块级作用域。
了解过作用域链就能知道,在JavaScript中能形成作用域的就只有函数以及全局。例如下列这个例子
function input() { for(var i=0; i < 10; i++) { console.log(i) } alert(i) //返回10}
我们可以看到,如果 for语句能形成块级作用域,那么 alert(i)
就应该会报错,但最后却成功返回了一个值,说明 for循环是无法形成块级作用域的。
02
如何模仿块级作用域
上文也说了,JavaScript中函数可以形成一个单独的作用域,所以我们就可以通过定义一个匿名函数并进行自调用的方式来模拟出一个块级作用域来。
看一下上述例子改造后是什么样子的
function input() { (function () { for(var i=0; i < 10; i++) { console.log(i) } })(); alert(i) //报错: i is not defined} input()
可以看到,我们定义了匿名函数,并将 for循环语句放到了该匿名函数内,同时对该匿名函数进行了自调用。此时的匿名函数内部就是一个单独的作用域,这就是我们模仿出来的块级作用域,当该匿名函数内部的代码全部执行完毕以后,因为该匿名函数没有被引用,所以垃圾回收机制待会儿会对它进行清除,那么匿名函数中定义的临时变量 i
也自然会被清除了。
紧接着就会进入下一个执行环境,也就是函数 input
所在的作用域内,代码 alert(i)
在当前的执行环境中的作用域链上找不到变量 i
了,所以就会报错 i is not defined
。这样做就达到了让一些只是临时使用的变量在用完后被清除从而减少占用内存的目的。
03
模仿块级作用域的作用
上面说了那么多,也介绍了JavaScript如何使用匿名函数来模仿块级作用域,那么模仿块级作用域到底有什么作用呢?
作用主要就是有两种:
- 减少闭包占用内存的问题
- 避免多人开发时造成的命名冲突
(1)减少闭包占用内存的问题
首先,我们先来看一个简单的例子。假设我们做一个前端页面,页面中有这样一个功能:打开网页时,判断当前的时间,并弹出窗口显示当前时间
<script> var now_time = new Date() alert(now_time)</script>
在这个例子中,获取了当前的时间并赋值给变量 now_time
,然后执行代码 alert(now_time)
。这段代码只会发生在页面刚打开时,并且后续不会再执行了,即变量 now_time
只被用到了一次,因为我们是将该变量定义在全局中的,所以该变量不会被垃圾回收机制清除,也就是说该变量会占用一定的内存。
所以我们可以通过匿名函数的闭包来让这段代码执行完后,将后续没有用的变量或函数被垃圾回收机制销毁掉。
<script> (function() { var now_time = new Date() alert(now_time) })()</script>
仔细阅读这段代码,当执行到该匿名函数时,调用自身,执行函数内的代码,执行完毕后,进入下一个执行环境,而在该匿名函数所形成的作用域内的变量属于局部变量并且没有被引用,所以垃圾回收机制就会清除该变量,释放一定的内存供后续其他变量使用。
(2)避免多人开发时造成的命名冲突
在公司,如果上级派一个开发项目给你以及你所在的开发团队,让你们其中4个人(你 、张三 、李四 、王五)负责开发其中一个前端页面。
第一天张三先在文件 index.js
中写了一些代码;第二天李四也在这个文件中写了一些代码;第三天王五也同样在这个文件中写一些代码;最后到你在这个文件中补充一些功能时,你会发现,之前他们三个写代码时,在全局定义了很多很多的全局变量,这时你在完成功能时就会思考,我定义的变量会不会和他们定义的变量重名啊?如果重名了就会引起一些不必要的bug,而且到时候这些bug会很难被查出来。
例如这个样子
/*-------------文件 index.js----------------*/ /*-----张三写的代码------*/var number = 0setTimeout(() => { number ++ alert(number)}, 1000) /*-----李四写的代码------*/var sum = 0var input = 'hello' /*-----王五写的代码------*/var height = 180var weight = 62 /*-----你写的代码------*/var number = 1 //跟张三定义的变量重名了,引起了bug
那么此时你会想,那每个人在开发时定义的变量前面都加上独有的前缀就好啦,比如张三开发时定义的变量前缀就是 zs-xxxx
、李四开发时定义的变量前缀就是 ls-xxxx
……
但你会发现这样的命名显得很复杂和臃肿,所以此时你就可以通过匿名函数闭包模仿块级作用域来解决这种问题。
我们来改进一下上述的代码
/*-------------文件 index.js----------------*/ /*-----张三写的代码------*/(function() { var number = 0 setTimeout(() => { number ++ alert(number) }, 1000)})() /*-----李四写的代码------*/(function() { var sum = 0 var input = 'hello'})() /*-----王五写的代码------*/(function() { var height = 180 var weight = 62})() /*-----你写的代码------*/(function() { var number = 1 //此时的变量number为局部变量,没有跟张三定义的变量number冲突})()
04
使用匿名函数闭包时的注意点
接下来讲几点使用匿名函数闭包的注意点,大家一定要看一下
(1)匿名函数自调用的理解
有人不懂匿名函数自调用是怎么个意思
(function() { //相关代码})()
其实上面的代码可以通过下面这段代码来理解,但他们不完全相同
var func = function() { //相关代码} func()
其实很简单,就相当于先定义了一个匿名函数,然后调用一下它。但要注意的是,匿名函数要用一个小括号包裹起来,然后再使用一个小括号用于调用该匿名函数
(2)匿名函数自调用前面要加一个分号;
直接来看代码
;(function() { //相关代码})()
在多人开发时,如果用到匿名函数自调用,最好在前面加一个分号,因为有时会遇到这样一个情况
function input() { return function(n) { alert(n) }}var output = input()(function() { //相关代码})()
这个例子中,匿名函数自调用的上一行是 input()
,有时解析代码时可能会把它解析成下面这种情况
var output = input()(function() { //相关代码 })()
这样就变成了,函数的链式调用三次,就会引发奇怪的bug。
所以我们只需要在前面加一个分号 ;
,就可以避免这样的尴尬情况了。
05
结束语
希望这篇文章对你们有所帮助。