全局对象
Node程序传递参数
正常情况下执行一个node程序,直接跟上我们的文件名称即可:node index.js;但是在执行某些node程序的过程中,我们可能希望给node传递一些参数,比如:node index.js coderwhy env=development,需要传递的参数直接写在文件名的后面即可
那么问题来了?我们要怎么获取到传递进去的参数呢?
- node中有内置一个全局对象
process,这个对象里面包含了很多信息,比如说版本号、操作系统、我们传递进去的参数等信息
- 传递进去的参数被放置在了process对象下的
argv属性对应的数组中,其第一个元素是node所在的目录、第二个元素是被执行文件所在的目录,后续的元素就是我们所传递进去的参数了
Node的输出方式
- 最常用的输出方式:
console.log - 清空控制台:
console.clear
console.log(1)
console.log(2)
console.clear()
console.log(3)
console.log(4)
在终端看到只能看到打印出了3和4,其实1和2也被打印出来了,只不过被清除掉了而已
- 打印函数调用栈:
console.trace
function a() {
b()
}
function b() {
console.trace()
}
a()
在下面可以看到console.trace语句被从内到外被调用的顺序,其在b函数中被执行,b函数又在a函数中被执行,a函数又在全局下被执行,当然后续还用到了一些node的api,比如说Module._compile等等
- console是一个全局对象,还有很多的方法在其上面,不过开发中用的最多的就是上面三个
特殊的全局对象
这些变量实际上是模块中的变量,知识每个模块都有,看起来像是全局对象;但他们和其他的全局对象不同,在命令行交互中是不可以使用的,包括__dirname、__filename、exports、module、require()
- __dirname表示当前文件所在目录对应的绝对路径
- __dirname表示当前文件对应的绝对路径
console.log(__dirname); // C:\Users\ASUS\Desktop\前端学习\算法
console.log(__filename); // C:\Users\ASUS\Desktop\前端学习\算法\test.js
常见的全局对象
- process对象:process提供了Node进程中相关的信息
- 比如Node的运行环境、参数信息等
- 我们还可以将一些环境变量读取到process的
env中
- console对象:提供了简单的调试控制台,在前面讲解输入内容时已经学习过了
- 定时器函数:在Node中使用定时器有好几种方式:
-
setTimeout,callback每delay毫秒后执行一次 -
setInterval,callback每delay毫秒重复执行一次 -
setImmediate(callback[ , ...args])- 这里先不展开讨论它和setTimeout(callback, 0)之间的区别
process.nextTick(callback[ , ...args])
global对象
global是一个全局对象,事实上前端我们提到的process、console、setTimeout等都有被放到global中方便我们去拿到这些api,很类似浏览器中的全局对象window,但又有些许区别
global和window的区别
在浏览器中,全局变量都是放在window上的,比如document、setInterval等等,如果我们在顶级范围内通过var定义了一个变量比如var a = 1,其默认会被添加到window对象上;但是在node中,我们通过var定义的一个变量,他只是在当前模块中有一个变量,不会放到全局中
因为在浏览器中,其实是没有模块的概念的,所以它会随随便便将我们定义的变量放置在window上面,但是在我们node当中,其实每个文件都是一个独立的模块,如果我们在node中定义变量都放置在global对象中,就有可能出现一种情况,我在另一个地方也定义了一个同名的变量,那就会把global中对象的属性给覆盖掉,其它人在调用的时候可能就会出错
模块化开发
什么是模块化呢?
- 事实上模块化开发最终的目的就是将程序划分成一个个小的结构
- 在这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其它结构使用
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等
上面所提到的结构,就是模块;按照这个结构划分开发程序的过程,就叫做模块化开发的过程
没有模块化带来很多的问题
早期没有模块化带来了很多的问题:比如命名冲突的问题,因为不同的js文件是可以访问到其他js文件所定义的变量的,甚至还有可能修改对应对象的属性值,在团队开发中,这是一个隐藏的问题
当然,我们有办法可以解决上面的问题:通过使用立即执行函数,将其他js文件所需要的变量导出
// a.js
var moduleA = (function(){
var a = 1
var b = 2
return {
a,
b
})()
// b.js
console.log(moduleA.a,moduleA.b) // 1,2
但是,我们其实带来了新的问题
- 我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用
- 代码写起来比较混乱,每个文件中的代码都需要包裹在一个匿名函数中来编写
- 在没有合适规范的情况下,每个人,每个公司都可能会任意命名、甚至出现模块名称相同的情况,这样就回到了原来的问题上
所以,我们会发现,通过自执行函数,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的
- 于是我们就需要指定一定的规范来约束每个人都按照这个规范去编写模块化的代码
- 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性
- JS社区为了解决上面的问题,涌现出了一系列好用的规范,比如
AMD、CMD、CommmonJs(Node当中最早采用的模块化方法)
CommonJs和Node(CommonJS是一个规范,Node实现了这个规范而已)
CommonJS是一个规范,最开始并不是应用到Node中的,Node只是CommonJS在服务器端一个具有代表性的实现而已,Browserify是CommonJS在浏览器中的一种实现,webpack打包工具具备CommonJS的支持和转换
所以,Node中对CommonJS进行了支持和实现,让我们在开发node程序的过程中可以方便的进行模块化开发
- 在Node中的每一个js文件都是一个单独的模块,自己的模块有单独的作用域
- 这个模块中包括CommonJS规范的核心变量:
exports、module.exports、require; - 我们可以使用这些变量来方便的进行模块化开发
前面我们提到过模块化的核心是导入和导出,Node中对其进行了实现
- exports和
module.exports可以负责对模块中的内容进行导出 - require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
exports导出
exports一开始是一个空对象,我们可以在这个对象中添加很多个属性,添加的属性就会随着exports对象导出出去
// bar.js
exports.name = name
exports.age = age
exports.sayHi = sayHi
另一个文件中可以通过require进行导入
// main.js
const bar = require('./bar')
上面这行代码完成了上面操作呢?
-
意味着main中的bar变量等于exports这个对象,赋值操作就相当于是引用值传递
- 也就是require通过各种查找方式,最终找到了exports这个对象
- 并且将这个exports对象赋值给了bar变量
- bar变量就是exports对象了
理解对象的引用赋值
对象是一个引用类型,其会在堆内存中开辟一块内存空间,这一块空间对应了唯一的内存地址,比如const obj = {name: 'why', age: 18},obj中保存的其实并不是真的这个对象,而是这个对象对应在堆内存中的内存地址,也就是这个对象的引用,我们可以通过obj变量保存的地址访问到这个变量,也可以说obj指向对象所在的那一块内存地址
const obj = {
name: 'why',
age: 18
}
const info = obj
info.name = 'kobe'
console.log(obj.name) // 'kobe'
将obj赋值给info,相当于是把obj对应的内存地址告诉info,这样info和obj就指向同一块内存地址了
切换到真实CommonJS的模块化场景中也一样,require导入后赋值的变量就指向exports的内存地址,只要在一个模块中改动了这个对象的属性,那么在另一个模块再获取到的,就是被更改过的对象;bar对象是exports对象的浅拷贝(引用赋值),浅拷贝的本质就是一种引用的赋值而已
重点:所以Node中实现CommonJS的本质就是对象的引用赋值
CommonJS实践
// test1.js
var name = 'sad'
var age = 18
function sayHi(name) {
console.log(name);
}
var obj = {
a: 1,
b: '1'
}
setTimeout(() => {
console.log(obj); // { a: 2, b: '1' },发现a属性确实被修改了,证明require导入的就是exports
}, 1000)
// 将要导出的值都添加到exports对象中
exports.name = name
exports.age = age
exports.sayHi = sayHi
exports.obj = obj
// test.js
// 利用require导入test1.js文件的exports对象,导入和导出的是同一个引用值
const { name, age, sayHi, obj } = require('./test1')
console.log(name); // 'sad'
console.log(age); // 18
sayHi('Hello World') // Hello World
setTimeout(() => {
obj.a = 2 // 500ms修改test1.js文件中导出的obj对象中的a属性
}, 500)
module.exports又是什么?
我们平常看到别人用Node导出东西的时候,好像很多都是用module.exports导出的,那么其和exports他有什么关系或者区别呢?
- CommonJS这个规范中是没有module.exports这个概念的
- 但是为了实现模块的导出,Node中使用的是Module类,每一个模块都是Module的一个实例,也就是module
- 所以在Node中真正用于导出的其实根本不是exports,而是module.exports
- 因为module才是导出的真正实现者
但是,为什么exports也可以导出呢?
- 这是因为module对象的exports属性是exports对象的一个引用,因为在node源码中,每个文件的顶层都加上了一句
module.exports = exports代码 - 也就是说
module.exports = exports = main中的bar
这样看来,最终导出都是通过module.exports,那么exports存在的意义是什么呢?
因为CommonJS的规范中要求有一个exports作为导出,这是Node为了满足commonjs做的一个妥协,其实可以完全没有exports
module.exports导出
// test1.js
exports.age = 1
console.log(module.exports); // { age: 1 },说明在模块顶层执行了module.exports = exports赋值操作
module.exports.age = 2
console.log(exports); // { age: 2 },说明赋值操作是引用值传递,可以通过module.exports更改exports的值
module.exports = {
age: 3
}
// main.js
const obj = require('./test1')
console.log(obj); // { age: 3 },说明导出的是module.exports而不是exports
module.exports = exports的赋值操作在模块的最前面
我们在一个文件的末尾给exports做一个赋值操作,再在main文件中导入这个模块,通过导入的结果来判断module.exports = exports语句是在模块的最前面还是最后面
// test1.js
// exports初始状态下是个空对象
exports = 1
// main.js
const val = require('./test1')
console.log(val); // {}
打印出来的结果是个空对象,说明赋值操作是在模块的最顶部执行的,因为如果是在模块最后面赋值的话,main.js中导入的值就应该是1才对,正因为在对顶部就执行了赋值操作,我们又将exports指向了其它变量,所以module.exports的值一直都是空对象,因此在main文件中接受到的也是一个空对象