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传入到函数中,并保存它的值。这样我们在调用各个元素指向的函数时,就可以获取到赋值时的值,也就是索引值。这显然是不正确的。

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

所以,当我们调用函数,执行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()依然持有对该作用域的引用,而这个引用,就叫作闭包。

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

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(上卷)》 一本好书啊

原文地址:https://www.cnblogs.com/kingsleylam/p/9691238.html

时间: 2024-08-29 16:56:50

JavaScript的词法作用域问题的相关文章

对于javascript的词法作用域的思考

曾经看到过这样一段有意思的程序: var a=3; function scopeTest(){ console.log(a); var a=2; console.log(a); } scopeTest(); 在控制台上打印结果为: undefined 2 对于第一次输出的undefined感觉到奇怪. 在查了一些资料后发现: javascript的运行其实要分为两个过程,第一个过程是词法分析,第二个过程是执行.在以上的这段程序中var a=3和var a=2这两个语句都可以分别看做是两部分:va

js 函数作用域, 块级作用域和词法作用域

函数作用域, 块级作用域和词法作用域 0 作用域: 0.1 作用域是程序源代码中定义变量的区域. 0.2 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限. 0.3 ECMAScript6之前只有全局作用域和函数作用域. 0.4 JavaScript采用词法作用域(lexical scoping),也就是静态作用域. var scope = "global scope"; function checkscope(){ var scope = "local sc

你不知道的Javascript(上卷)读书笔记之二 ---- 词法作用域

在前一篇文章中,我们把作用域定义为"管理.维护变量的一套规则",接下来是时候来深入讨论一下Js的作用域问题了,首先我们要知道作用域一般有两种主要的工作类型,一种是词法作用域,一种是动态作用域, Javascript采用的是词法作用域, 关于动态作用域的有兴趣的可以自行Google. 1.词法阶段 首先我们要理解"词法阶段"这个词语,我们已经了解到Js存在一个编译阶段,编译阶段的第一步就是分词/词法分析,我们可以简称为"词法阶段" 简单来说,词法作

《你不知道的JavaScript》读书笔记(二)词法作用域

JavaScript 采用的是 词法作用域 的工作模型. 定义 词法化:大部分标准语言编译器的第一个工作阶段叫词法化(单词化),这个过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词意义. 词法作用域:定义在 词法阶段 的作用域. 词法作用域由谁决定:由你在写代码时将 变量 和 块作用域 写在哪里来决定.因此大部分情况下,词法分析器处理代码时会保持作用于不变. [例] function foo(a){ var b = a * 2; function bar(c){ consol

深入理解javascript作用域系列第二篇——词法作用域和动态作用域

× 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作模型导致的,作用域分为词法作用域和动态作用域,分清这两种作用域模型就能够对变量查找过程有清晰的认识.本文是深入理解javascript作用域系列第二篇——词法作用域和动态作用域 词法作用域 第一篇介绍过,编译器的第一个工作阶段叫作分词,就是把由字符组成的字符串分解成

JavaScript词法作用域与调用对象

关于 Javascript 的函数作用域.调用对象和闭包之间的关系很微妙,关于它们的文章已经有很多,但不知道为什么很多新手都难以理解.我就尝试用比较通俗的语言来表达我自己的理解吧. 作用域 Scope Javascript 中的函数属于词法作用域,也就是说函数在它被定义时的作用域中运行而不是在被执行时的作用域内运行.这是犀牛书上的说法.但"定义时"和"执行(被调用)时"这两个东西有些人搞不清楚.简单来说,一个函数A在"定义时"就是 functio

[label][JavaScript]读nowmagic - js词法作用域、调用对象与闭包

原文链接:                 http://www.nowamagic.net/librarys/veda/detail/1305 作用域(scope) JavaScript 中的函数属于词法作用域,也就是说函数在它被“定义时”的作用域中运行,而不是在“被执行时”的作用域内运行. 什么是“定义时”? 什么是 “被执行时”? 定义时: 一个函数A在“定义时”就是 function A() { }这个语句执行的时候,就是定义这个函数的时候. 被执行时: A“被执行时”(被调用的时侯)是

JavaScript词法作用域(你不知道的JavaScript)

JavaScript并不是传统的块级作用域,而是函数作用域! 一.作用域 1. JavaScript引擎在代码执行前会对其进行编译,在这个过程中,像var a = 2 这样的声明会被分解成两个独立的步骤: 第一步(编译阶段):var a 在其作用域中声明新变量.这会在最开始的阶段,也就是代码执行前进行. 第二步(运行阶段):a = 2 会查询变量a(LHS查询)并对其进行赋值. 2. LHS & RHS(当前作用域->上级作用域->...->全局作用域) LHS(左侧):试图找到

javascript基础拾遗——词法作用域

本来是想写js面向对象笔记(二)关于封装的,但是在敲实例代码的时候,发现对作用域这个东西的概念有点模糊,翻阅了犀牛后,有点感觉了,就想着先记录下此时的感受. 之所以取名叫做词法作用域,是这个概念是js中相当基础也是极为重要的,很多想当然的错误或感觉怪异的问题都是和这个东西有关.所以,本文主要说下这个名词的概念以及讨论下他牵扯出来的有关变量.函数.闭包的问题. 1.由变量开始谈 习惯性先来段代码: 1 var x = "globol value";2 var getValue = fun