阅读 563

浅谈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,那么就大错特错了

分析:上述例子其实是一样的 不管有没有存在异步函数(这里的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(){
    // 内容
})()
复制代码
关注下面的标签,发现更多相似文章
评论