Skip to content

运行第一段程序:值、表达式与语句 ​

程序不是一页由上到下朗读的说明书,而是一组可以被执行的规则。先把运行环境和最小语法弄明白,后面讨论“结果为什么这样”才有共同的起点。

在哪里运行,运行的是什么 ​

把下面内容保存为 first.mjs,在文件所在目录运行 node first.mjs。本书核心示例以 Node.js 22 为运行基线;.mjs 表示模块文件,自动使用严格模式。暂时只需按这种方式运行,第七章再讲模块如何连接文件。

js
const price = 18;
let quantity = 2;
quantity = quantity + 1;
console.log(price * quantity); // 54

console.log(...) 是把括号内的值显示出来的工具。先认识它的用途即可,第二章会解释函数调用。18、2 是值,price * quantity 是产生值的表达式,const price = 18; 则是建立名字与值之间联系的声明。

浏览器开发者工具的 Console 也能计算这些表达式,但不要把控制台重复声明名字时的表现当作文件规则。控制台有自己的交互处理;刷新页面或换一份独立文件,才容易做可重复的实验。

JavaScript 语言提供数字、字符串、函数等机制;Node.js 提供文件系统等能力;浏览器提供页面、事件等能力。document 在普通 Node.js 程序中不存在,这不意味着 JavaScript 没装好,而是你向没有页面的环境索要了页面。

跟着执行,而不是跟着排版猜 ​

再看一段稍有变化的价格计算。先不运行,为每一句写出“读取了什么、产生了什么、保存到哪里”:

js
const price = 18;
let quantity = 2;
const subtotal = price * quantity;
quantity = 3;
console.log(subtotal, price * quantity); // 36 54
执行位置发生的动作执行后的关键值
声明 price计算字面量18,建立绑定price为18
声明 quantity计算字面量2,建立绑定quantity为2
声明 subtotal读取当时的18和2,执行乘法subtotal为36
重新赋值 quantity让quantity关联3subtotal仍是36
调用 console.log分别读取subtotal、重新计算乘法输出36和54

const subtotal = price * quantity 保存的是这次计算的结果,不是一个“以后随 quantity 自动更新”的公式。电子表格和某些响应式框架会提供自动重算能力,普通变量声明没有这项行为。以后需要重新计算时,可以再次写表达式,或在第二章把它包装成函数。

这张表也解释了为什么只写一行 price * quantity; 不会改变现有变量。表达式得到了结果,但既没保存也没输出,结果就没有被后续程序使用。控制台常把最后一个表达式结果额外显示出来,这是交互界面的方便功能,不是文件会自动打印每一个表达式。

声明、赋值和比较各做什么 ​

const 声明的绑定不能再次赋值;let 声明的绑定可以。初学时优先用 const,确实需要重新赋值才用 let。这里限制的是变量和某个值的联系,后面会看到它不自动冻结对象内部。

js
let remaining = 3;
remaining = remaining - 1;
console.log(remaining);       // 2
console.log(remaining === 2); // true

一个等号 = 把右侧结果赋给左侧;三个等号 === 判断是否严格相等。remaining === 2 不会修改 remaining。名字区分大小写,remaining 和 Remaining 是两个标识符。标识符可以包含字母、数字、下划线和 $ 等,但不能以数字开头,也不能直接使用 const 这样的保留字。

let pending; 合法,初始值是 undefined;普通的 const pending; 是语法错误,因为它需要初始化。旧代码里的 var 具有不同的作用域和初始化规则,第二章会完整讲解。

表达式能产生值,语句安排动作 ​

2 + 3、price * quantity 都是表达式,可以放到另一个表达式中。声明、条件和循环组织程序行为。花括号 { ... } 可以组成语句块;同一种标点在其他位置也可表示对象,判断时必须看上下文。

js
const hours = 2;
const minutes = hours * 60;
console.log('学习分钟数:', minutes); // 学习分钟数: 120

字符串用配对的单引号、双引号或反引号包围。前两种通常写单行;反引号可以包含换行,并通过 ${表达式} 插入结果。

js
const topic = '变量';
console.log(`今天学习${topic}`); // 今天学习变量
console.log('第一行\n第二行');

\n 是换行转义,\\ 表示一个反斜线,\' 可以在单引号字符串中表示单引号。注释 // 延续到本行结束,/* ... */ 可以跨行,块注释不能直接嵌套。

分号与错误信息 ​

本章示例显式写分号;后续章节也会出现省略写法,两种风格都须遵循语句边界。JavaScript 有自动分号插入规则,但它不是“每次换行就结束语句”。例如下一行以 ( 或 [ 开始时,可能被接到前一行上;第二章的 return 又存在相反的换行陷阱。先写清边界,再学省略规则,比让标点替你猜意图省心。

常见错误分三类:SyntaxError 表示代码结构无法解析;ReferenceError 常表示名字不存在或尚不能读取;TypeError 表示当前值不支持这项操作。错误信息中的文件、行号和调用位置比最后一句感叹号更有用。可以先复制最小失败片段,再只改一个因素观察结果。

下面是故意失败的独立实验,请分别运行:

js
// 取消其中一行注释,观察对应错误。
// const answer = ;        // SyntaxError
// console.log(noSuchId);  // ReferenceError
// const count = 1; count = 2; // TypeError

严格模式还会阻止未声明变量赋值等旧式写法。普通浏览器 <script> 默认不严格,可在开头写 'use strict';;<script type="module"> 和 .mjs 自动严格。不要混用两种模式的实验来证明同一个结论。

先区分解析失败和运行失败 ​

下面两段是独立的失败实验。第一段保存成单独文件运行:

js
// 取消末行注释再运行整个文件。
console.log('准备计算');
// const total = ; // SyntaxError,整份文件不能完成解析

取消注释后,连“准备计算”都不会输出。运行时必须先能理解这份源代码的结构,才能开始执行顶层语句。把错误写在文件后面,并不能让前半部分先蒙混过关。

再分别运行第二段:

js
// 取消末行注释以观察运行期失败。
console.log('准备计算');
// console.log(missingTotal); // ReferenceError,执行到这里才失败

这次先输出准备消息,再因为读取不存在的名字而中止。一个是解析前提没满足,一个是执行某一步时失败。排错时先看是否出现前面的日志,就能缩小调查范围;但不要依赖开发者工具对多次输入片段的处理来替代整文件实验。

分号的作用也发生在这条边界上。比如下面两行不是两项独立计算:

js
const total = 6
+ 2;
console.log(total); // 8

换行没有阻止 + 2 继续属于初始化表达式。语言允许省分号不等于换行具有“必定终止”的能力;写分号的价值是让读者看到你希望在哪里结束。

练习 ​

  1. 声明每页 12 行、当前 3 页,计算总行数;再把页数改成 4。哪些名字用 const,哪些用 let?

  2. 解释 let n = 3; n === 4; console.log(n); 的输出,再把第二句改成赋值比较。

  3. 把含 subtotal 的价格代码中的 quantity 改为4后,已经保存的 subtotal 会变吗?如果文件末尾存在语法错误,前面的日志是否必然先输出?

查看答案

用心记录每一步成长