作用域与闭包如何用js创建10个按钮标签,并在点击每个按钮时打印按钮对应的序号?看到上面的问题,如果你能看出这道题本质上是在考验对作用域的理解,那么恭喜你,你可以不用看这篇文章,说明你对作用域已经理解透彻了,但是如果你看不出这是关于作用域的问题,所以请看下面...工作模式在所有语言中,作用域一般有两种主要的工作模式:词法作用域和动态作用域。词法范围是在词法阶段定义的范围。由写代码时把变量和块作用域写在什么地方决定的,不会变。动态范围不关心函数和范围的声明方式和位置,只关心它们是从哪里调用的。JavaScript是一种词法作用域工作模式。看下面的例子体会:functionstatic(){varfoo=1alert(foo)}!function(){varfoo=2static();//如果是词法作用域,则打印1,如果是动态作用域,则打印2}();es6之前的javascript作用域只有全局作用域和局部作用域(函数作用域),没有块级作用域。es6中提供了let,可以简单的定义一个块级作用域变量。使用let可以在任意作用域(通常在{...}内)绑定变量,也就是说let声明的变量隐式劫持了它所在的块级作用域。为了便于理解作用域,需要了解以下概念:自由变量:不在当前作用域内的变量称为自由变量作用域链:不在当前作用域内定义的变量(自由变量)将一步一步作用于父级域寻找父作用域:哪个作用域定义了当前作用域,即当前作用域的父作用域vara=1functionfoo(){alert(a)//a是一个空闲foo函数作用域中的变量,因为a在foo中没有定义,所以会寻找父作用域(这里是全局作用域)}作用域提升关于作用域提升与js引擎线程的运行原理有关,js引擎会运行时执行三个步骤,第一步检查你的js代码是否有低级语法错误,第二步预编译,第三步解释一句话,按照代码顺序执行一句话。第一步和第三步很容易理解。下面重点讲解预编译的第二步。所谓预编译,就是在代码执行时,所有的变量声明和函数声明都会被预处理。当你写一句话vara=1时,javascript会把它当成两个操作:vara;和一个=1;第一个是预编译执行的,此时只声明了变量a,没有赋值。所以这个阶段a的值是未定义的。正是因为预编译的存在,javascript才会有作用域变量提升。看下面的例子更好理解:console.log(a)//undefinedvara=1//上面的代码可以理解为vara//此时a的值为undefinedconsole.log(a)a=1变量提升需要记住两点:只有声明被提升foo()foo=function(){//这只是一个赋值表达式,没有提升console.log(1)}functionfoo(){//作为函数的开头定义的函数是声明,会被提升console.log(2)}//可以这样理解functionfoo(){console.log(2)}foo()//2foo=function(){console.log(1)}每个作用域都会提升到当前作用域foo()functionfoo(){console.log(a)//undefinedvara=1}//可以这样理解functionfoo(){vara//undefinedconsole.log(a)a=1}foo()闭包闭包有多种定义。在KYLESIMPSON写的《你不知道的javascript》中定义如下:当一个函数可以记住和访问词法作用域存在时,就会产生一个闭包,即使该函数是在当前词法作用域之外执行的。网上也有不同版本的定义,有的说在函数中定义一个函数,返回一个函数就是闭包。其实它们几乎是一样的,每个版本都有一定的道理,但不一定都是对的,因为目前还没有一个完美的、被广泛认可的定义。所以这里不方便多解释闭包的定义。如果你认为一个概念定义可以帮助你理解闭包,我推荐《你不知道的javascript》中的闭包定义。让我们通过下面的小例子来熟悉闭包:functionfoo(){vara=1functionfn(){console.log(a)}returnfn()}varbar=foo()bar()//1上面是一个简单闭包的例子,fn函数是可以执行的,它是在fn函数定义的词法作用域之外执行的。通常由于js引擎的垃圾回收机制,一个普通函数的内部作用域和内部变量在执行后都会被销毁,通过垃圾机制回收和释放不再使用的内存空间。通常,当foo被执行时,foo函数的内部作用域会被销毁,但闭包会阻止垃圾回收。其实内部作用域还是存在的,因为fn函数还是使用了foo函数的内部作用域。至此,你应该对闭包有了初步的了解。回过头来看看开头保留的问题:如何用js创建10个按钮标签,并在点击每个按钮时打印按钮对应的序号?先看一个错误的例子:vari=1for(i=1;i<=10;i++){varbtn=document.createElement("BUTTON")btn.innerHTML=ibtn.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函数试试:vari=1for(i=1;i<=10;i++){(function(num){varbtn=document.createElement("BUTTON")btn.innerHTML=numbtn.addEventListener('click',function(event){alert(num)})document.getElementById("div").appendChild(btn)})(i)}每个循环创建一个IIFE函数,每个IIFE函数有自己的本地范围。这里,在IIFE函数中给IIFE函数传值,在IIFE函数中创建一个局部变量num,每个IIFE函数都有自己的num变量,这样点击执行alert(num)的时候就会在当前范围内找到num。
