深入浅出 JavaScript作用域
前言
JavaScript的执行过程始于解析,将源代码转换成抽象语法树,随后进行预编译,包括变量和函数声明提升。执行阶段逐行执行代码,可能涉及函数调用、条件语句等操作,并在运行时动态解析和执行代码。最后,一些引擎会通过优化技术,如JIT编译等,提高代码性能,包括内联缓存、去除未使用代码等。这些步骤相互配合,确保JavaScript代码能够高效地执行。
编译过程
编译过程是将源代码转换为目标代码的过程。在编译过程中,通常包括词法分析、语法分析、语义分析、优化和代码生成。
举个例子:
let x = 5;
let y = x + 3;
-
在词法分析阶段,会识别出
let
、x
、y
、=
、5
、+
、3
等词法单元。 -
在语法分析阶段,会构建出相应的语法树,确定变量声明和表达式的结构。
-
在语义分析阶段,会检查变量的使用是否合理,以及表达式的计算是否有意义等。
-
在代码生成阶段,会生成实际的可执行代码。
//最后生成 let x = 5; let y = x + 3;
作用域
作用域是指变量、函数等在代码中可被访问的范围。它决定了变量和函数在何处以及如何被查找和使用。
简单来说,作用域规定了哪些代码可以访问特定的变量或执行特定的函数。
作用域的作用
- 避免命名冲突:确保不同作用域中的变量和函数具有唯一的标识符,防止变量被意外覆盖或错误使用。
- 代码组织和维护:明确变量的有效范围,使代码结构更清晰,便于理解和维护。
- 实现封装性:将相关的代码和变量封装在特定作用域内,实现一定程度的隐藏和保护。
- 控制变量的生命周期:不同作用域中的变量在不同时间被创建和销毁,作用域控制有助于管理变量的存在时间。
有效标识符
有效标识符是在程序中,用户自定义的用来命名变量、函数、类、模块等的字符序列,它需满足以下条件:
- 由字母、数字、下划线(_)或美元符号($)组成。
- 不能以数字开头。
- 通常具有一定的语义,能直观地表达其代表的意义。
作用域的分类
作用域可以分为函数作用域,全局作用域和块级作用域。
- 全局作用域:在程序的最外层定义的变量拥有全局作用域,在整个程序中都可以访问。
- 函数作用域:在函数内部定义的变量属于函数作用域,只能在函数内部及嵌套在函数内的子作用域中被访问。
- 块级作用域:如用花括号括起来的代码块内定义的变量,具有块级作用域。
首先我们用例子开始。
eg1
var a = 1
console.log(a)
//输出:1
eg2
function b(){
var a = 1
}
b()
console.log(a)
//报错:a is not defined
eg3
var a = 1
function b(){
console.log(a)
}
b()
//输出:1
在eg2代码从上往下执行,运行了b()函数并定义了a=1,但是输出a会报错。但是eg1不报错。
在eg2中,a是在函数中定义的,而在eg1中,a是在全局中定义的。在eg2中,a是b函数的有效标识符,是在函数作用域中;在eg1中,a是在全局作用域中。我们可以从eg1和eg2中看出,全局作用域是无法访问内部作用域。
用eg2和eg3进行对比,我们可以看出,函数访问了全局变量。我们可以得出,内部作用域可以访问全局作用域。
作用域的规则:外层作用域是无法访问内部作用域,内部作用域可以访问外层作用域,最大的外层作用域是全局作用域。
这主要是由作用域的工作机制决定的。
当代码在执行时,内层作用域是在外层作用域的基础上创建的。内层作用域可以访问外层作用域中的变量和函数,这被称为“作用域链”。
而外层作用域无法直接访问内层作用域中的变量,这是为了避免变量冲突和混乱,保持代码的清晰性和可维护性。如果允许外层作用域随意访问内层作用域,可能会导致意外的结果和难以调试的问题。
eg4
if(true){
var a = 1
}
console.log(a)
//输出:1
eg5
if(true){
let a = 1
}
console.log(a)
//报错:a is not defined
我们用eg4和eg5进行对比,我们会发现区别就是var和let。
在eg5中在全局作用域中找不到变量,所有就一定存在一个作用域,但是里面并没有函数,所以不是函数作用域。那是什么作用域你?
是块级作用域。{let……}或{const……}可以构成一个块级作用域。
声明关键词
声明关键词由var、let和const。
-
let和var的区别
-
var存在声明提升,let不存在。
console.log(a) var a = 1 //返回:undefined //声明提升后是这样的效果 var a console.log(a) a=1
console.log(a); let a = 1 // 报错:Cannot access 'a' before initialization at Object
-
let会和{}形成块级作用域。
-
var可以重复声明变量let不可以(会报错)。
var a = 1 var a = 2 console.log(a) //输出:2
let重复声明会出现提示。
-
let存在暂时性死区。暂时性死区是指在变量使用之前,从该变量的作用域开始到该变量声明的位置之间的区域。
let a = 1 if (1) { console.log(a); //暂时性死区 let a = 2 } //报错:Cannot access 'a' before initialization at Object.
在这个例子中,
console.log(a)
这一行会报错。这是因为在
if
块内部,又使用let
重新声明了变量a
,在这个重新声明之前的区域就是暂时性死区,不能访问之前的变量a
。
-
-
let和const的区别
-
let可以重新赋值
let num = 5; num = 10; console.log(num); //输出10
-
const声明后不能重新赋值
const pi = 3.14 pi = 3.15; console.log(pi); //报错:Assignment to constant variable.
-
欺骗词法作用域
欺骗词法作用域(也称为打破词法作用域)是指通过一些特殊的手段或技术,在某种程度上绕过或改变原本的词法作用域规则。
-
eval()将原本不属于这里的代码变成就像天生就定义在了这里一样。
function foo(str, a) { eval(str) //var b=3 console.log(a, b); //1,3 } var b = 2 foo('var b = 3', 1)
-
with()用于修改一个对象的属性值,但如果修改的属性在原对象中不存在,那么该属性就会被泄漏到全局。
function foo(obj) { with (obj) { a = 2 } } var ol1 = { a: 3 } var ol2 = { b: 3 } foo(ol2) console.log(ol2); //输出:{b:3} console.log(a); //输出:2
小结
在这篇文章里,我们提到了词法分析、有效标识符、作用域、声明关键词、声明关键词之间的区别和欺骗词法作用域。
转载自:https://juejin.cn/post/7361852837267980315