浅谈var、let、闭包以及当即执行函数(namespace)

首先咱们来看个常见的例子

// 例子一
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循环是如何执行的,再一步一步剖析安全

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

  • 代码块实际就是闭包,因此才保护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声明会使变量提高(做用域),上述代码等价于以下: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(){
    // 内容
})()
复制代码
相关文章
相关标签/搜索