理解 JavaScript 中的做用域

前言

学习 JavaScript 也有一段时间,今天抽空总结一下做用域,也方便本身之后翻阅。javascript

什么是做用域

若是让我用一句简短的话来说述什么是做用域,个人回答是:java

其实做用域的本质是一套规则,它定义了变量的可访问范围,控制变量的可见性和生命周期。浏览器

既然做用域是一套规则,那么究竟如何设置这些规则呢?性能优化

先不急,在这以前,咱们先来理解几个概念。闭包

编译到执行的过程

下面咱们就拿这段代码来说述 JavaScript 编译到执行的过程。框架

var a = 2;
复制代码

首先咱们来看一下在这个过程当中,几个功臣所须要作的事。函数

  1. 引擎(总指挥):工具

    从头至尾负责整个 JavaScript 程序的编译及执行过程。性能

  2. 编译器(劳工):学习

    1. 词法分析(分词)

      解析成词法单元,vara=2

    2. 语法分析(解析)

      将单词单元转换成抽象语法树(AST)。

    3. 代码生成

      将抽象语法树转换成机器指令。

  3. 做用域(仓库管理员):

    负责收集并维护全部生命的标识符(变量)组成的一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符的访问权限。

而后咱们再来看,执行这段代码时,每一个功臣是怎么协同工做的。

引擎:

其实这段代码有两个彻底不一样的声明,var aa = 2,一个由编译器在编译时处理,另外一个则由引擎在运行时处理。

编译器:

  1. 一套编译器常规操做下来,到代码生成步骤。
  2. 遇到var a,会先询问做用域中是否已经存在同名变量,若是是,则忽略该声明,继续进行编译;不然它会要求做用域声明一个新的变量a
  3. 为引擎生成运行a = 2时所需的代码。

引擎:

会先询问做用域是否存在变量a,若是是,就会使用这个变量进行赋值操做;不然一直往外层嵌套做用域找(详见做用域嵌套),直至到全局做用域都没有时,抛出一个异常。

**总结:**变量的赋值操做会执行两个动做, 首先编译器会在当前做用域中声明一个变量( 若是以前没有声明过),而后在运行时引擎会在做用域中查找该变量, 若是可以找到就会对它赋值。

LHS & RHS 查询

从上面可知,引擎在得到编译器给出的代码后,还会对做用域进行询问变量。

聪明的你确定一眼就看出,LR的含义,它们分别表明左侧和右侧。

如今咱们把代码改为这样:

var a = b;
复制代码

这时引擎对a进行 LHS 查询,对b进行 RHS 查询,可是LR并不必定指操做符的左右边,而应该这样理解:

LHS 是为了找到赋值的目标。 RHS 是赋值操做的源头。也就是 LHS 是为了找到变量这个容器自己,给它赋值,而 RHS 是为了取出这个变量的值。

做用域嵌套

当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套,进而造成了一条做用域链。所以,在当前做用域中没法找到某个变量时,引擎就会在外层嵌套的做用域中继续查找,直到找到该变量, 或抵达最外层的做用域(也就是全局做用域)为止。

词法做用域

做用域分为两种:

  1. 词法做用域(较为广泛,JavaScript所使用的也是这种)
  2. 动态做用域(使用较少,好比 Bash 脚本、Perl 中的一些模式等)

词法做用域是由你在写代码时将变量和块做用域写在哪里来决定的。

看如下代码,这个例子中有三个逐级嵌套的做用域。

var a = 2; // 做用域1 全局
function foo(){ 
    var b = a * 2; // 做用域2 局部
    function bar(){
		var c = a * b; // 做用域3 局部
    }
}
复制代码
  1. 做用域是由你书写代码所在位置决定的。
  2. 子级做用域能够访问父级做用域,而父级做用域则不能访问子级做用域。

引擎对做用域的查找

做用域查找会在找到第一个匹配的标识符时中止,在多层的嵌套做用域中能够定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。也就是说查找时会从运行所在的做用域开始,逐级往上查找,直到碰见第一个标识符为止。

全局变量(全局做用域下定义的变量)会自动变成全局对象(好比浏览器中的 window对象)。

var a = 1;
function foo(){
    var a = 2;
    console.log(a); // 2
    function bar(){
        var a = 3;
        console.log(a); // 3
        console.log(window.a); // 1
    }
}
复制代码

非全局的变量若是被遮蔽了,就不管如何都没法被访问到,因此在上述代码中,bar内的做用域没法访问到foo下定义的变量a

词法做用域查找只会查找一级标识符,好比ab,若是是foo.bar,词法做用域查找只会试图查找foo标识符,找到这个变量后,由对象属性访问规则接管属性的访问。

欺骗语法

虽然词法做用域是在代码编写时肯定的,但仍是有方法能够在引擎运行时动态修改词法做用域,有两种机制:

  1. eval
  2. with

eval

JavaScript 的 eval函数能够接受一个字符串参数并做为代码语句来执行, 就好像代码是本来就在那个位置同样,考虑如下代码:

function foo(str){
    eval(str) // 欺骗
    console.log(a);
}
var a = 1;
foo("var a = 2;"); // 2
复制代码

仿佛eval中传入的参数语句本来就在那同样,会建立一个变量a,并遮蔽了外部做用域的同名变量。

注意

  • eval一般被用来执行动态建立的代码,能够根据程序逻辑动态地将变量和函数以字符串形式拼接在一块儿以后传递进去。
  • 在严格模式下,eval没法修改所在的做用域。
  • eval类似的还有,setTimeoutsetIntervalnew Function

with

with一般被看成重复引用同一个对象中的多个属性的快捷方式, 能够不须要重复引用对象自己。

使用方法以下:

var obj1 = { a:1,b:2 };
function foo(obj){
    with(obj){
        a = 2;
        b = 3;
    }
}
foo(obj1);
console.log(obj1); // {a: 2, b: 3}
复制代码

然而考虑如下代码:

var obj2 = { a:1,b:2 };
function foo(obj){
    with(obj){
        a = 2;
        b = 3;
        c = 4;
    }
}
foo(obj2);
console.log(obj2); // {a: 2, b: 3}
console.log(c); // 4 很差,c被泄露到全局做用域下
复制代码

尽管with能够将对象处理为词法做用域,可是这样块内部正常的var操做并不会限制在这个块的做用域下,而是被添加到with所在的函数做用域下,而不经过var声明变量将视为声明全局变量。

性能

evalwith会在运行时修改或建立新的做用域,以此来欺骗其余书写时定义的词法做用域,然而 JavaScript 引擎会在编译阶段进行性能优化,有些优化依赖于可以根据代码的词法进行静态分析,并预先肯定全部的变量和函数的定义位置,才能在执行过程当中快速找到标识符。可是经过evalwith来欺骗词法做用域会致使引擎没法知道他们对词法做用域作了什么样的改动,只能对部分不进行优化,所以若是在代码中大量使用evalwith就会致使代码运行起来变得很是慢。

函数做用域和块做用域

函数做用域

在 JavaScript 中每声明一个函数就会建立一个函数做用域,同时属于这个函数的全部变量在整个函数的范围内均可以使用。

块做用域

从 ES3 发布以来,JavaScript 就有了块做用域,建立块做用域的几种方式有:

  • with

    上面已经讲了,这里再也不复述。

  • try/catch

    try/catchcatch 分句会建立一个块做用域,其中声明的变量仅在 catch 内部有效。

    try{
    	throw 2;
    }catch(a){
    	console.log(a);
    }
    复制代码
  • letconst

    ES6 引入的新关键词,提供了除 var 之外的变量声明方式,它们能够将变量绑定到所在的任意做用域中(一般是{}内部)。

    {
        let a = 2;
    }
    console.log(a); // ReferenceError: a is not defined
    复制代码

    注意:使用 letconst 进行的声明不会在块做用域中进行提高。

提高

考虑这段代码:

console.log( a ); 
var a = 2;
复制代码

输入结果是undefined,而不是ReferenceError

为何呢?

前面说过,编译阶段时,会把声明分红两个动做,也就是只把var a部分进行提高。

因此第二段代码真正的执行顺序是:

var a; // 这时 a 是 undefined
console.log(a);
a = 2;
复制代码
  • 编译阶段时会把全部的声明操做提高,而赋值操做原地执行。
  • 函数声明会把整个函数提高,而不只仅是函数名。

函数优先

虽然函数和变量都会被提高,但函数声明的优先级高于变量声明,因此:

foo(); // 1
var foo;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
}
复制代码

由于这个代码片断会被引擎理解为以下形式:

function foo(){
    console.log(1);
}
foo(); // 1
foo = function() { 
  console.log( 2 );
};
复制代码

这个值得一提的是,尽管var foo出如今function foo()...以前,但因为函数声明会被优先提高,因此它会被忽略(由于重复声明了)。 注意:

JavaScript 会忽略前面已经声明过的声明,无论它是变量仍是函数,只要其名称相同。

后记

由于篇幅缘由,有一部份内容只是大概提到,并无太过于详细的讲解,若是你感兴趣,那么我推荐你看看**《你不知道的 JavaScript(上)》**这本书,书上对此内容有很详细的说明。

本文也是做者一边查看此书一边结合本身的理解来进行编写的。

其实做用域还有一个很是重要的概念,那就是闭包。但闭包也是 JavaScript 中的一个很是重要却又难以掌握的,因此须要另开一篇文章来介绍。

最后,我想说的就是,在这个框架工具流行的时代,咱们每每会被这些新东西所吸引,却忽略了最本质的东西,诸诸不知,偏偏是这些咱们所忽略的东西才是最重要的,全部的 JavaScript 框架工具都是基于这些内容。因此,不妨回过头来看看这些原生的东西,相信你会更上一层楼。

谢谢观看!

注:此文为原创文章,如需转载,请注明出处。

相关文章
相关标签/搜索