DRAFT 《现代 JavaScript 教程》前端基础系列 04:语言进阶——空值合并、循环结构与函数全解
约 5124 字大约 17 分钟
2026-04-17
在本篇中,我们将深入 JavaScript 的更高级语法结构,掌握如何处理缺失值、控制代码的重复执行,以及如何封装可重用的代码块。
这三块内容是构建复杂逻辑的基石,能够显著提升代码的健壮性与可维护性。
【本篇核心收获】
- 掌握空值合并运算符
??的使用场景与原理。 - 彻底理解
while、do...while和for三大循环结构的区别与跳出技巧。 - 掌握函数声明、函数表达式与箭头函数的三种形态,及参数、局部/外部变量的作用域边界。
空值合并运算符 ??
背景与认知:为什么需要它?
在实际开发中,我们经常需要为变量提供“默认值”。比如,如果用户没有设置昵称,就显示“匿名”。过去,我们常使用逻辑或运算符 || 来实现:
let nickName = null;
let name = nickName || "匿名";但这存在一个隐患:|| 无法区分 false、0、空字符串 "" 和 null/undefined。它们都会被当作“假值”(falsy values)处理。
如果一个变量的值恰好是有效的 0 或者空字符串 "" 呢?
let height = 0; // 0 是一个有效的默认高度
alert(height || 100); // 错误地返回了 100,因为 0 被认为是 false为了解决这种尴尬的局面,JavaScript 引入了 空值合并运算符??。它更精确,只在值确实是“缺失”(null 或 undefined)的情况下,才提供备用值。
落地:?? 的语法与应用
写法很简单,就是两个问号 ??。
a ?? b 的运算逻辑如下:
- 如果
a已定义(不是null也不是undefined),则返回a。 - 如果
a未定义(是null或是undefined),则返回b。
用传统代码翻译一下就是:
result = (a !== null && a !== undefined) ? a : b;常见场景一:提供默认值
let user; // 声明了但未赋值,默认是 undefined
alert(user ?? "匿名"); // 弹出: 匿名 (user 未定义)
let user2 = "John";
alert(user2 ?? "匿名"); // 弹出: John (user 已定义)常见场景二:从列表中选择第一个有效值
可以用连续的 ?? 从一串可能为空的变量中,精准定位到第一个真正有值的变量。
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 从前向后找,直到找到非 null/undefined 的值
alert(firstName ?? lastName ?? nickName ?? "匿名"); // 弹出: Supercoder避坑提示与进阶
- 与
||的核心差异(牢记!)
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0 (正确地保留了 0)- `||` 返回第一个 **真值(truthy)**。
- `??` 返回第一个 **已定义的值(defined value)**。
- 运算符优先级
??的优先级非常低(层级 3),仅略高于?和=。在复杂计算中,务必加括号。
let height = null;
let width = null;
// 必须使用括号!
let area = (height ?? 100) * (width ?? 50); // 100 * 50 = 5000如果不加括号:let area = height ?? 100 * width ?? 50;,由于 * 优先级高,会被解析成 height ?? (100 * width) ?? 50,这显然不是我们想要的。
- 禁止与
&&或||直接混用
为了防止逻辑混乱,JavaScript 语法规定,除非显式使用括号,否则??不能直接和&&或||连用。
let x = 1 && 2 ?? 3; // Syntax error: 语法错误
let y = (1 && 2) ?? 3; // 正常工作,加了括号小结:
??是处理null/undefined时的完美利器,能精准赋予默认值,有效避免0或空字符串带来的“误伤”。
循环:while 和 for
背景与认知:为什么需要循环?
开发中我们经常需要做重复的苦力活:遍历一个商品列表、打印 1 到 100 的数字。循环(Loop) 结构就是为了让机器自动重复执行某段代码而生的。
落地 1:while 循环
while 是最纯粹的条件循环。只要条件为真,就一直做。
while (condition) {
// 循环体(body)
}单次循环体的执行被称为 一次迭代(iteration)。
let i = 0;
while (i < 3) { // 当 i < 3 为 true 时,重复执行
alert( i ); // 依次弹出 0、1、2
i++; // 别忘了更新变量,否则就是死循环!
}提示:
while括号里的条件表达式会被自动转换为布尔值。比如while(i != 0)可以简写为while(i)。如果循环体只有一行,可以省略大括号{}:while (i) alert(i--);。
落地 2:do...while 循环
这是 while 的变体,区别在于:先做一次再说,再判断要不要继续。
do {
// 循环体
} while (condition);let i = 0;
do {
alert( i );
i++;
} while (i < 3); // 依次弹出 0、1、2这种形式较少使用,仅当你希望无论条件如何,循环体至少无条件执行一次时才用它。
落地 3:for 循环
for 循环是最全能、最常用的循环结构。它将初始化、条件检查和步进更新完美整合在了一行。
for (begin; condition; step) {
// 循环体
}执行顺序大起底:
- begin (初始化):
let i = 0。仅在循环启动时执行一次。 - condition (条件检查):
i < 3。每次循环前检查,若为false则终止整个循环。 - body (循环体):
alert(i)。条件为真时执行的核心代码。 - step (步进):
i++。每次循环体执行后执行。
for (let i = 0; i < 3; i++) {
alert(i); // 依次弹出 0, 1, 2
}灵活运用 for 循环:
for语句的任何部分都可以省略。- 省略
begin:let i = 0; for (; i < 3; i++) {...} - 省略
step:let i = 0; for (; i < 3;) { alert(i++); }(等同于while(i < 3)) - 全省略变无限循环:
for (;;) { ... }(两个分号;必须保留)
进阶控制:break、continue 与标签
1. 强制跳出:break
当我们需要在满足特定条件时立刻终止整个循环,就可以使用 break。
let sum = 0;
while (true) { // 表面上的死循环
let value = +prompt("请输入一个数字", '');
if (!value) break; // 如果用户取消输入或输入空值,立刻终止循环!
sum += value;
}
alert( '总和: ' + sum );2. 跳过本次:continue
continue 是“轻量级”的 break。它不会终止循环,而是立刻结束当前这一次迭代,强行进入下一次迭代。
for (let i = 0; i < 10; i++) {
if (i % 2 == 0) continue; // 遇到偶数,跳过本次循环后续代码
alert(i); // 只有奇数 1, 3, 5, 7, 9 会被打印
}避坑: 不能将
break/continue用在三元运算符?的右侧!(i > 5) ? alert(i) : continue;这是会报错的。这也提醒我们,不要乱用?替代if。
3. 破局嵌套循环:标签 (Labels)
如果我们有两层嵌套循环,内部循环满足条件时,想直接连带外部循环一起跳出,单纯的 break 做不到(只能跳出一层)。这时需要标签。
outerLabel: // 这是一个标签
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let input = prompt(`坐标 (${i},${j}) 的值`, '');
// 如果用户取消输入,直接跳出 outerLabel 指向的这层最外围循环
if (!input) break outerLabel;
}
}
alert('Done!');小结: 掌握了
while、for和break/continue,你就可以随心所欲地控制代码重复执行的节奏与边界了。
【任务(练习题)】
任务 1:最后一次循环的值
重要程度: 3
此代码最后一次 alert 值是多少?为什么?
let i = 3;
while (i) {
alert( i-- );
}解决方案:答案是 1。
每次循环都会打印 i 的旧值,然后将 i 减 1。
- 打印 3,变成 2。
- 打印 2,变成 1。
- 打印 1,变成 0。
当再次检查while(i)时,while(0)是 falsy,循环停止。
任务 2:while 循环显示哪些值?
重要程度: 4
以下两个循环的 alert 值是否相同?
- 前缀形式
++i:
let i = 0;
while (++i < 5) alert( i );- 后缀形式
i++
let i = 0;
while (i++ < 5) alert( i );解决方案:
- 前缀(从 1 到 4):
++i先增加再比较。1<5(打印 1) ->2<5(打印 2) ->3<5(打印 3) ->4<5(打印 4) ->5<5为假,结束。 - 后缀(从 1 到 5):
i++用旧值比较,但不管旧值新值,下方的alert总是拿到了增加后的新值。0<5为真 (打印 1) ... 直到4<5为真(此时i增加到了 5,打印 5)。下一步5<5为假,结束。
任务 3:"for" 循环显示哪些值?
重要程度: 4
两次循环 alert 值是否相同?
- 后缀形式:
for (let i = 0; i < 5; i++) alert( i );- 前缀形式:
for (let i = 0; i < 5; ++i) alert( i );解决方案:相同,都是 0 到 4。
在 for 循环语法中,步进运算 step 是独立执行的,它的返回值并没有被使用。所以在这里 i++ 和 ++i 没有区别。
任务 4:使用 for 循环输出偶数
重要程度: 5
使用 for 循环输出从 2 到 10 的偶数。
解决方案:
for (let i = 2; i <= 10; i++) {
if (i % 2 == 0) {
alert( i );
}
}任务 5:用 "while" 替换 "for"
重要程度: 5
将 for 循环更改为 while(输出保持不变)。
for (let i = 0; i < 3; i++) {
alert( `number ${i}!` );
}解决方案:
let i = 0;
while (i < 3) {
alert( `number ${i}!` );
i++;
}任务 6:重复输入,直到正确为止
重要程度: 5
编写一个提示用户输入大于 100 的数字的循环。如果用户输入其他数值 —— 请他重新输入。直到他输入正确、取消输入或输入空行。
解决方案:
let num;
do {
num = prompt("Enter a number greater than 100?", 0);
} while (num <= 100 && num);解析:这里需要 && num,因为如果用户点击取消,num 变成 null,而 null <= 100 会返回 true 导致死循环。
任务 7:输出素数(prime)
重要程度: 3
写一个可以输出 2 到 n 之间所有素数(只能被 1 和自身整除的数)的代码。当 n = 10 时,输出 2、3、5、7。
解决方案:
利用前面学过的标签来配合双层循环。
let n = 10;
nextPrime: // 定义外层循环的标签
for (let i = 2; i <= n; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime; // 只要能被整除,说明不是素数,直接跳过当前 i,继续检查下一个 i
}
alert( i ); // 经过内层循环都没跳出,说明是素数,打印!
}函数 (Functions):基础与声明
背景与认知:为什么需要函数?
当你需要在多处代码中显示同一个欢迎弹窗时,你是复制粘贴代码吗?如果某天文案改了,你需要改遍所有地方。
函数(Function) 就是用来解决代码复用的。它就像一个“黑盒机器”,你可以把一段常用的代码包装进去,贴个标签(命名),需要的时候喊它名字(调用)就能执行。
落地 1:函数声明 (Function Declaration)
这是最经典的定义函数的方式。
function name(parameter1, parameter2) {
// 核心逻辑:函数体 (body)
}// 声明一个函数
function showMessage() {
alert( 'Hello everyone!' );
}
// 通过名字调用它
showMessage();
showMessage(); // 重复调用,拒绝复制粘贴!落地 2:局部变量与外部变量(作用域初探)
函数内部是个相对私密的空间。
- 局部变量:在函数体内声明的变量,只在函数内部可见。外部休想访问。
- 外部变量:声明在函数外面的变量。函数内部可以访问并且修改它!
let userName = 'John'; // 全局(外部)变量
function showMessage() {
userName = "Bob"; // 1. 函数内部可以大胆修改外部变量
let message = 'Hello, ' + userName; // 2. message 是局部变量
alert(message);
}
showMessage(); // 弹出: Hello, Bob
alert(userName); // Bob (外部变量真的被改了!)
// alert(message); // 报错!message 在外部不可见同名遮蔽原则: 如果函数内部声明了一个和外部同名的局部变量(比如
let userName = "Bob"),那么在函数内部,局部变量会把外部变量“屏蔽”掉。外部变量毫发无损。
最佳实践: 尽量少用全局变量。优秀的函数应该是“自给自足”的,主要依赖传入的参数和局部变量干活。
落地 3:参数 (Parameters & Arguments)
函数可以通过参数接收外部传来的数据。
- 参数声明(parameters):定义函数时圆括号里的占位符变量。
- 参数传递(arguments):调用函数时实际扔进去的真实值。传进去的值会被复制给参数声明变量。
function showMessage(from, text) { // parameters: from 和 text
from = '*' + from + '*'; // 修改局部副本,不影响外部原变量
alert(from + ': ' + text);
}
let user = "Ann";
showMessage(user, "Hello"); // arguments: user 变量的值 和 "Hello"
alert(user); // "Ann",原变量未被修改落地 4:参数默认值 (Default Values)
如果调用函数时少传了参数,缺少的那个参数会变成 undefined。
通过 = 号,我们可以优雅地赋予默认值。
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}
showMessage("Ann"); // 弹出: Ann: no text given默认值甚至可以是另一个函数的返回值:
function showMessage(text = anotherFunction()),并且该函数只在没传参数时才会被临时计算执行。
落地 5:返回值 (Return Value)
函数干完活,可以把结果“交差”返回给调用者,使用 return 指令。
遇到 return,函数立刻停止执行,抛出结果。
function sum(a, b) {
return a + b; // 返回计算结果
}
let result = sum(1, 2); // result 拿到了 3- 可以有多个
return(常结合if分支使用)。 - 可以只写
return;用来强行提前结束函数执行。 - 注意: 函数如果没有写
return,或者只是空空的return;,调用它的结果都是undefined。 - 避坑: 绝对不要在
return关键字和后面的表达式之间换行!JavaScript 会自动在return后加分号,导致实际上变成了空返回。
进阶认知:函数命名规范
函数代表的是一个动作(行为),名字通常是动词。
好名字等于好注释。团队常约定前缀:
show...负责显示get...负责获取并返回calc...负责复杂计算check...负责校验返回布尔值
核心军规:一个函数只做一件事。
checkPermission就只做检查返回true/false,绝对不要越权去弹出一个拒绝访问的提示框!
【任务(练习题)】
任务 1:是否需要 “else”?
重要程度: 4
如果 else 被删除,函数的工作方式会不同吗?
// 变体 1
function checkAge(age) {
if (age > 18) {
return true;
} else {
return confirm('父母允许吗?');
}
}
// 变体 2
function checkAge(age) {
if (age > 18) {
return true;
}
return confirm('父母允许吗?');
}解决方案:
没有区别。因为当 age > 18 满足时,return true 执行后整个函数就结束了。下方的代码只有在 if 为假时才有机会执行,跟加不加 else 效果完全一样。
任务 2:使用 '?' 或者 '||' 重写函数
重要程度: 4
使用 ? 和 || 两种方式,把上题的 checkAge 缩减成一行代码(不使用 if)。
解决方案:
- 问号运算符
?版:
function checkAge(age) {
return (age > 18) ? true : confirm('父母允许吗?');
}- 逻辑或
||版(最简版):
利用短路特性,左边如果为真直接返回true;左边为假才执行右边。
function checkAge(age) {
return (age > 18) || confirm('父母允许吗?');
}任务 3:函数 min(a, b)
重要程度: 1
写一个返回数字 a 和 b 中较小数字的函数 min(a,b)。
解决方案:
function min(a, b) {
return a < b ? a : b;
}任务 4:函数 pow(x,n)
重要程度: 4
写一个函数 pow(x,n),返回 x 的 n 次方。必须提供页面交互。
解决方案:
function pow(x, n) {
let result = x;
// 乘 n-1 次
for (let i = 1; i < n; i++) {
result *= x;
}
return result;
}
let x = prompt("x?", '');
let n = prompt("n?", '');
if (n < 1) {
alert(`不支持负数和小数,请提供自然数`);
} else {
alert( pow(x, n) );
}进阶语法:函数表达式与箭头函数
背景与认知:函数居然也是个“值”?
在很多古老的语言中,函数就是个死板的代码结构。但在 JavaScript 里,函数是一种特殊的值(Value)。
既然是值,就可以像字符串、数字一样,被装进变量里、被当成参数传来传去!这就催生了更灵活的写法。
落地 1:函数表达式 (Function Expression)
除了经典的“函数声明”,我们还可以在等号右侧,直接“凭空捏造”一个匿名函数,然后把它塞进变量里。这就叫函数表达式。
// 注意,function 后面没有名字!结尾有分号 ;
let sayHi = function() {
alert( "Hello" );
};
sayHi(); // 通过变量名来调用证明它是个值的最强证据:
function sayHi() {
alert( "Hello" );
}
let func = sayHi; // 我们把函数本身复制给了 func 变量!(注意没带括号)
func(); // 现在 func 也能弹窗了!落地 2:回调函数 (Callback Functions)
既然函数可以赋值给变量,那自然也能作为参数传给另一个函数!这种作为参数传递,并准备在未来某个时间被调用的函数,叫做回调函数(Callback)。
// 这个大函数负责提问,并根据用户选择执行后续的不同逻辑
function ask(question, yesAction, noAction) {
if (confirm(question)) yesAction() // 调用传进来的 yes 函数
else noAction(); // 调用传进来的 no 函数
}
// 我们直接把两个“没有名字的函数表达式”当做参数传了进去!
ask(
"Do you agree?",
function() { alert("You agreed."); },
function() { alert("You canceled."); }
);回调思想是 JavaScript 异步编程的灵魂!
核心对比:函数声明 VS 函数表达式
区别不仅仅是写法,底层逻辑更是大相径庭!
- 创建时机(核心差异!)
// 【函数声明】可以“先调用,后定义”!
hello(); // 正常工作!
function hello() { alert("Hi"); }// 【函数表达式】必须先定义,后调用!
sayHi(); // 报错!Error: sayHi is not defined
let sayHi = function() { alert("Hi"); };- **函数表达式**:代码逐行运行到 `=` 号右侧那一行时,函数才会被创建。
- **函数声明**:在脚本正式运行之前(初始化阶段),JavaScript 引擎会把所有的函数声明统统预先创建好(这叫作用域提升 Hoisting)。
- 作用域(块级作用域)
在严格模式下,在if或for大括号里面用“函数声明”创建的函数,外面是访问不到的!如果你想在外部访问,必须在外面先用let定义一个变量,在里面用“函数表达式”赋值。
最佳实践: 优先使用“函数声明”(因为可读性强,且随意调用位置)。当“函数声明”满足不了需求(比如需要根据条件动态生成函数并暴露给外部)时,再上“函数表达式”。
落地 3:箭头函数 (Arrow Functions)
为了让函数表达式的写法爽到极致,ES6 搞出了箭头函数。对于简短回调,它是杀手锏。
基础语法:省去 function 关键字,用肥箭头=>
let func = (arg1, arg2) => expression;实战演示:一秒看懂
// 以前的写法:
let sumOld = function(a, b) { return a + b; };
// 箭头函数写法:
let sumNew = (a, b) => a + b;=> 右边的表达式会自动计算并直接 return 返回。
更极致的简写:
- 只有 1 个参数时:可以省略圆括号!
let double = n => n * 2; - 没有参数时:圆括号不能省,留空即可。
let sayHi = () => alert("Hello!");
多行箭头函数:
如果逻辑复杂,一行写不下,加上大括号 {}。注意:一旦加了 {},你必须自己手动写 return 关键字!
let sum = (a, b) => { // 加了花括号
let result = a + b;
return result; // 必须明确 return!
};【任务(练习题)】
任务 1:用箭头函数重写回调
用箭头函数重写下面的代码:
function ask(question, yes, no) {
if (confirm(question)) yes();
else no();
}
ask(
"Do you agree?",
function() { alert("You agreed."); },
function() { alert("You canceled the execution."); }
);解决方案:
ask(
"Do you agree?",
() => alert("You agreed."),
() => alert("You canceled the execution.")
);代码瞬间清爽了无数倍。
【本篇核心逻辑复盘】
??是拯救0和""等有效假值的天使,专注筛除null和undefined。- 循环不仅是体力的重复,更是艺术。
for循环全能,while循环纯粹,break/continue是控制方向盘。 - 函数的本质是可被存储与传递的行为值。函数声明让你自由调用,函数表达式让你在表达式中随手捏造,箭头函数让你追求极致的代码极简美学。掌握这些,你的 JS 代码就能做到千变万化,指哪打哪。
