• JavaScript的词法作用域问题


    多年以前,当我怀揣着前端工程师的梦想时,曾经认真阅读过《JavaScript高级程序设计(第2版)》。里面有一个问题(P147),让我一直百思不得其解。

     1 function createFunctions(){
     2     var result = new Array();
     3     
     4     for(var i = 0; i < 10; i++){
     5         result[i] = function() {
     6             return i;
     7         }
     8     }
     9     return result;
    10 }
    11 
    12 var funcs = createFunctions();
    13 
    14 for(var i = 0; i < funcs.length; i++) {
    15     console.log(funcs[i]());
    16 }

    表面上看,最终会输出各个元素对应的索引,依次输出0,1,2……9。但实际上却是输出10个10

    1. 词法作用域

    简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

    而无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

    最常见的作用域是函数作用域,除此之外,还有其他类型的作用域,例如with,try catch, let, const;

    2. 原因分析

    那么上面这题的原因是什么呢?

    我们预期,在result赋值时,会将i传入到函数中,并保存它的值。这样我们在调用各个元素指向的函数时,就可以获取到赋值时的值,也就是索引值。这显然是不正确的。

    上面我们说到,无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

    我们在给result数组赋值时,仅仅是将函数的引用赋给result的元素,而不是立即执行函数的内容,打印索引值。

    所以,当我们调用函数,执行console.log(i)时,将会从函数定义的位置开始查找其作用域里的变量。

    要注意的是,for循环的块,并不是作用域,所以变量i的作用域,是在整个函数里。也就是说,在createFunctions这个函数的作用域之内,存在一个变量名为i。

    那么调用函数里面找不到i, 就往上找,在createFunctions的作用域里终于找到了i。

    此时i经过循环,已经变成10. 所以无论是调用哪个元素指向的函数,都是打印10。

    下面的例子也是一样的原因。

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

    而这个问题,其实还引入了另一个知识点,闭包。

    什么是闭包?当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

    百度的解释是:闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

    意思差不多。

    那么下面这个是闭包吗?

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

    严格来说,这个不是闭包。我的理解是,foo()的调用并不能真正访问到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();

    这个就是闭包。在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

    而闭包的神奇之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。

    拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

    bar()依然持有对该作用域的引用,而这个引用,就叫作闭包。

    关于闭包,可以参考这个:

    带你一分钟理解闭包--js面向对象编程

    3. 如何解决

    我们很容易想到,如果给每个函数在赋值时,拥有自己的作用域,应该可以解决问题。IIFE可以尝试一下。

     1  function createFunctions(){
     2      var result = new Array();
     3      
     4      for(var i = 0; i < 10; i++){
     5         (function() {
     6              result[i] = function() {
     7                  return i;
     8              }
     9          })();
    10      }
    11      return result;
    12  }
    13  
    14  var funcs = createFunctions();
    15  
    16  for(var i = 0; i < funcs.length; i++) {
    17      console.log(funcs[i]());
    18  }

    这显然是不行的。虽然多了作用域,但其里面并没有i变量,还是得找到上一层作用域,那么找到的仍然是10.

     那么我们就在作用域里加上一个变量吧!

     1  function createFunctions(){
     2      var result = new Array();
     3      
     4      for(var i = 0; i < 10; i++){
     5         (function() {
     6              var j = i;
     7              result[i] = function() {
     8                  return j;
     9              }
    10          })();
    11      }
    12      return result;
    13  }
    14  
    15  var funcs = createFunctions();
    16  
    17  for(var i = 0; i < funcs.length; i++) {
    18      console.log(funcs[i]());
    19  }

    再进行改造一番。

     1  function createFunctions(){
     2      var result = new Array();
     3      
     4      for(var i = 0; i < 10; i++){
     5         (function(j) {
     6              result[i] = function() {
     7                  return j;
     8              }
     9          })(i);
    10      }
    11      return result;
    12  }
    13  
    14  var funcs = createFunctions();
    15  
    16  for(var i = 0; i < funcs.length; i++) {
    17      console.log(funcs[i]());
    18  }

    还有一种是利用ES6的新特性,let关键字来解决。

    仔细思考我们刚才的解决方法,我们使用IIFE在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个作用域。

    而let关键字,可以用来劫持块作用域,本质上这是将一个块成一个可以被关闭的作用域。

     1  function createFunctions(){
     2      var result = new Array();
     3      
     4      for(let i = 0; i < 10; i++){
     5         
     6          result[i] = function() {
     7              return i;
     8          }
     9      }
    10      return result;
    11  }
    12  
    13  var funcs = createFunctions();
    14  
    15  for(var i = 0; i < funcs.length; i++) {
    16      console.log(funcs[i]());
    17  }

    参考资料:

    《你不知道的JavaScript(上卷)》 一本好书啊

  • 相关阅读:
    VC开发,引用win8.1配置
    RabbitMQ——常见的函数介绍
    OpenLayer4——面(多边形)
    OpenLayer4——图形组合
    OpenLayer4——GeoJSON简介
    RabbitMQ——交换机
    RabbitMQ——helloworld
    OpenLayer4——添加Geojson
    RabbitMQ——消息头
    OpenLayer4——圆形
  • 原文地址:https://www.cnblogs.com/kingsleylam/p/9691238.html
Copyright © 2020-2023  润新知