Node.js面试题整理

1,678 阅读12分钟

首页 - 前端面试题宝典 (ecool.fun)

第1题:npm 是什么?

npm是Node.js的包管理工具,它的诞生也极大的促进了前端的发展,在现代前端开发中都离不开npm的身影。

常见的使用场景有以下几种:

· 允许用户从NPM服务器下载别人编写的第三方包到本地使用。

· 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。

· 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

第2题:common.js和es6中模块引入的区别?

Common]S是一种模块规范,最初被应用于Nodejs,成为Nodejs 的模块规范。

运行在浏览器端的JavaScript由于也缺少类似的规范,在ES6出来之前,前端也实现了一套相同的模块规范(例如: AMD),用来对前端模块进行管理。

自ES6起,引入了一套新的ES6 Module规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。

在使用上的差别主要有:

· CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用CommonJS模块是运行时加载,ES6模块是编译时输出接口。

· CommonJs是单个值导出,ES6 Module可以导出多个

· CommonJs是动态语法可以写在判断里,ES6 Module静态语法只能写在顶层CommonJs的this是当前模块,ES6 Module的this是undefined

第3题:说说你对Node.js 的理解?优缺点?应用场景?

一、是什么

Node.js 是一个开源与跨平台的 JavaScript 运行时环境

在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能

可以理解为 Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境

非阻塞异步

Nodejs采用了非阻塞型I/O机制,在做I/O操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作

例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率

事件驱动

事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数

比如读取一个文件,文件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进行处理

图片11.png

二、优缺点

优点:

· 处理高并发场景性能更佳

· 适合I/O密集型应用,值的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作

因为Nodejs是单线程,带来的缺点有:

· 不适合CPU密集型应用

· 只支持单核CPU,不能充分利用CPU

· 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃

三、应用场景

借助Nodejs的特点和弊端,其应用场景分类如下:

  • 善于I/O,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程

  • 大量并发的I/O,应用程序内部并不需要进行非常复杂的处理

  • 与 websocket 配合,开发长连接的实时交互应用程序

具体场景可以表现为如下:

· 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序

· 第二大类:基于web、canvas等多人联网游戏

· 第三大类:基于web的多人实时聊天客户端、聊天室、图文直播

· 第四大类:单页面浏览器应用程序

· 第五大类:操作数据库、为前端和移动端提供基于json的API

其实,Nodejs能实现几乎一切的应用,只考虑适不适合使用它

第4题:Node. js 有哪些全局对象?

一、是什么

在浏览器 JavaScript 中,通常 window 是全局对象, 而 Nodejs 中的全局对象是 global

在NodeJS里,是不可能在最外层定义一个变量,因为所有的用户代码都是当前模块的,只在当前模块里可用,但可以通过exports对象的使用将其传递给模块外部

所以,在NodeJS中,用var声明的变量并不属于全局的变量,只在当前模块生效

像上述的global全局对象则在全局作用域中,任何全局变量、函数、对象都是该对象的一个属性值

二、有哪些

将全局对象分成两类:

  •  真正的全局对象
  • 模块级别的全局变量

真正的全局对象

下面给出一些常见的全局对象:

  • Class:Buffer
  • process
  • console
  • clearInterval、setInterval
  • clearTimeout、setTimeout
  • global

Class:Buffer

可以处理二进制以及非Unicode编码的数据

在Buffer类实例化中存储了原始数据。Buffer类似于一个整数数组,在V8堆原始存储空间给它分配了内存

一旦创建了Buffer实例,则无法改变大小

process

进程对象,提供有关当前过程的信息和控制

包括在执行node程序的过程中,如果需要传递参数,我们想要获取这个参数需要在process内置对象中

启动进程:

node index.js 参数1 参数2 参数3

index.js文件如下:

process.argv.forEach((val, index) => { console.log(${index}: ${val}); });

输出如下:

/usr/local/bin/node /Users/mjr/work/node/process-args.js 参数1 参数2 参数3

除此之外,还包括一些其他信息如版本、操作系统等

console

用来打印stdout和stderr

最常用的输入内容的方式:console.log

console.log("hello");

清空控制台:console.clear

console.clear

打印函数的调用栈:

console.trace

function test() {
  demo();
}
function demo() {
  foo();
}
function foo() {
  console.trace();
}
test();

clearInterval、setInterval

设置定时器与清除定时器

setInterval(callback, delay[, ...args])

callback每delay毫秒重复执行一次

clearInterval则为对应发取消定时器的方法

clearTimeout、setTimeout

设置延时器与清除延时器

setTimeout(callback,delay[,...args])

callback在delay毫秒后执行一次

clearTimeout则为对应取消延时器的方法

global

全局命名空间对象,墙面讲到的process、console、setTimeout等都有放到global中

console.log(process === global.process) // true

模块级别的全局对象

这些全局对象是模块中的变量,只是每个模块都有,看起来就像全局变量,像在命令交互中是不可以使用,包括:

  • __dirname
  •  __filename
  • exports
  • module
  • require

__dirname

获取当前文件所在的路径,不包括后面的文件名

从 /Users/mjr 运行 node example.js:

console.log(__dirname);// 打印: /Users/mjr

__filename

获取当前文件所在的路径和文件名称,包括后面的文件名称

从 /Users/mjr 运行 node example.js:

console.log(__filename);// 打印: /Users/mjr/example.js

exports

module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容

exports.name = name;
exports.age = age;
exports.sayHello = sayHello;

module

对当前模块的引用,通过module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容

require

用于引入模块、 JSON、或本地文件。 可以从 node_modules 引入模块。

可以使用相对路径引入本地模块或JSON文件,路径会根据__dirname定义的目录名或当前工作目录进行处理

第5题:说说对 Node 中的 process 的理解?有哪些常用方法?

一、是什么

process 对象是一个全局变量,提供了有关当前 Node.js 进程的信息并对其进行控制,作为一个全局变量

我们都知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容器

当我们启动一个js文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共享

由于JavaScript是一个单线程语言,所以通过node xxx启动一个文件后,只有一条主线程

二、属性与方法

关于process常见的属性有如下:

  • process.env:环境变量,例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息
  • process.nextTick:这个在谈及 EventLoop 时经常为会提到
  • process.pid:获取当前进程id
  • process.ppid:当前进程对应的父进程
  • process.cwd():获取当前进程工作目录,
  • process.platform:获取当前进程运行的操作系统平台
  • process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
  • 进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听
  • 三个标准流: process.stdout 标准输出、 process.stdin 标准输入、 process.stderr 标准错误输出
  • process.title 指定进程名称,有的时候需要给进程指定一个名称

下面再稍微介绍下某些方法的使用

process.cwd()

返回当前 Node 进程执行的目录

一个 Node 模块 A 通过 NPM 发布,项目 B 中使用了模块 A。在 A 中需要操作 B 项目下的文件时,就可以用 process.cwd() 来获取 B 项目的路径

process.argv

在终端通过 Node 执行命令的时候,通过 process.argv 可以获取传入的命令行参数,返回值是一个数组:

  • 0: Node 路径(一般用不到,直接忽略)
  • 1: 被执行的 JS 文件路径(一般用不到,直接忽略)
  • 2~n: 真实传入命令的参数

所以,我们只要从 process.argv[2] 开始获取就好了

const args = process.argv.slice(2);

process.env

返回一个对象,存储当前环境相关的所有信息,一般很少直接用到。

一般我们会在 process.env 上挂载一些变量标识当前的环境。比如最常见的用 process.env.NODE_ENV 区分 development 和 production

在 vue-cli 的源码中也经常会看到 process.env.VUE_CLI_DEBUG 标识当前是不是 DEBUG 模式

process.nextTick()

我们知道NodeJs是基于事件轮询,在这个过程中,同一时间只会处理一件事情

在这种处理模式下,process.nextTick()就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行

例如下面例子将一个foo函数在下一个时间点调用

function foo() { 
    console.error('foo'); 
} 
process.nextTick(foo); console.error('bar');

输出结果为barfoo

虽然下述方式也能实现同样效果:

setTimeout(foo, 0); console.log('bar');

两者区别在于:

  • process.nextTick()会在这一次event loop的call stack清空后(下一次event loop开始前)再调用callback
  • setTimeout()是并不知道什么时候call stack清空的,所以何时调用callback函数是不确定的

第6题: 说说对中间件概念的理解,如何封装 node 中间件?

一、是什么

中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的

NodeJS中,中间件主要是指封装http请求细节处理的方法

例如在expresskoaweb框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函数

中间件.png 在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象、返回响应数据等操作

二、封装

koa是基于NodeJS当前比较流行的web框架,本身支持的功能并不多,功能都可以通过中间件拓展实现。通过添加不同的中间件,实现不同的需求,从而构建一个 Koa 应用

Koa 中间件采用的是洋葱圈模型,每次执行下一个中间件传入两个参数:

  • ctx :封装了request 和 response 的变量
  • next :进入下一个要执行的中间件的函数

下面就针对koa进行中间件的封装:

Koa 的中间件就是函数,可以是 async 函数,或是普通函数

//async 函数 
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); 
// 普通函数 
app.use((ctx, next) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); });

下面则通过中间件封装http请求过程中几个常用的功能:

token校验

module.exports = (options) => async (ctx, next) { try { 
// 获取 token 
const token = ctx.header.authorization if (token) { 
    try { 
    // verify 函数验证 token,并获取用户相关信息 
        await verify(token)
    } catch (err) { console.log(err) } 
} 
// 进入下一个中间件 
await next() } catch (err) { console.log(err) } }

日志模块

const fs = require('fs') module.exports = (options) => async (ctx, next) => { const startTime = Date.now() const requestTime = new Date() await next() const ms = Date.now() - startTime; let logout = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`; // 输出日志文件 fs.appendFileSync('./log.txt', logout + '\n') }

Koa存在很多第三方的中间件,如koa-bodyparserkoa-static

下面再来看看它们的大体的简单实现:

koa-bodyparser

koa-bodyparser 中间件是将我们的 post 请求和表单提交的查询字符串转换成对象,并挂在 ctx.request.body 上,方便我们在其他中间件或接口处取值

// 文件:my-koa-bodyparser.js const querystring = require("querystring"); module.exports = function bodyParser() { return async (ctx, next) => { await new Promise((resolve, reject) => { // 存储数据的数组 let dataArr = []; // 接收数据 ctx.req.on("data", data => dataArr.push(data)); // 整合数据并使用 Promise 成功 ctx.req.on("end", () => { // 获取请求数据的类型 json 或表单 let contentType = ctx.get("Content-Type"); // 获取数据 Buffer 格式 let data = Buffer.concat(dataArr).toString(); if (contentType === "application/x-www-form-urlencoded") { // 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body ctx.request.body = querystring.parse(data); } else if (contentType === "applaction/json") { // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body ctx.request.body = JSON.parse(data); } // 执行成功的回调 resolve(); }); }); // 继续向下执行 await next(); }; };

koa-static

koa-static 中间件的作用是在服务器接到请求时,帮我们处理静态文件

const fs = require("fs"); const path = require("path"); const mime = require("mime"); const { promisify } = require("util");
// 将 stat 和 access 转换成 Promise const stat = promisify(fs.stat); 
const access = promisify(fs.access) module.exports = function (dir) { return async (ctx, next) => { 
// 将访问的路由处理成绝对路径,这里要使用 join 因为有可能是 / 
let realPath = path.join(dir, ctx.path); try { 
// 获取 stat 对象 
let statObj = await stat(realPath); 
// 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html 
if (statObj.isFile()) { ctx.set("Content-Type", `${mime.getType()};charset=utf8`); ctx.body = fs.createReadStream(realPath); } else { let filename = path.join(realPath, "index.html"); 
// 如果不存在该文件则执行 catch 中的 next 交给其他中间件处理 
await access(filename); 
// 存在设置文件类型并响应内容 
ctx.set("Content-Type", "text/html;charset=utf8"); ctx.body = fs.createReadStream(filename); } } catch (e) { await next(); } } }

三、总结

在实现中间件时候,单个中间件应该足够简单,职责单一,中间件的代码编写应该高效,必要的时候通过缓存重复获取数据

koa本身比较简洁,但是通过中间件的机制能够实现各种所需要的功能,使得web应用具备良好的可拓展性和组合性

通过将公共逻辑的处理编写在中间件中,可以不用在每一个接口回调中做相同的代码编写,减少了冗杂代码,过程就如装饰者模式