第1章 1.2 REPL 与脚本模式

上章回顾:上一章我们搞懂了 Node.js 是什么(一个用 JavaScript 写后台的运行环境),还成功把它装到了电脑上。现在你电脑里应该已经有个能跑的 Node.js 了。

本章目标:这一章我们要学两件事——怎么用 REPL 快速试验代码,怎么用脚本模式跑完整的 .js 文件。学完你就能真正写出一个可运行的 Node.js 程序了。


🎯 开场 3 分钟:为什么要学这个?

场景引入

想象一下:你刚学做菜,正确的学习方式是:
- REPL 模式 = 先在厨房里一样一样调料都尝尝,知道"哦,这个是咸的、那个是辣的"
- 脚本模式 = 最后做一盘完整的菜,端上桌给人吃

你学 Python/JS 这种解释型语言也是一样的道理。

痛点

我见过太多新手踩这个坑:

「我想试试 console.log('你好') 能跑不,结果在 Node 官网找了半天不知道点哪里...」

或者:

「我写了个 test.js 文件,双击打不开,拖到 Node 图标上也行不通,这玩意儿到底怎么跑?」

学完能解决

  • 3 秒内验证一个 JavaScript 想法(不用建文件)
  • 跑一个完整的 .js 脚本文件
  • 读取命令行传入的参数,做出「可配置」的小工具

🧱 基础 25 分钟:核心概念

1.2.1 REPL 是什么?

REPL = Read-Eval-Print-Loop,翻译成人话就是「读取 → 执行 → 打印 → 循环」。

生活类比

就像你用计算器:
1. 你输入 2+2(Read)
2. 计算器算出 4(Eval)
3. 屏幕显示 4(Print)
4. 等待你输入下一道题(Loop)

REPL 就是 Node.js 自带的「计算器」,让你一句一句试 JavaScript。

怎么打开?

打开终端(Mac 是「终端」App,Windows 是「命令提示符」或 PowerShell),输入:

node

看到 > 符号了吗?恭喜你进入 REPL 了:

~
❯ node
Welcome to Node.js v20.0.0.
Type ".help" for more information.
>

第一个 REPL 命令

> 后面输入:

> console.log('你好,世界')
你好,世界
undefined

解释一下:console.log() 是打印函数,它会输出「你好,世界」,然后返回 undefined(这是 JavaScript 函数的默认值,下一章会细讲)。

再试一个数学计算:

> 3 + 5
8
> const 名字 = '小明'
undefined
> 名字 + '喜欢吃苹果'
'小明喜欢吃苹果'

配图1 - 配图1

退出 REPL

Ctrl + C 两次,或者输入 .exit

> .exit
~

常用 REPL 命令

命令 作用
.help 显示帮助
.exit 退出
.clear 清屏
.save 文件名 把当前会话保存为文件
.load 文件名 加载一个文件到 REPL

1.2.2 脚本模式:跑一个 .js 文件

REPL 适合尝鲜,但你想写一个「以后还能用」的程序,就得用脚本模式。

步骤

第一步:创建文件

用任意文本编辑器(VS Code、记事本都行),新建一个文件叫 hello.js,内容:

// 这是我的第一个 Node.js 脚本
console.log('你好,世界!');

// 算个数学题
const a = 10;
const b = 20;
console.log('10 + 20 =', a + b);

第二步:运行

在终端里,进入文件所在目录,然后:

node hello.js

输出:

你好,世界!
10 + 20 = 30

敲黑板

  • 文件后缀必须是 .js(Node.js 认识这个格式)
  • 运行命令是 node 文件名(不是 node.exe 文件名.js,别搞混)
  • 双击 .js 文件是打不开的(除非你装了 VS Code 的「Code Runner」插件)

配图2 - 配图2

1.2.3 process.argv:让脚本接受参数

这是本章最实用的知识点之一。

痛点

你想写一个程序,输出「你好,张三」,但张三这个名字每次可能不一样。你会怎么办?

难道每次都去改代码?太麻烦了!

解决方案:命令行参数

Node.js 有一个内置对象 process(进程),里面有个 argv 数组(argument values)。

创建一个文件 greet.js

// process.argv 是个数组
// argv[0] = node 的路径
// argv[1] = 当前脚本的路径
// argv[2] = 你传入的第一个参数
// argv[3] = 第二个参数...以此类推

const 名字 = process.argv[2] || '路人甲';

console.log('你好,' + 名字 + '!');

运行:

node greet.js 小明

输出:

你好,小明!

再试:

node greet.js

输出:

你好,路人甲!

这里用了 || '路人甲',意思是「如果没传参数,默认用路人甲」。

工作原理图解

命令: node greet.js 小明

process.argv 数组:
┌─────────────────────────────────┐
│ argv[0] │ "node 的安装路径"      │
│ argv[1] │ "greet.js 的路径"      │
│ argv[2] │ "小明"                 │  ← 我们用的就是这个
└─────────────────────────────────┘

1.2.4 小明购物清单实战

还记得上一章提到的小明吗?他有一张购物清单:

// shopping.js
const 清单 = ['苹果', '香蕉', '牛奶', '面包'];

// 用 for 循环遍历
for (let i = 0; i < 清单.length; i++) {
console.log('要买:' + 清单[i]);
}

运行 node shopping.js

要买:苹果
要买:香蕉
要买:牛奶
要买:面包

加点功能:按条件筛选

小明今天只想买水果(苹果、香蕉),用 if 判断:

// shopping.js
const 清单 = ['苹果', '香蕉', '牛奶', '面包'];
const 关键词 = process.argv[2] || '水果';

console.log('小明的购物清单(筛选:' + 关键词 + ')');
console.log('---');

for (let i = 0; i < 清单.length; i++) {
// 判断当前物品是否包含关键词
if (清单[i].includes(关键词) || 关键词 === '全部') {
    console.log('✓ ' + 清单[i]);
}
}

运行:

node shopping.js 水果
小明的购物清单(筛选:水果)
---
✓ 苹果
✓ 香蕉
node shopping.js 全部
小明的购物清单(筛选:全部)
---
✓ 苹果
✓ 香蕉
✓ 牛奶
✓ 面包

🔥 实战 35 分钟:3 个递进项目

项目 1:单位转换器(5 分钟)

场景:小明在美国网站看到一件衣服标注尺寸是 32 英寸,他想知道是多少厘米。

代码 convert.js

// 单位转换器
const 数值 = parseFloat(process.argv[2]);
const 类型 = process.argv[3];

if (!数值 || !类型) {
console.log('用法:node convert.js 数值 类型');
console.log('类型:inch(英寸转厘米)、cm(厘米转英寸)');
process.exit(1);
}

if (类型 === 'inch') {
const 结果 = 数值 * 2.54;
console.log(数值 + ' 英寸 = ' + 结果 + ' 厘米');
} else if (类型 === 'cm') {
const 结果 = 数值 / 2.54;
console.log(数值 + ' 厘米 = ' + 结果.toFixed(2) + ' 英寸');
} else {
console.log('未知类型:' + 类型);
}

运行

node convert.js 32 inch

输出

32 英寸 = 81.28 厘米

解释.toFixed(2) 是让小数保留 2 位,看起来更舒服。


项目 2:待办清单管理器(15 分钟)

场景:小明每天有一堆事要做,想用命令行管理待办事项,存在文件里。

代码 todo.js

const fs = require('fs');
const 文件名 = 'todo.json';

// 读取待办事项
function 读取待办() {
if (fs.existsSync(文件名)) {
    const 内容 = fs.readFileSync(文件名, 'utf-8');
    return JSON.parse(内容);
}
return [];
}

// 保存待办事项
function 保存待办(待办列表) {
fs.writeFileSync(文件名, JSON.stringify(待办列表, null, 2));
}

// 添加待办
function 添加待办(任务) {
const 待办列表 = 读取待办();
待办列表.push({
    任务: 任务,
    完成: false,
    时间: new Date().toLocaleString('zh-CN')
});
保存待办(待办列表);
console.log('已添加:' + 任务);
}

// 列出待办
function 列出待办() {
const 待办列表 = 读取待办();
if (待办列表.length === 0) {
    console.log('待办清单是空的!');
    return;
}
console.log('=== 小明的待办清单 ===');
待办列表.forEach((item, index) => {
    const 状态 = item.完成 ? '✓' : '□';
    console.log((index + 1) + '. [' + 状态 + '] ' + item.任务);
});
}

// 删除待办
function 删除待办(序号) {
const 待办列表 = 读取待办();
if (序号 > 0 && 序号 <= 待办列表.length) {
    const 已删除 = 待办列表.splice(序号 - 1, 1);
    保存待办(待办列表);
    console.log('已删除:' + 已删除[0].任务);
} else {
    console.log('序号无效');
}
}

// 主逻辑
const 操作 = process.argv[2];

if (操作 === 'list') {
列出待办();
} else if (操作 === 'add') {
const 任务 = process.argv[3];
if (任务) {
    添加待办(任务);
} else {
    console.log('请输入任务内容:node todo.js add "买牛奶"');
}
} else if (操作 === 'delete') {
const 序号 = parseInt(process.argv[3]);
if (!isNaN(序号)) {
    删除待办(序号);
} else {
    console.log('请输入有效序号');
}
} else {
console.log('用法:');
console.log('  node todo.js list              - 查看清单');
console.log('  node todo.js add "买牛奶"      - 添加任务');
console.log('  node todo.js delete 1          - 删除任务');
}

运行示例

node todo.js add "写代码"
node todo.js add "给妈妈打电话"
node todo.js list
node todo.js delete 1
node todo.js list

输出

已添加:写代码
已添加:给妈妈打电话
=== 小明的待办清单 ===
1. [□] 写代码
2. [□] 给妈妈打电话
已删除:写代码
=== 小明的待办清单 ===
1. [□] 给妈妈打电话

解释
- fs 是 Node.js 内置的文件系统模块(下一章会详细讲模块)
- 数据存在 todo.json 文件里,关闭程序再打开还在
- 用 JSON.stringify(待办列表, null, 2) 让 JSON 格式化输出,方便阅读


项目 3:简易日志分析器(15 分钟)

场景:小明是一个小电商运营,他有一份订单日志,想分析出今天的销售额和订单数。

准备:先创建 orders.log 文件:

订单编号,金额,商品
A001,99.5,手机壳
A002,299,蓝牙耳机
A003,59.9,数据线
A004,1999,移动电源
A005,45,手机膜

代码 analyzer.js

const fs = require('fs');

// 读取文件
const 文件路径 = process.argv[2];
if (!文件路径) {
console.log('用法:node analyzer.js 订单日志文件路径');
process.exit(1);
}

if (!fs.existsSync(文件路径)) {
console.log('文件不存在:' + 文件路径);
process.exit(1);
}

const 内容 = fs.readFileSync(文件路径, 'utf-8');
const 行数组 = 内容.trim().split('\n');

// 跳过表头,从第二行开始
const 订单列表 = 行数组.slice(1);

let 总金额 = 0;
let 订单数 = 0;

console.log('=== 订单分析报告 ===\n');

订单列表.forEach((行, index) => {
const 字段 = 行.split(',');
const 编号 = 字段[0];
const 金额 = parseFloat(字段[1]);
const 商品 = 字段[2];

总金额 += 金额;
订单数++;

console.log((index + 1) + '. ' + 编号 + ' | ' + 商品 + ' | ¥' + 金额);
});

console.log('\n--- 统计 ---');
console.log('总订单数:' + 订单数);
console.log('总销售额:¥' + 总金额.toFixed(2));
console.log('平均客单价:¥' + (总金额 / 订单数).toFixed(2));

运行

node analyzer.js orders.log

输出

=== 订单分析报告 ===

1. A001 | 手机壳 | ¥99.5
2. A002 | 蓝牙耳机 | ¥299
3. A003 | 数据线 | ¥59.9
4. A004 | 移动电源 | ¥1999
5. A005 | 手机膜 | ¥45

--- 统计 ---
总订单数:5
总销售额:¥2502.40
平均客单价:¥500.48

解释
- split('\n') 把多行文本拆成数组
- split(',') 把 CSV 格式的每一行拆成字段
- .toFixed(2) 确保金额显示两位小数


💪 进阶 20 分钟:常见坑 + 技巧

坑 1:process.argv 索引从 2 开始

// ❌ 错误:以为 argv[1] 是第一个参数
const 名字 = process.argv[1];

// ✅ 正确:前两个是 node 路径和脚本路径,从 2 开始才是用户输入
const 名字 = process.argv[2];

坑 2:忘了处理文件不存在

// ❌ 错误:直接读文件,不检查存不存在
const 内容 = fs.readFileSync('todo.json', 'utf-8');

// ✅ 正确:先检查再读
if (fs.existsSync(文件名)) {
const 内容 = fs.readFileSync(文件名, 'utf-8');
} else {
console.log('文件不存在,将创建新文件');
}

坑 3:JSON.parse 遇到空文件

// ❌ 错误:空文件会报错
const 数据 = JSON.parse(fs.readFileSync('empty.json', 'utf-8'));

// ✅ 正确:加上 try-catch 或者确保文件有内容
try {
const 数据 = JSON.parse(fs.readFileSync('empty.json', 'utf-8') || '[]');
} catch (e) {
const 数据 = [];
}

坑 4:中文乱码(Windows 常见)

// Windows 用户如果输出乱码,尝试指定编码
const 内容 = fs.readFileSync('文件.txt', 'utf-8');

坑 5:数字字符串直接运算

// ❌ 错误:字符串相加会变成拼接
const a = '10';
const b = '20';
console.log(a + b); // 输出 "1020"

// ✅ 正确:转成数字
const a = parseFloat('10');
const b = parseFloat('20');
console.log(a + b); // 输出 30

调试技巧:console.log 虽土但有用

// 在关键步骤加日志,看看变量实际是什么
console.log('调试:待办列表 =', 待办列表);
console.log('调试:argv =', process.argv);

✏️ 练习题

练习 1(2 分钟):改改改

greet.js 改成向小明说晚安,而不是问好。

  • 输入:node greet.js 小明
  • 预期输出:晚安,小明!

练习 2(3 分钟):加个判断

修改 convert.js,当用户输入负数时,提示「数值不能为负」并退出。

  • 输入:node convert.js -5 inch
  • 预期输出:数值不能为负

练习 3(5 分钟):新数据

创建一个新的日志文件 sales.log,包含你自己的 3 条「销售记录」(格式:编号,金额,商品),用 analyzer.js 分析它。

  • 输入:自定义日志文件
  • 预期输出:统计报告

练习 4(10 分钟):串起来

把项目 2(待办清单)和项目 3(日志分析)结合起来:当日志分析完成后,把分析结果自动添加到待办清单里。

  • 提示:需要调用 添加待办() 函数

练习 5(5 分钟):报错分析

小明运行这段代码:

node todo.js add

报错了:

请输入任务内容:node todo.js add "买牛奶"

这是 bug 还是正常行为?小明应该怎么输入才对?


作业:做一个命令行「体重记录工具」

需求描述
做一个能记录体重、查看历史、计算平均值的命令行工具。

功能点
1. 添加体重记录(带日期)
2. 查看所有记录
3. 计算平均体重
4. 删除指定记录

加分项
1. 支持按日期范围筛选
2. 能画个简单的 ASCII 图表显示趋势

验收标准
- node weight.js add 65.5 能添加一条记录
- node weight.js list 能显示所有记录
- node weight.js avg 能显示平均体重
- 数据保存到 weight.json 文件


📚 总结

本章核心 3 点

  1. REPL = 交互式试验场,适合快速验证想法
  2. 脚本模式 = node 文件名.js,跑完整的程序
  3. process.argv = 让程序接收命令行参数,做成「可配置工具」

延伸资源

互动钩子

你有没有遇到过「不知道该用 REPL 还是脚本模式」的情况?或者你在工作/学习中用过 Node.js 做什么有意思的小工具?评论区聊聊,老粉优先回复!


下章预告

学会了怎么跑代码,小明信心满满地写了一个 app.js,里面引用了另一个文件 utils.js,结果跑起来报错 Cannot find module...下一章我们来解决这个问题,一起搞懂 Node.js 的模块系统 CommonJS!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。