通过javascript引擎工作原理深入解析作用域和作用域链
javascript引擎在解析script时,首先进行语法检查,词法分析,然后就是解析运行。
通过了解javascript引擎解析运行阶段引擎工作原理就能非常清晰明白作用域和作用域链的工作机制了。
作用域
作用域可以理解为变量或函数起作用的范围,分为全局作用域和局部作用域。
全局作用域就是在任何函数的外面定义,局部作用域就是在函数或者对象里面定义。
javascript引擎解析全局作用域
javascript引擎在运行全局代码时,经过两个阶段,首先是预解析,然后是逐行向下运行代码。
多个script块时,从上到下逐个script块解析运行。
上面的公共空间里的声明保留到下一个script块,前面的script块无法使用后面script块的公共空间。
上面的script块报错不影响后面的script块解析运行,同时上面script块的公共区间仍会被继承。
预解析
所谓预解析,就是将代码声明的全局变量和全局函数提取到一个公共空间。
全局变量声明就是利用var关键字声明全局变量,预解析阶段全局变量不获取值,设为undefined。
全局函数声明就是利用function关键字声明全局函数,预解析阶段全局函数名作为左值被赋值为整个函数块。
全局变量名和全局函数名相同时,预解析阶段变量名或函数名作为左值被赋值为整个函数块。
多个全局函数名相同时,预解析阶段函数名作为左值被赋值为最后声明的函数块。
总之,后定义的值(非undefined)会覆盖前面定义的值,有值的会覆盖undefined。
逐行向下运行代码
预解析完成后,全局变量名获取了undefined的值或全局函数名获取了整个函数块的作为值。
之后开始逐行向下运行代码,运行阶段只有表达式才会起作用,声明不再起作用。
若变量声明和赋值在一个语句里面只有赋值部分会起作用。
例一:全局变量声明
// 预解析阶段: var a // a = undefined // 逐行运行代码阶段: alert(a); // 此时公共空间里a定义且被赋值为undefined因此输出undefined // 实际为两部分:声明部分var a;赋值部分a=1; // 声明部分不再起作用,起作用的仅为赋值部分,此时公共空间里a值改为1 var a = 1; alert(a); // 从公共空间里获得a为1,因此输出1
全局变量在预解析时被赋值为undefined。
例二:错误变量无声明
// 预解析阶段:没有声明,此时公共空间为空 // 逐行运行代码阶段: alert(a); // 公共空间里没有a,因此报错,停止向下运行 a = 1; alert(a);
因此在实际编程中不要忘记使用var进行变量声明!否则将可能出错造成不良后果。
例三:全局函数声明
// 预解析阶段: function a // a = function a() { // alert(1); // 注释 // } // 完整函数代码块,格式注释都保留不变 // 逐行运行代码阶段: alert(a); // 公共空间里a为完整函数代码块,输出完整函数代码块 // 函数声明语句不再起作用 function a() { alert(1); // 注释 } alert(a); // 再次输出完整函数代码块
全局函数在预解析时被赋值为完整函数块,包含格式和注释都不变。
例四:多个全局变量和全局函数声明
// 预解析阶段: var a和function a // a = function a() { // alert(4); // } // a赋值为最后一个名为a的完整的函数代码块 // 逐行运行代码阶段: alert(a); // 公共空间里a为完整函数代码块,输出函数字符串 a(); // 函数调用输出4 var a=1; // 赋值部分起作用,a赋值为1 alert(a); // 公共空间里此时a为1,因此输出1 // 函数声明语句不再起作用 function a() { alert(2); } alert(a); // a不变,输出1 var a=3; // 赋值部分起作用,a赋值为3 alert(a); // 公共空间里此时a为3,因此输出3 // 函数声明语句不再起作用 function a() { alert(4); } alert(a); // a不变,输出3 a(); // a为数字,当然不能函数调用,报错!
同名左值在预解析中,后定义的非undefined的值会覆盖前面定义的undefined的值,有值的覆盖undefined。
例五:多个script块的全局变量和全局函数声明
<script> // 预解析阶段: var a和function b // a = undefined // b = function b() { // alert(2); // } // 逐行运行代码阶段: alert(a); // 输出undefined var a = 1; // 赋值部分起作用,a变为1 alert(a); // 输出1 alert(b); // 输出完整函数块字符串 // 声明语句不起作用 function b() { alert(2); } alert(b); // b不变,输出完整函数块字符串 </script> <script> // 第一个script块解析运行结束后,公共空间为: // a = 1 // b = function b() { // alert(2); // } // 第二个script块开始解析运行 // 预解析阶段:var a和var b // a和b均有值,覆盖新区块声明获取的undefined,因此不变 // a = 1 // b = function b() { // alert(2); // } // 逐行运行代码阶段: alert(a); // 公共空间里a为1,输出1 var a=3; // 赋值部分起作用,a变为3 alert(a); // 公共空间里a为3,输出3 alert(b); // 公共空间里b为完整函数块,输出完整函数块字符串 var b=4; // 赋值部分起作用,b变为4 alert(b); // 公共空间里b为4,输出4 </script>
多个script块时,从上到下逐个解析运行,上面运行后公共空间会被下面的script块继承。
例六:script块顺序的重要性!
<script> // 预解析阶段: function b // b = function b() { // alert(2); // } // 逐行运行代码阶段: alert(a); // 公共空间里a未定义不存在,因此报错,停止本区块向下运行 alert(b); function b() { alert(2); } alert(b); </script> <script> // 第一个script块解析运行结束后,公共空间为: // b = function b() { // alert(2); // } // 第二个script块开始解析运行 // 预解析阶段:var a和var b // b已有值,覆盖新区块获取的undefined值,不变 // a为undefined // a = undefined // b = function b() { // alert(2); // } // 逐行运行代码阶段: alert(a); // 公共空间里a为undefined,输出undefined var a=3; // 赋值部分起作用,a变为3 alert(a); // 公共空间里a为3,输出3 alert(b); // 公共空间里b为完整函数块,输出完整函数块字符串 var b=4; // 赋值部分起作用,b变为4 alert(b); // 公共空间里b为4,输出4 </script>
多个script块时,从上到下逐个script块解析运行。
上面的公共空间里的声明保留到下一个script块,前面的script块无法使用后面script块的公共空间。
上面的script块报错不影响后面的script块解析运行,同时上面script块的公共区间仍会被继承。
因此在实际应用时要注意script的顺序!例如jQuery等框架应放在最上面,避免造成错误。
javascript引擎解析局部作用域
在函数,json块或对象里面声明定义即为局部作用域。
这里主要以函数形成的局部作用域为例,在函数调用时javascript引擎就会开始解析局部作用域。
解析运行同样分为两个阶段:预解析和逐行向下运行代码。
预解析
局部作用域的预解析和全局作用域基本一致,就是将代码声明的局部变量和局部函数提取到一个临时局部公共空间。
不同之处主要有:
公共空间为临时局部公共空间,解析运行完后就会被垃圾回收机制回收。 局部变量声明方式除了var关键字外还包括函数的参数。
逐行向下运行代码
局部作用域预解析完成后开始逐行向下运行代码。
以函数调用解析为例,第一行代码为传参,即参数赋值。
在运行代码的过程中遇到未定义的左值-变量或函数等,会尝试由里向外逐层查看公共空间是否存在相应的左值。
因此里层可以直接操作获取设置外层的左值。
这就是作用域链。
作用域链
所谓作用域链即某一个局部函数在定义时获取的可访问的从里到外层层向外的作用域列表。
在作用域链中,越靠近局部函数的的里层作用域具有越高的优先权,不管预解析结果是否为有值(非undefined)。
例一:var变量局部作用域解析
// 全局预解析: var a和function b // a = undefined // b = function b() { // alert(a); // var a = 2; // } // 全局逐层向下运行代码: var a = 1; // 赋值部分起作用a变为1 // 声明不起作用 function b(){ alert(a); var a = 2; } a = 3; // 赋值a变为3 b(); // 函数调用开始局部作用域解析运行 // 局部预解析: var a // a = undefined // 局部逐行向下运行 // (){ // 没有传参 // alert(a); // 临时局部公共空间里a为undefined,因此输出undefined // var a = 2; // 赋值部分起作用,临时局部公共空间里a变为2 // } // 结束运行,回收临时局部公共空间 alert(a); // 全局公共空间里a为3,因此输出3
例二:参数未传参局部作用域解析
// 全局预解析: var a和function b // a = undefined // b = function b() { // alert(a); // var a = 2; // } // 全局逐层向下运行代码: var a = 1; // 赋值部分起作用a变为1 // 声明不起作用 function b(a){ alert(a); a = 2; } a = 3; // 赋值a变为3 b(); // 函数调用开始局部作用域解析运行 // 局部预解析: 参数a // a = undefined // 局部逐行向下运行 // (a){ // 没有传参,a不变为undefined // alert(a); // 临时局部公共空间里a为undefined,因此输出undefined // a = 2; // 临时局部公共空间里a赋值为2 // } // 结束运行,回收临时局部公共空间 alert(a); // 全局公共空间里a为3,因此输出3
例三:参数传参局部作用域解析
// 全局预解析: var a和function b // a = undefined // b = function b() { // alert(a); // var a = 2; // } // 全局逐层向下运行代码: var a = 1; // 赋值部分起作用a变为1 // 声明不起作用 function b(a){ alert(a); a = 2; } a = 3; // 赋值a变为3 b(a); // 函数调用开始局部作用域解析运行 // 局部预解析: 参数a // a = undefined // 局部逐行向下运行 // (a){ // 传参全局作用域里的a即3,临时局部公共空间里a赋值为3 // alert(a); // 临时局部公共空间里a为3,因此输出3 // a = 2; // 临时局部公共空间里a赋值为2 // } // 结束运行,回收临时局部公共空间 alert(a); // 全局公共空间里a为3,因此输出3
例四:作用域链
// 全局预解析: var a和function b // a = undefined // b = function b() { // alert(a); // a = 2; // } // 全局逐层向下运行代码: var a = 1; // 赋值部分起作用a变为1 // 声明不起作用 function b(){ alert(a); a = 2; } a = 3; // a赋值为3 b(); // 函数调用开始局部作用域解析运行 // 局部预解析: 临时公共空间为空 // 局部逐行向下运行 // (){ // 没有传参 // alert(a); // 临时局部公共空间里a不存在未定义,尝试向上一层空间查看a为3,因此输出3 // a = 2; // a为上一层空间里的a,为其赋值为2 // } // 结束运行 alert(a); // 全局公共空间里a为2,因此输出2
当前局部作用域不存在相应的左值,则从里到外逐层向外获取外层作用域链里的相应左值,且获取的值为函数调用时相应左值在作用域公共空间里的值,不是函数定义声明时相应值。
例五:获取局部变量的方法
由于作用域链是由里到外,里层可以操作外层的变量但是外层不能操作里层的变量,因此在不使用闭包的情况下只能通过全局变量来操作局部作用域的变量。
// 方法一:直接利用作用域链在函数里操作外层作用域的变量 var a = 0; function add(){ a++; } function min() { a--; } add(); alert(a); //输出1 min(); alert(a); //输出0 // 方法二:通过将局部变量赋值给外层作用域的变量 var a=''; function b() { var c='局部变量'; a=c; } alert(a); // 上面两种方法比较简单就不分析了,请自行分析~ // 方法三:通过传参将一个函数里的局部变量传递给另外一个函数 // 全局预解析:function a和function c // a = function a() { // var b= 'a函数的全局变量b'; // c(b); // } // c = function c(d) { // alert(d); // } // 全局逐行向下运行代码: // 声明不再起作用 function a(){ var b = 'a函数的局部变量b'; c(b); } // 声明不再起作用 function c(d){ alert(d); } // 调用a函数 a(); // 输出`a函数的局部变量b` // a局部作用域预解析: var b // b = undefined // a局部作用域逐行运行代码: // (){ // 未传参 // var b = 'a函数的局部变量b'; // 临时局部公共空间里b赋值为'a函数的局部变量b' // c(b); // // 临时公共空间里c不存在,因此向上层获取c为函数,然后进行函数调用 // // c局部作用域预解析: 参数d // // d = undefined // // // c局部作用域逐行向下运行代码: // // (d){ // 传参b即'a函数的局部变量b',赋值给d // // 临时局部公共空间里d值为'a函数的局部变量b' // // alert(d); // 输出d,'a函数的局部变量b' // // } // 结束运行,回收临时局部公共空间 // // // // // } // 结束运行,回收临时局部公共空间
除了Firefox浏览器引擎外结构块不能形成局部作用域
所谓结构块就是判断结构,循环结构等{}大括号形成的块,里面并不会形成局部作用域。
为了兼容性主要是FF,全局的变量和全局的函数应该定义在结构体外面。
for循环里定义的函数
函数直接量作为右值时不会替换里面的变量值,因为不是函数调用!!!
因此最好不要在循环里面定义函数或者采用new Function利用字符串创建函数。
// 全局预解析:var b和var i // b=undefined // i=undefined // 全局逐行运行代码: // 声明不再起作用 var b=[]; for (var i=0;i<10;i++) { // 循环定义函数数组,i不断自增,注意函数直接量里的i不会被替换为相应的值 // b[0]=function() { alert(i); }; // b[1]=function() { alert(i); }; // . // . // . // b[9]=function() { alert(i); }; b[i]=function() {alert(i);}; } // 循环结束,i变为10,b为函数数组 alert(b); /* 输出数组内容:function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); },function() { alert(i); } */ b[0](); // 输出10 // 函数调用开始 // 局部作用域预解析:空 // 局部作用域逐行运行代码 // () { // 未传参 // alert(i); // 临时局部公共空间里i不存在未定义,因此向上一层作用域空间寻找 // // 调用函数时,i为10,因此输出10 // } b[1](); // 同理输出10
解决办法:new Function:
var b=[]; for (var i=0;i<10;i++) { b[i]=new Function('alert('+i+');'); } b[0](); // 0 b[1](); // 1
通过引擎工作原理的分析,作用域和作用域链应该非常清楚了。
小指才疏学浅,有任何疏漏之处,请不吝赐教~