Libx

我不知道的JavaScript:作用域

Word count: 2,383Reading time: 9 min
2018/07/12 Share

一直以来对于JS总是有一些一知半解的东西,因为这个假期没有留校,也还没有去找实习,就当给自己一些“私人时间”来对之前不求甚解的东西来梳理一遍吧。

一、作用域

所有语言都有作用域,这是一种语言的基本功能之一,就是能够存储变量的值,并且能够在之后对之进行访问和修改,也正是因为这种存储和访问变量的能力将状态带给了程序。要将变量引入程序就带来了相应的其他问题,怎么存,如何找到他们。相应的就需要一套规则来存储变量,并且之后可以方便的找到这些变量,这套规则就是作用域了。

1.1 编译

虽然JS是一门脚本语言,但是他也是一门“编译语言”,与传统C,JAVA等不同的是,他并非是提前编译的,而是在运行时进行编译(V8),JS的编译过程大概经过这几个过程:

  • 分词/词法分析(Tokenizing/Lexing)这个过程将字符串分解成对有意义的代码块,这些代码块被称为词法单元(Token)比如var a = 1; 这段代码会被分解为“var、a、=、1、;”
  • 解析/语法分析(Parsing) 这个过程将词法单元流转换为一个由元素嵌套所构成的代表程序语法机构的抽象语法书(AST,Abstract Syntax Tree)
  • 代码生成 AST树转化成可执行代码的过程。

以上可以说是非常笼统概括的一个简单的编译过程,事实上,JS的编译解释过程要复杂得多,中间还要包括比如说在词法分析和代码生成阶段的特定处理步骤来对运行的性能、冗余元素进行优化等。很容易想到,因为JS的编译发生在你要运行代码前,所以引擎使用了各种方法来使编译速度能够提升上来。

作用域嵌套

既然作用域是根据名称查找变量的一套规则,那么在实际的使用中,就需要同时顾及几个作用域。

当一个函数或块嵌套在另一个快或者函数中的时候,就发生了作用域的嵌套。因此,在当前的作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量(全局作用域为止)这其实也就是遍历嵌套作用域的规则。

一个很简单的例子:

function foo (a){
console.log(a+b);
}
var b = 2;
foo(2) // 4

对b的RHS引用无法在函数foo内部完成,于是到上一层作用域中寻找

词法作用域

作用域有两种基本的工作模型:词法作用域和动态作用域。词法作用域较为普通,大多数语言都采用这种模式,而动态作用域则也仍有一些语言在使用。

2.1 词法阶段

之前提到,大多数的语言编译器的第一个工作阶段叫做词法化,这也是词法作用域的名称来历的基础。

简单地说,词法作用域就是定义在词法阶段的作用域,也就是说,是由你在写代码的时候将代码放在哪里决定的,因此大部分情况下当词法分析器处理代码的时候会保持作用域不变。

来看一个例子;

function foo(a){ // 1
var b = a * 2; // 2
function bar(a,b,c){ // 3
console.log(a,b,c);
}
bar(1,2,3);
}
foo(2);

我们把他理解为三层机构

  1. 包含着全局作用域,只有一个标识符foo
  2. 包含着foo所创建的作用域,其中有 a bar b
  3. 包含bar 创建的作用域, 只有c一个标识符

现在暂时认为作用域尤其对应的代码块写在哪里决定,是逐级包含的

2.2 查找

作用域间的结构和位置关系给引擎提供了足够的位置信息,引擎使用这些信息来查找标识符的位置
作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这里叫作“遮蔽效应”

2.3 欺骗词法

既然词法作用域完全由函数的声明位置来决定,那么如何来在运行时改变词法作用域呢?
到了在众多风格指南中都会明令禁止使用的部分了:eval和with

  • eval

    eval接受一个字符串为参数,将其中的内容视为书写时就存在这个位置的代码。
    也就是说,在进行词法分析的时候,eval内部的代码假装自己本来就在那里,而引擎也并不拆穿他,如常地进行词法作用域查找
    看一个例子:

    function foo(str,a){
    eval(str);
    console.log(a,b);
    }
    var b = 2;
    foo("var b = 3;",1) //1,3

    这段代码所做的其实也就是在foo的作用域中创建了新的变量b,并遮蔽了外部的同名变量(严格模式下eval将拥有自己的词法作用域)

  • with
    with通常被当作重复引用同一个对象的多个属性的快捷方式比如:

    var xiaoming = {
    age: 10,
    weight: 40,
    height: 1.5
    }
    with(xiaoming){
    age: 12,
    weight: 45,
    height: 1.6
    }

    但是在with的使用过程中可能会:

    function foo (obj){
    with(obj){
    a = 2;
    }
    }
    var o1 = {a:3}
    var o2 = {b:3}
    foo(o1) // o1.a = 2
    foo(o2) // o2.a = undefined window.a = 2

    尽管with块可以将一个对象处理成词法作用域,但是这个块内声明的var并不会限制在这个块的作用域,而是被添加到with所处的函数作用域中。

函数作用域和块作用域

3.1函数作用域

Javascript具有基于函数的作用域吗,这意味着每声明一个函数都会为其创建作用域
首先看个例子:

function foo(a){
var b = 2;
function bar(){
// ...
}
var c = 3;
}
bar(); // bar is not defined
console.log(a,b,c) // all failed

函数作用域的含义是指:属于这个函数的全部变量都可以在整个函数的范围内使用和复用(嵌套作用域也可)

3.2 隐藏内部实现

在写代码的时候,我们一般会遵照最小特权或最小暴漏原则,即:在软件设计中,应最小限度的暴漏必要内容,而将其他内特容都隐藏起来。

这个原则可以引申到如何选择作用域来包含变量和函数。

简单地说,通常我们写代码是先声明一个函数再向里面加上代码,但是我们也经常做一些相反的事情,我们发现一段代码可以更加独立,于是我们抽出这段代码来封装成新的函数。其实就是将这些代码隐藏了,而结果就是,在这些代码的周围创建了新的作用域,可以说,把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。

3.3 立即执行函数

我们知道在代码外添加包装函数可以将内部的变量和函数“隐藏”起来,外部作用域无法访问包装函数内部的内容,但是很显然,这还需要一个函数名(将污染其所在作用域)并且还需显式调用函数才有效。上帝说要有立即执行函数于是就有了:

var a = 2
(function foo(){
var a = 3;
console.log(a)
})();
console.log(a) // 2

(function(b){b()})(function b(){})

3.4块作用域

先来一个非常简单的例子来引出块作用域:

function test (){
for(var i= 0;i<5;i++){
setTimeout(function(){console.log(i)},1000)
}
}
test()

很可惜JavaScript直接来看的话并没有块级作用域,但是其实也是有的:

  1. with
  2. try/catch
    在ES3中,try/catch会创建一个块作用域,其中声明的变量只有catch内部有效

    try{
    undefined();
    }catch(err){console.log(err)}
    console.log(err)// err not defined
  3. ES6的出现改变了这一乱象,let const关键字可以将变量绑定到任意作用域上,也就是说,let为其声明得的变量隐式的附加在了一个已经存在的块级作用域上。

    3.5变量提升

    Javascript的代码并不完全按照从上到下执行

    // 1
    a = 2;
    var a;
    console.log(a) //2
    // 2
    console.log(a) //undefined
    var a = 2;

变量和函数的声明都会在代码执行之前被处理

比如var a = 2解释器会将其认为是两段代码var a a = 2,首先是声明,其后是赋值,定义声明是在编译阶段进行的,赋值会留在原地等待执行阶段。

也就是说:2中的代码的执行顺序是这样的:var a ;console.loe(a); a = 2

这个过程好像变量和函数声明被移动到了最上面,这就叫_提升

  • 函数会被提升但函数表达式不会被提升:

    foo() // success
    function foo(){
    // ...
    }
    foo() // TypeError
    var foo =function bar(){}
  • 函数优先

    foo() // 1
    var foo;
    function foo(){
    console.log(1)
    }
    var foo = function(){
    cosnole.log(2)
    }

先写到这里,下一篇将对闭包来进行梳理

CATALOG
  1. 1. 一、作用域
    1. 1.1. 1.1 编译
    2. 1.2. 作用域嵌套
  2. 2. 词法作用域
    1. 2.1. 2.1 词法阶段
    2. 2.2. 2.2 查找
    3. 2.3. 2.3 欺骗词法
  3. 3. 函数作用域和块作用域
    1. 3.1. 3.1函数作用域
    2. 3.2. 3.2 隐藏内部实现
    3. 3.3. 3.3 立即执行函数
    4. 3.4. 3.4块作用域
    5. 3.5. 3.5变量提升