由javascript的闭包引申到程序语言编译上的自由变量作用域的考量
function foo() { var x = 10; return function bar() { console.log(x); }; } // "foo"返回的也是一个function // 并且这个返回的function可以随意使用内部的变量x var returnedFunction = foo(); // 全局变量 "x" var x = 20; // 支持返回的function returnedFunction(); // 结果是10而不是20
EMCAScript使用的是静态作用域/语法作用域[static/lexical scope]。上面的x变量就是在函数bar的语法作用域里搜寻到的是10。如果采用动态作用域[dynamic scope], 那么上述的x应该被解释为20。
// 全局变量 "x" var x = 10; // 全局function function foo() { console.log(x); } (function (funArg) { // 局部变量 "x" var x = 20; // 这不会有歧义 // 因为我们使用"foo"函数的[[Scope]]里保存的全局变量"x", // 并不是caller作用域的"x" funArg(); // 10, 而不是20 })(foo); // 将foo作为一个"funarg"传递下去
同样把外部函数传入内部函数去执行一样存在同样的选择, js使用静态作用域,自由变量是函数创建时候上下文里最近的那个,所以值是10
-------------------------------------------------------------------关于作用域---------------------------------------------------------------------------------
维基百科上的解释:
静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。词法变量有一个确定的作用域。词法变量的作用域可以是一个函数或一段代码段,该变量在这段代码区域内存在;在这段区域以外该变量不存在(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。
大多数现在程序设计语言都是采用静态作用域规则,如C/C++、C#、Python、Java、JavaScript……
相反,采用动态作用域的变量叫做动态变量。只要程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。这意味着如果有个函数f
,里面调用了函数g
,那么在执行g
的时候,f
里的所有局部变量都会被g
访问到。在静态作用域的情况下,g
不能访问f
的变量。动态作用域里,取变量的值时,会由内向外逐层检查函数的调用链,并打印第一次遇到的那个绑定的值。显然,最外层的绑定即是全局状态下的那个值。
采用动态作用域的语言有Emacs Lisp、Common Lisp(兼有静态作用域)、Perl(兼有静态作用域)。
自己的理解,首先明白所谓“自由变量”的概念,就是一个函数非本地定义也不是函数参数的变量。这个变量从哪里取,就衍生了这两种规则。
从实现的角度,所谓静态还是动态作用域,它们对“自由变量”的定位是不一样的。 动态作用域中查找自由变量,是顺着函数调用活动纪录形成的堆栈反向查找(当然这只是一种实现方式,还有别的实现方式),所谓的“动态链”。(就是程序调用顺序会影响变量的定位位置)
静态作用域中查找自由变量,是在函数定义时的环境中查找。为了让静态作用域更直观,可以把函数实现为一个闭包(即包含代码和定义时的环境的一个二元组),这样查找自由变量就方便多了。
-------------------------------------------------------------------关于作用域---------------------------------------------------------------------------------
闭包是一系列代码块(在ECMAScript中是函数),并且静态保存所有父级的作用域。通过这些保存的作用域来搜寻到函数中的自由变量。个人理解狭义的闭包是一个写法风格,将自由变量和函数写在一个代码块里,让静态作用域的概念更直观,满足以下两点就可以理解为狭义的闭包:
- 使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
如果一种语言不采用静态作用域,那闭包也就无从谈起。
而广义的闭包就是任何函数都是闭包,因为函数都在作用域链之中。这里有一个特殊情况就是用Function函数构造new出来的函数,因为这种方式不论在哪里调用都是直接关联到顶层的全局作用域,不存在调用位置的链结构了。
“闭包”要实现将局部变量在上下文销毁后仍然保存下来,基于栈的实现显然是不适用的,因为方法调用栈完了之后那些在栈里创建的变量都要清除。因此在这种情况下,上层作用域的闭包数据是通过动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。
一个例子var data = [];
for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, but not 0 data[1](); // 3, but not 1 data[2](); // 3, but not 2
---------------------------------------------------------------- //换一种写法 var data = []; for (var k = 0; k < 3; k++) { data[k] = (function (x) { return function () { alert(x); }; })(k); // 将k当做参数传递进去 } // 结果正确 data[0](); // 0 data[1](); // 1 data[2](); // 2
----------------------------------------------------------------
//另外的写法
var data = [];
for (var k = 0; k < 3; k++) {
(data[k] = function () {
alert(arguments.callee.x);
}).x = k; // 将k作为函数的一个属性
}
// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2
第一种写法得不到想要的结果,是因为三个函数执行的时候都要去找自由变量k,而k在外部最后被赋值为3,所以结果都是3.
第二种写法对外部函数来说k已经不是自由变量了,是入参,所以结果正确。另外涉及到一个关于内存的知识点,这里js函数参数传值的是值传递,也就是将k的值赋值了一份传入内部函数,当然内部函数的栈里会保存这个值,自然也就不会去外面找了。对内部函数来说,x是自由变量,要到作用域链里去找,就找到了外部函数的入参,这里对内部函数来说相当于是一个闭包:父级作用域有变量,有函数。
第三种写法与闭包无关了,因为与自由变量无关,是把值写死在调用函数的属性上了,这种写法知道就行,玩玩而已。