第1章 1.5 模板字符串与运算符

「上一章我们搞懂了数据的类型和它们之间怎么互相转换,现在你脑子里装着一堆不同类型的"食材"——字符串、数字、布尔值。但光有食材没用,你得学会怎么把它们"炒成一盘菜",怎么用合适的工具去处理它们。这一章我们就来解决这个问题——学会用模板字符串把各种材料拼成一句话,用各种运算符对它们进行"加工"。学完这章,你就能写出真正有意思的代码了。」


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

你有没有遇到过这种场景?

// 你想给用户发一条问候语
let name = "小明";
let age = 18;
let score = 95;

// 老方法:用 + 号拼接,想想就觉得烦
let message = "大家好,我叫" + name + ",今年" + age + "岁,这次考试考了" + score + "分!";

数数上面有几个 + 和引号?光是配对就够让人头大了。

学完这章你能:
- 用一种「像写句子一样自然」的方式拼接字符串
- 快速\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n从对象里取出想要的数据(解构赋值)
- 安全地访问可能不存在的属性(可选链)
- 用各种运算符做数学计算、逻辑判断


🧱 基础 25 分钟:核心概念

5.1 模板字符串——字符串拼接的革命

是什么?
模板字符串就是用反引号 ` 包裹的字符串,里面可以放变量和表达式。

为什么要用?
告别 + 号拼接的噩梦,可读性提升 100%。

怎么用?

let name = "小明";
let age = 18;

// 用 ${} 插入变量,叫「插值」
let greeting = `大家好,我叫${name},今年${age}岁!`;
console.log(greeting);
// 输出:大家好,我叫小明,今年18岁!

看!没有加号,没有引号混乱,一眼看懂。


5.2 在模板字符串里写表达式

${} 里不只能放变量,还能放表达式(计算、函数调用、三元运算符都行)。

let price = 99;
let count = 3;

// 表达式直接算
let total = `总价:${price * count}元`;
console.log(total);
// 输出:总价:297元

// 调用函数也行
let name = "小红";
let sayHi = `${name.toUpperCase()}说:你好!`;
console.log(sayHi);
// 输出:小红说:你好!

// 三元运算符(条件表达式)
let hour = 14;
let timeTip = `现在是${hour < 12 ? "上午" : "下午"}`;
console.log(timeTip);
// 输出:现在是下午

5.3 标签模板——模板字符串的高级玩法

是什么?
标签模板就是函数名写在模板字符串前面,像贴了个「标签」一样。

为什么要用?
可以自定义字符串的加工逻辑,比如过滤 HTML 标签、国际化处理。

怎么用?

// 定义一个「安全标签」函数
function safeTag(strings, ...values) {
// strings 是字符串部分数组,values 是插入的值数组
let result = "";
for (let i = 0; i < values.length; i++) {
    // 把值里的 < 和 > 转成 HTML 实体,防止 XSS
    let safeValue = String(values[i]).replace(/</g, "&lt;").replace(/>/g, "&gt;");
    result += strings[i] + safeValue;
}
result += strings[strings.length - 1];
return result;
}

let userInput = "<script>alert('黑客攻击')</script>";
let name = "用户";
let output = safeTag`你输入的内容是:${userInput}`;
console.log(output);
// 输出:你输入的内容是:&lt;script&gt;alert('黑客攻击')&lt;/script&gt;

简单理解:普通模板字符串是「照本宣科」,标签模板是「有个翻译员先过一遍」


5.4 解构赋值——从对象数组里「捡现成的」

是什么?
把对象或数组里的值「拆出来」赋给变量,一行搞定。

为什么要用?
以前要一个个取:let name = person.name; let age = person.age;
现在一行:let { name, age } = person;

怎么用?

// 从对象解构
let person = { name: "小明", age: 18, city: "北京" };

// 传统方式写 3 行
let name1 = person.name;
let age1 = person.age;
let city1 = person.city;

// 解构赋值 1 行搞定
let { name, age, city } = person;
console.log(name, age, city);
// 输出:小明 18 北京

带默认值 + 重命名:

let book = { title: "活着", author: "余华" };

// 解构时给默认值,并重命名变量
let { title: bookTitle, author: writer = "未知作者", pages = 0 } = book;
console.log(bookTitle, writer, pages);
// 输出:活着 余华 0

从数组解构:

let colors = ["红", "绿", "蓝"];

// 按位置对应,左边数组结构,右边值自动填入
let [first, second, third] = colors;
console.log(first, second, third);
// 输出:红 绿 蓝

// 跳过第二个
let [f, , t] = colors;
console.log(f, t);
// 输出:红 蓝

// 剩余值收在一起
let [f2, ...rest] = colors;
console.log(f2, rest);
// 输出:红 [ '绿', '蓝' ]

5.5 扩展运算符——把东西「拆开」或「合并」

是什么?
三个点 ... 就是扩展运算符,可以把数组或对象「拆开」变成单独元素,也可以把多个东西「合并」成一个。

为什么要用?
合并数组、拷贝对象、函数传参,以前要写循环或写一堆代码,现在一个符号搞定。

怎么用?

// 合并数组
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let merged = [...arr1, ...arr2];
console.log(merged);
// 输出:[1, 2, 3, 4, 5, 6]

// 拷贝数组(不是引用,是真拷贝)
let original = [1, 2, 3];
let copied = [...original];
copied.push(4);
console.log(original);  // [1, 2, 3] 不变
console.log(copied);    // [1, 2, 3, 4]

// 合并对象
let base = { name: "小明", age: 18 };
let extra = { city: "北京", hobby: "篮球" };
let person = { ...base, ...extra };
console.log(person);
// 输出:{ name: '小明', age: 18, city: '北京', hobby: '篮球' }

// 函数传参用扩展运算符「展开」
let numbers = [3, 1, 4, 1, 5];
console.log(Math.max(...numbers));
// 输出:5

5.6 可选链——访问属性再也不怕空指针

是什么?
?. 替代 . 来访问属性,如果中间有 nullundefined,直接返回 undefined 而不是报错。

为什么要用?
以前要写一堆 if (obj && obj.user && obj.user.profile) 来防止报错。
现在直接 obj?.user?.profile,简洁又安全。

怎么用?

let user = { name: "小明", profile: { height: 175 } };

// 安全访问嵌套属性
console.log(user?.profile?.height);
// 输出:175

// 中间有空,直接返回 undefined 不报错
let emptyUser = {};
console.log(emptyUser?.profile?.height);
// 输出:undefined

// 数组索引也可以用
let arr = [1, 2, 3];
console.log(arr?.[5]);
// 输出:undefined

// 方法调用也可以
let noMethod = null;
noMethod?.sayHello?.();
// 输出:undefined(不报错)

对比一下有多爽:

// 以前要这样写
let height1 = user && user.profile && user.profile.height;

// 现在这样
let height2 = user?.profile?.height;

5.7 运算符汇总

类型 运算符 说明
算术 + - * / % ** 加减乘除取余幂
自增 ++ -- +=1 或 -=1
赋值 = += -= *= /= %= **= 先运算再赋值
比较 == != === !== > < >= <= ==会转类型,===不转
逻辑 && \|\| ! 与或非
三元 条件 ? 值1 : 值2 条件成立取1,否则取2
空值 ?? 左侧是 null/undefined 用右侧
// 三元运算符
let score = 85;
let result = score >= 60 ? "及格" : "不及格";
console.log(result);
// 输出:及格

// 空值合并运算符
let a = null;
let b = a ?? "默认值";
console.log(b);
// 输出:默认值

let c = 0;
let d = c ?? "默认值";
console.log(d);
// 输出:0(因为 0 不是 null/undefined)

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

项目 1:个人信息生成器(5 分钟)

目标: 用模板字符串快速生成格式化自我介绍

// 个人信息生成器
let name = "李明";
let age = 28;
let job = "前端工程师";
let skills = ["JavaScript", "Vue", "React"];
let salary = 25000;

// 用模板字符串拼接
let intro = `
====================
个人信息卡
====================
姓名:${name}
年龄:${age}
职业:${job}
技能:${skills.join("、")}
月薪:${salary}元
税后:${(salary * 0.8).toFixed(2)}元
====================
`;

console.log(intro);

预期输出:

====================
个人信息卡
====================
姓名:李明
年龄:28
职业:前端工程师
技能:JavaScript、Vue、React
月薪:25000元
税后:20000.00元
====================

一句话解释:模板字符串让多行文本拼接变得像写普通句子一样自然。


项目 2:学生成绩处理工具(15 分钟)

目标: 从 JSON 数据中解构出需要的信息,计算统计指标

// 模拟从接口获取的学生成绩数据
let students = [
{ name: "小明", chinese: 92, math: 88, english: 95 },
{ name: "小红", chinese: 78, math: 95, english: 82 },
{ name: "小强", chinese: 85, math: 90, english: 91 },
{ name: "小丽", chinese: 96, math: 87, english: 89 }
];

// 用解构 + 箭头函数处理每个学生
let reports = students.map(({ name, chinese, math, english }) => {
let total = chinese + math + english;
let avg = (total / 3).toFixed(1);
let grade = avg >= 90 ? "优秀" : avg >= 80 ? "良好" : avg >= 60 ? "及格" : "不及格";

return {
    name,
    total,
    average: avg,
    grade,
    // 解构时重命名
    rank: 0  // 待会计算
};
});

// 计算排名
reports.sort((a, b) => b.total - a.total);
reports.forEach((student, index) => {
student.rank = index + 1;
});

// 输出成绩单
console.log("📊 成绩排行榜");
console.log("=".repeat(40));

reports.forEach(({ name, total, average, grade, rank }) => {
let medal = rank === 1 ? "🥇" : rank === 2 ? "🥈" : rank === 3 ? "🥉" : "  ";
console.log(`${medal}第${rank}名 ${name} | 总分${total} | 均分${average} | ${grade}`);
});

console.log("=".repeat(40));

// 统计全班情况
let { longestName, ...stats } = reports.reduce((acc, student) => {
acc.totalStudents++;
acc.totalScore += student.total;
if (student.name.length > acc.longestName.length) {
    acc.longestName = student.name;
}
return acc;
}, { totalStudents: 0, totalScore: 0, longestName: "" });

console.log(`全班平均分:${(stats.totalScore / stats.totalStudents).toFixed(1)}`);
console.log(`名字最长:${longestName}`);

预期输出:

📊 成绩排行榜
========================================
🥇第1名 小红 | 总分255 | 均分85.0 | 良好
🥈第2名 小强 | 总分266 | 均分88.7 | 良好
🥉第3名 小丽 | 总分272 | 均分90.7 | 优秀
第4名 小明 | 总分275 | 均分91.7 | 优秀
========================================
全班平均分:89.2
名字最长:小明

一句话解释:解构赋值让我们能优雅地「拆包」对象,扩展运算符让 reduce 计算统计更方便。


项目 3:命令行待办清单工具(15 分钟)

目标: 组合模板字符串、解构赋值、扩展运算符做个命令行小工具

// 待办清单工具 v1.0

// 模拟数据存储(用 localStorage 时这里换成 JSON.parse)
let todoList = [
{ id: 1, text: "完成 JavaScript 作业", priority: "high", done: false },
{ id: 2, text: "买菜做饭", priority: "medium", done: true },
{ id: 3, text: "给妈妈打电话", priority: "high", done: false },
{ id: 4, text: "看一集动画片", priority: "low", done: false }
];

// 格式化显示单个待办
function formatTodo({ id, text, priority, done }) {
let checkbox = done ? "✅" : "☐";
let tag = priority === "high" ? "🔴" : priority === "medium" ? "🟡" : "🟢";
let status = done ? "(已完成)" : "(进行中)";
return `${checkbox} [${id}] ${text} ${tag} ${status}`;
}

// 显示所有待办
function showAll() {
console.log("\n📋 所有待办事项:");
console.log("-".repeat(40));
todoList.forEach(todo => console.log(formatTodo(todo)));
console.log("-".repeat(40));
console.log(`共 ${todoList.length} 项\n`);
}

// 按优先级筛选显示
function showByPriority(priority) {
let filtered = todoList.filter(t => t.priority === priority);
console.log(`\n🔍 筛选「${priority}」优先级的待办:`);
filtered.forEach(todo => console.log(formatTodo(todo)));
console.log("");
}

// 添加新待办(用扩展运算符合并)
function addTodo(text, priority = "medium") {
let newId = Math.max(...todoList.map(t => t.id)) + 1;
let newTodo = { id: newId, text, priority, done: false };
todoList = [...todoList, newTodo];
console.log(`✅ 添加成功:${text}`);
}

// 删除待办(排除指定 id)
function deleteTodo(id) {
let before = todoList.length;
todoList = todoList.filter(t => t.id !== id);
if (todoList.length < before) {
    console.log(`🗑️ 删除成功:ID=${id}`);
} else {
    console.log(`❌ 未找到 ID=${id} 的待办`);
}
}

// 切换完成状态
function toggleDone(id) {
let todo = todoList.find(t => t.id === id);
if (todo) {
    todo.done = !todo.done;
    console.log(`${todo.done ? "✅ 标记完成" : "⬜ 标记未完成"}:${todo.text}`);
} else {
    console.log(`❌ 未找到 ID=${id}`);
}
}

// 生成统计报告
function showStats() {
let done = todoList.filter(t => t.done).length;
let total = todoList.length;
let percent = ((done / total) * 100).toFixed(0);

let byPriority = { high: 0, medium: 0, low: 0 };
todoList.forEach(t => byPriority[t.priority]++);

console.log(`
📈 待办统计报告
==================
总任务:${total}
已完成:${done} (${percent}%)
未完成:${total - done}
------------------
优先级分布:
🔴 高优先级:${byPriority.high}项
🟡 中优先级:${byPriority.medium}项
🟢 低优先级:${byPriority.low}项
==================`);
}

// 模拟操作
console.log("=== 待办清单工具 v1.0 ===\n");

addTodo("学习模板字符串", "high");
addTodo("练习解构赋值", "medium");

showAll();

toggleDone(1);
toggleDone(5);

deleteTodo(2);

showStats();

showByPriority("high");

预期输出:

=== 待办清单工具 v1.0 ===

✅ 添加成功:学习模板字符串
✅ 添加成功:练习解构赋值

📋 所有待办事项:
----------------------------------------
☐ [1] 完成 JavaScript 作业 🔴 (进行中)
☐ [2] 买菜做饭 🟡 (已完成)
☐ [3] 给妈妈打电话 🔴 (进行中)
☐ [4] 看一集动画片 🟢 (进行中)
☐ [5] 学习模板字符串 🔴 (进行中)
☐ [6] 练习解构赋值 🟡 (进行中)
----------------------------------------
共 6 项

✅ 标记完成:完成 JavaScript 作业
⬜ 标记未完成:学习模板字符串
🗑️ 删除成功:ID=2

📈 待办统计报告
==================
总任务:5
已完成:2 (40%)
未完成:3
------------------
优先级分布:
🔴 高优先级:2项
🟡 中优先级:1项
🟢 低优先级:1项
==================

🔍 筛选「high」优先级的待办:
☐ [1] 完成 JavaScript 作业 🔴 (进行中)
☐ [3] 给妈妈打电话 🔴 (进行中)
☐ [5] 学习模板字符串 🔴 (进行中)

一句话解释:模板字符串让我们能「像写句子一样」生成格式化的待办输出,扩展运算符让数组操作(添加、过滤)变得简洁。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:模板字符串里别乱用引号

// ❌ 错误:模板字符串里再用反引号,要转义
let wrong = `She said: "Hello, `world`"`;

// ✅ 正确:嵌套普通引号即可
let right = `She said: "Hello, world"`;

坑 2:解构数组要小心顺序

let arr = [1, 2, 3];

// ❌ 错误:变量名随便写,但位置要对应
let [c, b, a] = arr;
console.log(c, b, a);  // 1, 2, 3 不是 3, 2, 1

// ✅ 正确:想取哪个位置就按顺序写
let [first, second, third] = arr;
console.log(first, second, third);  // 1, 2, 3

坑 3:===== 的区别

// ❌ 坑:== 会自动转类型,0、""、false 都会相等
console.log(0 == false);  // true(容易出 bug)

// ✅ 正确:绝大多数情况用 === 严格相等
console.log(0 === false); // false(更安全)

坑 4:扩展运算符「浅拷贝」的坑

let obj = { user: { name: "小明" } };

// ❌ 错误:浅拷贝,内层对象还是共享引用
let copy1 = { ...obj };
copy1.user.name = "小红";
console.log(obj.user.name);  // 小红(被改坏了!)

// ✅ 正确:需要深拷贝时用 JSON 方法
let copy2 = JSON.parse(JSON.stringify(obj));
copy2.user.name = "小红";
console.log(obj.user.name);  // 小明(安全)

坑 5:可选链遇到函数调用

let obj = {};

// ❌ 错误:可选链只管属性访问,不管方法
// obj.method?.() 这样写是对的,但别忘了括号

// ✅ 正确:方法调用时记得 ()
obj.sayHi?.("小明");

性能小贴士:字符串拼接选对方法

// 少量拼接用模板字符串,清晰
let a = `Hello ${name}`;

// 大量拼接(比如循环里)用数组 + join 性能更好
let parts = [];
for (let i = 0; i < 1000; i++) {
parts.push(`item${i}`);
}
let result = parts.join("");

调试技巧:善用断点 + console

// 复杂解构时,用 console.table 看结构
let data = { user: { profile: { avatar: "xxx" } } };
console.table([data]);

// 标签模板里加日志调试
function debugTag(strings, ...values) {
console.log("字符串部分:", strings);
console.log("值部分:", values);
return strings[0];
}
let x = 1, y = 2;
debugTag`x=${x} + y=${y}`;

✏️ 练习题 + 作业题

练习题(5 道,10 分钟)

练习 1(2 分钟):模板字符串改写
- 输入:let name = "张三"; let age = 25;
- 预期输出:我叫张三,今年25岁
- 提示:用反引号和 ${}


练习 2(2 分钟):解构赋值
- 输入:let point = { x: 10, y: 20, z: 30 };
- 预期输出:用解构取出 x 和 z,分别输出 1030
- 提示:let { x, z } = point;


练习 3(2 分钟):扩展运算符合并数组
- 输入:arr1 = [1,2,3], arr2 = [4,5,6]
- 预期输出:合并后打印 [1, 2, 3, 4, 5, 6]
- 提示:[...arr1, ...arr2]


练习 4(2 分钟):可选链安全访问
- 输入:let data = { items: [10, 20] };let empty = {};
- 预期输出:data.items?.[1] 输出 20empty.user?.name 输出 undefined
- 提示:. 换成 ?.


练习 5(2 分钟):找出错误
- 代码:

let arr = [1, 2, 3];
let [a, b] = arr;
console.log(a + b + c);  // 报错!
  • 预期输出:分析为什么报错,并写出修复方法
  • 提示:只声明了 a 和 b,c 从哪来?

作业题(30 分钟 - 2 小时)

作业:做一个「通讯录管理工具」

需求描述:
做一个命令行通讯录,支持添加、删除、搜索、显示联系人。

功能点:
1. 添加联系人 - 输入姓名、手机号、邮箱,用扩展运算符添加到列表
2. 删除联系人 - 按姓名删除(用 filter)
3. 搜索联系人 - 支持按姓名模糊搜索(用 includes)
4. 显示通讯录 - 用模板字符串格式化输出,显示序号、姓名、手机(脱敏)、邮箱

加分项:
1. 用解构赋值处理联系人信息
2. 用可选链安全访问可能的空数据
3. 数据用 localStorage 持久化(前端环境)或写入本地文件(Node.js)

验收标准:
- 能成功添加 3 个以上联系人
- 能搜索到匹配的联系人
- 能删除指定联系人
- 代码有适当注释

提交方式: 评论区贴代码或 GitHub 链接


📚 总结 + 资源

本文学到的 3 个核心点:
1. 模板字符串让字符串拼接变得「像写句子一样自然」
2. 解构赋值 + 扩展运算符让数据提取和合并变得极其优雅
3. 可选链让访问嵌套属性再也不用担心空指针报错

延伸学习资源:
1. MDN 官方文档 - 模板字符串 - 最权威的参考
2. 《JavaScript 高级程序设计》第 4 章 - 变量、作用域、内存
3. JavaScript.info 教程 - 体系完整,配有大量示例


互动钩子:

「你在开发中有没有遇到过解构赋值特别香的场景?比如从接口拿到一堆数据,只需要其中几个字段的时候?评论区聊聊你的经历,老粉优先回复!」

下章预告:

「学会了怎么"备料"(数据类型)和怎么"加工"(运算符),下一章我们就要开始学习怎么让代码做判断、走分支——也就是传说中的控制流。if/switch/三元运算符,它们到底是干啥的?且听下回分解。」

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