浅谈javascript执行作用域

An old Cherokee told his grandson “my son, there is a battle between 2 wolves inside us all. One is Evil. It is anger, jealousy, greed, resentment, inferiority, lies and ego. The other is Good. It’s joy, kindness, empathy and truth”. The boy thought about it and asked “Gran, which wolf wins?”. The old man quietly replied “The one you feed”.


最近看看javascript基础知识,今天来粗浅的谈谈javascript里函数作用域。

说到函数作用域,就不得不提到闭包。子函数任意访问父函数的变量,并对其长期持有。父函数外部不能访问内部变量。这就是闭包。

直接上代码:

var globalVariable = 1;
(function() {
  var localVariable = 1;
  this.add = function() {
    localVariable += globalVariable;
    console.log(localVariable);
  }
})();

add(); // 2

上面的代码结构是这样的:

(function() { /* logic goes here */ })();

第一个括号内定义了一个匿名函数(以下叫做一次性匿名函数),第二个括号的作用是立即执行这个函数。上面代码中的this其实是指向外部环境。this上挂载了一个add属性。这个属性的值是一个函数字面量。也就是一次性匿名函数的内部函数。之前提到子函数可以访问父函数变量并长期持有。即使一次性匿名函数执行完后被回收掉,add()仍然是存在的(因为add方法是挂载在全局的)。由于add 长期持有localVariable的引用。我们就可以通过add() 访问localVariable,这就形成了闭包。

实例:

在实际项目里,我就遇到了一个由于变量作用域导致的问题。应用场景大概是这样的:
在一个地图上画一个多边形,我有一个对象数组,这个数组里是每一个对象代表地图上多边形的每个折点。现在需要给每一个折点添加dragend事件,鼠标拖动点到新的位置时,更新对象数组中相应点的经纬度位置。起初我是这么做的:(注:以下仅是一段伪代码)

Points = [pt1, pt2, pt3, pt4, pt5]
//foreach Points as tmpPt, i
for(var i=0; i<Points.length; i++) {
  var tmpPt = Points[i];
  tmpPt.addEventListener('dragend', function(e){
      Points[i].latlng = e.lat + ',' + e.lng;
  });
}

可是执行的时候,无论我改变哪一个点的位置,Points中点的经纬度都没有变化。这是因为当我给每个点注册了事件后,循环结束,i已经变成了Points.length。dragend触发后程序试图修改Points[Points.length](其实这个对象根本就不存在)。

这里真正需要的是在回调函数执行时能够取得该点在数组中的索引值。那么就必须把每次循环里的i变成回调函数的内部参数,使其能够长期持有。我的做法是:

tmpPt.addEventListener('dragend', (function(index){
    return function(e) {
        Points[index].latlng = e.lat + ',' + e.lng;
    }
})(i)
);

注册dragend事件是会立即执行这个一次性匿名函数,把每次循环里的i作为参数传递给一次性执行函数,最后返回一个function (这个返回的function就是回调函数)。此时的i就是一次性匿名函数的内部参数。这里形成了闭包。无论鼠标拖动哪个点,都能够在回调函数中得到正确的索引值。

tips: 学习过程中顺便了解到了函数定义方法 var a = function(){} 和 function a(){} 的区别。 两个函数的调用和功能实现都是一致的。只是function语句(即后者)在解析时会发生被提升的情况。也就是说无论function被放置在哪里,都会被移动到所在作用域的顶层。

console.log(getA); // 输出结果: function getA()
function getA(){
    return 'a';
}

console.log(getB); // 输出结果: undefined
var getB = function(){
    return 'B';
}

function getA(){…}在解析时就会被提到作用域顶层。 但是为了不导致混乱,还是建议大家在写代码是先定义再调用。

参考资料