JavaScript是一门编译语言,而并不是通常所说的“动态”或“解释执行语言”。它不是提前编译的,编译结果也不能在分布式系统中进行移植。
编译语言执行之前的三个步骤:
将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。示例:var a = 3; 在这个过程中会分解成下面这些词法单元: var、a、=、3、; 。
将词法单元流(数组)转换成一个由元素逐渐嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(AST)。
将抽象语法树(AST)转化成可执行代码的过程。
JavaScript引擎不止上面的三个步骤,它的步骤要复杂得多。
引擎: 从头到尾负责整个JavaScript程序的编译以及执行过程。
编译器: 负责语法分析以及代码生成。
作用域: 收集并维护由所有的标识符(及变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些变量的访问权限。
编译器在当前作用域中声明该变量(如果之前没有声明过)。
运行时引擎会在作用域中查找该变量,如果找到就对它进行赋值操作。
LHS: 变量出现在赋值操作的左侧执行LHS查询。(找到目标变量)
RHS: 变量出现在赋值操作的右侧执行RHS查询。(取到它的源值)
当一个块或函数在另一个块或函数中,就发生了作用域的嵌套。
当在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量或者抵达最外层的作用域(全局作用域)。
如果RHS查询在所有的嵌套作用域中都找不到所需的变量,引擎就会抛出ReferenceError异常。
如果LHS查询在所有的嵌套作用域中都找不到所需的变量,则会在全局作用域中创建一个具有该名称的变量,并将其返回给引擎。(程序在非严格模式下)。
如果RHS查询找到了一个变量,但是你对其进行不合理的操作,比如:对一个非函数类型的值进行函数调用;引用null或undefined类型中的属性,引擎会抛出TypeError的异常。
作用域的工作模型:词法作用域和动态作用域(少数编程语言使用)。
词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。因此词法分析器在处理代码时会保持作用域不变。也就是我们在JavaScript中所说的作用域。
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符(遮蔽效应)。
注意:
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数声明时所处的位置决定。
词法作用域只会查找一级标识符。
eval(…)函数可以接受一个字符串作用参数,并将其中内容好像在书写代码时就存在于程序中的这个位置。
示例:
function foo(str, a) {
eval(str) //欺骗的目的
console.log(a, b);
}
var b = 2;
foo("var b = 3", 10); //执行结果10,3
上面的代码中b不会输出全局作用域中的2。因为传进去的是一个var b = 3;在执行到eval函数时,eval里面的代码会被执行,从而在foo函数作用域中创建了一个变量b。从而遮蔽了全局作用域中的b。
注意:在严格模式中,eval(…)函数在运行时有其自己的词法作用域,所以此时无法修改所在的作用域。
with通常被当作重复引用同一个对象的多个属性的快捷方式,可以不需要重复引用对象本身。
示例:
function foo(obj) {
with(obj){
a = 2;
}
}
var o1 = {
a: 3;
}
var o2 = {
b: 3;
}
foo(o1);
console.log(o1.a); //2
foo(o2);
console.log(o2.a) //undefined
console.log(a) //2
上述代码在执行13行时,将对象o1传进了函数foo,此时进行了with操作,里面的a=2,会在全局作用域中创建一个变量a,并把2赋值给它。
严重影响运行性能。(不要使用它们)
用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。
catch分句会创建一个块级作用域,其中声明的变量仅在catch内部有效。
let关键字可以将变量绑定到所在的任意作用域中(通常是{ … }内部),也就是let关键字为其声明的变量隐式地劫持了所在的块作用域。
示例:
var foo = true;
if(foo){
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
console.log(bar) //ReferenceError
let声明的bar变量,在外部访问时会报ReferenceError的错误。
只要声明是有效的,在声明中的任意位置都可以使用一对大括号{ … }来为let创建一个用于绑定的块。
let声明不会提升,即不会像var一样被提前进行说明。
ES6中引入了sonst,同样可以用来创建块作用域变量,其值是固定的(常量)。定义之后去修改它的值会引起错误。
引擎会在解释JavaScript代码之前首先对其进行编译,编译的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
作用域提升的过程就好像变量和函数声明从它们在代码中出现的位置被“被移动”到了最上面。所以是先有声明,后有赋值。
注意:
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码的执行顺序,会造成非常严重的破坏。
每个作用域都会进行提升操作。函数表达式不会被提升。
即使具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用。
foo(); //TypeError(非法操作)
bar(); //ReferenceError(bar未声明)
var foo = function bar(){
//...
}
提升之后可以看出这样的形式:
var foo;
foo(); //TypeError(非法操作)
bar(); //ReferenceError(bar未声明)
foo = function bar(){
//...
}
函数优先原则:
函数声明和变量声明都会被提升。二者都存在时,是函数首先会被提升,然后才是变量。
foo(); //1
var foo; //重复的声明(被忽略)
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
会输出1,而不是2。
上述代码被引擎理解为:
function foo(){
console.log(1);
}
foo(); //1
foo = function(){
console.log(2);
}
函数比变量先提升。
后面的函数声明会覆盖前面的函数声明。
本文全方面的概括了JavaScript中的作用域,从JS编译原理出发,分析JS代码执行过程,然后详细介绍了js的词法作用域、函数作用域、块作用域等,最后对作用域提升做了详细分析。
但愿这篇文章能解决你的某些问题与困惑。如有不妥之处,及时指出!