”JavaScript中的函数运行在它们被定义的做用域里,而不是它们被执行的做用域里.” ——权威指南javascript
在JavaScript中,一切皆对象,包括函数。函数对象和其它对象同样,拥有能够经过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被建立的做用域中对象的集合,这个集合被称为函数的做用域链,它决定了哪些数据能被函数访问。html
在一个函数被定义的时候, 会将它定义时刻的scope chain连接到这个函数对象的[[scope]]属性.前端
在一个函数对象被调用的时候,会建立一个活动对象(也就是一个对象), 该对象包含了函数的全部局部变量、命名参数、参数集合以及this,而后此对象会被推入做用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。java
在每次调用一个函数的时候 ,就会进入一个函数内的做用域,当从函数返回之后,就返回调用前的做用域.git
var sayHello = function(l,s){ var word = "hello world"; } sayHello();
在执行sayHello定义语句的时候, 会建立一个这个函数对象的[[scope]]属性。github
将这个[[scope]]属性, 连接到定义它的做用域链上。此时由于func定义在全局环境, 因此此时的[[scope]]只是指向全局活动对象window active object.app
在调用sayHello的时候, 会建立一个活动对象(假设为fObj),并建立arguments属性。而后会给这个对象添加俩个命名属性fObj.l, fObj.s; 对于每个在这个函数中申明的局部变量和函数定义, 都做为该活动对象的同名命名属性。对于局部变量,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined.函数
将调用参数赋值给形参,对于缺乏的调用参数,赋值为undefined。优化
将这个活动对象作为scope chain的最前端, 并将func的[[scope]]属性所指向的,定义sayHello时候的顶级活动对象, 加入到scope chain.this
在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每个活动对象的属性,若是找到同名的就返回。找不到,那就是这个标识符没有被定义。
做用域链全过程解析:
function factory() { var name = 'laruence'; var intro = function(){ alert('I am ' + name); } return intro; } function app(para){ var name = para; var func = factory(); func(); } app('eve');
首先当调用app的时候, scope chain是由: {window活动对象(全局)}->{app的活动对象} 组成。此时的scope chain以下:(对于局部变量,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined.
)
[[scope chain]] = [ { para : 'eve', name : undefined, func : undefined, arguments : [] }, { window call object } ]
当调用进入factory的函数体的时候, 此时的factory的scope chain为:
[[scope chain]] = [ { name : undefined, intor : undefined }, { window call object } ]
注意: 此时的做用域链中, 并不包含app的活动对象.(JavaScript中的函数运行在它们被定义的做用域里,而不是它们被执行的做用域里.)
在定义intro函数的时候, intro函数的[[scope]]为:
[[scope chain]] = [ { name : 'laruence', intor : undefined }, { window call object } ]
从factory函数返回之后,在app体内调用intor的时候, 发生了标识符解析, 而此时的sope chain是:
[[scope chain]] = [ { intro call object }, { name : 'laruence', intor : undefined }, { window call object } ]
因此, name标识符解析的结果(在上面的做用域链中一层层往上匹配)应该是factory活动对象中的name属性, 也就是’laruence’。
标识符解析过程:
该过程从做用域链头部,也就是从活动对象开始搜索,查找同名的标识符,若是找到了就使用这个标识符对应的变量,若是没找到继续搜索做用域链中的下一个对象,若是搜索完全部对象都未找到,则认为该标识符未定义。函数执行过程当中,每一个标识符都要经历这样的搜索过程。
从做用域链的结构能够看出,在运行期上下文的做用域链中,标识符所在的位置越深,读写速度就会越慢。全局变量老是存在于运行期上下文做用域链的最末端,所以在标识符解析的时候,查找全局变量是最慢的。因此,在编写代码的时候应尽可能少使用全局变量,尽量使用局部变量。一个好的经验法则是:若是一个跨做用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。例以下面的代码:
function changeColor(){ var doc=document; doc.getElementById("btnChange").onclick=function(){ doc.getElementById("targetCanvas").style.backgroundColor="red"; }; }
function initUI(){ with(document){ var bd=body, links=getElementsByTagName("a"), i=0, len=links.length; while(i < len){ update(links[i++]); } getElementById("btnInit").onclick=function(){ doSomething(); }; } }
with 语句的做用是将代码的做用域设置到一个特定的对象中
当代码运行到with语句时,运行期上下文的做用域链临时被改变了。一个新的可变对象被建立,它包含了参数指定的对象的全部属性。这个对象将被推入做用域链的头部,这意味着函数的全部局部变量如今处于第二个做用域链对象中,所以访问代价更高了。
参考文章:Javascript做用域原理、理解 JavaScript 做用域和做用域链
个人博客地址:http://bigdots.github.io、http://www.cnblogs.com/yzg1/