作用域是什么?
编译原理
尽管通常将JavaScript归类为“动态”或“解释执行”语言,但事实上它是一门编译语言
程序在执行之前会经历三个步骤,统称为”编译”:
- 分词/词法分析
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token) - 解析/词法分析
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。 - 代码生成
将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关
JavaScript的编译过程不是发生在构建之前的,对于JavaScript来说,大部分情况编译发生在代码执行前的几微秒(甚至更短)的时间内。
任何JavaScript代码片段在执行前都要进行编译
理解作用域
作用域是根据名称查找变量的一套规则
- 引擎
从头到尾负责整个JavaScript程序的编译及执行过程 - 编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容) - 作用域
引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
遍历嵌套作用域的规则:
擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止
词法作用域
作用域主要分为两种工作模型:
- 第一种是被大多数编程语言所采用的词法作用域
- 第二种是动态作用域
大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。回忆一下,词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义
简单来说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
查找
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
函数作用域和块作用域
函数作用域
匿名函数的缺点
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
- 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
- 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明
立即执行函数表达式
两种方式:
- 函数表达式被包含在
()
中,然后再后面使用另一个()
括号来调用
(function foo(){...})()
- 用来调用的
()
括号被移进了用来包装的()
括号中
(function(){...}())
IFE的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。
提升
函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域
先有蛋(声明)后有鸡(赋值)
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏,同样每个作用域都会进行提升操作。
注意:即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用
函数优先
函数声明和变量声明都会被提升,需要注意的细节:函数会首先被提升,然后才是变量
foo()
var foo;
function foo() {
console.log(1)
}
foo = function() {
console.log(2)
}
输出结果为:1
虽然var foo
在函数foo()
声明之前,但它是重复声明,会被忽略,因为函数声明会提升到普通变量之前,虽然重复的var声明会被忽略,但出现在后面的函数声明还是可以覆盖前面的
尽量避免在块内部声明函数
作用域闭包
闭包
无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo() {
var a = 2;
function baz() {
console.log(a)
}
bar(baz)
}
function bar(fn) {
fn()
}
foo() // 2
把内部函数baz传递给bar,当调用这个内部函数时(现在叫做fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a
模块
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() { console.log(something); }
function doAnother() { console.log(another.join(" ! ")); }
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule()
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
这个模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露