阅读 9500

Node.js入门系列(三)开发调试、全局内置函数和变量

写在前面

Node.js入门系列是一整套参考教程,写到本篇已经是第四篇了,系列以结构脑图、文字解释、概括总结、练习实例、面试考点的形式讲述知识点。虽然学习这个系列不能让你立刻进阶资深,但学到的知识正是通往资深前端路上的基石。

同时,金九银十到了,很多小伙伴也开启了面试之旅,为助力大家拿到满意的offer,我将每天参加面试的小伙伴,遇到的各个公司的面试真题,分类整理成了文档,在群内与各位小伙伴分享,避免复习走弯路,帮助更多的小伙伴找到前端面试热点,查找自身知识薄弱点。如果感兴趣,在系列文章的第二篇中有惊喜。

上集回顾

上集我们谈了:Node.js内部的模块概念,以及REPL的知识,顺便还讲了讲前端模块化的起因发展以及和Node.js的关系。

如果首次翻到这篇文章,强烈建议大家从第一节开始读起,你将收获更多,这个系列没有夸大的标题,内容却有足够的广度,相信一定能让你看到不一样的解读。同时,每一篇字数都不多,5-10分钟可读完,如果暂时时间不充裕,可点击关注或赞,日后在“动态”中可找到此系列文。

请点击这里跳转需要阅读的章节:


漫谈Node.js入门
Node.js入门系列(一)特点、适用场景、解决的前端痛点问题
Node.js入门系列(二)模块、REPL



Node.js有着“万物皆模块”的口号,这一点很像在前端JavaScript的“万物皆对象”的口号。
这个特点有好处也有坏处。好处在于,万物皆为统一的一个概念,方便学习和记忆;坏处在于,很多时候这个概念会让我们产生疑惑,就像前端JavaScript中说的,Array、String都是对象一样,都会让人迷惑一阵子。伴随着深入学习,这些迷惑会慢慢的消失。下面就正式进入本节内容。

本节学习目标

两个目标:
1)如何调试开发程序?
2)Node.js有哪些方法和变量

本节学习目录

一 什么是Node.js控制台?如何使用控制台调试应用程序?

二 Node.js的全局作用域,以及Node.js提供的全局函数和变量

三 本节相关面试考点

四 本节小结

五 下期预告:Node.js事件机制

六 文末

一、什么是Node.js控制台?如何使用控制台调试应用程序?

在《Node.js入门系列(二)模块、REPL》我们讲到Node.js的模块,其中一个模块就是console模块,不记得的小伙伴可以看这里:

这个模块是一个内置对象,它具有很多方法,作用是输出信息用于开发调试,它其实就像我们在客户端JavaScript中用的console内置对象,记忆的时候可以一同记忆。它的用法有两种,一种是在模块中写,在执行模块时输出,一种是在REPL中直接写,脱离文件模块。以下列出它具有的方法,在用的时候对比查找即可。

扩展

在第二节探讨了REPL,一个交互式解释器,提供调试和运行的环境,本节又讲到了console模块,也可以提供调试功能,那么它们有什么区别呢?是不是我们就有两个调试工具了呢?

先从根本上看看它们是什么:在第二节,演示REPL例子的时候,我们看到REPL可以执行表达式,访问最近一次记录,并且还提供了一些基础指令,REPL提供的是一个运行环境。再看console,它是Node.js的一个内置模块,可以js文件中使用,也可以直接在REPL中输出。

所以REPL与console的关系是:一个是运行环境,一个是调试工具,调试工具依赖于运行环境。

二、Node.js的全局作用域,以及Node.js提供的全局函数和变量

Node.js的全局作用域

在第一节中,我们讲到,Node.js模块解决了模块化私有作用域的问题,每一个模块都有一个私有的作用域,外部不可以直接访问模块内部作用域,只有通过exports导出的对象才能被访问。
尽管Node.js十分强调模块的重要性,但是还是设计了全局作用的概念,用于全局内任何一个模块内部访问和使用,增加代码可复用性,这个全局的命名空间被称为global,它是一个对象。在global上挂载了很多属性、方法和类。可以通过启动REPL环境,输入以下内容来查看挂载在这个全局命名空间的内容:

console.log(global)
复制代码

需要注意的是,全局命名空间有且只有一个,如果方法和变量不是全局公用,不要将其挂载到全局;同时,如果不是必须,尽量避免覆盖全局的方法和属性。

Node.js提供的全局函数和变量

1) setTimeout 和 clearTimeout

setTimeout与客户端JavaScript中的setTimeout作用类似,表示从此刻起,指定时间后开始执行回调函数。而clearTimeout作用是取消该回调函数的调用。
语法如下:

setTimeout(callback, ms, [arg], [...])
复制代码

该函数中使用两个以上的参数,其中前两个参数为必传参数,第一个参数值为需 要执行的回调函数,第二个参数值为一个整数,用于指定经过多少毫秒后执行该回调函数, 该值必须在1~2147483647(也就是24.8天)之间,如果指定值过大,则被自动修改为1。从第三个参数开始,为需要向回调函数中传入的参数。该函数返回值一个定时器对象。
用法与客户端JavaScript中定时器用法一致。
2) setInterval函数与clearInterval函数

setInterval与客户端JavaScript中的setInterval作用类似,表示每隔指定时间执行一次回调函数。而clearInterval是取消该回调函数的调用。
语法如下:

setInterval(callback, ms, [arg], [...])
复制代码

参数意义与setTimeout一致。
3) unref方法与ref方法

前面说到,setTimeout方法与setInterval方法均返回一个定时器对象,Node.js为这两个定时器对象各添加了unref和ref方法。

这两个方法在客户端JavaScript中是没有的,那么它们有什么用呢?

简单来说,unref方法用于取消setTimeout方法与setInterval方法中callback回调函数的执行;而ref作用正好相反,用于恢复调用callback回调函数。


语法如下:
const timer = setInterval(callback, ms, [arg], [...]) // setTimeout、setInterval都定时器返回一个定时器对象

timer.unref() // 取消callback函数的执行

timer.ref() // 恢复callback函数的执行
复制代码

小结unref方法与ref方法:

1)unref()和ref()方法都是通过定时器返回的对象进行调用的,是挂载到定时器对象上的;
2)unref()和ref()方法取消回调和恢复回调只是取消和恢复了定时器对象中的回调函数,并不影响定时器的存在,要想清理定时器依然还是用clearInterval或clearTimeout。
3)过多的使用unref()方法会对程序产生负面的性能影响,因此,一般慎用unref()方法(可以思考下为什么)
4)unref()和ref()使得我们可以在代码的任何位置控制某个定时器在何时执行回调,又在何时取消回调

4) 与require模块相关的全局方法和属性

Node.js遵循common.js标准规范,引入模块时使用require方法。
语法如下:

const foo = require('../foo.js) // 接收一个字符串形式的路径名作为参数
复制代码

同一个模块,首次加载后将缓存在内存中,此后,对于相同模块的多次引用,得到的都是同一个模块,不会引起模块内代码的多次执行。
这里会有一个问题:如果在多处引入了某个模块,由于首次引入后,后续将从内存中直接读取,所以,当我们修改模块内容时并不会引起引入模块的代码更新,可我每次都想用最新的模块内容,怎么办呢?
稍后我们在require.cache讲解中说到解决方案。这里先接着介绍require.
如果传给require的参数是一个模糊的文件名,不含有文件类型,这个时候,输出结果又会如何?
const foo = require('foo') // 只传一个字符串形式名称,不指定文件类型
复制代码

传给require的参数是一个模糊的文件名,Node.js并不会直接报错,而是有一套查找规则,总结如下:

  1. 先加载文件,优先级为:.js > .json >.node
  2. 没有文件加载文件夹:
      *先看有没有package.json,有的话,加载package.json里main属性指定的文件。
      *没有package.json,加载该目录下的index.js文件
扩展:

前端在使用cli脚手架构建的React项目,在引入组件的时候,同样是省略了.js后缀文件名,但是依然可以找到对应的文件,其查找原理跟这里相同,感兴趣的小伙伴可以去探索一下。

使用require.resolve函数查询完整模块名
在Node.js中,可以使用require.resolve函数来查询某个模块文件的带有完整绝对路径的 文件名,代码如下所示:

require.resolve('./testModule.js');
复制代码

当在REPL执行时,输出的结果为该模块所在的磁盘绝对路径。
使用此方法查找自定义模块会输出带有完整绝对路径的 文件名,那么如果对内置模块使用该方法会如何呢? 答案是:直接输出名称

注意:

require.resolve()方法并不会引入该模块,更不会执行模块内方法,只是查找模块的完整绝对路径。

使用require.cache对象查看已缓存的模块

前面在说require的时候说到:同一个模块,首次加载后将缓存在内存中,此后,对于相同模块的多次引用得到的都是同一个模块,不会引起模块内代码的多次执行。可以通过require.cache查看当前已经缓存的模块列表。语法如下,在REPL中输入:

console.log(require.cache)
复制代码

即可查看已缓存的所有模块。如果我们想查看某个具名模块,可使用如下语法:

console.log(require.cache[require.resolve('./testModule.js')])
复制代码

前面遗留下的一个问题是:由于模块引入后就会缓存,导致修改的模块不会立即在引入的文件中更新,这个问题怎么解决呢?

这里来说一个解决思路:

如果我们能在需要最新模块的位置,删除掉已缓存的模块,然后再重新引入模块,这样是不是就能保证引入的是最新的了?

这个方法涉及到两个操作:删除模块,引入模块;引入模块用require,那么删除模块呢?

所幸,Node.js提供了delete关键字,用于删除缓存模块。语法如下:

const testModule1 =require( './testModule.js') // 初始引入模块,模块会被缓存在内存中

delete require.cache[require.resolve('./testModule.js')]  // 删除缓存中模块

const testModule2 = require( './testModule.js') // 再次重新加载模块

复制代码
拓展:

在实际项目中,要求使用最新的模块时,可以将此机制封装成一个方法,通过传入模块路径参数,在指定地方调用该方法,就可以保证使用的是最新更改的模块。

与文件系统模块相关的全局变量

在Node.js中定义了两个文件模块相关的变量,一个是__filename,一个是__dirname。

__filename用于获取当前模块文件的完整绝对路径文件名。语法如下:

console.log(__filename)
复制代码

__dirname用于获取当前模块文件所在目录的完整绝对路径。语法如下:

console.log(__dirname)
复制代码
疑惑:

前面说到require.resolve()方法可以获取当前模块文件所在目录的完整绝对路径,此处的__filename也能达到一样的效果,那么它们有什么区别呢?总结如下

1)require.resolve()是挂载到require对象上的方法,而__filename是一个全局变量,一个是方法,一个是变量,性质不同;
2)require.resolve()使用时需要传入参数,而__filename不需要传参,直接就代表路径,一个要传参,一个不需要传参,使用方法不同;
3)require.resolve()由于可以传入模块名,所以可以找到任意指定模块,而__filename代表仅仅是当前模块的绝对路径,在这方面,显然require.resolve()要比__filename强大;
4)在实际项目中,通常以__filename为基础进行路径变化,进而切换到其他文件路径,使用起来比require.resolve()方便简洁。

三、本节相关面试考点

1)使用Node.js开发,你有哪些调试方法?
2)REPL与console之间关系是什么?
3)你是如何选用require.resolve()和__filename的?
4)前端模块化会带来什么问题?
5)在引入模块时不指定文件后缀,Node.js查找规则是什么?

四、本节小结

本节主要内容是两个内容:console模块和全局作用域中的内置方法和变量。

这两块内容虽然不难,但是在文章中扩展了很多知识,目的在于触类旁通,既然Node.js被划归到前端,就应该把它跟前端联系到一起。

真正面试时从来不会直接问你概念,更不会直接问理论背的熟不熟。工具服务于业务,工具怎么用,用到哪,用的水平如何,出了问题怎么解决,才是区分初级与资深的关键。

五、下期预告:Node.js事件机制

Node.js具有单线程、非阻塞、事件驱动的特点,这决定了事件机制在Node.js中举足轻重的地位。同时呢,这也绝不是一两句话就能讲清楚的。

所以准备单独拿出一节详细聊聊事件机制,同时也减少本节阅读的负担,欢迎继续关注更新,我们下期接着细聊。

六、文末

只写看得懂、读得懂、有价值的技术文章;

看完觉得有收获,点赞 + 关注,平台会为你推荐更多优质主题文章;

最后,我将连载系列文《从零学webpack》,如果你也感兴趣,欢迎关注阅读。