面试题:JavaScript预编译、作用域链

前言

在专栏《你不知道的JavaScript》中,也就是上一篇《"深入探索JavaScript:从词法分析到词法作用域的欺骗"》,深入探讨了编译过程:从词法分析->语法分析->代码生成,那么在JavaScript这门语言中这个编译过程是怎么样的呢?

比起传统的只有3个步骤的语言的编译器,JavaScript引擎要复杂的多,但总体来看,JavaScript编译过程只有下面三个步骤: 1. 语法分析 2. 预编译 3. 解释执行

引入面试题

执行结果是什么

javascript 复制代码
function test(a, b) {
    console.log(a);  // 1
    c = 0
    var c;
    a = 3
    b = 2
    console.log(b); // 2
    function b() { }
    console.log(b); //2
}
test(1)

分析过程

众所周知,如果你不了解编译原理,一定会以就近原则定论,那么接下来就让我们探讨一下原理,剖析完原理之后,再返回来看这道题目,届时你一定会豁然开朗。

在 JavaScript 中,预编译是在代码执行前进行的一项操作,它会把变量声明提前,函数声明也提前,把这些按照一定的规则,放在创建的对象里面去。这个过程主要包括以下几个步骤:

  • 发生在全局
    1. 创建GO对象
    2. 找变量声明,将变量名作为GO的属性名,值为undefined
    3. 在全局找函数声明,将函数名作为GO的属性名,值为该函数体
  • 发生在函数体内
    1. 创建 AO(Activation Object)对象:在函数执行前,会创建一个 AO 对象,用于存储函数内部的局部变量。
    2. 找形参和变量声明:在函数体内,找到形参和变量声明,并将变量和形参名作为 AO 属性名,值为 undefined
    3. 将实参和形参统一:将实参和形参统一到函数体中。
    4. 找函数声明:在函数体内,找到函数声明,并将函数值赋予函数体。

通过预编译,JavaScript 可以在函数执行前准备好所需的变量和函数,提高代码的执行效率。

我们一起来看这一份代码,理解了上述分析过程,那么在JavaScript引擎的眼里这个分析过程应该是这样的:

  • 首先看见了一个函数,发生在全局,那么创建一个Global Object GO{ 找变量,没找到,找函数,找到了,因此在这个对象中就有属性test,值为:function(){}}
  • 接下来就读到了调用,但是在调用之前,引擎需要知道要执行什么东西,因此要对这个函数进行预编译
  • 发生在函数体内的预编译,建立一个AO对象{找形参、变量声明,值为undefined,因此就有:a:undefined,b:undefined,c:undefinded,然后将形参与实参统一,即a:1,接下来找函数声明,即b:function(){}},预编译完成之后就开始执行代码,此时AO{

a:1,

b:function(){},

c:undefined

} 引擎从上往下开始执行,1.执行打印a,a为1, 2.将0赋值给c,即c=0,a=3,b=2,此时AO{ c=0, a=3, b=2 },接下来就到了打印b的代码,即b=2,接下来又是一个打印b的代码,即b=2.

因此正确答案就是a=1,b=2,b=2

思考

如果JavaScript引擎在AO里面没找到要找的属性怎么办?他会去哪里找?

举个例子

ini 复制代码
function a() {
    function b() {
        b = 22;
    }
    var a = 111;
    b();
    console.log(a);
}
var glob = 100;
a();

分析

在 JavaScript 中,作用域可以分为以下几种类型:

  1. 全局作用域:变量在整个脚本中都可访问。
  2. 函数作用域:变量仅在函数内部可访问。

作用域链的形成与函数的调用和嵌套有关。当函数被调用时,会创建一个新的作用域,并将其添加到作用域链中。函数内部的变量和函数可以访问外部作用域中的变量和函数,从而形成了作用域链的结构。

首先创建GO对象,里面有属性a,值为这个a的函数体,然后有属性glob,值为100。然后就碰到了调用a函数,此时JavaScript引擎就会去预编译a函数,创建AO对象,里面有属性a,值为111,属性b,值为这个function函数体,紧接着调用b函数,又创建一个AO对象,里面有b属性,值为22。

可以看见,首先a的作用域会先指向这个GO对象,他先创建GO对象。那么作用域0号位置,指向这个GO对象,紧接着,他又创建了AO对象,那么此时,a的作用域的0号位置就会优先指向这个AO对象,然后1号位置指向GO,后来引出了b,那么同理,b的作用域0号位置首先指向他自己创建的AO对象然后再指向引出他的a(1号位置)。

作用域链由一系列的作用域组成,每个作用域都包含了可访问的变量和函数。当代码中访问一个变量时,JavaScript 会沿着作用域链依次查找该变量。

那么在上述的这个分析过程,这么一个东西就是我们的作用域链了,大家可以通过画图理解。

由此我们就知道了,为什么内层函数可以访问外层,而外层函数无法访问到我们的内层。当JavaScript引擎没有在我们的AO对象中找到我们需要的属性时,他就会去下一个作用域找,也就是我们的GO对象中继续找。

小结

通过这篇文章,相信你对JavaScript预编译以及作用域又有了更加深刻的理解,当面试官给你写了一串代码,问你输出结果时,你能够冷静应对了吗?

相关推荐
crary,记忆24 分钟前
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
前端·webpack·angular·angular.js
漂流瓶jz1 小时前
让数据"流动"起来!Node.js实现流式渲染/流式传输与背后的HTTP原理
前端·javascript·node.js
SamHou01 小时前
手把手 CSS 盒子模型——从零开始的奶奶级 Web 开发教程2
前端·css·web
我不吃饼干1 小时前
从 Vue3 源码中了解你所不知道的 never
前端·typescript
开航母的李大2 小时前
【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解
前端·redis·nginx·缓存·微服务·kafka
Bruk.Liu2 小时前
《Minio 分片上传实现(基于Spring Boot)》
前端·spring boot·minio
鱼樱前端2 小时前
Vue3+d3-cloud+d3-scale+d3-scale-chromatic实现词云组件
前端·javascript·vue.js
coding随想2 小时前
JavaScript中的原始值包装类型:让基本类型也能“变身”对象
开发语言·javascript·ecmascript
zhangxingchao2 小时前
Flutter入门:Flutter开发必备Dart基础
前端
佚名猫2 小时前
vue3+vite+pnpm项目 使用monaco-editor常见问题
前端·vue3·vite·monacoeditor