Node.js常见面试题

1、NodeJS中的this为什么是一个空对象?

2、NodeJS中为什么可以直接使用exports、require、module、__filename、__dirname?

3、NodeJS中为什么不能直接exports赋值,而可以给module.exports赋值?

4、通过require导入包的时候应该使用var/let还是const?

5、require和import的区别

6、浏览器的事件循环

  • 6.1、JS是单线程的
  • 6.2、执行顺序
  • 6.3、宏任务和微任务
  • 6.4、常见的宏任务和微任务
  • 6.5、完整执行顺序
  • 7、NodeJS的事件循环(Event Loop)
  • 7.1、概述
  • 7.2、NodeJS事件循环和浏览器事件循环区别
  • 7.3、NodeJS中的任务队列
  • 7.4、NodeJS-EventLoop面试题
  • 8、自定义本地包和全局包
  • 8.1、包的规范(了解)
  • 8.2、package.json字段分析(了解)
  • 8.3、自定义包实现步骤
  • 8.4、==将自定义包发布到官网==

1、NodeJS中的this为什么是一个空对象?

因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中,this都被修改为module.exports。(详细请看之前的Node模块原理分析【详细】那篇文章)。

nodejs前端demo nodejs前端面试题_前端

nodejs前端demo nodejs前端面试题_javascript_02

2、NodeJS中为什么可以直接使用exports、require、module、__filename、__dirname?

因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中,这些属性都被通过参数的形式传递过来了。 // var args = [this.exports, require, module, filename, dirname]; // var result = compiledWrapper.call(this.exports, args); 都会被包裹到下面函数中:

(function (exports, require, module, __filename, __dirname) {
exports.名 = 值;
});

3、NodeJS中为什么不能直接exports赋值,而可以给module.exports赋值?

<!--
(function (exports, require, module, __filename, __dirname) {
    exports = "lnj";
});
jsScript.call(module.exports, module.exports);
return module.exports;

相当于
let exports = module.exports;
exports = "lnj";
return module.exports;

nodejs前端demo nodejs前端面试题_nodejs前端demo_03

exports是形参,module.exports传递给它,两者指向同一个对象。如果直接给exports赋值(exports=‘aaa’)则相当于修改了它的指向,但最后却返回module.exports。

nodejs前端demo nodejs前端面试题_javascript_04

nodejs前端demo nodejs前端面试题_node.js_05

4、通过require导入包的时候应该使用var/let还是const?

导入包的目的是使用包而不是修改包,所以导入包时使用const接受。

5、require和import的区别

import和require都是被模块化所使用。在ES6当中,用export导出接口,用import引入模块。但是在node模块中,使用module.exports/exports导出接口,使用require引入模块,

  1. 区别一:出现的时间不同。
  • require表示的是运行时加载/调用,所以理论上可以运用在代码的任何地方。
  • import表示的是编译时加载(效率更高),由于是编译时加载,所以import命令会提升到整个模块的头部。
  • import输入的变量是只读的,引用类型可以,其他模块也可以读到改后的值,但不建议修改。
  • import是静态执行,不能使用表达式和变量。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
  • import是Singleton模式。
import { foo } from 'my_module';
  import { bar } from 'my_module';
  // 等同于
  import { foo, bar } from 'my_module';
  // 虽然foo和bar在两个语句加载,但是他们对应的是同一个my_module实例
  • Singleton模式。 即单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前的进程中只有一个实例(根据需要,也有可能一个线程中属于单列模式,如:仅线程上下文内使用同一个实例)。定义::保证一个类仅有一个实例,并提供一个访问它的全局访问点(类似node的global)。
  1. 遵循的模块化规范不同。
  • require是AMD规范引入方式。
  • import是ES6的一个语法标准,如果要兼容浏览器的话必须转化成ES5的语法。
  1. 本质
  • require是赋值过程。module.exports后面的内容是什么,require的结果就是什么,比如对象、数字、字符串、函数等,然后再把require的结果赋值给某个变量,它相当于module.exports的传送门。
  • import是结构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require。
  • import虽然是es6中的语法,但就目前来说,所有的引擎都还没有实现import。
  • import语法实际上会被转码为require。这也是为什么在模块导出时使用module.exports,在引入模板时使用import仍然起效,因为本质上,import会被转码为require去执行。

6、浏览器的事件循环

6.1、JS是单线程的

JS中的代码都是串行的,前面没有执行完毕后面不能执行。

6.2、执行顺序

  1. 程序运行会从上至下依次执行所有的同步代码。
  2. 在执行的过程中如果遇到异步代码会将异步代码放到事件循环中。
  3. 当所有同步代码都执行完毕后,JS会不断检测事件循环中的异步代码是否满足条件。
  4. 一旦满足条件就执行满足条件的异步代码。

6.3、宏任务和微任务

在JS的异步代码中又区分“宏任务(MacroTask)”和“微任务(MicroTask)”。

宏任务:宏/大的意思,可以理解为比较费时比较慢的任务。微任务:微/小的意思,可以理解为相对没那么费时没那么慢的任务。

6.4、常见的宏任务和微任务

宏任务:setTimeout、setInterval、setImmediate(IE独有)…微任务:Promise、MutationObserver、process.nextTick(node独有)…

注意点:所有的宏任务和微任务都会放到自己的执行队列中,也就是有一个宏任务队列和一个微任务队列。所有放到队列中的任务都采用“先进先出原则”,也就是多个任务同时满足条件,那么会先执行先放进去的。

6.5、完整执行顺序

nodejs前端demo nodejs前端面试题_nodejs前端demo_06

nodejs前端demo nodejs前端面试题_javascript_07

  1. 从上至下执行所有同步代码。
  2. 执行微任务队列中的所有代码。
  3. 执行宏任务中满足条件代码。
  4. 执行微任务队列中所有代码。
  5. 执行宏任务中满足条件代码。 … …

每次执行完一个宏任务都会立刻检查微任务队列有没有被清空,如果没有就立刻清空。

附:

1、setImmediate(IE浏览器独有)

nodejs前端demo nodejs前端面试题_事件循环_08

2、MutationObserver是专门监听节点的变化

nodejs前端demo nodejs前端面试题_javascript_09

// html
<body>
<div></div>
<button class="add">添加节点</button>
<button class="del">删除节点</button>
</body>

// js
let oDiv = document.querySelector("div");
    let oAddBtn = document.querySelector(".add");
    let oDelBtn = document.querySelector(".del");
    oAddBtn.onclick = function () {
        let op = document.createElement("p");
        op.innerText = "我是段落";
        oDiv.appendChild(op);
    }
    oDelBtn.onclick = function () {
        let op = document.querySelector("p");
        oDiv.removeChild(op);
    }
    let mb = new MutationObserver(function () {
        console.log("执行了");
    });
    mb.observe(oDiv, {
        "childList": true
    });
    console.log("同步代码Start");
    console.log("同步代码End");

nodejs前端demo nodejs前端面试题_node.js_10

7、NodeJS的事件循环(Event Loop)

7.1、概述

和浏览器中一样NodeJS中也有事件循环(Event Loop),但是由于代码执行的宿主环境和应用场景不同,所以两者的事件循环也有所不同。

扩展阅读: 在NodeJS中使用libuv实现了Event Loop。 源码地址:https://github.com/libuv/libuv

7.2、NodeJS事件循环和浏览器事件循环区别

nodejs前端demo nodejs前端面试题_前端_11

  1. 任务队列个数不同。
  • 浏览器事件循环有2个事件队列(宏任务队列和微任务队列)。
  • NodeJS事件循环有6个事件队列。
  1. 微任务队列不同。
  • 浏览器事件循环中有专门存储微任务的队列。
  • NodeJS事件循环中没有专门存储微任务的队列。
  1. 微任务执行时机不同。
  • 浏览器事件循环中每执行完一个宏任务都会去清空微任务队列。
  • NodeJS事件循环中只有同步代码执行完毕和其他队列之间切换的时候会去清空微任务队列(切换队列的时候会清空任务队列)。
  1. 微任务优先级不同
  • 浏览器事件循环中如果多个微任务同时满足执行条件,采用先进先出。
  • NodeJS事件循环中如果多个微任务同时满足执行条件,会按照优先级执行。

7.3、NodeJS中的任务队列

图一:

nodejs前端demo nodejs前端面试题_node.js_12

  1. 注意点:

和浏览器不同的是没有宏任务队列和微任务队列的概念。 宏任务被放到了不同的队列中,但是没有队列是存放微任务的队列。 微任务会在执行完同步代码和队列切换的时候执行。

什么时候切换队列? 当队列为空(已经执行完毕或者没有满足条件回倒)或者执行的回调函数数量达到系统设定的阈值时任务队列就会切换。

  1. 注意点: 在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务。

图二:NodeJS完整执行顺序。

nodejs前端demo nodejs前端面试题_前端_13

注意点:

执行完poll,会查看check队列是否有内容,有就切换到check。如果check队列没有内容,就会查看timers是否有内容,有就切换到timers。如果check队列和timers队列都没有内容,为了避免资源浪费就会阻塞在poll。

7.4、NodeJS-EventLoop面试题

/*
注意点: 如下代码输出的结果是随机的
        在NodeJS中指定的延迟时间是有一定的误差的, 所以导致了输出结果随机的问题
* */
/*
setTimeout(function () {
    console.log("setTimeout");
}, 0);
setImmediate(function () {
    console.log("setImmediate");
});
 */

// 但是在下面的代码中输出结果都是固定的,即无论setTimeout、setImmediate顺序怎样,都会先执行setImmediate代码。
const path = require("path");
const fs = require("fs");

fs.readFile(path.join(__dirname, "04.js"), function () {
    setTimeout(function () {
        console.log("setTimeout");
    }, 0);
    setImmediate(function () {
        console.log("setImmediate");
    });
});

原因:

nodejs前端demo nodejs前端面试题_nodejs前端demo_14

8、自定义本地包和全局包

  1. 什么是包? 包就是一个文件夹,用来管理模块和模块之间的各种关系。
  2. 包使用 npm install xxx 安装包 const xxx = require(xxx); 使用包

8.1、包的规范(了解)

  • package.json必须在包的顶层目录下。
  • 二进制文件应该在bin目录下。
  • JavaScript代码应该在lib目录下。
  • 文档应该在doc目录下。
  • 单元测试应该在test目录下。

8.2、package.json字段分析(了解)

  • name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
  • description:包的简要说明。
  • version:符合语义化版本识别规范的版本字符串。
  • 主版本号:当你做了不兼容的API修改。
  • 子版本号:当你做了向下兼容的功能性新增。
  • 修订号:当你做了向下兼容的问题修正。
  • keywords:关键字数组,通常用于搜索。
  • maintainers:维护者数组,每个元素要包含name、email(可选)、web(可选)字段。
  • contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一个元素。
  • bugs:提交bug的地址,可以是网站或者电子邮件地址。
  • licenses:许可证数组,每个元素要包含type(许可证名称)和URL(连接到许可证文本的地址)字段。
  • repositories:仓库托管地址数组,每个元素要包含type(仓库类型,如git)、url(仓库地址)和path(相对于仓库的路径,可选)字段。
  • dependencies:生产环境包的依赖,一个关键数组,由包的名称和版本号组成。
  • devDependencies:开发环境包的依赖,一个关联数组,由包的名称和版本号组成。
  • main:指定包入口文件,如果没有配置main,默认会将index.js作为入口,如果包中没有index.js,那么就必须配置main。
  • scripts:1、保存一些常用的指令,可以通过npm run key方式运行。2、应用场景:每次执行某个js文件都需要传递参数,并且每次传递的参数都是一样的,那么就可以通过将指令保存到script中来简化输入指令的操作。(通过指令执行某个文件的时候可以传递一个参数,并且该文件可以通过process.argv拿到这个参数。scripts可以简化执行命令)执行npm run start就相当于执行node lgg.js name=lgg age=3

8.3、自定义包实现步骤

  1. 创建一个包文件夹。
  2. 初始化一个package.json文件。
  3. 初始化一个包入口js文件。 注意点:如果没有配置main,默认会将index.js作为入口,如果包中没有index.js,那么就必须配置main。
  4. 根据包信息配置package.json文件。 注意点:通过scripts可以帮助我们记住指令,然后通过npm run xxx方式,就可以执行该指令。如果指令的名称叫做start或者test,那么执行的时候可以不加run。

全局包:一般全局包都是些工具包比如nrm、yarn、cnpm等,工具包的特点需要自定义指令。

  1. 给package.json添加bin属性,告诉系统执行全局命令式时需要执行哪一。个JS文件。
  2. 在全局命令执行的JS文件中添加#! /usr/bin/envnode。 (node环境执行)
  3. 通过npm link 将本地包放到全局方便我们调试。

nodejs前端demo nodejs前端面试题_javascript_15

nodejs前端demo nodejs前端面试题_nodejs前端demo_16

先npm link

nodejs前端demo nodejs前端面试题_前端_17

8.4、将自定义包发布到官网

  1. 在https://www.npmjs.com注册账号。
  2. 先切换到包所在目录下,比如:06\node_modules\lgg\
  3. 在终端输入npm addUser。
  4. 在终端输入npm publish。

注意:在第三步前,要把源切换为官网。 nrm ls; nrm use npm;