front-Thinking

志存高远,天道酬勤
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

聊一下JS中的作用域scope和闭包closure

Posted on 2015-03-06 00:06  front-Thinking  阅读(4382)  评论(15编辑  收藏  举报

聊一下JS中的作用域scope和闭包closure

  scope和closure是javascript中两个非常关键的概念,前者JS用多了还比较好理解,closure就不一样了。我就被这个概念困扰了很久,无论看别人如何解释,就是不通。不过理越辩越明,代码写的多了,小程序测试的多了,再回过头看看别人写的帖子,也就渐渐明白了闭包的含义了。咱不是啥大牛,所以不搞的那么专业了,唯一的想法就是试图让你明白什么是作用域,什么是闭包。如果看了这个帖子你还不明白,那么多写个把月代码回过头再看,相信你一定会有收获;如果看这个帖子让你收获到了一些东西,告诉我,还是非常开森的。废话不多说,here we go!


 

  1、function

  在开始之前呢,先澄清一点(废话咋这么多捏),函数在JavaScript中是一等公民。什么,你听了很多遍了?!!!。那这里我需要你明白的是,函数在JavaScript中不仅可以调用来调用去,它本身也可以当做值传递来传递去的。


 

  2、scope及变量查询

  作用域,也就是我们常说的词法作用域,说简单点就是你的程序存放变量、变量值和函数的地方。

  块级作用域

  如果你接触过块级作用域,那么你应该非常熟悉块级作用域。简单说来就是,花括号{}括起来的代码共享一块作用域,里面的变量都对内或者内部级联的块级作用域可见。

  基于函数的作用域

  在JavaScript中,作用域是基于函数来界定的。也就是说属于一个函数内部的代码,函数内部以及内部嵌套的代码都可以访问函数的变量。如下:

  上面定义了一个函数foo,里面嵌套了函数bar。图中三个不同的颜色,对应三个不同的作用域。①对应着全局scope,这里只有foo②是foo界定的作用域,包含、b、bar③是bar界定的作用域,这里只有c这个变量。在查询变量并作操作的时候,变量是从当前向外查询的。就上图来说,就是③用到了a会依次查询③、②、①。由于在②里查到了a,因此不会继续查①了。

  这里顺便讲讲常见的两种error,ReferenceError和TypeError。如上图,如果在bar里使用了d,那么经过查询③、②、①都没查到,那么就会报一个ReferenceError;如果bar里使用了b,但是没有正确引用,如b.abc(),这会导致TypeError。

  严格的说,在JavaScript也存在块级作用域。如下面几种情况:

  ①with

1 var obj = {a: 2, b: 2, c: 2};
2 with (obj) { //均作用于obj上
3      a = 5;
4      b = 5;
5      c = 5;  
6 }

  ②let

  let是ES6新增的定义变量的方法,其定义的变量仅存在于最近的{}之内。如下:

var foo = true;
if (foo) {
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
}
console.log( bar ); // ReferenceError

  ③const

  与let一样,唯一不同的是const定义的变量值不能修改。如下:

1 var foo = true;
2 if (foo) {
3     var a = 2;
4     const b = 3; //仅存在于if的{}内
5     a = 3;
6     b = 4; // 出错,值不能修改
7 }
8 console.log( a ); // 3
9 console.log( b ); // ReferenceError!

  


  3、scope的如何确定

  无论函数是在哪里调用,也无论函数是如何调用的,其确定的词法作用域永远都是在函数被声明的时候确定下来的。理解这一点非常重要。


  4、变量名提升

  这也是个非常重要的概念。理解这个概念前,需要了解的是,JS代码的执行过程分为编译过程和执行。举例如下:

 

1 var a = 2;

 

  以上代码其实会分为两个过程,一个是 var a; 一个是 a = 2;  其中var a;是在编译过程中执行的,a =2是在执行过程中执行的。理解了这个,那么你就应该知道下面为何是这样的结果了:

1 console.log( a );//undefined
2 var a = 2;

  其执行效果如下:

1 var a;
2 console.log( a );//undefined
3 a = 2;

  我们看到,变量声明提前了,这就是为什么叫变量名提升了。所以在编译阶段,编译器会将函数里所有的声明都提前到函数体内的上部,而真正赋值的操作留在原来的位置上,这也就是上面的代码打出undefined的原因。需要注意的是,变量名提升是以函数为界的,嵌套函数内声明的变量不会提升到外部函数体的上部。希望你懂这个概念了,如果不懂,可以参考我之前写的《也谈谈规范JS代码的几个注意点》及评论回答部分。


  5、闭包

  了解这些了后,我们来聊聊闭包。什么叫闭包?简单的说就是一个函数内嵌套另一个函数,这就会形成一个闭包。这样说起来可能比较抽象,那么我们就举例说明。但是在距离之前,我们再复习下这句话,来,跟着大声读一遍,“无论函数是在哪里调用,也无论函数是如何调用的,其确定的词法作用域永远都是在函数被声明的时候确定下来的”。

1 function foo() {
2     var a = 2;
3     function bar() {
4         console.log( a ); // 2
5     }
6     bar();
7 }
8 foo();

  我们看到上面的函数foo里嵌套了bar,这样bar就形成了一个闭包。在bar内可以访问到任何属于foo的作用域内的变量。好,我们看下一个例子:

1 function foo() {
2     var a = 2;
3     function bar() {
4         console.log( a );
5     }
6     return bar;
7 }
8 var baz = foo();
9 baz(); // 2

  在第8行,我们执行完foo()后按说垃圾回收器会释放foo词法作用域里的变量,然而没有,当我们运行baz()的时候依然访问到了foo中a的值。这是因为,虽然foo()执行完了,但是其返回了bar并赋给了baz,bar依然保持着对foo形成的作用域的引用。这就是为什么依然可以访问到foo中a的值的原因。再想想,我们那句话,“无论函数是在哪里调用,也无论函数是如何调用的,其确定的词法作用域永远都是在函数被声明的时候确定下来的”。

  来,下面我们看一个经典的闭包的例子:

1 for (var i=1; i<10; i++) {
2     setTimeout( function timer(){
3     console.log( i );
4     },1000 );
5 }

  运行的结果是啥捏?你可能期待每隔一秒出来1、2、3...10。那么试一下,按F12,打开console,将代码粘贴,回车!咦???等一下,擦擦眼睛,怎么会运行了10次10捏?这是肿么回事呢?咋眼睛还不好使了呢?不要着急,等我给你忽悠!

  现在,再看看上面的代码,由于setTimeout是异步的,那么在真正的1000ms结束前,其实10次循环都已经结束了。我们可以将代码分成两部分分成两部分,一部分处理i++,另一部分处理setTimeout函数。那么上面的代码等同于下面的:

 1   // 第一个部分
 2    i++;
 3    ... 
 4    i++; // 总共做10次
 5 
 6    // 第二个部分
 7    setTimeout(function() {
 8       console.log(i);
 9    }, 1000);
10    ...
11    setTimeout(function() {
12       console.log(i);
13    }, 1000); // 总共做10次

  看到这里,相信你已经明白了为什么是上面的运行结果了吧。那么,我们来找找如何解决这个问题,让它运行如我们所料!

  因为setTimeout中的匿名function没有将 i 作为参数传入来固定这个变量的值, 让其保留下来, 而是直接引用了外部作用域中的 i, 因此 i 变化时, 也影响到了匿名function。其实要让它运行的跟我们料想的一样很简单,只需要将setTimeout函数定义在一个单独的作用域里并将i传进来即可。如下:

1 for (var i=1; i<10; i++) {
2     (function(){
3      var j = i;
4      setTimeout( function timer(){
5           console.log( j );
6      }, 1000 );
7     })();
8 }

  不要激动,勇敢的去试一下,结果肯定如你所料。那么再看一个实现方案:

1 for (var i=1; i<10; i++) {
2     (function(j){
3         setTimeout( function timer(){
4             console.log( j );
5         }, 1000 );
6     })( i );
7 }

  啊,居然这么简单啊,你肯定在这么想了!那么,看一个更优雅的实现方案:

1 for (let i=1; i<=10; i++) {
2     setTimeout( function timer(){
3         console.log( i );
4     }, 1000 );
5 }

  咦?!肿么回事呢?是不是出错了,不着急,我这里也出错了。这是因为let需要在strict mode中执行。具体如何使用strict mode模式,自行谷歌吧!


  6、运用

  撤了这么多,你肯定会说,这TM都是废话啊!囧,那么下面就给你讲一个用处的例子吧,也作为本文的结束,也作为一个思考题留给你,看看那里用到了闭包及好处。

 1 function Person(name) {
 2     function getName() {
 3         console.log( name );
 4     }
 5     return {
 6         getName: getName
 7     };
 8 }
 9 var littleMing = Person( "fool" );
10 littleMing.getName();

 


 哎,码了个把小时文字,也是挺累的啊!凑巧你看到这个文章了,又凑巧觉得有用,赞一个呗!(欢迎吐槽!)