JavaScript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域。 在 js 中作用域分为全局作用域和函数作用域,函数作用域可以互相嵌套。 每个函数都会创建自己的作用域,作用域在函数定义的时候就已经确定了,与函数是否被调用无关。 通过作用域,可以知道作用域范围内的变量和函数有哪些,却不知道变量的值是什么。所以作用域是静态的。
作用域内部原理
预编译
以 var a = 2;为例。 简单来说,编译过程就是编译器把程序分解成词法单元(token),然后把词法单元解析成语法树(AST),再把语法树变成机器指令等待执行的过程。
- 分词
// var a = 2;被分解成为下面这些词法单元:var、a、=、2、;。这些词法单元组成了一个词法单元流数组。
// 词法分析后的结果
[
"var" : "keyword", //关键字
"a" : "identifier",//标识符
"=" : "assignment",//分配
"2" : "integer",//整数
";" : "eos" (end of statement)//结束语句
]
- 代码生成
将AST转换为可执行代码的过程被称为代码生成 var a=2;的抽象语法树转为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将值2储存在a中。 实际上,javascript引擎的编译过程要复杂得多,包括大量优化操作,上面的三个步骤是编译过程的基本概述。 任何代码片段在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微秒。javascript编译器首先会对var a=2;这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
- 解析
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称为“抽象语法树” (Abstract Syntax Tree, AST)。
VariableDeclaration:{
operation: "=",
left: {
keyword: "var",
right: "a"
}
right: "2"
}
查询
从字面意思去理解,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。 更准确地讲,RHS查询与简单地查找某个变量的值没什么区别,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。
function foo(a){
console.log(a);//2
}
foo( 2 );
// 这段代码中,总共包括4个查询,分别是:
// 1、foo(…)对foo进行了RHS引用
// 2、函数传参a = 2对a进行了LHS引用
// 3、console.log(…)对console对象进行了RHS引用,并检查其是否有一个log的方法
// 4、console.log(a)对a进行了RHS引用,并把得到的值传给了console.log(…)
执行
经过编译之后,代码会开始执行 引擎运行时会首先查询作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量,并且把这个已经存在的a的值设置为2;如果否,引擎就会在当前作用域创建一个叫作a的变量并赋值2。 依据编译器的编译原理,javascript中的重复声明是合法的
嵌套
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
异常
RHS查询失败,引擎会抛出ReferenceError(引用错误)异常 如果RHS查询找到了一个变量,但尝试对变量的值进行不合理操作,比如对一个非函数类型值进行函数调用,或者引用null或undefined中的属性,引擎会抛出另外一种类型异常:TypeError(类型错误)异常 当引擎执行LHS查询时,如果无法找到变量,全局作用域会创建一个具有该名称的变量,并将其返还给引擎。 如果在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常
词法作用域
词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
动态作用域
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();
// 词法作用域:输出为2
// 动态作用域:输出为3
// 两种作用域的区别:
// 简而言之,词法作用域是在定义时确定的,而动态作用域是在运行时确定的
遮蔽效应
作用域查找从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。 在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符。
作用域链
各个作用域的嵌套关系组成了一条作用域链。 使用作用域链主要是进行标识符(变量和函数)的查询,标识符(变量和哈数)解析就是沿着作用域链一级一级地搜索标识符的过程,而作用域链就是保证对变量和函数的有序访问。 如果自身作用域中声明该变量,则无需使用作用域链。 如果自身作用域中未声明该变量,则需要使用作用域链进行查找 如果标识符找不到,则抛出 ReferenceError(引用错误)异常。
自由变量
自由变量就是在当前作用域中存在但未在当前作用域中声明的变量
执行环境
执行环境(execution context) 也叫执行上下文、执行上下文环境。 执行上下文环境在函数调用时确定的。 每个执行上下文环境包含了作用域内所有的变量和函数的值。 一定要区分执行环境和变量对象。执行环境会随着函数的调用和返回,不断的重建和销毁。但变量对象在有变量引用的情况下,将留在内存中不被销毁。 执行上下文环境是动态的。
执行流
代码的执行顺序叫做执行流,程序源代码并不是按照代码的书写顺序一行一行往下执行,而是和函数的调用顺序有关。
执行环境栈
执行环境栈有序地保存着当前程序中存在的执行环境。当执行流进入一个函数时,函数的环境会被压入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。javascript 程序中的执行流正是由这个机制控制。 其实就是一个压栈出栈的过程。