JS 相关面试原理

90 阅读6分钟

Babel 原理

了解完成 babel 的基础使用后,我们来分析 babel 的工作原理。babel 作为一个编译器,主要做的工作内容如下:

  1. 解析源码,生成 AST
  2. 对 AST 进行转换,生成新的 AST
  3. 根据新的 AST 生成目标代码

整体流程图下:

根据上图中的流程,我们依次进行分析。

CommonJS 和 ES6 module

Vite 核心原理

Vite是新一代的前端构建工具,在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+ Webpack-dev-server。其主要利用浏览器ESM特性导入组织代码,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。生产中利用Rollup作为打包工具,号称下一代的前端构建工具。

Vite有如下特点:

  • 快速的冷启动: No Bundle + esbuild 预构建
  • 即时的模块热更新: 基于ESM的HMR,同时利用浏览器缓存策略提升速度
  • 真正的按需加载: 利用浏览器ESM支持,实现真正的按需加载

Webpack是近年来使用量最大,同时社区最完善的前端打包构建工具,新出的5.x版本对构建细节进行了优化,在部分场景下打包速度提升明显。Webpack在启动时,会先构建项目模块的依赖图,如果在项目中的某个地方改动了代码,Webpack则会对相关的依赖重新打包,随着项目的增大,其打包速度也会下降。

Vite相比于Webpack而言,没有打包的过程,而是直接启动了一个开发服务器devServer。Vite劫持浏览器的HTTP请求,在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器(整个过程没有对文件进行打包编译)。所以编译速度很快。

几个 for 之间的区别

getMinNode 函数中使用 for...in 循环遍历数组并不是最佳选择。因为 for...in 循环会遍历对象的属性,而数组也是对象,可能会遍历到原型链上的属性,这会导致不确定的行为。应该使用 for...of 循环或者普通的 for 循环来遍历数组。

区别:

  1. 语法差异:

    • forEach 是数组的原生方法,用于遍历数组元素,语法为 array.forEach(callback[, thisArg])
    • for 循环是一种通用的循环结构,可以遍历任何可迭代对象,语法为 for (initialization; condition; increment/decrement) { ... }
  2. 返回值:

    • forEach 没有返回值(返回值为 undefined),它会遍历数组中的每个元素并对其执行回调函数。
    • for 循环可以使用 breakcontinue 控制语句来提前终止循环或跳过当前迭代。
  3. 作用域:

    • forEach 会创建一个新的函数作用域,因此在回调函数中的 this 指向的是全局对象(在严格模式下是 undefined)。
    • for 循环中的变量作用域在循环块内部,可以在循环内访问和修改循环条件之外的变量。

场景区分:

  1. 数组遍历:

    • 如果只需要遍历数组元素并对其执行一些操作,可以使用 forEach,它提供了更简洁的语法。
    • 如果需要在遍历过程中对索引进行控制、实现特定的逻辑或需要提前终止循环,可以使用 for 循环。
  2. 对象遍历:

    • 对于数组,通常推荐使用 forEach
    • 对于对象,则通常需要使用 for...in 循环或 Object.keys() 配合 forEach,因为 forEach 不能直接用于对象的遍历。
  3. 性能考虑:

    • 一般来说,for 循环比 forEach 更快,尤其是对于大型数组而言。因此,在需要高性能的场景下,可以优先考虑使用 for 循环。

请简述 JavaScript 中的 this

JS 中的 this 是一个相对复杂的概念,不是简单几句能解释清楚的。粗略地讲,函数的调用方式决定了 this 的值。我阅读了网上很多关于 this 的文章,「Arnav Aggrawal」 写的比较清楚。this 取值符合以下规则:

在调用函数时使用 new 关键字,函数内的 this 是一个全新的对象。 如果 apply、call 或 bind 方法用于调用、创建一个函数,函数内的 this 就是作为参数传入这些方法的对象。 当函数作为对象里的方法被调用时,函数内的 this 是调用该函数的对象。比如当 obj.method() 被调用时,函数内的 this 将绑定到 obj 对象。 如果调用函数不符合上述规则,那么 this 的值指向全局对象(global object)。浏览器环境下 this 的值指向 window 对象,但是在严格模式下('use strict'),this 的值为 undefined。 如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定 this 的值。 如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,this 被设置为它被创建时的上下文。

说说你对 AMD 和 CommonJS 的了解。

它们都是实现模块体系的方式,直到 ES2015 出现之前,JavaScript 一直没有模块体系。CommonJS 是同步的,而 AMD(Asynchronous Module Definition) 从全称中可以明显看出是异步的。CommonJS 的设计是为服务器端开发考虑的,而 AMD 支持异步加载模块,更适合浏览器。

我发现 AMD 的语法非常冗长,CommonJS 更接近其他语言 import 声明语句的用法习惯。大多数情况下,我认为 AMD 没有使用的必要,因为如果把所有 JavaScript 都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS 语法上更接近 Node 编写模块的风格,在前后端都使用 JavaScript 开发之间进行切换时,语境的切换开销较小。

我很高兴看到 ES2015 的模块加载方案同时支持同步和异步,我们终于可以只使用一种方案了。虽然它尚未在浏览器和 Node 中完全推出,但是我们可以使用代码转换工具进行转换。

请解释下面代码为什么不能用作 IIFE:function foo(){ }();,需要作出哪些修改才能使其成为 IIFE?

IIFE(Immediately Invoked Function Expressions)代表立即执行函数。 JavaScript 解析器将 function foo(){ }(); 解析成 function foo(){ }和(); 。其中,前者是函数声明;后者(一对括号)是试图调用一个函数,却没有指定名称,因此它会抛出 Uncaught SyntaxError: Unexpected token ) 的错误。

修改方法是:再添加一对括号,形式上有两种:(function foo(){ })() 和 (function foo(){ }())。以上函数不会暴露到全局作用域,如果不需要在函数内部引用自身,可以省略函数的名称。

你可能会用到 void 操作符:void function foo(){ }();。但是,这种做法是有问题的。表达式的值是 undefined,所以如果你的 IIFE 有返回值,不要用这种做法。例如

const foo = void (function bar() {
  return 'foo';
})();

console.log(foo); // undefined

null、undefined和未声明变量之间有什么区别?如何检查判断这些状态值?