大厂面试官忍不住问的JS底层执行机制

239 阅读6分钟

🐟前言

JavaScript 就是“web三件套”中的一个,它支持面向对象编程和函数式编程特性,其中面向对象编程绝对是JavaScript的特色。本文将深入探讨JavaScript中的作用域概念、变量声明与赋值过程以及引擎如何处理这些操作,理解JS执行机制。

面试官问:var a = 1 ;console.log(a),对这句代码进行多层理解

答:声明了一个全局变量a,把数值 1 赋值给 a ,再把 a 打印到控制台

问:没了?

答:还有吗?....

面试官其实想看看你对JavaScript底层编译执行和作用域的理解。好趴!简单的代码,成功送走的offer

现在,让我们攻破这道“简单”面试题

先了解一下JS执行机制。这段代码其实在执行前可是经历级几个过程的:

  1. 分词分析
  • 这一步将源代码转换成一系列的标记(tokens)。每个标记代表源代码中的一个基本单位,如关键字、标识符、操作符等。
  • 例如,var a = 2; 会被分解成 ['var', 'a', '=', '2', ';']
  1. 语法分析

    • 这一步将标记序列转换成抽象语法树(Abstract Syntax Tree, AST)表示程序结构的数据结构。
    • AST 是一个树状结构,表示代码的逻辑结构。
    • 例如,var a = 2; 可能会被解析成一个表示变量声明和赋值的树结构。
  2. 代码生成

  • 将 AST 转换为可执行代码。AST转化为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在 a 中。

  • 生成的代码可能包括以下几个步骤:

    1. 分配内存给变量 a
    2. 将数值 2 载入。
    3. 将数值 2 存储到变量 a 中。

4.最后执行

  • 生成的代码到编译器里面执行

理解作用域

在JavaScript中,变量的作用域决定了变量可以在哪些部分被访问。主要有以下几种作用域:

  • 全局作用域:在整个程序中都可访问的变量。
  • 局部作用域:仅在特定函数或块内可用的变量。
  • 块级作用域:由letconst关键字引入,限于代码块内部。

为了更好的理解作用域,下面先介绍 var a = 1 编译和执行过程所涉及到的角色

  • 引擎:负责整个程序的编译和执行
  • 编译器:负责语法分析和代码生成等
  • 作用域:负责收集和维护所有声明的标识符,并确定当前执行代码对这些标识符的访问权限。

三兄弟开始对话: 遇到“var a = 12”

  1. 编译器会先问作用域是否已有同名变量 a ,没有就声明新变量,有忽略该声明,继续编译。
  2. 然后编译器为引擎生成处理赋值操作的代码 a = 12,引擎运行时会先问当前的作用域有没有变量“a”,有就拿来赋值,没有就顺着作用域链继续找(作用链下文讲解)
  3. 总之,变量赋值操作分两步,先声明变量(若已存在则忽略),然后引擎先在当前作用域查找找到赋值结束,没有找到就沿着作用域链继续查找,如果还是没有找到就会抛出 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 的值。

为什么要区分LSHRHS

这是因为在变量还没有被声明的时候(就是变量哪里都找不到的时候),他们两种查询方式得到的结果不一样的! 我们来看两段例子去理解上面这句话

function foo(a) {
    console.log(a + b);
    b = a
}
foo(2) 

image.png 大家可知道上面代码的执行结果

想必大家都能知道 因为b确实没定义过,但是下面这段呢?

function foo() {
    b = 2
}
foo()
console.log(b)

image.png

什么?他居然输出了b,b我们没有定义呀,这不科学!不,这很科学,这就是细节中的细节,因为他们两个的查询操作不同,第一段代码是RHS,第二段是LHS.

  1. 不成功的 RHS 引用会导致抛出 ReferenceError 异常。
  2. 不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下

受到大佬文章启发:# 大厂面试官爱深挖的作用域底层原理(看完征服面试官)

启用严格模式

通过在函数或文件的顶部添加'use strict';指令,可以启用严格模式。严格模式下,JavaScript会对代码进行更加严格的检查,例如不允许使用未声明的变量,防止意外创建全局变量等。

END

理解JavaScript的作用域、变量声明与赋值机制以及引擎的工作原理,对于写出高质量的JavaScript代码非常重要。同样,正确地使用letconst可以帮助避免变量提升带来的问题,而理解作用域链和严格模式则有助于编写更加安全和高效的代码。这时候面试官可以给你offer了