// 例子一
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,那么就大错特错了javascript
分析:上述例子实际上是同样的 无论有没有存在异步函数(这里的setTimeout就是异步函数),例子一我执行a6和例子二执行异步函数都是for循环执行完之后再去执行的 因此此时的i值就是10或者6而不是按照咱们想要的结果输出6或者12345,那么如何获得想要的结果呢?请看下面分析java
首先咱们讲到如何获得咱们的效果以前先看看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不在同一个做用域,有各自单独的做用域ui
// 这块代码块是函数表达式 若是该做用域下存在相关引用变量就会与这个函数造成闭包
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 a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
复制代码
这就很诡异了为何都是变量声明差异这么这么大,缘由就是var声明会使变量提高(做用域),上述代码等价于以下:spa
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声明并不能由于压根就造成不了闭包.net
闭包的概念是:《你不知道的JavaScript》书中,对闭包的解释大概是这样的:对函数类型的值进行传递时,保留对它被声明的位置所处的做用域的引用。不少人会误认为闭包就是函数实际否则,闭包是变量和函数做用的代码块code
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关键字声明一个函数,但未给函数命名,因此叫匿名函数,匿名函数属于函数表达式,匿名函数有不少做用,赋予一个变量则建立函数,赋予一个事件则成为事件处理程序或建立闭包等等。
函数声明和函数表达式不一样之处在于,1、Javascript引擎在解析javascript代码时会‘函数声明提高’(Function declaration Hoisting)当前执行环境(做用域)上的函数声明,而函数表达式必须等到Javascirtp引擎执行到它所在行时,才会从上而下一行一行地解析函数表达式,2、函数表达式后面能够加括号当即调用该函数,函数声明不能够,只能以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(){
// 内容
})()
复制代码