作用域
JavaScript引擎
:从头到尾
负责JavaScript程序的编译以及执行。编译器:
负责语法分析以及代码生成等。
作用域:
收集并维护所有编译阶段
所声名的变量组成一系列严格的查询规则(其实就是作用域链),用来确定
当前所执行的代码是否拥有对这些变量的访问权限
。
作用域链
作用域链中的每个作用域都包含了在该作用域中声明的变量,以及对外部作用域的引用。这些引用形成了一个链条,允许引擎按照特定的规则(LHS、RHS等)在不同的作用域中(逐级向上
)查找变量的值。
-
LHS:
在赋值操作中查找变量的存放位置
,为赋值操作寻找目标。 -
RHS:
查找变量中存放的值
。(字面量不会进行RHS查询,比如数字" 5 "
)。
JavaScript引擎、作用域与编译器之间的关系
词法作用域
作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域
。另外一种叫作动态作用域
,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等),而JavaScript所采用词法作用域
。
对词法作用域的理解
在代码编写后,JavaScript 解析器
会对代码进行分析,识别出变量声明和作用域嵌套关系。这个过程在语法分析阶段完成。在这个阶段,解析器
会创建作用域链
,并确定变量在不同作用域中的可见性和访问权限。
因此,词法作用域是在代码编写阶段就确定的
,它在代码执行之前就已经被建立起来,换句话说,词法作用域是由你在写代码时将变量和块作用域(ES6)写在哪里来决定的
,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的,因为有eval
和with
这俩小子搞特殊😫 )。这也是为什么 JavaScript 的作用域是词法作用域
(也叫静态作用域)的原因,因为作用域的规则是在代码编写时静态决定的
,而不是在代码执行时动态确定的。
注意:
JavaScript解析器 整个JavaScript引擎
示例代码
javascript
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c); // 2,4 12
}
bar(b * 3);
}
foo(2);
作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。(有先找自己的,自己没有就找别人的,别人没有那就玩完了😏,此时报错在向你招手😍。)
函数作用域与块作用域
函数作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(嵌套的作用域中也可以使用,毕竟有作用域链查找)
示例代码
javascript
function foo(a) { // <-- 开端
var b = a;
console.log(b);
}// <-- 结尾
foo(2);
注意:
JavaScript中var
声名的变量只存在函数作用域
和全局作用域
,而块作用域
是在ES6
中let
和const
中才存在。
块作用域
示例代码
javascript
if (666) {
var a = 10;
console.log(a); // 10
}
console.log(a); // 依旧可以访问到 a ===> 10
而let与const不存在此问题
javascript
if (666) { // <-- 块作用域Start
let a = 10;
console.log(a); // 10
}// <-- 块作用域End
console.log(a); // Uncaught ReferenceError: a is not defined
javascript
if (666) {// <-- 块作用域Start
const b = 20;
console.log(b); // 20
}// <-- 块作用域End
console.log(b); // Uncaught ReferenceError: b is not defined
for循环与块作用域
javascript
for (var i = 1; i <= 3; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
执行了3次没错,但是为什么都是4?这是由于var出手了😁,请看下图
那该如何做呢?
用let😙
javascript
for (let i = 1; i <= 3; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
使用let
关键字声明变量i
,会在每次循环迭代时创建
一个新的块级作用域
(变量i在循环过程中不止被声明一次)。每个迭代都会创建一个新的作用域,每个迭代都会使用上一个迭代
结束时的值
来初始化 i
这个变量,确保在定时器回调函数内部捕获到的i
值是该迭代中的值。
var的变量提升
变量和函数(函数首先会被提升,然后才是变量)
在内的所有声明
都会在编译阶段
,也就是任何代码被执行前首先被处理。
只有声明本身会被提升
,而赋值或其他运行逻辑会留在原地。
var声名提升示例代码
javascript
console.log(a);
var a = 2;
提升后:
javascript
var a; // 声名提升
console.log(a); // undefined
a = 2; // 赋值操作留在原地
函数声名提升示例代码
javascript
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
提升后:
javascript
function foo() {
console.log(1);
}
var foo; // 变量声明被提升,但由于函数声明存在,被忽略(因为重名了)
foo(); // 1
foo = function () {
console.log(2);
};
var foo被忽略是因为函数声明的提升优先于变量声明的提升,因此在遇到这种情况时,函数声明会覆盖同名的变量声明。
另一种情况: 存在多个同名的函数声明,后面的函数声明会覆盖前面的函数声明。
javascript
foo(); // 3
function foo() {
console.log(1);
}
var foo = function () {
console.log(2);
};
function foo() {
console.log(3);
}
提升后
javascript
// 后来居上!(被覆盖了)
function foo() {
console.log(1);
}
function foo() {
console.log(3);
}
var foo; // 变量声明被提升,但由于函数声明存在,被忽略(因为重名了)
foo();// 3
foo = function () {
console.log(2);
};
闭包
- 当函数可以
记住并访问
所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 - 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
javascript
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()函数本身当作一个值类型(baz)进行传递。赋值给变量baz并调用baz(),实际上只是通过不同的标识符
引用调用
了内部的函数bar()。
基于bar()所声明的词法作用域位置,它拥有涵盖foo()内部作用域的引用,使得该作用域(foo)能够一直存活
,以供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫作闭包。
参考资料来源:《你不知道的JavaScript 上卷》------ Kyle Simpson