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


    聊一下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();

     


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

     

  • 相关阅读:
    spring AOP概述和简单应用
    log4j输出指定功能的log配置方式区别
    java项目配置常见问题
    android 浮动按钮的伸缩效果
    Android之探究viewGroup自定义子属性参数的获取流程
    javaWeb之maven多数据库环境的配置信息
    mybatis generator配置生成代码的问题
    java之初识服务器跨域获取数据
    java之Maven配置和springMvc的简单应用
    UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xe5
  • 原文地址:https://www.cnblogs.com/front-Thinking/p/4317020.html
Copyright © 2020-2023  润新知