JavaScript的词法作用域问题
多年以前,当我怀揣着前端工程师的梦想时,曾经认真阅读过《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的词法作用域问题的更多相关文章
- 关于JavaScript的词法作用域及变量提升的个人理解
关于JavaScript的作用域,最近听到一个名词:“词法作用域”:以前没有听说过(读书少),记录一下对此的理解,加深印象. 词法作用域:在JavaScript中,一个函数的作用域,在这个函数定义好的 ...
- JavaScript 使用词法作用域,没有动态作用域
function foo() { console.log( a ); } function bar() { var a = 3; foo(); } var a = 2; bar(); 上面的代码,控制 ...
- javascript 欺骗词法作用域
如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来"修改"(也可以说欺骗)词法作用域呢? JavaScript 中有两种机制来实现这个目的.社区普遍认为 ...
- javascript的词法作用域
这个概念是js中相当基础也是极为重要的,很多想当然的错误或感觉怪异的问题都是和这个东西有关.所以,本文主要说下这个名词的概念以及讨论下他牵扯出来的有关变量.函数.闭包的问题. 由变量开始谈 习惯性先来 ...
- 对于javascript的词法作用域的思考
曾经看到过这样一段有意思的程序: var a=3; function scopeTest(){ console.log(a); var a=2; console.log(a); } scopeTest ...
- JavaScript高级之词法作用域和作用域链
主要内容: 分析JavaScript的词法作用域的含义 解析变量的作用域链 变量名提升时什么 一.关于块级作用域 说到JavaScript的变量作用域,与咱们平时使用的类C语言不同. ...
- 网易JS面试题与Javascript词法作用域说明
调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量).函数参数及Arguments对象都在函数内的作用域中--这意味着它们隐藏了作用域链更上层的任何同名的属性. 2010年9月14日, ...
- JavaScript深入之词法作用域和动态作用域
作用域 作用域是指程序源代码中定义变量的区域. 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限. JavaScript 采用词法作用域,也就是静态作用域. 静态作用域与动态作用域 因 ...
- 前端知识体系:JavaScript基础-作用域和闭包-词法作用域和动态作用域
词法作用域和动态作用域 1.作用域: 作用域是指程序代码中定义变量的区域 JavaScript采用词法作用域,也就是静态作用域 2.词法作用域和动态作用域 因为JavaScript采用的是词法作用域, ...
随机推荐
- C语言和C++中的字符串(string)
知识内容: 1.C\C++字符串简述 2.C字符串相关操作 3.C++ string类相关操作 一.C\C++字符串简述 1.C语言字符串 C语言字符串是字符的数组.单字节字符串顺序存放各个字符串,并 ...
- 关于服务器raid的一个记录
今天下午,在装操作系统的时候,特意的测试了下raid1的性能. 1. 开启操作系统 直接正常开启操作系统,操作系统的硬盘做的是raid1,从而数据写俩份,从而在损坏一张盘之后,另外一张盘并不会收到影响 ...
- MyEclipse显示 Install new software 在线安装插件选项
转自:https://blog.csdn.net/greatpresident/article/details/8950869 昨天不知道怎么就删除了电脑中的eclipse 我x,还原不回来了. 今天 ...
- Linux运维常见故障排查和处理的33个技巧汇总
作为linux运维,多多少少会碰见这样那样的问题或故障,从中总结经验,查找问题,汇总并分析故障的原因,这是一个Linux运维工程师良好的习惯.每一次技术的突破,都经历着苦闷,伴随着快乐,可我们还是执着 ...
- Elasticsearch-2.4.3的3节点安装(多种方式图文详解)(含 head、kopf、marvel、shield和watcher插件安装和使用)
前提: Elasticsearch-2.4.3的下载(图文详解) Elasticsearch-2.4.3的单节点安装(多种方式图文详解) 我这里,以192.168.80.10(HadoopMaster ...
- MyBatis 学习记录5 MyBatis的二级缓存
主题 之前学习了一下MyBatis的一级缓存,主要涉及到BaseExecutor这个类. 现在准备学习记录下MyBatis二级缓存. 配置二级缓存与初始化发生的事情 首先二级缓存默认是不开启的,需要自 ...
- PHP - 闭合标签
最最开始的时候经常遇到这个问题,就是如果一个文件里面全部都是php代码的话,我写了前闭合和后闭合的时候,文件一多就容易报错,老是说什么有关输出的错误,貌似大概就是header已经发了. 手册上面这个样 ...
- aws s3 python sdk
http://boto3.readthedocs.io/en/latest/reference/services/s3.html#S3.Client.get_object abort_multipar ...
- 695. Max Area of Island最大岛屿面积
[抄题]: 求最多的联通的1的数量 Given a non-empty 2D array grid of 0's and 1's, an island is a group of 1's (repre ...
- 监控windows
一.zabbix server和zabbix agent(windows)的地址说明 zabbix server的ip为:192.168.1.106 zabbix agent的ip为:192.168. ...