作用域与闭包
如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号?
看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,那么请看下文…
工作模式
在所有的语言中,作用域一般有两种主要的工作模式:词法作用域和动态作用域。词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,不会改变。而动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。javascript是词法作用域工作模式。 看下面的例子体会一下:
function static () {
var foo=1
alert(foo)
}
!function () {
var foo=2
static(); // 如果是词法作用域会打印1,如果是动态作用域则打印2
}();
作用域
在es6之前javascript的作用域只有全局作用域和局部作用域(函数作用域),是没有块级作用域的。在es6中提供了let,可以简单的定义一个块级作用域变量。使用let可以将变量绑定在所在的任意作用域中(通常是{…}内部),也就是说let为其声明的变量隐式的劫持了所在的块级作用域。
为了方便理解作用域,需要知道下面几个概念:
- 自由变量:当前作用域没有的变量就称为自由变量
- 作用域链:当前作用域没定义的变量(自由变量),会逐级向父级作用域寻找
- 父级作用域: 哪个作用域定义了当前作用域,那就是当前作用域的父级作用域
var a = 1
function foo () {
alert(a) // 在foo函数作用域中a就是自由变量,因为在foo中没有定义a,便向父级作用域(此为全局作用域)查找
}
作用域提升
关于作用域提升与js引擎线程运行原理有关,js引擎运行时会执行三步操作,第一步是先检查你的js代码有没有低级的语法错误,第二步是预编译,第三步是根据代码顺序解释一句执行一句。
第一步和第三步都很好理解,重点解释一下第二步预编译,所谓预编译就是在执行代码会把所有的变量声明和函数声明预先处理。当你写了一句var a = 1时,javascript会当成两个操作:var a;和a = 1;第一个是在预编译中执行的,此时只是声明了a这个变量,没有赋值操作,所以此阶段a的值为undefined。
正是因为预编译存在,所以javascript会存在作用域变量提升。看下面的例子可以更好的理解:
console.log(a) // undefined
var a = 1
//上述代码可以这样理解
var a // 此时a的值为undefined
console.log(a)
a = 1
变量提升有两点需要记住:
- 只有声明才会被提升
foo()
foo = function() { // 这里只是赋值表达式,不会被提升
console.log(1)
}
function foo() { // 以function开头定义的函数才是声明,会被提升
console.log(2)
}
// 可以这样理解
function foo() {
console.log(2)
}
foo() // 2
foo = function() {
console.log(1)
}
- 每个作用域都会提升,提升到当前作用域
foo()
function foo () {
console.log(a) // undefinded
var a = 1
}
//可以这样理解
function foo () {
var a // undefined
console.log(a)
a = 1
}
foo()
闭包
对于闭包的定义,各种说法都有,在KYLE SIMPSON著的《你不知道的javascript》中是这样定义的:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前此法作用域之外执行。
还有网络上不同版本的定义,还有的说在函数中定义函数,并返回函数,就是闭包。其实都差不多,各个版本都有一定的道理,但也不一定全对,因为目前还没有一个完美的、得到广泛认可的公认的定义。所以这里我们对闭包的定义也不便做更多的解释。如果你觉得有一个概念定义会对你理解闭包有帮助,我比较推荐《你不知道的javascript》中对闭包的定义。
从下面一个小例子先来认识一下闭包:
function foo () {
var a = 1
function fn() {
console.log(a)
}
return fn()
}
var bar = foo()
bar() // 1
上述就是一个简单的闭包的例子,fn函数可以被执行,并且是在fn函数被定义的词法作用域的外面执行。
通常由于js引擎的垃圾回收机制,一个普通的函数在执行之后内部作用域以及内部变量会被销毁,垃圾机制用来回收释放不再使用内存空间。
正常来说,当foo执行之后,foo函数内部作用域会被销毁,但是闭包就会阻止垃圾回收,事实上内部作用域还存在,因为fn函数还在使用使用foo函数的内部作用域。
到现在为止应该对闭包有个初步的认识了,下面就来回过头去看看最开始预留的问题:如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号?
先看一个错误的示例:
var i = 1
for (i=1;i<=10;i++){
var btn = document.createElement("BUTTON")
btn.innerHTML = i
btn.addEventListener('click',function(event){
alert(i)
})
document.getElementById("div").appendChild(btn)
}
大家可以把上面的代码测试一下,你会发现屏幕上出现了10个按钮,序号从0到9,但是当你点击每一个按钮的时候发现都是弹出11,这是因为当你点击按钮的时候for循环早已经执行完毕,这时i的值已经变成11,当点击执行到alert(i)的时候,发现当前作用域没有i,便去父作用域寻找i,这时i的值为11,所以会打印出11。
那么应该怎样才能达到我们想要的效果呢,我们知道IIFE函数其实也是普通函数,既然是函数就可以可以有自己的作用域,不妨利用IIFE函数来试试:
var i = 1
for (i=1;i<=10;i++){
(function(num){
var btn = document.createElement("BUTTON")
btn.innerHTML = num
btn.addEventListener('click',function(event){
alert(num)
})
document.getElementById("div").appendChild(btn)
})(i)
}
每次循环创建一个IIFE函数,每个IIFE函数都有自己的局部作用域,这里通过向IIFE函数传值的方式在IIFE函数中创建局部变量num,每一个IIFE函数都有自己的num变量,这样在点击执行alert(num)的时候就会在当前作用域找到num。