zoukankan      html  css  js  c++  java
  • 聊一下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();

     


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

     

  • 相关阅读:
    Windows server 2016 解决“无法完成域加入,原因是试图加入的域的SID与本计算机的SID相同。”
    Windows Server 2016 辅助域控制器搭建
    Windows Server 2016 主域控制器搭建
    Net Framework 4.7.2 覆盖 Net Framework 4.5 解决办法
    SQL SERVER 2012更改默认的端口号为1772
    Windows下彻底卸载删除SQL Serever2012
    在Windows Server2016中安装SQL Server2016
    SQL Server 创建索引
    C#控制台或应用程序中两个多个Main()方法的设置
    Icon cache rebuilding with Delphi(Delphi 清除Windows 图标缓存源代码)
  • 原文地址:https://www.cnblogs.com/front-Thinking/p/4317020.html
Copyright © 2011-2022 走看看