作用域解析和闭包

ricoyu 2010-05-15

作用域解析和闭包
作用域是指对某一属性(变量)或方法(函数)具有访问权 限的代码空间. 在javascript中, 作用域是在函数中进行维护的.

例1
变量myVariable的作用域会被限制在 myFunction () 中. 如果你在这个函数之外访问myVariable, 结果将是无效的
function myFunction (){
  var myVariable = 'inside';
}
myFunction ();
alert(myVariable); //报js错误,
myVariable is not defined,  IE与FF都是这个错误

例2
在 上面的例子中,因为在函数内部使用了var关键字来维护作用域链, 所以执行 myFunction ()函数对于其外部作用域中的 myVariable没有影响.
如果在
myFunction ()内部去掉为 myVariable赋值时使用的var关键字, 那么 myVariable的作 用域将会解析到 myFunction ()的外部,因此会修改到外部的变量.
function myFunction (){
  myVariable = 'inside';
}
var myVariable = 'outside';
myFunction ()
alert(myVariable); //提示'inside'

作用域链是用来描述一种路径的术语,沿着该路径可以确定变量的值(或者当函数被调用时要使用的方法). 当给myVariable赋'inside'值时,
myFunction ()的作用域中没有使用var关键字,因此赋值操 作会沿着作用域链查找到执行 myFunction ()的作用域中(在这种情况下是window对 象内部),并修改其中
myVariable实例的值. 从本质上来说, var关键字决定了哪个函数是特定变量的作用域的终点. 同样的逻辑也适用于取得变量值的情况.

闭包是与作用域相关的一个概念,它指的是内部函数即使在外部函数执行完成并终止之后,仍然可以访问其外部函数的属性. 当引用一个变量或方法时, javascript会沿着由对象
执行路径构成的作用域链对作用域进行解析,查找变量最近定义的值, 一旦找到, 即使用该值.

一个残缺的代码
function initAnchors(W3CEvent){
  for(var i=0;i<=3;i++){
    var anchor = document.getElementById('anchor'+i);
    anchor.addEventListener('click',function(){
      alert('My Id is anchor'+i);
    }, false);
  }
}

window.addEventListener('load', initAnchors, false);
这时, 不论点击哪个a元素, 弹出的都是My Id is anchor4

要 得到正确的结果,需要把事件侦听器的注册转移到一个独立的函数中,并通过该函数的参数传递适当的值:

function registerListener(anchor, i){
  anchor.addEventListener('click', function(){
    alert('My id is anchor'+i);
  }, false);
}

function initAnchors(e){
  for(var i=0;i<=3;i++){
    var anchor = document.getElementById('anchor'+i);
    registerListener(anchor, i);
  }
}
window.addEventListener('load', initAnchors, false);
由于在作用域链中定义了额外的函数和变量, 提示信息中保持了正确的值. 因为click事件侦听器现在的外部作用域变成了registerListener 函数,
该函数的每个实例 的 内部作用域中都为i维护了一个唯一的值.
实例   每次调用registerListener 函数都会生成一个该函数的一个副本,以维护正确的变量作用域.

另外一种方式是使用一个自执行的匿名函数.

function initAnchors(){
  for(var i=0;i<=3;i++){
      (function(x){
      var anchor = document.getElementById('anchor'+x);
        anchor.addEventListener('click', function(){
        alert('My id is anchor'+x);
      }, false);
      })(i);
}

s海若 2010-05-30
老生常谈。不过不知道你是怎么得出这个结论的:
引用
每次调用registerListener 函数都会生成一个该函数的一个副本,以维护正确的变量作用域.

据我所知函数的作用域链是他定义时确定的,调用时只是在作用域链最前端加了个调用对象,并用arguments初始化。
这应该是一个参数传递的问题,比如做如下修改:
function registerListener(anchor,iObj){
  anchor.addEventListener('click', function(){
    alert('My id is anchor'+iObj.i);
  }, false);
} 
function initAnchors(e){ 
  for(var i=0,iObj={};i<=3;i++){
    iObj.i=i; 
    var anchor = document.getElementById('anchor'+i); 
    registerListener(anchor, iObj); 
  } 
} 

registerListener中的iObj还是同一个Object,所以结果和在initAnchors里定义一样。
因为javascript调用函数的时候是传值的,i是原始类型,所以每次调用registerListener都把i当前的值传过去了,自然每次alert出来的形参i都不是同一个i.
ricoyu 2010-05-31
小伙子言辞很犀利。 不过有一点,你的代码根本就不能正常工作。
下面是我用你的代码在FF中的demo,你有兴趣的话将下面的代码保存为一个HTML文件然后在你的浏览器中测试,不过IE版本的demo我没有提供哦~ 自己写一个测试一下吧!
<html>
<head>
<script language="javascript">
function registerListener(anchor,iObj){  
   anchor.addEventListener('click', function(){  
     alert('My id is anchor'+iObj.i);  
   }, false);  
 }   
function initAnchors(e){   
  for(var i=0,iObj={};i<=3;i++){  
    iObj.i=i;   
    var anchor = document.getElementById('anchor'+i);   
    registerListener(anchor, iObj);   
  }   
} 
window.addEventListener('load', initAnchors, false);
</script>
</head>
<body>
<a href="#" id='anchor0'>a1</a>
<a href="#" id='anchor1'>a2</a>
<a href="#" id='anchor2'>a3</a>
<a href="#" id='anchor3'>a4</a>


</body>
</html>
ricoyu 2010-05-31
另外,你可能对for循环没有很好的理解。下面我稍微改动一下你的代码,你再测试一下结果又什么不同。
<html>
<head>
<script language="javascript">
function registerListener(anchor,iObj){  
   anchor.addEventListener('click', function(){  
     alert('My id is anchor'+iObj.i);  
   }, false);  
 }   
function initAnchors(e){   
  for(var i=0;i<=3;i++){  
    var iObj = {};
    iObj.i=i;   
    var anchor = document.getElementById('anchor'+i);   
    registerListener(anchor, iObj);   
  }   
} 
window.addEventListener('load', initAnchors, false);
</script>
</head>
<body>
<a href="#" id='anchor0'>a1</a>
<a href="#" id='anchor1'>a2</a>
<a href="#" id='anchor2'>a3</a>
<a href="#" id='anchor3'>a4</a>
</body>
</html>

我理解你要表达的意思,从你的代码中我看出你有Java等面向对象编程经验,但是可以肯定的是对于JavaScript,你没有一个很好的理解。

然后,你再对比一下经我改动过后的你的有问题的代码和我之前提供的匿名函数版本的那段代码,哪一个更加优雅。
mixmaster 2010-06-08
“函数的一个副本”这个提法确实很模糊,不够准确。

更准确的说法应该是:每次函数被调用时,系统都会创建一个context(上下文)对象,用于存储参数和局部变量。在函数结束时,这个context对象通常会被自动销毁。如果函数内创建了闭包,使得context中某些数据在函数结束后依然被引用,系统会保留context对象,延长其生命期,以使引用持续有效。

我相信你说的“副本”就是指context对象,只是context对象不是函数开始被调用时对当前上下文环境的拷贝,而是系统视情况在函数结束时对当前上下文环境的保留。发生的时间点和实现手段不一样。
ricoyu 2010-06-10
确实是你说的这个意思,我没有表达好。我觉得这篇东西有助于大家对闭包的理解,所以发来跟大家分享。

引用
只是context对象不是函数开始被调用时对当前上下文环境的拷贝,而是系统视情况在函数结束时对当前上下文环境的保留


你的这段话非常精辟,抓住了关键点!
select*from爱 2010-06-28
犀利哥,你抽的是什么牌子的烟?
ricoyu 2010-06-30
以前一般抽中华,不过现在戒了
lvhjean 2010-07-21
学习了。
引用
只是context对象不是函数开始被调用时对当前上下文环境的拷贝,而是系统视情况在函数结束时对当前上下文环境的保留

这个地方有关上下文的描述,的确给自己上了一课。
hyj1254 2010-07-21
每次函数被调用时,系统都会创建一个context(上下文)对象,用于存储参数和局部变量。在函数结束时,这个context对象通常会被自动销毁。如果函数内创建了闭包,使得context中某些数据在函数结束后依然被引用,系统会保留context对象,延长其生命期,以使引用持续有效

精辟,说出了我想说却不能准确地说的话。
Global site tag (gtag.js) - Google Analytics