由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#PythonJavaJavaScript……

  相反,采用动态作用域的变量叫做动态变量。只要程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。这意味着如果有个函数f,里面调用了函数g,那么在执行g的时候,f里的所有局部变量都会被g访问到。在静态作用域的情况下,g不能访问f的变量。动态作用域里,取变量的值时,会由内向外逐层检查函数的调用链,并打印第一次遇到的那个绑定的值。显然,最外层的绑定即是全局状态下的那个值。

  采用动态作用域的语言有Emacs LispCommon 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是自由变量,要到作用域链里去找,就找到了外部函数的入参,这里对内部函数来说相当于是一个闭包:父级作用域有变量,有函数。

第三种写法与闭包无关了,因为与自由变量无关,是把值写死在调用函数的属性上了,这种写法知道就行,玩玩而已。

 

posted @ 2014-05-06 16:40  寂静沙滩  阅读(490)  评论(0编辑  收藏  举报