编程语言中的变量作用域与闭包

您所在的位置:网站首页 编程语言的作用 编程语言中的变量作用域与闭包

编程语言中的变量作用域与闭包

#编程语言中的变量作用域与闭包| 来源: 网络整理| 查看: 265

编程语言中的变量作用域与闭包2016-05-28编程语言约 3664 字 预计阅读 8 分钟文章目录

【注意】最后更新于 December 4, 2020,文中内容可能已过时,请谨慎使用。

如果你写过 javascript,应该听说过变量提升(hoisting),如果你自诩“Life is short, I use Python”,那么多多少少会用过global、nonlocal这两个关键字。无论新手还是老手,遇到这些时都会觉得很别扭,稍不留神就会出现意想不到的 bug,如果你仔细观察就会发现,它们其实是一个问题:变量作用域的问题。

其次,随着函数式编程的日趋火热,闭包逐渐成为了 buzzword,但我相信没几个人(希望你是那少数人)能够准确概括出闭包的精髓,而其实闭包这一概念也是解决变量作用域问题。

这篇文章首先介绍作用域相关的知识,主要是比较 dynamic scope 与 static(或lexical) scope 语言的优劣势;然后分析 Python 中为什么需要global和nonlocal,Javascript 为什么有变量提升,我这里不仅仅是介绍what,更重要的是why,要知道这两门语言的设计者都是深耕CS领域多年的老手,不会轻易犯错的,肯定有“不为人知”的一面,但遗憾的是网上大部分文章就是解释what,很少有涉及到why的,希望我这篇文章能够填充这一空缺;最后介绍闭包这一重要概念。

作用域

简单来说,作用域限定了程序中变量的查找范围。

在编程语言中有子过程(subroutine,也称为函数、过程)之前,所有的变量都在一个称为“global”的环境中,现在来看这当然是非常不合理,所以在之后有子过程的大部分静态语言(变量的类型不可变)里面,不同的 block(像if、while、for、函数等),具有不同的环境。例如:

1 2 3 4 5 6 7 8 9 10 11 #include int main() { if(1 == 1) { int i = 1; } else { int i = 2; } printf("i = %d\n", i); // error: use of undeclared identifier 'i' return 0; }

上面代码片段是一简单的 C 语言程序,在执行 if 代码块时,会新创建一个环境(称为E1,其外围环境为全局环境E0。见下图),然后在 E1 中定义变量i,在 if 代码块结束后,E1 这个环境就会被删除,这时 main 函数后面的程序就无法访问 if 代码块的变量了。

 if 代码块示意图

上面这一做法符合我们的直观印象,也是比较合理的设计。但是在一些动态语言(变量的类型可以任意改变)中,并没有变量声明与使用的区别,而是在第一次使用时去声明这个变量,像下面这个 Python 示例:

1 2 3 4 5 6 if 1 == 1: i = 1 else: i = 2 print i # 输出 1

在 Python 中,执行 if 代码块时不会去创建新的环境,而是在 if 代码块所处的环境中去执行。

根据我目前所了解到的:

静态语言(C、Java、C#等)具有块级别(block level,包含if、while、for、switch、函数等)的变量作用域;动态语言(Javascript、Python、Ruby等)只具有函数级别(function level)的变量作用域dynamic scope vs. static scope

首先声明一点,这里的dynamic与static是指的变量的作用域,不是指变量的类型,与动态语言与静态语言要区分开。

在上面我们了解到,所有的高级语言都具有函数作用域。我们一般是这样使用函数的,先声明再使用,也就是说函数的声明与使用是分开的,这就涉及到一个问题,函数作用域的外围环境是声明时的还是运行时的呢?不同的外围环境对应不同的语言:

dynamic scope 的语言,函数作用域的外围环境是运行时static scope 的语言,函数作用域的外围环境是声明时

看下面这个 Python 示例:

1 2 3 4 5 6 7 8 9 # foo.py s = "foo" def foo(): print s # bar.py from foo import foo s = "bar" foo() # 输出 foo

上面的示例包括两个文件:foo.py、bar.py,在bar.py中调用foo.py的foo函数,因为 Python 属于 static scope 的语言,所以这时的环境是这样的:

 在 bar 中调用 foo 函数时的环境示意图 在调用 foo 时,会创建一新环境E1,E1 虽然是在 bar 的全局环境中创建的,但是其外围指向的是 foo 的全局环境。在执行 foo 函数时,变量的查找顺序是这样的: 1. 首先在 E1 中找到,找不到就会去其外围环境中去查找;找到则直接返回 2. 在E1外围环境中查找,如果找到直接返回,如果找不到则再在外围环境的外围环境中继续查找,止到外围环境为空(foo、bar 模块的全局环境的外围指向均为空) 3. 去语言内置的变量中去查找,找到则直接返回;找不到就会报错。

static scope 是比较符合正常思维的,也是比较正确的实现方式,否则我们在使用第三份类库时,很容易就会发生变量冲突或覆盖的情况。采用 dynamic scope 的语言都是比较古老的,现在还比较常见的是 Shell,想想大家在写 Shell 时是多痛苦就知道 dynamic scope 是多么反人类了。

JavaScript 中的变量作用域

就像前面说的,Javascript 具有 function level 的 static scope,但是这里有一个常见的问题,具体代码:

1 2 3 4 5 6 7 8 9 10 11 var list = document.getElementById("list"); for (var i = 1; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3