浅谈var、let、闭包以及立即执行函数(namespace)
栏目: JavaScript · 发布时间: 5年前
内容简介:上述答案有同学可能回答 6和12345,那么就大错特错了分析:上述例子其实是一样的 不管有没有存在异步函数(这里的setTimeout就是异步函数),例子一我执行a首先我们讲到如何得到我们的效果之前先看看for循环是如何执行的,再一步一步剖析
// 例子一 var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 // 例子二 for (var i = 1; i <= 5; i++) { setTimeout( function timer(){ console.log(i); // 6 },i*1000)} // for循环执行顺序是先出初始化声明var i=1;再判断i<=5;执行中间{}代码块再执行i++ 复制代码
上述答案有同学可能回答 6和12345,那么就大错特错了
分析:上述例子其实是一样的 不管有没有存在异步函数(这里的setTimeout就是异步函数),例子一我执行a 6 和例子二执行异步函数都是for循环执行完以后再去执行的 所以此时的i值就是10或者6而不是按照我们想要的结果输出6或者12345,那么如何得到想要的结果呢?请看下面分析
首先我们讲到如何得到我们的效果之前先看看for循环是如何执行的,再一步一步剖析
for循环是如何执行的
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6 很神奇的得到我们想要的结果 复制代码
上诉问题得到的答案确实是我们想要的答案,那有可能有人就说了let声明是块级作用域,所以才会得到我们想要是结果,第一块级作用域的概念是: 任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域 哦,概念是这样讲的没错,那么我let声明变量不会再for循环外是访问不到的,对我们得到的有用信息是这样的,然后这块得到的信息对我们解决这个问题就占一小部分,实际的原因是如下:
-
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
-
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc 复制代码
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
- 代码块实际就是闭包,所以才保护i变量不被栈回收
// 这块代码块是函数表达式 如果该作用域下存在相关引用变量就会与这个函数形成闭包 a[i] = function () { console.log(i); }; // 实际相当于 { //进入第一次循环 let i=0; //注意:因为使用let使得for循环为块级作用域,此次let i=0在这个块级作用域中,而不是在全局环境中。 a[0]=function(){ console.log(i); }; //注意:由于循环时,let声明i,所以整个块是块级作用域,那么a[0]这个函数就成了一个闭包。 }// 声明: 我这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。 复制代码
对比var声明
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 复制代码
这就很诡异了为什么都是变量声明差别这么这么大,原因就是var声明会使 变量提升(作用域) ,上述代码等价于如下:
var a = []; var i for (i = 0; i < 10; i++) { // 这块代码块是函数表达式 该作用域下没有存在引用的变量,形成不了闭包 a[i] = function () { console.log(i); // 这里的变量i我们在这个for循环的作用域下找不到该变量就到全局去找发现找到了,而此时i的变量的值为10 }; } a[6](); // 10 复制代码
问题得到解决了如果用let声明我们就可以与a[i] = function () {console.log(i);}形成闭包从而保护这个i变量不被回收(每循环一次声明一个i变量),然而用var声明并不能因为压根就形成不了闭包
什么是闭包
闭包的概念是:《你不知道的JavaScript》书中,对闭包的解释大概是这样的:对函数类型的值进行传递时,保留对它被声明的位置所处的作用域的引用。很多人会误认为闭包就是函数实际不然, 闭包是变量和函数作用的代码块
for (let i = 1; i <= 5; i++) { setTimeout( function timer(){ console.log(i); },i*1000); } 复制代码
上述可以等价于
{ //进入第一次循环 let i=0; //注意:因为使用let使得for循环为块级作用域,此次let i=0在这个块级作用域中,而不是在全局环境中。 setTimeout( function timer(){ console.log(i); },i*1000); //注意:由于循环时,let声明i,所以整个块是块级作用域,那么a[0]这个函数就成了一个闭包。 }// 声明: 我这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。 复制代码
变量i与setTimeout的回调函数形成闭包,从而保护变量不被回收继续存在于栈中我们才能去访问变量i
闭包的其它写法
for (var i = 0; i < 10; i++) { (function(i) { setTimeout(function() { console.log(i) }, 100 * i) })(i) } 复制代码
上面的闭包是有传入的参数i(也是变量)和 立即执行函数 组合形成闭包,而且执行顺序是这样的每次循环遇到这个立即执行函数就立即执行(这个立即执行就是个壳或者环境你可以这样理解)有同学可能看不懂上面这种闭包写法,没关系,上述写法可以理解成:
console.log(i) // undefined for (var i = 0; i < 10; i++) { var j=i //声明一个变量J (function() { setTimeout(function() { console.log(j) }, 100 * j) })() } console.log(j) // 直接报错 复制代码
上面的写法是声明一个变量j,这个变量j很明显就是局部作用域而不是全局的,你在外面访问直接报错,而且每循环一次声明一次,该变量j与立即执行函数形成闭包
上述问题中引入了立即执行函数的概念,有同学可能一脸懵逼,没关系我们理理清楚:
什么是立即执行函数
( function(){…} )()和( function (){…} () )是两种javascript立即执行函数的常见写法,最初我以为是一个括号包裹匿名函数, 再在后面加个括号调用函数,最后达到函数定义后立即执行的目的,后来发现加括号的原因并非如此。要理解立即执行函数,需要先理解一些函数的基本概念
函数声明、函数表达式、匿名函数
函数声明:function fnName () {…};使用function关键字声明一个函数,再指定一个函数名,叫函数声明。
函数表达式 var fnName = function () {…};使用function关键字声明一个函数,但未给函数命名,最后将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式。
匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数属于函数表达式,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。
函数声明和函数表达式不同之处在于,一、Javascript引擎在解析javascript代码时会‘函数声明提升’(Function declaration Hoisting)当前执行环境(作用域)上的函数声明,而函数表达式必须等到Javascirtp引擎执行到它所在行时,才会从上而下一行一行地解析函数表达式,二、函数表达式后面可以加括号立即调用该函数,函数声明不可以,只能以fnName()形式调用 。以下是两者差别的两个例子。
举个例子:
fnName(); function fnName(){ ... }//正常,因为‘提升’了函数声明,函数调用可在函数声明之前 fnName(); var fnName=function(){ ... }//报错,变量fnName还未保存对函数的引用,函数调用必须在函数表达式之后 var fnName=function(){ alert('Hello World'); }();//函数表达式后面加括号,当javascript引擎解析到此处时能立即调用函数 function fnName(){ alert('Hello World'); }();//语法错误,Uncaught SyntaxError: Unexpected token ),这个函数会被js引擎解析为两部分: //1.函数声明 function fnName(){ alert('Hello World'); } //2.分组表达式 () 但是第二部分作为分组表达式语法出现了错误,因为括号内没有表达式,把“()”改为“(1)”就不会报错 //但是这么做没有任何意义,只不过不会报错,分组表达式请见: //分组中的函数表达式http://www.nowamagic.net/librarys/veda/detail/1664 function(){ console.log('Hello World'); }();//语法错误,Uncaught SyntaxError: Unexpected token ( 复制代码
在理解了一些函数基本概念后,回头看看( function(){…} )()和( function (){…} () )这两种立即执行函数的写法, 最初我以为是一个括号包裹匿名函数,并后面加个括号立即调用函数,当时不知道为什么要加括号,后来明白,要在函数体后面加括号就能立即调用,则这个函数必须是函数表达式,不能是函数声明。
举个例子:
function(a){ console.log(a); //报错,Uncaught SyntaxError: Unexpected token ( }(12); (function(a){ console.log(a); //firebug输出123,使用()运算符 })(123); (function(a){ console.log(a); //firebug输出1234,使用()运算符 }(1234)); !function(a){ console.log(a); //firebug输出12345,使用!运算符 }(12345); +function(a){ console.log(a); //firebug输出123456,使用+运算符 }(123456); -function(a){ console.log(a); //firebug输出1234567,使用-运算符 }(1234567); var fn=function(a){ console.log(a); //firebug输出12345678,使用=运算符 }(12345678) //需要注意的是:这么写只是一个赋值语句,即把函数匿名函数function(a){...}()的返回值赋值给了fn,如果函数没有返回值,那么fn为undefined, //下面给出2个例子,用来解答读者的疑惑: var fn=function(a){ console.log(a); //firebug输出12345678,使用=运算符 }(12345678); console.info(fn);//控制台显示为undefined; fn(123);//函数未定义报错,fn is undefiend var fn=function(a){ console.log(a); //firebug输出12345678,使用=运算符 return 111; }(12345678); console.info(fn);//会发现fn就是一个返回值111,而不是一个函数 fn(123);//报错,因为fn不是一个函数 复制代码
-
可以看到输出结果,在function前面加!、+、 -甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、=等运算符,都将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义,告诉javascript引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码。
-
加括号是最安全的做法,因为!、+、-等运算符还会和函数的返回值进行运算,有时造成不必要的麻烦。
-
javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,可以使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量, 而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。
// js引擎执行到这块就会马上执行,跟我们平时写在js文件代码块一样 只不过这样写的好处防止变量污染,也就是立即执行函数可以当做命名空间(namespace)使用 // 立即执行函数就是个壳或者执行空间 (function(){ // 内容 })() 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
松本行弘的程序世界
松本行弘 / 柳德燕、李黎明、夏倩、张文旭 / 人民邮电出版社 / 2011-8 / 75.00元
《松本行弘的程序世界》是探索程序设计思想和方法的经典之作。作者从全局的角度,利用大量的程序示例及图表,深刻阐述了Ruby编程语言的设计理念,并以独特的视角考察了与编程相关的各种技术。阅读《松本行弘的程序世界》不仅可以深入了解编程领域各个要素之间的关系,而且能够学到大师的思考方法。 《松本行弘的程序世界》面向各层次程序设计人员和编程爱好者,也可以供相关技术人员参考。一起来看看 《松本行弘的程序世界》 这本书的介绍吧!