原文:http://blogs.msdn.com/b/jscript/archive/2007/07/26/scope-chain-of-jscript-functions.aspx


在JavaScript中,函数的作用域链是一个很难理解的东西.这是因为,JavaScript中函数的作用域链和其他语言比如C, C++中函数的作用域链相差甚远.本文详细解释了JavaScript中与函数的作用域链相关的知识,理解这些知识可以帮助你在处理闭包的时候避免一些可能出现的问题.

在JavaScript中,函数可以让你在一次调用中执行一系列的操作.有多种方式来定义一个函数,如下:

函数声明:

function maximum(x, y) {
if (x > y) return x;
else return y;
} maximum(5, 6) //返回6;

这种语法通常用来定义全局作用域下的函数(全局函数).

函数表达式:

var obj = new Object();
obj.maximum = function (x, y) {
if (x > y) return x;
else return y;
};

obj.maximum(5, 6) //返回6;

这种语法通常用来定义一个作为对象方法的函数.

Function构造函数:

var maximum = new Function("x", "y", "if(x > y) return x; else return y;");
maximum(5, 6); //返回6;

以这种形式定义函数通常没有很好的可读性(没有缩进),只在特定情况下使用.

函数定义:

函数定义指的是在JavaScript引擎内部创建一个函数对象的过程.如果是全局函数的话,这个函数对象会作为属性添加到全局对象上,如果是内部函数(嵌套函数)的话,该函数对象会作为属性添加到上层函数的活动对象上,属性名就是函数名.需要指出的是,如果函数是以函数声明的方式定义的,则函数的定义操作会发生在脚本解析的时候.如下例中,当JavaScript引擎完成脚本解析时,就已经创建了一个函数对象func,该函数对象作为属性添加到了全局对象中,属性名为"func".

/*func函数可以被访问到,因为在脚本开始执行前func函数就已经存在了.*/
alert(func(2)); //返回8

//执行该语句会覆盖func的值为true.
var func = true;

alert(func); //返回"true";

/*在脚本开始执行前,解析下面的语句就会定义一个函数对象func.*/
function func(x) {
return x * x * x;
}

在下面的例子中,存在内部函数的情况.内部函数innerFn的定义操作发生在外部函数outerFn执行的时候(其实也是发生在执行前的解析阶段),同时,内部函数会作为属性添加到外部函数的活动对象上.

function outerFn() {
function innerFn() {}
}
outerFn(); //执行outerFn函数的时候会定义一个函数innerFn

:  对于使用Function构造函数定义的函数来说,函数定义操作就发生在执行Function构造函数的时候.

作用域链:

函数的作用域链是由一系列对象(函数的活动对象+0个到多个的上层函数的活动对象+最后的全局对象)组成的,在函数执行的时候,会按照先后顺序从这些对象的属性中寻找函数体中用到的标识符的值(标识符解析).函数会在定义时将它们各自所处环境(全局上下文或者函数上下文)的作用域链存储到自身的[[scope]]内部属性中. 首先看一个内部函数的例子:

function outerFn(i) {
return function innerFn() {
return i;
}
}
var innerFn = outerFn(4);
innerFn(); //返回4

当innerFn函数执行时,成功返回了变量i的值4,但变量i既不存在于innerFn函数自身的局部变量中,也不存在于全局作用域中.那么变量i的值是从哪儿得到的? 你也许认为内部函数innerFn的作用域链是由innerFn函数的活动对象+全局对象组成的.但这是不对的,只有全局函数的作用域链包含两个对象,这并不适用于内部函数.让我们先分析全局函数,然后再分析内部函数.

全局函数:

全局函数的作用域链很好理解.

var x = 10;
var y = 0; function testFn(i) {
var x = true;
y = y + 1;
alert(i);
}
testFn(10);

全局对象: JavaScript引擎在脚本开始执行之前就会创建全局对象,并添加到一些预定义的属性"Infinity", "Math"等.在脚本中定义的全局变量也会成为全局对象的属性.

活动对象当JavaScript引擎调用一些函数时,该函数会创建一个新的活动对象,所有在函数内部定义的局部变量以及传入函数的命名参数和arguments对象都会作为这个活动对象的属性.这个活动对象加上该函数的[[scope]]内部属性中存储的作用域链就组成了本次函数调用的作用域链.

内部函数:

让我们分析一下下面的JavaScript代码.

function outerFn(i, j) {
var x = i + j;
return function innerFn(x) {
return i + x;
}
}
var func1 = outerFn(5, 6);
var func2 = outerFn(10, 20);
alert(func1(10)); //返回15
alert(func2(10)); //返回20

在调用func1(10)和func2(10)时,你引用到了两个不同的i .这是怎么回事?首先看下面的语句,

var func1 = outerFn(5,6);

调用outerFn (5, 6)的时候定义了一个新的函数对象innerFn,然后该函数对象成为了outerFn函数的活动对象的一个属性.这时innerFn的作用域链是由outerFn的活动对象和全局对象组成的. 这个作用域链存储在了innerFn函数的内部属性[[scope]]中,然后返回了该函数,变量func1就指向了这个innerFn函数.

alert(func1(10));//返回15

在func1被调用时,它自身的活动对象被创建,然后添加到了[[scope]]中存储着的作用域链的最前方(新的作用域链,并不会改变[[scope]]中存储着的那个作用域链).这时的作用域链才是func1函数执行时用到的作用域链.从这个作用域链中,你可以看到变量‘i’的值实际上就是在执行outerFn(5,6)时产生的活动对象的属性i的值.下图显示了整个流程.

现在让我们回到问题,"在调用func1(10)和func2(10)时,你引用到了两个不同的i .这是怎么回事?".让我们从下图中看一下func2执行时的情况,答案就是在定义func1和func2时,函数outerFn中产生过两个不同的活动对象.

现在又出现了一个问题, 一个活动对象在函数执行的时候创建,但在函数执行完毕返回的时候不会被销毁吗? 我用下面的三个例子来讲解这个问题.

i) 没有内部函数的函数

function outerFn(x) {
return x * x;
}
var y = outerFn(2);

如果函数没有内部函数,则在该函数执行时,当前活动对象会被添加到该函数的作用域链的最前端.作用域链是唯一引用这个活动对象的地方.当函数退出时,活动对象会被从作用域链上删除,由于再没有任何地方引用这个活动对象,则它随后会被垃圾回收器销毁.

ii) 包含内部函数的函数,但这个内部函数没有被外部函数之外的变量所引用

function outerFn(x) {
//在outerFn外部没有指向square的引用
function square(x) {
return x * x;
}
//在outerFn外部没有指向cube的引用
function cube(x) {
return x * x * x;
}
var temp = square(x);
return temp / 2;
}
var y = outerFn(5);

在这种情况下,函数执行时创建的活动对象不仅添加到了当前函数的作用域链的前端,而且还添加到了内部函数的作用域链中.当该函数退出时,活动对象会从当前函数的作用域链中删除,活动对象和内部函数互相引用着对方,outerFn函数的活动对象引用着嵌套的函数对象square和cube,内部函数对象square和cube的作用域链中引用了outerFn函数的活动对象.但由于它们都没有外部引用,所以都将会被垃圾回收器回收.

iii)  包含内部函数的函数,但外部函数之外存在指向这个内部函数的引用

例1:

function outerFn(x) {
//内部函数作为outerFn的返回值被引用到了外部
return function innerFn() {
return x * x;
}
} //引用着返回的内部函数
var square = outerFn(5);
square();

例2:

var square;

function outerFn(x) {
//通过全局变量引用到了内部函数
square = function innerFn() {
return x * x;
}
}
outerFn(5);
square();

在这种情况下,outerFn函数执行时创建的活动对象不仅添加到了当前函数的作用域链的前端,而且还添加到了内部函数innerFn的作用域链中(innerFn的[[scope]]内部属性).当外部函数outerFn退出时,虽然它的活动对象从当前作用域链中删除了,但内部函数innerFn的作用域链仍然引用着它. 由于内部函数innerFn存在一个外部引用square,且内部函数innerFn的作用域链仍然引用着外部函数outerFn的活动对象,所以在调用innerFn时,仍然可以访问到outerFn的活动对象上存储着的变量x的值.

多个内部函数:

更有趣的场景是有不止一个的内部函数,多个内部函数的作用域链引用着同一个外部函数的活动对象.该活动对象的改变会反应到三个内部函数上.

function createCounter(i) {
function increment() {
++i;
} function decrement() {
--i;
} function getValue() {
return i;
} function Counter(increment, decrement, getValue) {
this.increment = increment;
this.decrement = decrement;
this.getValue = getValue;
}
return new Counter(increment, decrement, getValue);
}
var counter = createCounter(5);
counter.increment();
alert(counter.getValue()); //返回6

上图表示了createCounter函数的活动对象被三个内部函数的作用域链所共享.

闭包以及循环引用:

上面讨论了JavaScript中函数的作用域链,下面谈一下在闭包中可能出现因循环引用而产生内存泄漏的问题.闭包通常指得是能够在外部函数外面被调用的内部函数.下面给出一个例子:

function outerFn(x) {
x.func = function innerFn() {}
}
var div = document.createElement("DIV");
outerFn(div);

在上例中,一个DOM对象和一个JavaScript对象之间就存在着循环引用. DOM 对象div通过属性‘func’引用着内部函数innerFn.内部函数innerFn的作用域链(存储在内部属性[[scope]]上)上的活动对象的属性‘x’ 引用着DOM对象div. 这样的循环引用就可能造成内存泄漏.

译者注:猜测作者是为了使文章更易懂,故意不提及执行上下文的概念,本文中出现[[scope]]内部属性的地方也是我加的.

[译]JavaScript:函数的作用域链的更多相关文章

  1. javascript笔记:javascript的关键所在---作用域链

    javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript技巧也是围绕作用域进行的,今天我要总结一下关于 ...

  2. javascript的关键所在---作用域链

    javascript的关键所在---作用域链 javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript ...

  3. javascript 函数和作用域(闭包、作用域)(七)

    一.闭包 JavaScript中允许嵌套函数,允许函数用作数据(可以把函数赋值给变量,存储在对象属性中,存储在数组元素中),并且使用词法作用域,这些因素相互交互,创造了惊人的,强大的闭包效果.[upd ...

  4. JavaScript面向对象的作用域链(转载)

    JavaScript的作用域一直以来是前端开发中比较难以理解的知识点,对于JavaScript的作用域主要记住几句话,走遍天下都不怕... 一.“JavaScript中无块级作用域” 在Java或C# ...

  5. JavaScript函数之作用域 / 作用链域 / 预解析

    关于作用域和作用链域的问题,很多文章讲的都很详细,本文属于摘录自己觉得对自己有价值的部分,留由后用,仅供参考,需要查看详细信息请点击我给出的原文链接查看原文件 做一个有爱的搬运工~~ -------- ...

  6. javascript闭包和作用域链

    最近在学习前端知识,看到javascript闭包这里总是云里雾里.于是翻阅了好多资料记录下来本人对闭包的理解. 首先,什么是闭包?看了各位大牛的定义和描述各式各样,我个人认为最容易一种说法: 外部函数 ...

  7. Javascript——闭包、作用域链

    1.闭包:是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见方式:在一个函数内部创建另一个函数. function f(name){ return function(object){ var ...

  8. JavaScript中的作用域链原理

    执行环境 作用域链的形成与执行环境(Execution Environment)相关,在JavaScript当中,产生执行环境有如下3中情形: 1 进入全局环境 2 调用eval函数 3 调用func ...

  9. 认识javascript范围和作用域链

    范围 作用域就是变量和函数的可訪问范围.控制着变量和函数的可见性与生命周期,在JavaScript中变量的作用域有全局作用域和局部作用域. 全局和局部作用域以下用一张图来解释: 单纯的JavaScri ...

随机推荐

  1. LNK2005 连接错误解决办法

    nafxcwd.lib(afxmem.obj) : error LNK2005: "void * __cdecl operator new(unsigned int)" (??2@ ...

  2. Powershell常用命令

    Powershell常用命令1.Get-Command 得到Powshell所有命令2.Get-Process 获取所有进程3.Set-Alias 给指定命令重命名 如:Set-Alias aaa G ...

  3. CSS clearfix

    The problem happens when a floated element is within a container box, that element does not automati ...

  4. JetBrains WebStorm 7.0 Build 131.202 Win/Mac/Liniux

    JetBrains WebStorm 7.0 Build 131.202 (Win/Mac/Liniux) | 121.6/106/133 Mb WebStorm 7 — Everything you ...

  5. FastCgi与PHP-fpm之间是个什么样的关系

    刚开始对这个问题我也挺纠结的,看了<HTTP权威指南>后,感觉清晰了不少. 首先,CGI是干嘛的?CGI是为了保证web server传递过来的数据是标准格式的,方便CGI程序的编写者. ...

  6. 【云计算】Docker云平台—Docker进阶

    Docker云平台系列共三讲,此为第二讲:Docker进阶 参考资料: 五个Docker监控工具的对比:http://www.open-open.com/lib/view/open1433897177 ...

  7. 75 int类型数组中除了一个数出现一次或两次以外,其他数都出现三次,求这个数。[2行核心代码]

    [本文链接] http://www.cnblogs.com/hellogiser/p/single-number-of-array-with-other-three-times.html [题目] i ...

  8. Android开发之模拟器的选择

    在做Android app开发的时候由于机器配置不是特别高,而Android自带的模拟器非常耗资源,性能极其差.所以常常由于模拟器性能差而抓狂,相信不少开发者都会面临和我一样的问题.于是换了一台平常很 ...

  9. PHP负数判空

    2014年11月6日 10:08:09 $a = -1; $b = '-1'; $c = empty($a); $d = empty($b); var_dump($c, $d); // bool(fa ...

  10. 转数据库Sharding的基本思想和切分策略

    本文着重介绍sharding的基本思想和理论上的切分策略,关于更加细致的实施策略和参考事例请参考我的另一篇博文:数据库分库分表(sharding)系列(一) 拆分实施策略和示例演示 一.基本思想 Sh ...