参考:javascript
Javascript做用域原理html
做用域就是变量与函数的可访问范围,即做用域控制着变量与函数的可见性和生命周期。java
在JavaScript中,变量的做用域有 全局做用域和 局部做用域两种。markdown
在 代码中任何地方都能访问到的对象拥有全局做用域,通常来讲如下几种情形拥有全局做用域:app
(1)最外层函数和在最外层函数外面定义的变量拥有全局做用域,例如:函数
var authorName="山边小溪"; function doSomething(){ var blogName="梦想天空"; function innerSay(){ alert(blogName); } innerSay(); } alert(authorName); //山边小溪 alert(blogName); //脚本错误 doSomething(); //梦想天空 innerSay() //脚本错误
(2)全部末定义直接赋值的变量自动声明为拥有全局做用域,例如:性能
function doSomething(){ var authorName="山边小溪"; blogName="梦想天空"; alert(authorName); } doSomething(); //山边小溪 alert(blogName); //梦想天空 alert(authorName); //脚本错误
变量blogName
拥有全局做用域,而authorName
在函数外部没法访问到。优化
和全局做用域相反,局部做用域通常只在固定的代码片断内可访问到,最多见的例如函数内部,全部在一些地方也会看到有人把这种做用域称为 函数做用域,例以下列代码中的blogName
和函数innerSay
都只拥有局部做用域。this
function doSomething(){ var blogName="梦想天空"; function innerSay(){ alert(blogName); } innerSay(); } alert(blogName); //脚本错误 innerSay(); //脚本错误
函数对象其中一个内部属性是[[Scope]]
,由ECMA-262
标准第三版定义,该内部属性包含了 函数被建立的做用域中对象的集合,这个集合被称为函数的 做用域链,它决定了哪些数据能被函数访问。
请看例子:
function add(num1,num2) { var sum = num1 + num2; return sum; }
在函数add
建立时,它的做用域链中会填入一个全局对象,该全局对象包含了全部全局变量,以下图所示(注意:图片只例举了所有变量中的一部分):
函数add的 做用域将会在执行时用到。
例如执行以下代码:
var total = add(5,10);
执行此函数时会建立一个称为“运行期上下文(execution context)”
的内部对象,运行期上下文定义了函数执行时的环境。
每一个运行期上下文都有本身的做用域链,用于标识符解析,当运行期上下文被建立时,而它的做用域链初始化为当前运行函数的[[Scope]]
所包含的对象。
这些值按照它们出如今函数中的顺序被复制到运行期上下文的做用域链中,它们共同组成了一个新的对象,叫“活动对象(activation object)”
,该对象包含了函数的全部局部变量
、命名参数
、参数集合
以及this
,而后此对象会被推入做用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。
新的做用域链以下图所示:
在函数执行过程当中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。
该过程从做用域链头部,也就是从活动对象开始搜索,查找同名的标识符,若是找到了就使用这个标识符对应的变量,若是没找到继续搜索做用域链中的下一个对象;
若是搜索完全部对象都未找到,则认为该标识符未定义。
函数执行过程当中,每一个标识符都要经历这样的搜索过程。
JS权威指南 中有一句很精辟的描述:
JavaScript中的函数运行在它们被定义的做用域里,而不是它们被执行的做用域里.
在JS中,做用域的概念和其余语言差很少, 在每次调用一个函数的时候 ,就会进入一个函数内的做用域,当从函数返回之后,就返回调用前的做用域.
JS
的做用域的实现具体过程以下(ECMA262中所述):
任何执行上下文时刻的做用域, 都是由做用域链(
scope chain
, 后面介绍)来实现.在一个函数被定义的时候, 会将它
定义时刻
的scope chain
连接到这个函数对象的[[scope]]
属性.在一个函数对象被调用的时候,会建立一个活动对象(也就是一个对象), 而后对于每个函数的形参,都命名为该活动对象的命名属性, 而后将这个活动对象作为此时的做用域链(
scope chain
)最前端, 并将这个函数对象的[[scope]]
加入到scope chain
中.
看个例子:
函数对象的[[scope]]
属性是在定义一个函数的时候决定的, 而非调用的时候, 因此以下面的例子:
var name = 'laruence'; function echo() { alert(name); } function env() { var name = 'eve'; echo();markdown previewmarkdown previewmarkdown previewmarkdown preview } env(); // 运行结果是: laruence
结合上面的知识, 咱们来看看下面这个例子:
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的活动对象}
组成.
在刚进入app
函数体时, app的活动对象有一个arguments
属性, 俩个值为undefined
的属性: name
和func
. 和一个值为’eve’
的属性para
;
此时的scope chain
以下:
[[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
的活动对象.
在定义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 } ]
由于scope chain
中,并不包含factory
活动对象. 因此, name
标识符解析的结果应该是factory活动对象中的name属性, 也就是’laruence’
.
因此运行结果是:
I am laruence
从做用域链的结构能够看出,在运行期上下文的做用域链中,标识符所在的位置越深,读写速度就会越慢。
全局变量老是存在于运行期上下文做用域链的最末端,所以在标识符解析的时候,查找全局变量是最慢的。
因此,在编写代码的时候应尽可能少使用全局变量,尽量使用局部变量。
一个好的经验法则是:若是一个跨做用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。
例以下面的代码:
function changeColor(){ document.getElementById("btnChange").onclick=function(){ document.getElementById("targetCanvas").style.backgroundColor="red"; }; }
这个函数引用了两次全局变量document,查找该变量必须遍历整个做用域链,直到最后在全局对象中才能找到。
这段代码能够重写以下:
function changeColor(){ var doc=document; doc.getElementById("btnChange").onclick=function(){ doc.getElementById("targetCanvas").style.backgroundColor="red"; }; }
这段代码比较简单,重写后不会显示出巨大的性能提高,可是若是程序中有大量的全局变量被从反复访问,那么重写后的代码性能会有显著改善。
函数每次执行时对应的运行期上下文都是独一无二的,因此屡次调用同一个函数就会致使建立多个运行期上下文,当函数执行完毕,执行上下文会被销毁。
每个运行期上下文都和一个做用域链关联。
通常状况下,在运行期上下文运行的过程当中,其做用域链只会被 with 语句
和 catch 语句
影响。
with语句是对象的快捷应用方式,用来避免书写重复代码。
例如:
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
语句来避免屡次书写document
,看上去更高效,实际上产生了性能问题。
当代码运行到with
语句时,运行期上下文的做用域链临时被改变了。
一个新的可变对象被建立,它包含了参数指定的对象的全部属性。
这个对象将被推入做用域链的头部,这意味着函数的全部局部变量如今处于第二个做用域链对象中,所以访问代价更高了。
以下图所示:
所以在程序中应避免使用with
语句,在这个例子中,只要简单的把document
存储在一个局部变量中就能够提高性能。
另一个会改变做用域链的是try-catch
语句中的catch
语句。
当try
代码块中发生错误时,执行过程会跳转到catch
语句,而后把异常对象推入一个可变对象并置于做用域的头部。
在catch
代码块内部,函数的全部局部变量将会被放在第二个做用域链对象中。
示例代码:
try{ doSomething(); }catch(ex){ alert(ex.message); //做用域链在此处改变 }
请注意,一旦catch
语句执行完毕,做用域链机会返回到以前的状态。
try-catch
语句在代码调试和异常处理中很是有用,所以不建议彻底避免。
你能够经过优化代码来减小catch
语句对性能的影响。
一个很好的模式是将错误委托给一个函数处理,例如:
try{ doSomething(); }catch(ex){ handleError(ex); //委托给处理器方法 }
优化后的代码,handleError
方法是catch
子句中惟一执行的代码。
该函数接收异常对象做为参数,这样你能够更加灵活和统一的处理错误。
因为只执行一条语句,且没有局部变量的访问,做用域链的临时改变就不会影响代码性能了。
在JS
中, 是有预编译的过程的, JS
在执行每一段JS
代码以前, 都会首先处理var
关键字和function
定义式(函数定义式和函数表达式).
如上文所说, 在调用函数执行以前, 会首先建立一个活动对象, 而后搜寻这个函数中的局部变量定义
,和函数定义
, 将变量名和函数名都作为这个活动对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined
.
而对于函数的定义,是一个要注意的地方:
这就是函数定义式和函数表达式的不一样, 对于函数定义式, 会将函数定义提早. 而函数表达式, 会在执行过程当中才计算.
var name = 'laruence'; age = 26;
咱们都知道不使用var关键字
定义的变量, 至关因而全局变量
, 联系到咱们刚才的知识:
在对age
作标识符解析的时候, 由于是写操做, 因此当找到到全局的window
活动对象的时候都没有找到这个标识符的时候, 会在window活动对象的基础上, 返回一个值为undefined
的age
属性.
如今, 也许你注意到了我刚才说的: JS在执行每一段JS代码.
<script> alert(typeof eve); //结果:undefined </script> <script> function eve() { alert('I am Laruence'); } </script>