JavaScript攻略:作用域
前言
在学习JavaScript这门语言的过程中,我们一定要深入理解其底层机制和原理。在本篇文章中,作者将介绍JavaScript中两个非常重要的概念:编译原理和作用域。了解这些概念不仅有助于我们编写更加优雅的代码,还能帮助我们更好地排查解决问题。本篇文章通过详细介绍JavaScript的编译过程及其作用域规则,并通过一些恰当的代码示例帮助读者更好地理解。
编译原理
在深入学习JavaScript作用域之前我们一定要先了解JavaScript引擎的编译原理。每一段代码在执行之前都要先经历一个编译过程,这个过程大致可以分为三个阶段:词法分析、语法分析和代码生成。
以下列这段简单的代码为例,我将在介绍每个编译步骤时,分析这段代码的一些编译过程以便读者可以更好的理解。
var a = 1;
function foo(){
var a = 2
console.log(a);
}
foo()
词法分析
词法分析是编译过程的第一个阶段,在这个阶段JavaScript引擎将字符串分解成 词法单元 。这些词法单元是编程语言中最基本的元素,如关键字、标识符、字面量(如数字、字符串)、操作符和分隔符(如逗号、分号)等。空格也可以是一个词法单元,这主要取决于空格在这行代码中是否有意义。例如:
var a = 1; 这行代码被分解为:var、a、=、1、; 。
语法分析
语法分析也称之为解析,这个过程中会将词法单元流(数组)转换成一个由元素逐级嵌套所组成的树,即语法抽象树(Abstract Syntax Tree, AST)。AST代表了程序的语法结构,但不包括代码中的任何实际执行操作,如示例代码中的 console.log(a); 和 foo() 。
代码生成
代码生成的过程是将 AST抽象语法树转换为一个可执行的代码。以var a = 1;为例,就是将上一步骤中这一行代码形成的AST转换为机器指令,创建一个变量a,将值1存储于a中。
作用域与变量声明
作用域
作用域是指变量在程序中可访问的区域。JavaScript的主要作用域有:词法作用域、全局作用域和函数作用域,ES6又引入了块级作用域。
- 词法作用域
- 词法作用域是一套规则,用于管理 JavaScript引擎在当前作用域下或者嵌套的子作用域中根据标识符的名称进行变量查找。
- 在JavaScript中,函数的作用域是在函数声明时决定的,而不是在函数调用时决定的。这种作用域决定了函数内部如何查找变量,即首先查找函数内部是否有该变量,如果没有,则向外查找父级作用域,依此类推,直到找到全局作用域(作用域查找会在找到第一个匹配的标识符时停止)。
- 全局作用域
- 在浏览器环境下,全局作用域是
window
对象,在Node.js环境下,全局作用域是global
对象。 - 在全局作用域中声明的变量或函数会成为全局变量或全局函数,可以在整个代码的任何地方被访问。
- 函数作用域
- 函数作用域是定义在函数体内部的变量或函数,它们只能在函数体内部被访问。
- 块级作用域
- ES6引入了
let
和const
声明方式,使得JavaScript拥有了块级作用域。块级作用域是指变量或函数仅在它们被声明的块({}
)内部可见。
作用域规则
内部作用域可以访问外部作用域的变量,但外部作用域不能访问内部作用域的变量。
在下面这个例子中,变量 a 是在函数 foo 内部声明的,当尝试在全局作用域中访问 a 时,JavaScript会报错。由此我们可以得出结论:外层作用域不能访问内层作用域的变量。
function foo(){
var a = 1;
}
foo()
console.log(a); // 报错:a is not defined
在下面这个例子中,变量 a 是在全局声明的,函数 foo 能够访问并打印在全局作用域中声明的变量 a。由此我们得出结论:内部作用域可以访问外部作用域的变量。
var a = 1;
function foo(){
console.log(a); // 输出:1
}
foo()
变量声明
在JavaScript中,变量声明决定了变量的作用域和生命周期。
- var声明
- var 是JavaScript最早的变量声明方式,使用 var 声明的变量具有函数作用域或者全局作用域,而没有块级作用域。
- var 存在声明提升的特性,这意味着无论 var ���明在函数的哪个位置,它都会被处理成好像是在函数的最顶部声明的一样。
- var 可以重复声明变量。
在以下这段代码中,变量a按照一般情况是应该报错的,但是经过var声明后,JavaScript引擎编译后a的声明提升,在console.log(a);语句之前就已经声明了,但是赋值语句并没有提升,只有声明本身提升了,所以结果是undefined。
console.log(a); // undefined
var a = 1
// v8 编译成这样:
// var a
// console.log(a);
// a = 1
- let声明
- let 是ES6引入的一种新的变量声明方式,let 会和{}形成块级作用域。
- 与 var 不同,let 声明的变量不会被提升,因此它们只能在声明之后被访问,且let 也不可以重复声明变量。 使用 let 可以避免许多由 var 引起的常见错误,特别是在循环和条件语句中。let声明的变量a只在块级作用域内生效也就是if语句中,所以在全局作用域中找不到 a。
if (true) {
let a = 1;
console.log(a); // 1
}
console.log(a); // 报错:a is not defined
- const声明
- const 也是ES6引入的,用于声明一个常量,也就是说这个值在初始化后不可以改变。
- const 也具有块级作用域,并且不会被提升,不可重复声明变量,也不可以再被赋予新的值(但如果是对象或数组的话,可以在不涉及到改变对象或数组的引用的条件下修改这个对象或数组的内部状态,比如添加、删除或修改属性,或者在数组中添加元素)。
欺骗词法作用域
虽然JavaScript的作用域是静态且不可变的,但eval
和with
语句提供了修改或“欺骗”词法作用域的方式。但是大家在实际开发中,应尽量避免使用这两种语句,因为它们可能导致代码难以理解和维护,特别是当它们用于修改词法作用域时,并且欺骗词法作用域会导致性能下降。
eval
eval
函数可以将传入的字符串当作JavaScript代码来执行。由于 eval 内部的代码是在调用它的上下文中执行的,因此它可以访问和修改原本不属于其词法作用域的变量。
function foo(str, a){
eval(str);
console.log(a, b); // 输出:1, 3
}
var b = 2;
foo("var b = 3;", 1);
with
with
用于修改一个对象中的属性值,但是如果修改的属性在原对象中不存在,那么该属性就会被泄露到全局。
function foo(obj){
with(obj){
a = 2
}
}
var o2 = { b: 4 }
foo(o2);
console.log(o2); // {b: 4}
console.log(o2.a); // undefined
console.log(a); // 2
总结
通过认真阅读本篇文章,相信大家对于JavaScript的编译过程和作用域概念有了更好地了解,如果在你阅读的过程中有发现本篇文章的疏漏之处,欢迎评论区指正。
本篇文章除了作者自己的认识外,也参考了以下这本书籍:
《你不知道的JavaScript》
转载自:https://juejin.cn/post/7393307982639087625