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

 function createFunctions(){
var result = new Array(); for(var i = 0; i < 10; i++){
result[i] = function() {
return i;
}
}
return result;
} var funcs = createFunctions(); for(var i = 0; i < funcs.length; i++) {
console.log(funcs[i]());
}

表面上看,最终会输出各个元素对应的索引,依次输出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。

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

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

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

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

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

意思差不多。

那么下面这个是闭包吗?

 function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo();

严格来说,这个不是闭包。我的理解是,foo()的调用并不能真正访问到foo作用域里的变量,这和闭包的定义不一样。

 function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();

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

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

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

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

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

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

3. 如何解决

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

  function createFunctions(){
var result = new Array(); for(var i = 0; i < 10; i++){
(function() {
result[i] = function() {
return i;
}
})();
}
return result;
} var funcs = createFunctions(); for(var i = 0; i < funcs.length; i++) {
console.log(funcs[i]());
}

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

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

  function createFunctions(){
var result = new Array(); for(var i = 0; i < 10; i++){
(function() {
var j = i;
result[i] = function() {
return j;
}
})();
}
return result;
} var funcs = createFunctions(); for(var i = 0; i < funcs.length; i++) {
console.log(funcs[i]());
}

再进行改造一番。

  function createFunctions(){
var result = new Array(); for(var i = 0; i < 10; i++){
(function(j) {
result[i] = function() {
return j;
}
})(i);
}
return result;
} var funcs = createFunctions(); for(var i = 0; i < funcs.length; i++) {
console.log(funcs[i]());
}

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

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

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

  function createFunctions(){
var result = new Array(); for(let i = 0; i < 10; i++){ result[i] = function() {
return i;
}
}
return result;
} var funcs = createFunctions(); for(var i = 0; i < funcs.length; i++) {
console.log(funcs[i]());
}

参考资料:

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

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

  1. 关于JavaScript的词法作用域及变量提升的个人理解

    关于JavaScript的作用域,最近听到一个名词:“词法作用域”:以前没有听说过(读书少),记录一下对此的理解,加深印象. 词法作用域:在JavaScript中,一个函数的作用域,在这个函数定义好的 ...

  2. JavaScript 使用词法作用域,没有动态作用域

    function foo() { console.log( a ); } function bar() { var a = 3; foo(); } var a = 2; bar(); 上面的代码,控制 ...

  3. javascript 欺骗词法作用域

    如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来"修改"(也可以说欺骗)词法作用域呢?    JavaScript 中有两种机制来实现这个目的.社区普遍认为 ...

  4. javascript的词法作用域

    这个概念是js中相当基础也是极为重要的,很多想当然的错误或感觉怪异的问题都是和这个东西有关.所以,本文主要说下这个名词的概念以及讨论下他牵扯出来的有关变量.函数.闭包的问题. 由变量开始谈 习惯性先来 ...

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

    曾经看到过这样一段有意思的程序: var a=3; function scopeTest(){ console.log(a); var a=2; console.log(a); } scopeTest ...

  6. JavaScript高级之词法作用域和作用域链

    主要内容: 分析JavaScript的词法作用域的含义 解析变量的作用域链 变量名提升时什么 一.关于块级作用域         说到JavaScript的变量作用域,与咱们平时使用的类C语言不同. ...

  7. 网易JS面试题与Javascript词法作用域说明

    调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量).函数参数及Arguments对象都在函数内的作用域中--这意味着它们隐藏了作用域链更上层的任何同名的属性. 2010年9月14日, ...

  8. JavaScript深入之词法作用域和动态作用域

    作用域 作用域是指程序源代码中定义变量的区域. 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限. JavaScript 采用词法作用域,也就是静态作用域. 静态作用域与动态作用域 因 ...

  9. 前端知识体系:JavaScript基础-作用域和闭包-词法作用域和动态作用域

    词法作用域和动态作用域 1.作用域: 作用域是指程序代码中定义变量的区域 JavaScript采用词法作用域,也就是静态作用域 2.词法作用域和动态作用域 因为JavaScript采用的是词法作用域, ...

随机推荐

  1. sublime中开启表格插入

    1. 导入包 tableedit 2. ctrl+shift +p 输入teecv,然后开启后面连个东西: 3 按tab可以插入表格

  2. 基于七牛Python SDK写的一个批量下载脚本

    前言 上一篇基于七牛Python SDK写的一个同步脚本所写的脚本只支持上传,不支持文件下载. 虽然这个需求不太强烈,但有可能有人(在备份.迁移时)需要,而官方有没提供对应的工具,所以我就把这个功能也 ...

  3. sysbench基准测试工具使用

    1.源码编译安装 源码下载地址(目前有0.4/0.5/1.0三个分支版本):https://github.com/akopytov/sysbench 编译安装: unzip sysbench-1.0. ...

  4. CBV 验证装饰器的使用

    下面是3种方式: from django.shortcuts import render, redirect from django.views import View # Create your v ...

  5. 表单元素(控件)不可见,你用visibility还是display?(转)

    属性大比拼:visibility和display的介绍 今天在做一个表单时涉及到这方面,当选中相应的选项后设置相应的几个元素(控件可见或不可见),后来还是用了visibility来实现.我们先来看下v ...

  6. nginx基本配置与参数说明-【转】

    #运行用户 user nobody; #启动进程,通常设置成和cpu的数量相等 worker_processes 1; #全局错误日志及PID文件 #error_log logs/error.log; ...

  7. Mysql 中的伪列用法

    SELECT ( @rowNO := @rowNo + 1 ) AS rowno, A.*FROM ( SELECT * FROM t_user ) a, ( SELECT @rowNO := 0 ) ...

  8. 【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 16—Recommender Systems 推荐系统

    Lecture 16 Recommender Systems 推荐系统 16.1 问题形式化 Problem Formulation 在机器学习领域,对于一些问题存在一些算法, 能试图自动地替你学习到 ...

  9. Zookeeper的基本概念和重要特性

    目录 1. 什么是Zookeeper 2. Zookeeper集群角色 3. Zookeeper的数据模型 3.1 Znode的类型 3.2 Znode的结构 4. Zookeeper的事件监听机制 ...

  10. 高性能Web服务器Nginx的配置与部署研究(2)Nginx入门级配置与部署及“Hello World”

    1. Nginx 程序包 目前最新的开发版本时1.1.12: Linux/Unix:nginx-1.1.12.tar.gz Windows:nginx-1.1.12.zip 我们可以下载稳定版尝试: ...