搜尋
首頁web前端js教程【整理分享】一些node模組相關的面試題目及答案(收藏)

這篇文章跟大家整分享一些關於node模組的面試題,希望幫助你快速了解常見的模組問題的坑點,順利通過面試。

【整理分享】一些node模組相關的面試題目及答案(收藏)

熱更新

# 如何在不重啟node 進程的情況下熱更新一個js/json 檔案? 這個問題本身是否有問題?

node.js快取和熱更新密切相關,我們先簡單看下Node.js的模組機制(下圖來自 hyj1991大神)。

簡單的說就是,require A模組之後,會把A模組放入到快取裡,第二次取的時候就取快取了,所以你僅僅改變了檔案並不會讓這個檔案重新加載,所以我們就需要把快取去掉,讓這個檔案能重新加載。 【相關教學推薦:nodejs影片教學程式設計教學

#簡單地說父模組A 引進子模組B 的步驟如下:

  • 判斷子模組B 快取是否存在
  • 如果不存在則對B 進行編譯解析
    • 新增B 模組緩存至 require.cache(其中key 為模組B 的全路徑)
    • #新增B 模組引用至父模組A 的 children 陣列中
  • 若存在,判斷父模組A 的 children 陣列中是否存在B,如不存在則新增B 模組參考。

所以在node.js做熱更新是十分麻煩的,有些函式庫做的也不夠好。這個問題的終極解決方案是藉助一些第三方工具,例如k8s,k8s可以輕鬆實現滾動升級,也就是如果要做熱更新,k8s會把新的服務起起來,然後把流量切換到新的服務上( pod上),然後舊服務再關閉。

不過熱更新 json 之類的設定檔的話, 還是可以簡單的實現的, 可以直接存到後端的資料庫裡,這樣就避免node.js的快取問題了。

模組機制

模組機制太常見的問題了,下面我們要搞清楚什麼是commonjs的實作原理。

模組機制詳細分析文章可以參考我之前寫的一篇文章NodeJS有難度的面試題,你能答對幾個? ,一開始就討論了commonjs的模組機制。

我們這裡貼我之前對於模組機制的討論:

1.1 請介紹一下node裡的模組是什麼

Node中,每個檔案模組都是一個對象,它的定義如下:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

module.exports = Module;
var module = new Module(filename, parent);

所有的模組都是Module 的實例。可以看到,目前模組(module.js)也是 Module 的一個實例。

1.2 請介紹require的模組載入機制

這題基本上就可以了解到面試者對Node模組機制的了解程度基本上是面試提到

1、先計算模組路徑 2、如果模組在快取裡面,取出緩存 3、加載模組 4.的輸出模組的exports屬性即可

// require 其实内部调用 Module._load 方法
Module._load = function(request, parent, isMain) {
  //  计算绝对路径
  var filename = Module._resolveFilename(request, parent);

  //  第一步:如果有缓存,取出缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否为内置模块
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }
  
  /********************************这里注意了**************************/
  // 第三步:生成模块实例,存入缓存
  // 这里的Module就是我们上面的1.1定义的Module
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  /********************************这里注意了**************************/
  // 第四步:加载模块
  // 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:输出模块的exports属性
  return module.exports;
};

接著上一題繼續提問

1.3 載入模組時,為什麼每個模組都有__dirname,__filename屬性呢, new Module的時候我們看到1.1部分沒有這兩個屬性的,那麼這兩個屬性是從哪裡來的

// 上面(1.2部分)的第四步module.load(filename)
// 这一步,module模块相当于被包装了,包装形式如下
// 加载js模块,相当于下面的代码(加载node模块和json模块逻辑不一样)
(function (exports, require, module, __filename, __dirname) {
  // 模块源码
  // 假如模块代码如下
  var math = require('math');
  exports.area = function(radius){
      return Math.PI * radius * radius
  }
});

也就是說,每個module裡面都會傳入__filename, __dirname參數,這兩個參數並不是module本身就有的,是外在傳入的

#1.4 我們知道node導出模組有兩種方式,一種是exports.xxx=xxx和Module .exports={}有什麼差別嗎

exports其實就是module.exports 其實1.3問題的程式碼已經說明問題了,接著我引用廖雪峰大神的講解,希望能講的更清楚

module.exports vs exports

#很多時候,你會看到,在Node環境中,有兩種​​方法可以在一個模組中輸出變數:

方法一:對module.exports賦值:

// hello.js

function hello() {
    console.log('Hello, world!');
}

function greet(name) {
    console.log('Hello, ' + name + '!');
}

module.exports = {
    hello: hello,
    greet: greet
};

方法二:直接使用exports :

// hello.js

function hello() {
    console.log('Hello, world!');
}

function greet(name) {
    console.log('Hello, ' + name + '!');
}

function hello() {
    console.log('Hello, world!');
}

exports.hello = hello;
exports.greet = greet;

但是你不可以直接對exports賦值:

// 代码可以执行,但是模块并没有输出任何变量:
exports = {
    hello: hello,
    greet: greet
};

如果你對上面的寫法感到十分困惑,不要著急,我們來分析Node的載入機制:

首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:

var module = {
    id: 'hello',
    exports: {}
};
load()函数最终返回module.exports:

var load = function (exports, module) {
    // hello.js的文件内容
    ...
    // load函数返回:
    return module.exports;
};

var exportes = load(module.exports, module);

也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写:

exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };

也可以写:

module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };

换句话说,Node默认给你准备了一个空对象{},这样你可以直接往里面加东西。

但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值:

module.exports = function () { return 'foo'; };

给exports赋值是无效的,因为赋值后,module.exports仍然是空对象{}。

结论 如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值;

如果要输出一个函数或数组,必须直接对module.exports对象赋值。

所以我们可以得出结论:直接对module.exports赋值,可以应对任何情况:

module.exports = {
    foo: function () { return 'foo'; }
};

或者:

module.exports = function () { return 'foo'; };

最终,我们强烈建议使用module.exports = xxx的方式来输出模块变量,这样,你只需要记忆一种方法。

上下文 Vm模块

通过上面的问题,面试官又抛出一个问题,每个require的js文件,作用域如何保证独立呢?

其实每一个require的js文件,本身就是一个字符串, 文件是不是字符串嘛,所以我们需要一种机制能够把字符串编译为可以运行的javascript语言。

实际上从上面的讨论我们知道,require会把引入的js包裹在function中,所以它的作用域天然就是独立的。

接着讲本章的vm模块,vm模块和function都可以建立自己独立的作用域,并且vm、function、eval还可以把字符串当做目标代码执行。所以这三者的区别就需要面试者了解。

  • eval
  • Function
  • vm

eval、Function,在执行目标代码时,会有一个最大的问题就是安全性,无论如何目标代码不能影响我正常的服务,也就是说,这个执行环境得是一个沙盒环境,而eval显然并不具备这个能力。如果需要一段不信任的代码放任它执行,那么不光服务,整个服务器的文件系统、数据库都暴露了。甚至目标代码会修改eval函数原型,埋入陷阱等等。

function也有一个安全问题就是可以修改全局变量,所有这种new Function的代码执行时的作用域为全局作用域,不论它的在哪个地方调用的,它访问的都是全局变量。

所以也有一定的安全隐患,接下来我们的主角vm模块登场。

安全性

使用vm的模块会比eval更为安全,因为vm模块运行的脚本完全无权访问外部作用域(或自行设置一个有限的作用域)。 脚本仍在同一进程中运行,因此为了获得最佳安全性。当然你可以给上下文传入一些通用的API方便开发:

vm.runInNewContext(`
  const util = require(‘util’);
  console.log(util);
`, {
  require: require,
  console: console
});

此外,另一个开源库vm2针对vm的安全性等方面做了更多的提升,vm2。避免了一些运行脚本有可能“逃出”沙盒运行的边缘情况,语法也跟易于上手,很推荐使用。

包管理

npm的包管理机制你一定要了解,不仅仅是node需要,我们前端浏览器项目本身也会引用很多第三方模块。面试必备知识点。

下图摘自抖音前端团队的npm包管理机制

本图如果你理解的话,后面的内容就不用看了。

讲npm install 要从嵌套结构讲起

嵌套结构

在 npm 的早期版本中,npm 处理依赖的方式简单粗暴,以递归的方式,严格按照 package.json 结构以及子依赖包的 package.json 结构将依赖安装到他们各自的 node_modules 中。

如下图:这样的方式优点很明显, node_modules 的结构和 package.json 结构一一对应,层级结构明显,并且保证了每次安装目录结构都是相同的。

从上图这种情况,我们不难得出嵌套结构拥有以下缺点:

  • 在不同层级的依赖中,可能引用了同一个模块,导致大量冗余
  • 在 Windows 系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题

扁平结构

2016 年,yarn 诞生了。yarn 解决了 npm 几个最为迫在眉睫的问题:

  • 安装太慢(加缓存、多线程)
  • 嵌套结构(扁平化)
  • 无依赖锁(yarn.lock)
  • yarn 带来对的扁平化结构:

如下图,我们简单看下什么是扁平化的结构:

没错,这就是扁平化依赖管理的结果。相比之前的嵌套结构,现在的目录结构类似下面这样: 假如之前嵌套的结构如下:

node_modules
├─ a
|  ├─ index.js
|  |- node_modules -└─ b
|  |                ├─ index.js
|  |                └─ package.json
|  └─ package.json

那么扁平化处理以后,就编程下面这样,被拍平了

node_modules
├─ a
|  ├─ index.js
|  └─ package.json
└─ b
   ├─ index.js
   └─ package.json

但是扁平化的结构又会引出新的问题:

最主要的就是依赖结构的不确定性!

啥意思,我就懒得画图了,拿网上的一个例子来说:

想象一下有一个 library-a,它同时依赖了 library-b、c、d、e:

而 b 和 c 依赖了 f@1.0.0,d 和 e 依赖了 f@2.0.0

这时候,node_modules 树需要做出选择了,到底是将 f@1.0.0 还是 f@2.0.0 扁平化,然后将另一个放到嵌套的 node_modules 中?

答案是:具体做那种选择将是不确定的,取决于哪一个 f 出现得更靠前,靠前的那个将被扁平化。

还有一个问题就是幽灵依赖,明明只安装a包,你却可以引用b包,因为a引用了b,并且扁平化处理了。

lock文件

这就是为啥要有lock文件的原因,lock文件可以保证安装包的扁平化结构的稳定。

使用新的npm包管理工具?

pnpm? 可以简单介绍一下为啥它能解决上面扁平化结构和幽灵依赖的问题。

补充问题

  • a.js 和 b.js 两个文件互相 require 是否会死循环? 双方是否能导出变量? 如何从设计上避免这种问题?

答:不会, 先执行的导出其 未完成的副本, 通过导出工厂函数让对方从函数去拿比较好避免. 模块在导出的只是 var module = { exports: {...} }; 中的 exports。以下摘自阮一峰老师的博客:

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

让我们来看,官方文档里面的例子。脚本文件a.js代码如下。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

再看b.js的代码。

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了"循环加载"。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

a.js已经执行的部分,只有一行。

exports.done = false; 因此,对于b.js来说,它从a.js只输入一个变量done,值为false。

然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运行结果如下。

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

exports.done = true;

ES6模块的循环加载

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。请看下面的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代码中,m1.js的变量foo,在刚加载时等于bar,过了500毫秒,又变为等于baz。

让我们看看,m2.js能否正确读取这个变化。

$ babel-node m2.js

bar
baz

上面代码表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

请看下面的例子(摘自 Dr. Axel Rauschmayer 的《Exploring ES6》)。

// a.js
import {bar} from './b.js';
export function foo() {
  bar();  
  console.log('执行完毕');
}
foo();
// b.js
import {foo} from './a.js';
export function bar() {  
  if (Math.random() > 0.5) {
    foo();
  }
}

按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

但是,ES6可以执行上面的代码。

$ babel-node a.js

执行完毕

a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

  • 如果 a.js require 了 b.js, 那么在 b 中定义全局变量 t = 111 能否在 a 中直接打印出来?

会,作用域链的嘛。。。。

更多node相关知识,请访问:nodejs 教程

以上是【整理分享】一些node模組相關的面試題目及答案(收藏)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:掘金社区。如有侵權,請聯絡admin@php.cn刪除
JavaScript引擎:比較實施JavaScript引擎:比較實施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

神秘的JavaScript:它的作用以及為什麼重要神秘的JavaScript:它的作用以及為什麼重要Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python還是JavaScript更好?Python還是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用