本文将继续就如下几点做出一些归纳,更多的是对ECMA-262中相应知识点的一些翻译并掺杂笔者的一些拙见,仅供参考。由于对个别专业词汇的解释拿捏不定,在罗列demo的同时,对于结论的后续推断将留给读者自身分析一些余地,见谅。
在X贴过程中,笔者参阅了ECMA-262(3rd,5th,以下简称文档),Lich_Ray的中文翻译项目组提供的文档(http://code.google.com/p/ecma-262/),一并表示感谢:)
文档指出,当在函数声明中声明变量时,变量将作为函数局部变量(准确来说,挂在与该函数相关联的执行环境中变量对象下成为其属性),否则,他们将成为全局变量。变量是在执行流进入函数执行环境时被创建的。文档同时指出,ECMAScript没有块级作用域的概念。只有程序(各种Statement与FunctionDeclaration的集合)或函数声明能创建一个新的作用域。
变量创建后默认被初始化为undefined,如果提供初始化器,则在变量声明被执行的时候赋值(笔者认为赋值的时间点在创建时或在执行时,对编码影响不大,FIXME)。
值得一提的是,文档所提到的variable statement,是不能缺少var关键字的,隐藏含义即,缺少var,即使定义(严谨来说不算定义,理解为不声明而直接初始化之)在函数声明中,也是全局变量。给出相关语法:
来看一些demo
var n0;//初始化为undefined var n1 = 1;//全局变量 n2 = 2;//全局变量 //window.n2=2;//与上一行等价,但IE部分版本不支持 //n2;//错误语法,抛出ReferenceError,必须提供初始化器,或提供var关键字 function demo() { var n3 = 3;//局部变量 n4 = 4;//全局变量,不推荐这样定义 } alert(n0);//undefined alert(n1);//1 alert(n2);//2 demo();//别漏掉这行,否则n4不会创建 //alert(n3);//抛出ReferenceError alert(n4);//4 alert(window.n4);//虽IE不支持,但每个版本都可以通过它来取值
再补充一些demo
全局变量的属性是无法删除的(后来发现局部的也是如此)
var n1 = 1; delete n1;//无效 //delete window.n1;//IE不支持,证明其{DontDelete}特点,chrome支持,但都无效:( alert(n1);//1 //顺便熟悉一下delete语法 var r1 = new Object();//reference r1.name = "jack"; alert(r1.name);//jack delete r1.name; alert(r1.name);//undefined var p1 = "hello";//primitive p1.name = "mike";//不可在原始类型上动态添加属性,不会导致错误但无效 alert(p1.name);//undefined
为了或得更好的页面性能,可尝试尽早的释放全局变量,尤其是引用类型的全局变量。(置为null,解除引用),否则只能依赖垃圾回收或应用的关闭了。至于局部变量,因为在执行流退出其执行环境的时候,环境本身会释放,所以不用操心。
一些猜想:即使能delete也只能导致其值为undefined,而垃圾回收应只针对空指针引用(null)而非undefined,大概这是ECMA-262从第三版引入Undefined类型的原因之一,正式区分空指针和未初始化变量,便于更有效的管理内存。FIXME
文档中指出全局变量不可delete,是否局部的可以呢(尽管这样实在没什么必要。。),笔者YY了一些代码,仅供参考:
function demo() { var n = 1; delete n;//无效。。 delete arguments.callee.n;//也无效。。。但都不会报错 alert(n); } demo();
当执行流进入函数时,即进入某一个执行环境,该环境关联一个变量对象(variable object),该对象以property形式保存在该环境中声明的所有变量及函数,换句话说,函数中声明的所有变量其base object都是variable object,根据delete操作符的语法,找到base object才有可能删除其下的属性,虽然ECMA-262并未明文指出variable object的明确定义,但在文档中多处提到“using xxx as the variable object”,上面这个demo中的callee就是持有arguments的对象,但删除失败,只能证明这个variable object和其关联的activation object(活动对象)是无法通过代码引用的。FIXME
ECMAScript语法类似Java,但语法较为松散。变量在声明时无需指定类型,被定义函数也无需在调用前进行声明(像C语言那样)
语法的松散特点之一体现在变量不光是值,其类型也可以在生命周期中改变(不推荐反复更改一个变量的值类型)
变量包含2种:原始类型(Undefined,Boolean,Null,Number,String) 和引用类型
原始类型保存于栈内存,占有固定大小空间,具有索引速度快的特点
引用类型为保存于堆内存中的对象,大小不固定,索引相对较慢,变量实际上保存的是指向某对象的一个指针
不同类型变量,操作方式也不一样,下面仅浅析部分特点(FIXME):
原始类型在对变量进行复制时,会在栈内存中创建一个新值交由新变量持有,而引用类型,则是在栈内存中复制指针地址值,交由新变量持有,但此时,堆中对象仍然保持一个不变,即2指针指向同一对象。
var n1 = 1; var n2 = n1; n1 = 2; alert(n2);//2,不受n1影响 var r1 = new Object(); r1.name = "jack"; var r2 = r1; alert(r2.name);//jack r1.name = "mike"; alert(r2.name);//mike,受r1影响
在谈及变量作用域的时候,execution context(执行环境)是一个非常关键的概念,并且相关伴随很多内部机制,理解其工作原理,能帮助我们更好的识别变量的行为和状态。
当执行流进入一段可执行代码时,便进入一个执行环境,活动的执行环境组成一个环境栈,栈顶端的执行环境即为当前正在运行的环境。当环境退出时,相关资源也会得以释放。
我们知道JavaScript是一个基于对象的语言(object-based),那么对于变量及函数的管理也是基于这个特点。每个执行环境都会关联一个变量对象(variable object),变量对象是在进入某一个环境的时候进行创建并初始化的,所有在环境中声明的变量和函数都会作为其property挂载在这个对象上,包括函数中的参数。
那么我们所关注的变量,其在使用过程中是如何被访问,当遇到同名变量时,如何对其进行追溯并确认的呢?在其内部,是通过一个名为作用域链(scope chain)这样的线性数据结构来保证对环境中所有变量和函数的有序访问的。作用域链是在执行流进入一个执行环境的时候进行初始化的,里面填充的对象,根据进入的代码类型不一样,会产生动态变化,但大多数填充的都是每个执行环境当中的变量对象,并且,在作用域的前端保存的为当前正在运行的执行环境的变量对象(也有一个说法,称之为活动对象activation object)。文档同时指出,还可以通过2种方式来改变作用域链的格局,with和try-catch。
对标识符的解析,是一个沿着作用域链从左至右的搜索过程,直至找到标识符并返回为止。就变量的可见性而言,作用域链成为当前环境可以访问到的变量的严格范围,即如果访问一个当前环境所关联的作用域以外的变量,则会抛出类似引用不到的错误(注意,不是undefined)。FIXME
文档对何时会进入一个新的执行环境进行了明确的定义:
函数和构造器的调用,包括函数的递归调用,都会导致进入一个新的环境,return则退出,如果抛出异常,无catch的情况下也会导致进入新环境。
好了,将这段信息结合上文理解,那么问题来了,读者可能会提出这样的大哉问:不同的执行环境都关联着这样一个作用域,那么,在某一个环境中所能访问到的变量,随着执行流的forward,是如何因执行环境的不同而产生变化的?更进一步的说,环境所占用的资源,又是怎样释放的?要分析这个问题,对作用域在不同时期状态的理解,将成为解决问题的关键。
下面我们一起来看2个demo,笔者主要结合环境栈和作用域来尝试进行分析:
var n1 = 1; alert(n1);//mark1 function first() { var n1 = 2; var n3 = 3; alert(n1);//mark2 second(); //alert(n4);//mark4 } function second() { var n4 = 4; //alert(n3);//mark3 } first(); alert(n1);//mark5
第一步:当执行流进入全局执行环境(对web浏览器来说,我们一般认为是window对象)的时候,会创建该环境的一个变量对象,在这个变量对象中,保存n1及2个函数声明。这时window执行环境会被压入环境栈,并根据该window执行环境的变量对象初始化作用域链。
环境栈:
window执行环境 |
...... |
作用域链:
window变量对象 |
那么在代码mark1处n1的值为1。刁钻的读者可能会问,为什么不是在first函数中定义的n1=2呢,first函数不是也有一个相应的变量对象吗?要知道,虽然在window执行环境中存在2个函数的定义,并且他们也确实保存在window执行环境的变量对象下,但此时执行流并未进入first函数的执行环境,所以与first函数执行环境对应的变量对象此时还并未创建。
第二步:执行流在运行至倒数第二行时进入first函数执行环境,并将此环境执行压栈操作,因为并未退出window对象,所以此时环境栈自顶向下依次为first环境、window环境。
first执行环境 |
window执行环境 |
...... |
对作用域链插入新的变量对象时,是从前端操作的。此时它演变成:
first变量对象 | window变量对象 |
在mark2处对同名标识符n1的解析,将从first变量对象(作用域链的前端)中开始查找,此时找到n1的值为2并返回,也就不会再在作用域链的下一个(window变量中)查找了。
第三步: 此时,在first函数内部对second函数进行调用,执行流离开first执行环境并进入second执行环境,程序对first环境执行出栈操作,然后对second环境执行压栈操作,因为没遇到first函数结束符“}”,first环境虽然出栈,但所占用资源仍然不会释放。
second执行环境 |
window执行环境 |
...... |
而此时,作用域链则产生如下变化,首先first环境的出栈,将会导致从前端移除first变量对象,然后将当前活动的执行环境(second环境)的变量对象添加在作用域前端。
second变量对象 | window变量对象 |
那么在mark3处如试图对在first环境中定义的变量n3求值,则会导致错误(不是undefined),因为此时n3在second变量对象和window变量对象中,都找不到定义。
读者此时应该注意到一个规律,环境栈中的顺序与作用域链中的顺序是一致的。除此之外,我们说,在环境栈中,下方的环境相对于上方相邻的这个环境来说为包含环境,换言之,在一个环境中能访问到的变量,除开自身环境的变量定义外(优先查找,因为在作用域链前端),还有来自包含环境及其级联包含环境的。
第四步:执行流退出second环境,返回first环境,相应的second环境出栈,first环境压栈,由于执行流遇到second函数结束符“}”,second环境在出栈的同时,所占用的资源也得以释放。
first执行环境 |
window执行环境 |
...... |
相应的作用域链移除second变量对象,然后在前端插入first变量对象,变为
first变量对象 | window变量对象 |
在mark4处试图访问second环境中定义的n4变量也是会导致错误的(过程不难分析,略)
第五步:执行流离开first环境, 回到window环境,first环境出栈,同时释放其所占资源,作用域链移除前端first变量对象,只留下window变量对象。mark5处n1值为1。(图、分析略)
再来看另一个demo
var n1 = 1; function outter() { var n1 = 2; alert(n1);//mark1 function inner() { var n3 = 3; alert(n1);//mark2 } inner();//mark0 //alert(n3);//mark3 } outter(); alert(n1);//mark4
第一步:执行流运行至mark1时,n1值为2。分析同上。环境栈及作用域链状况如下
outter执行环境 |
window执行环境 |
...... |
outter变量对象 | window变量对象 |
第二步:在mark0处对inner函数的调用,使执行流进入inner执行环境,但此时,并不会导致outter执行环境的出栈,仅仅是在环境栈原有2个执行环境的基础上继续对inner环境执行压栈,因为当前活动的执行环境(inner),其函数是声明在outter环境中的,即这个inner函数本身就是作为outter环境的变量对象中properties当中的一员。这一点与上例有着本质上的区别(上例,2个函数之间的关系为并列关系,都是window变量对象的一员)。环境栈及作用域链状况如下
inner执行环境 |
outter执行环境 |
window执行环境 |
...... |
inner变量对象 | outter变量对象 | window变量对象 |
在mark2处,n1值为2,在outter变量对象中找到(分析略)
第三步:在mark3处,尝试访问n3会报错(图、分析略)
第四步:在mark4处,n1值为1(图、分析略)
最后补充一个带with的demo:
var n1 = 1; function outter() { var n1 = 2; function inner() { var n1 = 3; } inner(); with (n1) { //var n1;//mark3 //var n1 = 4;//mark2 var n2 = n1 + 4; } alert(n2);//mark1 } outter();
看读者能否自行分析出,mark1处n2值为6的结果,提示:
1、with/try-catch语句,都会在作用域链的前端添加一个变量对象(with后所跟参数的所有属性、方法所作的变量声明)
2、执行流进入with后所跟的“{}”块,并不会导致进入一个新的执行环境,因为没有块级作用域一说,所以在这个块中声明的变量会被添加在所在执行环境的变量对象中。
读者可尝试把mark2和mark3处代码交替取消注释运行查看结果。
关于变量及其作用域的分析,笔者认为在大多数时候都是比较直观简单的,没必要如本文中如此这般细掰,但是对于程序错误的分析定位,闭包分析,内存管理,则尤为重要。对于复杂抽象的技术(设计模式)我们应该善于转变成简单的案例进行理解把握,但对于基本功,则相对应该深入探究。
关于本文所涉及的参考,读者大多部分都可以从ECMA-262第十章节Execution contexts中找到,good luck