资讯专栏INFORMATION COLUMN

JavaScript 作用域学习笔记

赵连江 / 1366人阅读

摘要:每一个运行期上下文都和一个作用域链关联。这个对象将被推入作用域链的头部,这意味着函数的所有局部变量现在处于第二个作用域链对象中,因此访问代价更高了。在代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。

参考:

Javascript作用域原理

理解 JavaScript 作用域和作用域链

JavaScript 作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。

在JavaScript中,变量的作用域有 全局作用域局部作用域两种。

全局作用域(Global Scope)

代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:

(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在函数外部无法访问到。

局部作用域(Local Scope)

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为 函数作用域,例如下列代码中的blogName和函数innerSay都只拥有局部作用域。

function doSomething(){
    var blogName="梦想天空";
    function innerSay(){
        alert(blogName);
    }
    innerSay();
}
alert(blogName); //脚本错误
innerSay(); //脚本错误
JavaScript 的作用域链(Scope Chain) [[scope]] 属性

函数对象其中一个内部属性是[[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的属性: namefunc. 和一个值为’eve’的属性para;

此时的scope chain如下:

[[scope chain]] = [
{
     para : "eve",
     name : undefined,
     func : undefined,
     arguments : []
}, {
     window call object
}
]

当调用进入factory的函数体的时候, 此时的factoryscope 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 语句

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存储在一个局部变量中就可以提升性能。

catch语句

另外一个会改变作用域链的是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子句中唯一执行的代码。

该函数接收异常对象作为参数,这样你可以更加灵活和统一的处理错误。

由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。

Javascript 的预编译

JS中, 是有预编译的过程的, JS在执行每一段JS代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式).

如上文所说, 在调用函数执行之前, 会首先创建一个活动对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个活动对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined.

而对于函数的定义,是一个要注意的地方:

这就是函数定义式和函数表达式的不同, 对于函数定义式, 会将函数定义提前. 而函数表达式, 会在执行过程中才计算.

    var name = "laruence";
         age = 26;

我们都知道不使用var关键字定义的变量, 相当于是全局变量, 联系到我们刚才的知识:

在对age做标识符解析的时候, 因为是写操作, 所以当找到到全局的window活动对象的时候都没有找到这个标识符的时候, 会在window活动对象的基础上, 返回一个值为undefinedage属性.

现在, 也许你注意到了我刚才说的: JS在执行每一段JS代码.


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78873.html

相关文章

  • javascript学习笔记(1)

    摘要:关于作用域实现的描述任何执行上下文时刻的作用域,都是由作用域链来实现的。在一个函数被定义的时候,会将它此时的作用域链链接到这个函数对象的属性。参考资料鸟哥作用域原理理解作用域和作用域链阮一峰老师微博上的关于作用域的一道题 javascript作用域原理学习   在每次调用一个函数的时候,就会进入一个函数内的作用域,当从函数返回 以后,就会返回调用前的作用域。   ECMA262关于作...

    Chaz 评论0 收藏0
  • JavaScript学习第五天笔记作用

    摘要:函数的作用域也可被分为全局作用域和局部作用域函数作用域被定义在指定函数内部的函数被称为局部函数或内部函数。局部变量在函数内部声明的变量被成为局部变量,它只能在函数的内部进行访问。 作用域 概述 变量和函数都具有作用域 作用域就是变量和函数的可被访问的范围 控制着变量和函数的可见性和生命周期。变量的作用域可被分为全局作用域和局部作用域(函数作用域) 如果变量是被定义在全局作用域的话 在J...

    Sleepy 评论0 收藏0
  • 学习笔记JavaScript 闭包是怎么通过作用链霸占更多内存的?

    摘要:闭包是怎么通过作用域链霸占更多内存的本文是作者学习高级程序设计第一小节的一点个人理解,详细教程请参考原教材。函数执行过程创建了一个函数的活动对象,作用域链的最前端指向这个对象。函数执行完毕返回值后执行环境作用域链和活动对象一并销毁。 JavaScript 闭包是怎么通过作用域链霸占更多内存的? 本文是作者学习《JavaScript 高级程序设计》7.2第一小节的一点个人理解,详细教程请...

    HmyBmny 评论0 收藏0
  • Js学习笔记:闭包

    摘要:一前言这个周末,注意力都在学习基础知识上面,刚好看到了闭包这个神圣的东西,所以打算把这两天学到的总结下来,算是巩固自己所学。因此要注意闭包的使用,否则会导致性能问题。五总结闭包的作用能够读取其他函数内部变量。 # 一、前言 这个周末,注意力都在学习基础Js知识上面,刚好看到了闭包这个神圣的东西,所以打算把这两天学到的总结下来,算是巩固自己所学。也可能有些不正确的地方,也请大家看到了,麻...

    Crazy_Coder 评论0 收藏0
  • [学习笔记] JavaScript 作用

    摘要:全局执行环境的变量对象始终是作用域链中的最后一个变量对象。综上,每个函数对应一个执行环境,每个执行环境对应一个变量对象,而多个变量对象构成了作用域链,如果当前执行环境是函数,那么其活动对象在作用域链的前端。 1.几个概念 先说几个概念:函数、执行环境、变量对象、作用域链、活动对象。这几个东东之间有什么关系呢,往下看~ 函数 函数大家都知道,我想说的是,js中,在函数内部有两个特殊...

    ?xiaoxiao, 评论0 收藏0
  • JavaScript作用学习笔记

    摘要:我们再来看一下第一段代码小红小黑脚本出错脚本出错在这段代码中变量与函数,都拥有局部作用域。作用域链的最前端,始终都是当前执行代码所在的作用域的变量对象。 个人博客原址 无论什么语言中,作用域都是一个十分重要的概念,在JavaScript中也不例外,作用域定义了变量或者函数有权访问的范围,决定了它们各自的行为。要理解JavaScript中的作用域首先就要知道:在let出现之前,JS中变...

    jerryloveemily 评论0 收藏0

发表评论

0条评论

赵连江

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<