Skip to content

DRAFT 《现代 JavaScript 教程》前端基础系列 04:语言进阶——空值合并、循环结构与函数全解

约 5124 字大约 17 分钟

2026-04-17

在本篇中,我们将深入 JavaScript 的更高级语法结构,掌握如何处理缺失值、控制代码的重复执行,以及如何封装可重用的代码块。
这三块内容是构建复杂逻辑的基石,能够显著提升代码的健壮性与可维护性。

【本篇核心收获】

  • 掌握空值合并运算符 ?? 的使用场景与原理。
  • 彻底理解 whiledo...whilefor 三大循环结构的区别与跳出技巧。
  • 掌握函数声明、函数表达式与箭头函数的三种形态,及参数、局部/外部变量的作用域边界。

空值合并运算符 ??

背景与认知:为什么需要它?

在实际开发中,我们经常需要为变量提供“默认值”。比如,如果用户没有设置昵称,就显示“匿名”。过去,我们常使用逻辑或运算符 || 来实现:

let nickName = null;
let name = nickName || "匿名";

但这存在一个隐患:|| 无法区分 false0、空字符串 ""null/undefined。它们都会被当作“假值”(falsy values)处理。
如果一个变量的值恰好是有效的 0 或者空字符串 "" 呢?

let height = 0; // 0 是一个有效的默认高度
alert(height || 100); // 错误地返回了 100,因为 0 被认为是 false

为了解决这种尴尬的局面,JavaScript 引入了 空值合并运算符??。它更精确,只在值确实是“缺失”(nullundefined)的情况下,才提供备用值。

落地:?? 的语法与应用

写法很简单,就是两个问号 ??

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

避坑提示与进阶

  1. || 的核心差异(牢记!)
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0 (正确地保留了 0)
- `||` 返回第一个 **真值(truthy)**。
- `??` 返回第一个 **已定义的值(defined value)**。
  1. 运算符优先级
    ?? 的优先级非常低(层级 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,这显然不是我们想要的。

  1. 禁止与 &&|| 直接混用
    为了防止逻辑混乱,JavaScript 语法规定,除非显式使用括号,否则 ?? 不能直接和 &&|| 连用。
let x = 1 && 2 ?? 3; // Syntax error: 语法错误

let y = (1 && 2) ?? 3; // 正常工作,加了括号

小结: ?? 是处理 null/undefined 时的完美利器,能精准赋予默认值,有效避免 0 或空字符串带来的“误伤”。


循环:whilefor

背景与认知:为什么需要循环?

开发中我们经常需要做重复的苦力活:遍历一个商品列表、打印 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) {
  // 循环体
}

执行顺序大起底:

  1. begin (初始化)let i = 0。仅在循环启动时执行一次
  2. condition (条件检查)i < 3。每次循环检查,若为 false 则终止整个循环。
  3. body (循环体)alert(i)。条件为真时执行的核心代码。
  4. step (步进)i++。每次循环体执行执行。
for (let i = 0; i < 3; i++) { 
  alert(i); // 依次弹出 0, 1, 2
}

灵活运用 for 循环:

  • for 语句的任何部分都可以省略。
  • 省略 beginlet i = 0; for (; i < 3; i++) {...}
  • 省略 steplet i = 0; for (; i < 3;) { alert(i++); } (等同于 while(i < 3))
  • 全省略变无限循环:for (;;) { ... } (两个分号 ; 必须保留)

进阶控制:breakcontinue 与标签

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!');

小结: 掌握了 whileforbreak/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 值是否相同?

  1. 前缀形式 ++i:
let i = 0;
while (++i < 5) alert( i );
  1. 后缀形式 i++
let i = 0;
while (i++ < 5) alert( i );

解决方案

  1. 前缀(从 1 到 4)++i 先增加再比较。1<5 (打印 1) -> 2<5 (打印 2) -> 3<5 (打印 3) -> 4<5 (打印 4) -> 5<5 为假,结束。
  2. 后缀(从 1 到 5)i++ 用旧值比较,但不管旧值新值,下方的 alert 总是拿到了增加后的新值。 0<5为真 (打印 1) ... 直到 4<5 为真(此时 i 增加到了 5,打印 5)。下一步 5<5 为假,结束。

任务 3:"for" 循环显示哪些值?

重要程度: 4
两次循环 alert 值是否相同?

  1. 后缀形式:
for (let i = 0; i < 5; i++) alert( i );
  1. 前缀形式:
for (let i = 0; i < 5; ++i) alert( i );

解决方案相同,都是 0 到 4。
for 循环语法中,步进运算 step 是独立执行的,它的返回值并没有被使用。所以在这里 i++++i 没有区别。

任务 4:使用 for 循环输出偶数

重要程度: 5
使用 for 循环输出从 210 的偶数。

解决方案

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
写一个可以输出 2n 之间所有素数(只能被 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)。

解决方案

  1. 问号运算符 ? 版:
function checkAge(age) {
  return (age > 18) ? true : confirm('父母允许吗?');
}
  1. 逻辑或 || 版(最简版):
    利用短路特性,左边如果为真直接返回 true;左边为假才执行右边。
function checkAge(age) {
  return (age > 18) || confirm('父母允许吗?');
}

任务 3:函数 min(a, b)

重要程度: 1
写一个返回数字 ab 中较小数字的函数 min(a,b)

解决方案

function min(a, b) {
  return a < b ? a : b;
}

任务 4:函数 pow(x,n)

重要程度: 4
写一个函数 pow(x,n),返回 xn 次方。必须提供页面交互。

解决方案

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 函数表达式

区别不仅仅是写法,底层逻辑更是大相径庭!

  1. 创建时机(核心差异!)
// 【函数声明】可以“先调用,后定义”!
hello(); // 正常工作!
function hello() { alert("Hi"); }
// 【函数表达式】必须先定义,后调用!
sayHi(); // 报错!Error: sayHi is not defined
let sayHi = function() { alert("Hi"); };
- **函数表达式**:代码逐行运行到 `=` 号右侧那一行时,函数才会被创建。
- **函数声明**:在脚本正式运行之前(初始化阶段),JavaScript 引擎会把所有的函数声明统统预先创建好(这叫作用域提升 Hoisting)。
  1. 作用域(块级作用域)
    在严格模式下,在 iffor 大括号里面用“函数声明”创建的函数,外面是访问不到的!如果你想在外部访问,必须在外面先用 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"" 等有效假值的天使,专注筛除 nullundefined
  • 循环不仅是体力的重复,更是艺术。for 循环全能,while 循环纯粹,break/continue 是控制方向盘。
  • 函数的本质是可被存储与传递的行为值。函数声明让你自由调用,函数表达式让你在表达式中随手捏造,箭头函数让你追求极致的代码极简美学。掌握这些,你的 JS 代码就能做到千变万化,指哪打哪。
贡献者: weew12