🐟前言
JavaScript 就是“web三件套”中的一个,它支持面向对象编程和函数式编程特性,其中面向对象编程绝对是JavaScript的特色。本文将深入探讨JavaScript中的作用域概念、变量声明与赋值过程以及引擎如何处理这些操作,理解JS执行机制。
面试官问:
var a = 1 ;console.log(a),对这句代码进行多层理解答:声明了一个全局变量a,把数值 1 赋值给 a ,再把 a 打印到控制台
问:没了?
答:还有吗?....
面试官其实想看看你对JavaScript底层编译执行和作用域的理解。好趴!简单的代码,成功送走的offer
现在,让我们攻破这道“简单”面试题
先了解一下JS执行机制。这段代码其实在执行前可是经历级几个过程的:
- 分词分析 :
- 这一步将源代码转换成一系列的标记(tokens)。每个标记代表源代码中的一个基本单位,如关键字、标识符、操作符等。
- 例如,
var a = 2;会被分解成['var', 'a', '=', '2', ';']。
-
语法分析
- 这一步将标记序列转换成抽象语法树(Abstract Syntax Tree, AST)表示程序结构的数据结构。
- AST 是一个树状结构,表示代码的逻辑结构。
- 例如,
var a = 2;可能会被解析成一个表示变量声明和赋值的树结构。
-
代码生成
-
将 AST 转换为可执行代码。AST转化为一组机器指令,用来创建一个叫作
a的变量(包括分配内存等),并将一个值储存在 a 中。 -
生成的代码可能包括以下几个步骤:
- 分配内存给变量
a。 - 将数值
2载入。 - 将数值
2存储到变量a中。
- 分配内存给变量
4.最后执行
- 生成的代码到编译器里面执行
理解作用域
在JavaScript中,变量的作用域决定了变量可以在哪些部分被访问。主要有以下几种作用域:
- 全局作用域:在整个程序中都可访问的变量。
- 局部作用域:仅在特定函数或块内可用的变量。
- 块级作用域:由
let和const关键字引入,限于代码块内部。
为了更好的理解作用域,下面先介绍 var a = 1 编译和执行过程所涉及到的角色
- 引擎:负责整个程序的编译和执行
- 编译器:负责语法分析和代码生成等
- 作用域:负责收集和维护所有声明的标识符,并确定当前执行代码对这些标识符的访问权限。
三兄弟开始对话: 遇到“var a = 12”
- 编译器会先问作用域是否已有
同名变量 a,没有就声明新变量,有忽略该声明,继续编译。 - 然后编译器为引擎生成处理
赋值操作的代码 a = 12,引擎运行时会先问当前的作用域有没有变量“a”,有就拿来赋值,没有就顺着作用域链继续找(作用链下文讲解)。 - 总之,变量赋值操作分两步,先声明变量(若已存在则忽略),然后引擎先在当前作用域查找找到赋值结束,没有找到就沿着作用域链继续查找,如果还是没有找到就会抛出
ReferenceError异常。
作用域链
可以通俗地理解为:查找a ,遵守作用域规则 当前作用域->父级作用域 一直到全局作用域。当在当前作用域中找不到某个变量时,引擎就会像在教学楼里找人一样,一层一层地往外层嵌套的作用域中继续查找,直到找到该变量,或者到达最外层的全局作用域为止。如果在整个查找过程中都没有找到,就会出现相应的错误。
整个对话结束,在引擎和作用域对话过程中尤为趣味。下面我们来偷窥他们的内情
如果引擎在当前作用域没有找到变量,会顺着作用域链继续查找,直到找到该变量或者抵达最外层的全局作用域。如果在整个作用域链中都没有找到,那么对于不成功的 LHS 引用(即找到变量的容器),在非严格模式下会自动隐式地创建一个全局变量;对于不成功的 RHS 引用(即查询某个变量的值),会抛出 ReferenceError 异常。
LHS (Left-Hand Side) 与 RHS (Right-Hand Side) 查找
在JavaScript中,变量查找分为两种类型:
- RHS 查找:当变量出现在表达式的右侧时,引擎会尝试获取该变量的值。如果查找失败,会抛出
ReferenceError。 - LHS 查找:当变量出现在赋值操作的左侧时,引擎会尝试找到一个可以存储值的位置。如果查找失败,在非严格模式下会自动创建一个全局变量;在严格模式下则会抛出
ReferenceError。
一段代码进行加强理解
function foo(a) {
console.log(a+b);
}
var b = 2;
foo(2) //4
对于变量 b 的 RHS 引用,在函数 foo 内部是 没办法实现的。不过,可以在它的上一级作用域中完成,在这个例子里,上一级作用域就是全局作用域。也就是说,当在函数 foo 内试图获取变量 b 的值(即进行 RHS 查询)失败后,引擎会顺着作用域链到全局作用域去查找 b 的值。
为什么要区分LSH和RHS
这是因为在变量还没有被声明的时候(就是变量哪里都找不到的时候),他们两种查询方式得到的结果不一样的! 我们来看两段例子去理解上面这句话
function foo(a) {
console.log(a + b);
b = a
}
foo(2)
大家可知道上面代码的执行结果
想必大家都能知道 因为b确实没定义过,但是下面这段呢?
function foo() {
b = 2
}
foo()
console.log(b)
什么?他居然输出了b,b我们没有定义呀,这不科学!不,这很科学,这就是细节中的细节,因为他们两个的查询操作不同,第一段代码是RHS,第二段是LHS.
- 不成功的 RHS 引用会导致抛出 ReferenceError 异常。
- 不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(
非严格模式下)
受到大佬文章启发:# 大厂面试官爱深挖的作用域底层原理(看完征服面试官)
启用严格模式
通过在函数或文件的顶部添加'use strict';指令,可以启用严格模式。严格模式下,JavaScript会对代码进行更加严格的检查,例如不允许使用未声明的变量,防止意外创建全局变量等。
END
理解JavaScript的作用域、变量声明与赋值机制以及引擎的工作原理,对于写出高质量的JavaScript代码非常重要。同样,正确地使用let和const可以帮助避免变量提升带来的问题,而理解作用域链和严格模式则有助于编写更加安全和高效的代码。这时候面试官可以给你offer了