JavaScript: From First Steps to Professional

迈出踏入广阔JavaScript世界的第一步,并掌握成为一名专业JavaScript程序员所需的核心技能!通过一系列动手实战项目,你将学习构建动态网站的基础模块。实时修改网页内容,使用函数编写可复用代码,响应用户交互事件,通过条件语句做出逻辑判断,并从API获取数据!这就是你继续探索并成为高效JavaScript开发者所需的全部知识!

0-introduction

  • 欢迎与介绍
    • 讲师:Anjana Vakil
    • 课程名称:JavaScript First Steps
    • 课程资料网址:https://anjana.dev/javascript-first-steps/
  • 课程理念
    • 目标:为编程新手或从其他语言转来的开发者提供坚实的基础。
    • 学习是一个无限过程:学习 JavaScript 是一项终身的事业,不可能一次性学完。
    • JavaScript 是“活的语言”:它像人类语言一样不断发展,每年都会出现新的特性(就像新的俚语一样)。
  • 学习方法
    • 项目驱动:课程将通过几个项目来帮助理解和应用知识。
    • 强调“即时学习” (Just-in-time learning):关注于学习当下为完成某个任务所需要的东西,而不是为了“以防万一”而去记忆可能永远用不到的知识点。
  • 讲师背景 (问答环节)
    • 如何接触 JavaScript
      • 讲师没有传统的计算机科学背景,之前学习哲学和语言学。
      • 通过纽约的一个名为 Recurse Center 的优秀社区(一个为程序员举办的编程静修营)进入软件行业。
      • 在 2015 年,她决定深入学习 JavaScript,因为它在社区中很酷。
    • 为何喜欢 JavaScript
      • 通过“函数式编程” (Functional Programming) 的视角发现了 JavaScript,并发现两者结合得很好。
      • 讲师有另一门相关课程:“Functional Programming First Steps”。
    • 课程的延伸:本课程学到的基础知识将有助于后续学习更高级的主题,如函数式编程、React 框架和 TypeScript。

1-course-overview

  • 课程结构 (三部分/三天)
    • 第一天:JavaScript 中的“事物” (Values)
      • 主题:DOM (文档对象模型)、数据与数据类型、数组 (Arrays) 和对象 (Objects)。
      • 项目:在浏览器中制作一个井字棋 (Tic-Tac-Toe) 游戏。
    • 第二天:在 JavaScript 中“做事” (Actions)
      • 主题:函数 (Functions)、事件 (Events,响应用户行为)、条件语句 (Branches) 和循环 (Loops)。
      • 项目:制作一个问答小游戏。
    • 第三天:JavaScript 中更棘手的部分
      • 主题:获取网络数据 (Fetching data)、异步代码 (Asynchronous code)、错误处理与调试 (Errors and debugging)、代码重构 (Refactoring)。
      • 项目:一个与狗相关的游戏。
  • 期望与基本规则
    • 包容性:无论你的经验水平如何,都欢迎你。请互相尊重。
    • 提问:没有“愚蠢的问题”。如果你有疑问,其他人很可能也有同样的疑问。
    • 目标:专注于个人知识的成长,而不是死板地完成项目。目标是带着新的见解和工具离开,这比完成所有练习更重要。
  • 时间安排 (问答环节)
    • 课程将持续地进行代码编写和调整,主要以小的代码片段为主。
    • 同时,每个部分也会有专门的练习时间(大约 15 分钟)来完成项目。

2-what-is-javascript

  • 什么是 JavaScript?
    • 一门编程语言。
    • Web 的语言。
    • 帮助网站展示内容并与其他网站“对话”。
    • 用于修改 HTML 并与之交互。
    • 一门动态编程语言。
    • 让 Web 变得互动,处理网站的行为。
  • 核心事实
    • 简称:JS。
    • 创建:1995 年由在 Netscape 工作的 Brendan Eich 创造。
    • 开发时间:在大约 10 天内完成。
    • 初衷:最初设计为一种嵌入在网页浏览器中、用于操作网页的脚本语言。
  • 现代 JavaScript 的生态
    • 早已超越了浏览器的范畴。
    • 浏览器端:本课程的重点。
    • 服务器端:通过 Node.js 运行。
    • 嵌入式设备:用于物联网 (IoT) 设备。
    • 它不仅是 Web 的“通用语”(lingua franca),甚至成为其他语言(如 TypeScript, ClojureScript, Elm)的编译目标,这些语言最终被翻译成 JavaScript 在浏览器中运行。
  • 能力
    • 可以做很多神奇的事情,从连接脑控设备到通过物联网与现实世界互动。
    • 几乎可以对网站做任何想做的操作。

3-where-to-write-javascript

  • Web 三巨头:HTML, CSS, JavaScript
    • 它们是“永远的好朋友”(BFFs)。
    • 比喻 1 (语法)
      • HTML:名词 (内容、结构)。
      • CSS:形容词 (样式、外观)。
      • JavaScript:动词 (行为、交互)。
    • 比喻 2 (舞台剧)
      • HTML:演员和道具 (内容)。
      • CSS:服装和布景 (样式)。
      • JavaScript:舞蹈和动作 (行为、生命力)。
  • 学习过程
    • 起初,编写代码可能感觉像在念听不懂的“咒语”。
    • 课程的目标是让你理解这些“咒语”背后的原理,从而真正掌控这门语言。
  • 在哪里编写 JavaScript?
    • 浏览器控制台 (Browser Console):一个实时的 JavaScript 解释器。这是第一天的主要工作环境。
    • 文本文件:可以在 .html 文件中使用 <script> 标签,或编写单独的 .js 文件。
    • 文本编辑器 / IDE
      • 系统自带的简单编辑器。
      • 功能强大的集成开发环境 (IDE),如 VS Code(在 JS 开发者中非常流行)。
    • 在线代码演练场 (Online Playgrounds)
  • 如何打开浏览器控制台
    1. 在浏览器中打开一个网页。
    2. 在页面任意位置右键单击。
    3. 选择“检查”(Inspect)。
    4. 在弹出的开发者工具面板中,找到并点击“控制台”(Console) 标签页。
    5. 现在你可以在提示符 (>) 后面输入 JavaScript 代码并按回车执行。
    6. 示例:输入 console.log("hell yes JS!"); 会在控制台打印出这句话。

4-document-object-model

  • DOM: 文档对象模型 (Document Object Model)
    • 当 JavaScript 查看一个 HTML 文档时,它看到的是一个名为 DOM 的表示形式。
    • DOM 是对文档结构的一个模型化表示,让 JavaScript 能够理解和操作页面内容。
  • 将 HTML 视为一棵树
    • HTML 文档的结构可以用一棵树来表示。
    • 顶层元素(根节点): <html>
    • <html> 的子节点: <head><body>
    • <body> 的子节点 (示例中): <header> 和一个 <div>
    • <div> 是一个通用的分组元素,常被比作一个容器或一个空箱子。
  • JavaScript 与 DOM 的关系
    • JavaScript 会根据 HTML 的树状结构,在内部创建一个对应的模型(一个“对象”)。
    • 通过这个模型,JavaScript 可以访问和操作 HTML 文档的每一个部分。
    • 在控制台中,可以通过输入一个特殊的词 document 来访问这个顶层的文档对象。
    • <!DOCTYPE> 声明定义了文档类型,我们可以认为 document 对象在层级上位于 <html> 元素之上。

5-finding-elements-in-a-web-page

  • 用 JavaScript 访问 DOM
    • 不同浏览器(Chrome, Firefox, Safari 等)的开发者工具(审查器和控制台)外观可能略有不同,但底层的 JavaScript 对象和工作方式是相同的。
  • 查找元素的常用“咒语”
    • document:代表整个 HTML 文档。
    • document.title:获取页面标题(显示在浏览器标签页上的文字)。
    • document.body:获取页面的 <body> 元素。
    • document.body.children:获取 <body> 元素下所有直接子元素的集合。
  • 查找特定元素的方法
    • 通过 ID (id)
      • document.getElementById('board'):查找页面上 ID 为 board单个元素。ID 在文档中应当是唯一的。
      • document.querySelector('#board'):使用 CSS 选择器语法的替代方法。# 符号代表 ID。querySelector 只返回第一个匹配的元素。
    • 通过标签名 (TagName)
      • document.getElementsByTagName('h1'):查找所有 <h1> 标签的元素。返回一个元素集合。
      • document.querySelectorAll('h1'):功能相同的 CSS 选择器版本。返回所有匹配的元素。
    • 通过类名 (ClassName)
      • document.getElementsByClassName('player'):查找所有 classplayer 的元素。返回一个元素集合。
      • document.querySelectorAll('.player'):功能相同的 CSS 选择器版本。. 符号代表 class。
  • 核心要点:在 JavaScript 中,通常有多种方法可以完成同一件事。使用 CSS 选择器的 querySelectorquerySelectorAll 功能强大且非常常用。

6-length-textcontent

  • 处理元素集合
    • HTMLCollection vs. NodeListgetElementsBy... 系列方法通常返回一个 HTMLCollection,而 querySelectorAll 返回一个 NodeList。这两种集合类型有细微差别,但目前我们可以将它们视为功能相似的元素组。
    • .length:这是一个属性,用于获取一个集合(如 getElementsByClassNamequerySelectorAll 返回的结果)中包含的元素数量。
      • 示例document.getElementsByClassName('player').length 将返回 2
      • 为什么叫 length 而不是 count:编程中的命名是个难题,很多命名是出于历史原因或语言创建者的个人选择。
  • 访问元素内容
    • .textContent:这是一个属性,用于获取(或之后会学到,设置)单个元素内部的文本内容。
      • 示例document.getElementById('p1-name').textContent 将会返回字符串 "Anjana"
  • 最重要的资源:MDN
    • MDN (Mozilla Developer Network):网址是 developer.mozilla.org
    • 这是 Web 开发(HTML, CSS, JavaScript)方面一个权威、全面且非常重要的参考文档。
    • 当你忘记一个方法的名字、拼写、用法,或者想深入了解某个概念(比如 NodeListHTMLCollection 的区别)时,都应该去查询 MDN。
    • 你不需要记住所有东西,但你需要知道去哪里查找它们。

7-finding-elements-exercise

  • 寻宝游戏练习 (Scavenger Hunt):在井字棋页面上练习查找不同的元素和数据。
  • 任务 1:获取页面中所有的 p 元素。
    • 解决方案document.getElementsByTagName('p')
    • 注意getElementsByName 查找的是 name 属性,而不是标签名。如果用错,会返回一个空的集合 (length: 0)。
    • 可以用 .length 属性来检查找到了多少个元素。
  • 任务 2:获取文本 "X"。
    • 解决方案 1document.querySelector('#p1-symbol').textContent
    • 解决方案 2document.getElementById('p1-symbol').textContent
    • 要点:属性名是大小写敏感的。textContent (C 大写) 是正确的,如果写成 textcontent (全小写) 会得到 undefined
    • undefined 是 JavaScript 中的一个特殊值,我们稍后会讨论,它通常在你试图访问一个不存在的属性时出现。
  • 任务 3:获取棋盘上格子的数量。
    • 解决方案document.querySelectorAll('.square').length
    • 这条命令首先找到所有 classsquare 的元素,然后通过 .length 获取这个集合的数量。
  • 任务 4:获取文本 "a game you know"。
    • 解决方案document.querySelector('h2').textContent
    • 因为这个 h2 元素没有 ID 或 class,所以通过它的标签名来选择是一个好方法。
  • 控制台小技巧
    • 向上箭头 (Up Arrow):在控制台中按向上箭头可以翻阅你的历史命令,方便修改和重新执行。
    • Tab 键:可以自动补全浏览器给出的代码提示。

8-changing-a-web-page

  • 用 JavaScript 编辑 DOM
    • JavaScript 不仅能读取 DOM,还能修改它,这使得网页变得动态和可交互。
  • 赋值操作符 (=)
    • = 符号用于给一个属性赋予新的值,会覆盖原有的值。
    • 修改页面标题
      • document.title = "My Page";
      • 这会直接改变浏览器标签页上显示的标题。
    • 修改元素文本
      • document.getElementById('p1-name').textContent = "Sofia";
      • 这条命令会找到 ID 为 p1-name 的元素,并将其内部的文本内容完全替换为 "Sofia"。
  • 向现有内容添加内容 (.append())
    • .append() 是一个方法,它会在一个元素现有内容的末尾添加新的内容。
    • 追加文本
      • document.getElementById('p1-name').append(" & friends");
      • 如果原来的文本是 "Sofia",执行后会变为 "Sofia & friends"。
      • 这个方法是添加,而不是替换。

9-changing-a-web-page-exercise

  • 页面操作练习:一个练习修改井字棋页面的任务清单。
  • 任务 1:修改玩家名称。
    • 解决方案
      • document.querySelector('#p1-name').textContent = "你的名字";
      • document.querySelector('#p2-name').textContent = "邻居的名字";
    • 常见错误:在 querySelector 中忘记为 ID 选择器加 #
      • querySelector('p1-name') 会尝试查找一个名为 <p1-name> 的 HTML 标签,因为找不到,所以返回 null
      • 尝试在 null 上设置 .textContent 会导致报错。
      • null 是 JavaScript 中另一个特殊值,表示“没有值”或“没有找到对象”。
  • 任务 2:交换玩家符号 (X 和 O)。
    • 解决方案
      • document.getElementById('p1-symbol').textContent = 'O';
      • document.getElementById('p2-symbol').textContent = 'X';
  • 任务 3:将副标题改为 "a game you know and love"。
    • 方案 1 (追加): document.querySelector('h2').append(' and love');
      • 更精确的选择器可以是 document.querySelector('header h2')
    • 方案 2 (替换): document.querySelector('h2').textContent = 'a game you know and love';
  • 讨论的重要概念
    • 撤销操作:在控制台中执行的命令没有简单的“撤销”或 Ctrl+Z 功能。如果犯了错,你必须写新的命令来修正它(例如,用 .textContent = ... 重新设置文本)。
    • 持久性 (Persistence):通过控制台对页面所做的修改是非永久性的。它们只存在于当前浏览器标签页的当前页面实例中。
    • 刷新页面:一旦刷新页面,所有用 JavaScript 做出的更改都会丢失,页面会恢复到其原始的 HTML 源码状态。
    • 要使更改永久生效,你必须直接编辑 HTML 源文件。

10-values-data-types

  • 回顾与引入
    • 我们之前一直在使用各种“咒语”(代码),现在我们来深入理解一下我们操作的这些信息。
    • 这些信息被称为数据 (data),或者特定类型的值 (values of certain types)
  • 什么是值 (Values) 和数据类型 (Data Types)
    • 值是 JavaScript 中的信息块。
    • 不同的值代表不同类型的信息,它们属于不同的数据类型
    • 例如,"Tic Tac Toe"(带引号)和 9(不带引号)是两种完全不同的数据类型。
  • 基本数据类型
    • 字符串 (String)
      • 表示文本数据。
      • 在 JavaScript 中,可以用双引号 ""单引号 ''反引号 ```` 来创建。
      • 可以包含字母、数字、符号,甚至是 Unicode 字符(如 emoji)。
      • 示例: "Hello", 'I like single-quotes', I can include an emoji 😎
      • 重要:带引号的数字也是字符串,例如 "42" 是一个字符串,而 42 是一个数字。
    • 使用 typeof 操作符检查类型
      • typeof 是一个“咒语”,可以告诉你一个值的具体数据类型。
      • typeof "42" 会返回 "string"
      • typeof 42 会返回 "number"
    • 数字 (Number)
      • 表示数值。
      • 可以是整数 (9)、大数 (525600)、小数/浮点数 (3.45) 或负数。
      • JavaScript 甚至有一个特殊值 Infinity(无穷大)。
    • 布尔值 (Boolean)
      • 表示真或假。
      • 只有两个可能的值:truefalse(不带引号)。
      • typeof true 会返回 "boolean"
      • "true" (带引号) 是一个字符串,不是布尔值。
    • undefinednull
      • 都表示“值的缺失”或“空无”。
      • 区别:
        • undefined: “Ain't nothing but a mistake” (纯属意外)。通常表示一个变量被声明了但没有被赋值,或者你想访问一个不存在的东西,是一个意外的“空”。
        • null: “I want it that way” (我就是这么想的)。通常是开发者有意为之地赋予一个变量“空值”,表示这里明确地什么都没有。
  • JavaScript 中的两大类数据类型
    1. 原始数据类型 (Primitive Data Types):
      • 包括 String, Number, Boolean, undefined, null 等。
      • 它们是语言最基础的数据类型。
    2. 对象 (Objects):
      • 除了原始类型,JavaScript 中的几乎所有东西都是对象。
      • document 就是一个对象,代表了整个 HTML 文档。

11-values-data-types-exercise

  • 练习: 使用 typeof 操作符检查不同值的类型。
  • 练习结果
    • typeof false"boolean"
    • typeof "true""string"
    • typeof document.title"string" (页面标题是文本)
    • typeof "some string".length"number" (字符串的长度是一个数字)
  • typeof null 的特例 (历史遗留问题)
    • typeof undefined"undefined" (符合预期)
    • typeof null"object"
    • 这是一个著名的 JavaScript 历史遗留 bug。尽管 null 本身是一个原始数据类型,但 typeof 操作符错误地将其报告为 "object"
    • 这个例子告诉我们,JavaScript 有时会因为历史原因出现一些不符合逻辑的、怪异的行为 (常被称为 "wat")。
  • 问答:为什么 typeof 的结果是带引号的字符串?
    • typeof 操作符的返回值永远是一个字符串,这个字符串的内容是对应数据类型的名称。
    • 例如,typeof 42 返回的是字符串 "number",而不是 number 这个类型本身。

12-strings

  • 深入理解字符串 (Strings)
  • 字符串的构成:字符 (Characters)
    • 字符串是由零个或多个字符组成的序列。
    • 可以把字符想象成串成友谊手链的字母珠子。
  • 字符串的 .length 属性
    • 返回字符串中字符的数量。
    • 示例:
      • "super".length5
      • "some string".length11 (空格也是一个字符)
      • "".length0 (空字符串的长度为 0,但它仍然是一个有效的字符串)
  • 字符的顺序与索引 (Index)
    • 字符串中的字符是有特定顺序的。
    • 每个字符的位置都由一个数字来标识,这个数字被称为索引 (index)。
    • 核心规则: 在 JavaScript 中,索引从 0 开始
    • 示例: 在字符串 "ALOHA" 中:
      • A 的索引是 0
      • L 的索引是 1
      • O 的索引是 2
      • H 的索引是 3
      • A 的索引是 4
    • 可以把索引想象成每个字符珠子的编号,或者每个字符开始前的位置标记。

13-index

  • 通过索引访问字符
    • 使用方括号 [] 语法。
    • 示例:
      • "ALOHA"[0]"A" (获取第一个字符)
      • "ALOHA"[2]"O" (获取第三个字符)
  • 查找字符的索引 .indexOf()
    • 返回指定字符在字符串中首次出现的位置的索引。
    • 示例:
      • "ALOHA".indexOf("L")1
      • "ALOHA".indexOf("A")0 (返回第一个 "A" 的索引)
    • 如果字符不存在,返回 -1。这是一个重要的约定。
      • "ALOHA".indexOf("Q")-1
    • .indexOf()大小写敏感的。
      • "ALOHA".indexOf("l")-1
  • 检查子字符串
    • .includes('substring'):检查字符串是否包含某个子字符串,返回一个布尔值 (truefalse)。
      • "ALOHA".includes("HA")true
    • .startsWith('substring'):检查字符串是否以某个子字符串开头,返回一个布尔值。
      • "ALOHA".startsWith("AL")true
  • 查找子字符串的索引
    • .indexOf() 同样适用于查找子字符串,它会返回子字符串的起始索引。
      • "ALOHA".indexOf("HA")3
      • "ALOHA".indexOf("LOL")-1 (因为 "LOL" 这个完整的子串不存在)
  • 连接字符串 (Concatenation)
    • 使用加号 + 操作符可以将两个或多个字符串“粘”在一起。
    • "ALOHA" + "!""ALOHA!"
  • 改变大小写
    • .toLowerCase():返回一个所有字符都转换为小写的新字符串。
    • "ALOHA".toLowerCase()"aloha"

14-working-with-strings-exercise

  • 练习: 结合字符串操作和 DOM 操作。
  • 任务 1:给玩家名字添加姓氏。
    • 方案一 (追加): document.getElementById('p1-name').append(' Vakil'); (注意前面的空格)
    • 方案二 (连接并赋值): document.querySelector('#p1-name').textContent = 'Paul' + ' ' + 'Quigley';
  • 任务 2:获取页面标题中的第一个 "T" 字符。
    • 两步法:
      1. 先找到 "T" 的索引: document.title.indexOf('T')9
      2. 再用索引获取字符: document.title[9]'T'
  • 任务 3:检查页面标题是否包含 "JavaScript"。
    • 解决方案: document.title.includes('JavaScript')
    • 注意: 此方法大小写敏感。如果页面标题是 JavaScriptTacToe,那么 includes('javascript') (全小写) 会返回 false
  • 任务 4:将标题 "Tic Tac Toe" 变为全大写。
    • 方案一 (通过 CSS): 修改元素的 style 属性。
      • document.querySelector('h1').style.textTransform = 'uppercase';
      • 这展示了 JS 可以直接操控 CSS 样式。
    • 方案二 (通过字符串方法): 获取文本,转换大小写,再赋值回去。
      • document.querySelector('h1').textContent = document.querySelector('h1').textContent.toUpperCase();
      • 这里引入了 .toUpperCase() 方法。
  • 总结:
    • 字符串有非常多有用的方法。
    • 如果想了解全部,请查阅 MDN 的 String 文档。

15-operators

  • 什么是操作符 (Operators)
    • 操作符是用来对值进行运算的特殊符号或关键字。
    • 我们已经见过的例子:+ (用于连接字符串),typeof
  • 算术操作符 (Arithmetic Operators)
    • + : 加法
    • -: 减法
    • *: 乘法 (星号)
    • / : 除法 (斜杠)
    • 操作符重载 (Overloaded Operator): + 就是一个例子,它在作用于数字时执行加法,作用于字符串时执行连接。
  • 运算顺序 (Order of Operations)
    • JavaScript 遵循标准的数学运算顺序 (先乘除,后加减)。
    • 示例: 4 + 1 * 2 会得到 6 (先算 1 * 2)。
    • 可以使用圆括号 () 来改变运算顺序,括号内的表达式会优先计算。
    • 示例: (4 + 1) * 2 会得到 10

16-operators-exercise

  • 练习: 使用算术操作符进行计算。
  • 核心要点:括号的重要性
    • 在计算“一周可以抚摸的狗的数量”时,表达式为 (24 - 8) * 7
    • 这里的括号是必需的,因为它确保先计算出每天清醒的小时数 (24 - 8),然后再乘以天数。
    • 如果没有括号 24 - 8 * 7,根据运算顺序会先算 8 * 7,导致结果完全错误。
  • 发现更多操作符
    • JavaScript 还有很多其他操作符,例如:
      • *:幂运算 (Exponentiation),如 2 ** 3 结果是 8
      • %:模运算 (Modulo),返回除法的余数,如 10 % 3 结果是 1
    • 如何查找? 老朋友 MDN,搜索 "JavaScript operators" 可以找到完整的列表和解释。

17-comparison-equality-operators

  • 比较操作符 (Comparison Operators)
    • 用于比较两个值的大小关系。
    • > (大于), < (小于), >= (大于等于), <= (小于等于)。
    • 这类操作符的返回值是一个布尔值 (truefalse)。
    • 示例: 5 > 4true
  • 相等操作符 (Equality Operators)
    • JavaScript 提供了两种相等性比较,这非常重要。
    • 1. 严格相等 (Strict Equality) (推荐使用)
      • === (严格等于)
      • !== (严格不等于)
      • 它会比较两个值的值和数据类型是否都相同。
    • 2. 宽松相等 (Loose Equality) (不推荐)
      • == (宽松等于)
      • != (宽松不等于)
      • 如果两个值的数据类型不同,它会尝试进行类型转换 (Type Coercion),将它们转换为相同类型后再进行比较。
  • 严格 vs 宽松 的关键区别
    • 1 === 1true
    • 1 == 1true (类型和值都相同,结果一致)
    • 1 === '1'false (值相同,但一个是 number,一个是 string,类型不同)
    • 1 == '1'true (因为宽松比较会将字符串 '1' 转换为数字 1 再比较)
  • 最佳实践:
    • 几乎在所有情况下,都应该使用严格相等操作符 (===!==)
    • 这可以避免因意外的类型转换而导致的 bug。
  • 其他逻辑操作符
    • JavaScript 还有 && (逻辑与), || (逻辑或), ++ (自增) 等操作符。
    • 同样,在 MDN 上可以查到所有这些操作符的详细信息。

18-expressions

  • 什么是表达式 (Expressions)
    • 我们在代码中使用的,由值和操作符组成的片段,例如 4 / 2 * 10"Frontend" + "Masters",就是表达式。
    • 一个表达式本身不是一个值,但它能求值 (evaluates to)解析 (resolves to) 为一个值。
    • 表达式的核心是“表达”一个值。
  • 类比: 自然语言中的表达式
    • 就像 "a New York minute" (纽约一分钟) 这个英文短语,它本身不是一个具体的时间单位,但它表达了“一瞬间”或“非常快的时间”这个
  • JavaScript 中的表达式
    • 一个复杂的算术运算 ((4 + 1) * 2 * 4) + 2 是一个表达式,它最终会计算出一个数字值。
    • 你可以将表达式用在任何需要一个值的地方。
    • 示例:
      • "FrontendMasters".includes("Front" + "end")
      • 这里 "Front" + "end" 是一个表达式,JavaScript 会先将其求值为字符串 "Frontend",然后再执行 .includes() 方法。
      • 结果会是 true

19-declaring-assigning-variables

  • 为什么需要变量 (Variables)
    • 变量可以帮助我们“记住”值,避免重复编写复杂的代码。
    • 例如,不用每次都写 document.querySelector(...),我们可以把找到的元素存入一个变量,之后直接使用这个变量名。
  • 声明和赋值变量
    • 示例: let remember = "Sept 21";
    • 逐个分析:
      • let: 这是一个 JavaScript 关键字 (keyword),用于声明 (declare) 一个新的变量。它告诉 JavaScript:“我要创建一个新变量了。”
      • remember: 这是我们为变量选择的名称 (name)
      • =: 这是赋值操作符 (assignment operator),它将右边的值赋给左边的变量。
      • "Sept 21": 这是一个字符串值 (string value),是我们想让变量记住的内容。
      • ;: 分号表示这条语句结束了。像英语句子末尾的句号。
  • 声明 vs. 赋值
    • 仅声明: let bankruptcy;
      • 这行代码只创建了一个名为 bankruptcy 的变量,但没有给它任何值。
      • 此时,bankruptcy 的值是 undefined
    • 仅赋值: myDeclaredVariable = "so value much wow";
      • 这行代码给一个已经声明过的变量赋予一个新的值。
    • 声明并赋值: let remember = "Sept 21";
      • 在一行代码中同时完成声明和赋值。
  • undefined vs. null in Variables
    • let bankruptcy;bankruptcyundefined (意外的空,因为你没给值)。
    • let bankruptcy = null;bankruptcynull (你故意让它为空)。
  • 变量的心智模型 (Mental Model)
    • 当你执行 let remember = "Sept 21"; 时,JavaScript 做了两件事:
      1. 创建了一个名为 remember 的变量。
      2. 创建了一个值为 "Sept 21" 的字符串。
      3. 在这两者之间建立了一个指向 (pointer)引用 (reference) 的关系 (remember"Sept 21")。

20-const-accessing-variables

  • 另一个关键字 const
    • constlet 的替代品,也用于声明变量。
    • 核心区别: 用 const 声明的变量,其值不能被重新赋值 (reassigned)
    • 规则:
      1. 使用 const 声明变量时,必须同时进行赋值。不能只声明不赋值 (const myVar; 是无效的)。
      2. 一旦赋值,这个变量就永远指向这个值,不能再指向其他值。
  • let vs. const
    • let 声明的变量,之后可以用 = 重新赋值。
    • const 声明的变量,之后不能再用 = 重新赋值。
    • 可以把 const 理解为一个“永恒的”指向。
  • 使用变量
    • 一旦声明并赋值,你就可以在代码中像使用普通值一样使用变量名。
    • 示例:
      const answerToLife = 42;
      console.log(answerToLife - 10); // 输出 32
      
    • JavaScript 在表达式中看到变量名时,会自动查找它所指向的值,并用该值参与运算。
  • 动态类型 (Dynamic Typing) - (问答)
    • 问题: 可以在 JavaScript 中为变量指定数据类型吗 (比如只能是字符串或数字)?
    • 回答:
      • 不可以。JavaScript 是动态类型语言,变量可以持有任何类型的值,并且可以随时改变其值的类型。
      • 这为 JS 带来了灵活性,但也可能导致错误。
      • 为了解决这个问题,社区创造了 TypeScript。TypeScript 是 JavaScript 的一个超集,它增加了静态类型系统,允许你为变量指定类型,从而在开发阶段就能发现类型错误。
  • 变量命名规则
    • 有效 (Valid):
      • 通常包含字母、数字、下划线 _
      • 最常见的风格是驼峰命名法 (camelCase),例如 validVariable
      • 其他风格如 snake_case (下划线连接) 也有效,但在 JS 中不常用。
    • 无效 (Invalid):
      • 不能以数字开头。
      • 不能包含特殊字符,如 !
    • 最佳实践: 在 JavaScript 中,优先使用 camelCase

21-variables-exercise

  • 练习: 声明和赋值变量。
  • 任务 1:用变量记住你的名字。
    • const myName = "Anjana"; (使用 const 是个不错的选择,因为名字通常不会改变)
  • 任务 2:用变量记住父母年龄总和,并使用表达式。
    • let combinedParentsAge = 23 + 24;
    • 要点: JavaScript 会先计算表达式 23 + 24 的值得到 47,然后将结果 47 赋给变量。你无法从变量中找回原始的 2324
  • 任务 3:用变量记住 ID 为 board 的元素。
    • let board = document.querySelector('#board');
    • 好处: 现在你可以直接使用 board 这个更短的名称来操作该元素,例如 board.children.length,而无需重复输入 document.querySelector...
  • 关键字 var (问答)
    • 问题: var 也可以声明变量,它和 let 有什么区别?
    • 回答:
      • var 是 JavaScript 早期版本中声明变量的唯一方式。
      • letconst 是在 ES2015 (ES6) 中引入的新方式
      • varlet作用域 (scope) 规则上存在重要差异(我们之后会学到)。let 的行为更可预测,更不容易出错。
      • 最佳实践: 在现代 JavaScript 开发中,优先使用 letconst,避免使用 var

22-what-are-variables

  • 变量到底是什么?
    • 避免使用“盒子”模型:将变量视为“装有值的盒子”可能会产生误导,因为它与 JavaScript 的实际工作方式不完全相符。
    • 推荐使用“指针”或“通讯录”模型:
      • 变量更像是指向 (pointing to) 一个值的 标签
      • 类比:
        • 旧式电话的快速拨号:按一个键就能拨出一个预存的号码。
        • 手机通讯录:你把一个电话号码 555-5555 存为联系人“Stephen”。当你让 Siri “呼叫 Stephen”时,手机查找的是号码,而不是联系人名字本身。变量名就像“Stephen”,值就像号码。

23-evaluating-code

  • 代码执行过程剖析

    1. let answerToLife = ((4 + 1) * 2 * 4) + 2; 执行时:

      • JavaScript 看到 let answerToLife,于是创建一个新变量 answerToLife
      • 接着计算右侧的表达式,得到结果 42
      • 最后,建立从变量 answerToLife 到值 42 的一个指向
    2. 当以下代码执行时:

      let a = "1";
      let b = a;
      a = "2";
      
      • Line 1: 创建变量 a,并让它指向字符串值 "1"
      • Line 2: 创建变量 b。JavaScript 会计算右侧 a 表达式的值,此时 a 指向的是 "1",所以 b 也指向了同一个字符串值b 指向的不是 a 变量本身,而是 a 当时的值
      • Line 3: 改变了 a 的指向,让它指向一个新的字符串值 "2"
      • 结果: b 的指向没有改变b 仍然指向它最初被赋值时的那个字符串值。
  • 重要结论: 变量赋值时,赋的是表达式在那一刻计算出的,而不是对另一个变量的动态引用。

24-statements-vs-expressions

  • 表达式 (Expressions) vs. 语句 (Statements)
    • 这个区别有助于理解代码的构成,但不是日常编程中时刻需要纠结的概念。
  • 表达式 (Expressions) - “问一个值”
    • 一个表达式总会产生一个值
    • 可以把它看作是向 JavaScript 提问:“这个东西的值是什么?”
    • 示例:
      • 6 + 4 (问: 值是多少? 答: 10)
      • myAssignedVariable (问: 这个变量的值是什么?)
      • document.getElementById(...) (问: 这个元素是什么?)
  • 语句 (Statements) - “做一件事”
    • 一个语句是告诉 JavaScript 去执行一个动作完成一个指令
    • 示例:
      • let myVar = 10; (这是一个指令,包含创建变量、求值、赋值等多个动作)
      • console.log("Hello"); (指令: 把这个东西打印到控制台)
      • if (...) { ... } (条件语句,指令: 根据条件做不同的事)
      • for (...) { ... } (循环语句,指令: 重复做某事)
  • 简要总结:
    • 表达式: 求值为一个值。
    • 语句: 执行一个动作。
    • 通常,语句以分号 ; 结尾,表示指令结束。
    • 表达式可以作为语句的一部分(例如在 let myVar = 6 + 4; 中,6 + 4 是表达式)。

25-arrays

  • 引入数组 (Arrays)
    • 数组是 JavaScript 中用于将多个相关的值组合在一起的数据结构。
    • 可以把数组看作一个有序的集合 (ordered collection)
  • 数组的基本语法
    • 使用方括号 [] 来定义一个数组,元素之间用逗号 , 分隔。
    • 示例: let synonyms = ["plethora", "array", "cornucopia"];
  • 数组与字符串的相似之处
    • 索引 (Index):和字符串一样,数组中的每个元素都有一个从 0 开始的索引。
      • synonyms[0]"plethora"
      • synonyms[1]"array"
      • synonyms[2]"cornucopia"
    • .length 属性:返回数组中元素的数量。
    • .indexOf(value) 方法:返回指定值在数组中首次出现的索引。
    • .includes(value) 方法:检查数组是否包含某个值,返回 truefalse
  • 修改数组
    • 数组和字符串的一个重要区别是:数组是可以被修改的 (mutable)
    • 修改特定位置的元素:
      • synonyms[1] = "variety"; 会将索引为 1 的元素从 "array" 替换为 "variety"
    • .pop() 方法:
      • 移除并返回数组的最后一个元素。
      • 这会改变原始数组。
    • .push(value) 方法:
      • 在数组的末尾添加一个或多个新元素。
      • 这也会改变原始数组。
  • 数组的多样性
    • 空数组: 可以创建一个不包含任何元素的空数组 []
    • 单元素数组: 可以创建一个只包含一个元素的数组 ["lonely"]
      • 注意: ["lonely"] (一个数组) 和 "lonely" (一个字符串) 是完全不同的两个值。
    • 混合类型数组: 数组可以包含任何类型的数据,包括字符串、数字、布尔值、甚至是其他对象和数组。JavaScript 不关心数组里装的是什么。
      • let mixedArray = ["string", 42, false, document];

26-useful-array-methods

  • 更多有用的数组方法
  • .sort()
    • 对数组的元素进行排序,它会直接修改原数组
    • 默认行为:
      • 对于字符串,它按字母顺序排序。
      • 对于数字,它会先将数字转换为字符串,然后按字符串的规则排序,这可能导致非预期的结果。
      • 示例: [100, 2, 50].sort() 会得到 [100, 2, 50],因为字符串 "100" 排在 "2" 前面。
  • .join('separator')
    • 将数组中的所有元素连接成一个单一的字符串
    • 可以指定一个“分隔符”字符串,用于插入到元素之间。
    • 示例: ['lions', 'tigers', 'bears'].join(' and ')"lions and tigers and bears"
  • .concat(otherArray)
    • 用于合并两个或多个数组。
    • 不会修改现有的数组,而是返回一个新的数组
    • 示例: [1, 2, 3].concat([4, 5, 6])[1, 2, 3, 4, 5, 6]
  • .push() vs. .concat()
    • .push()修改 原数组。
    • .push() 可以接受一个数组作为参数,但它会把整个数组作为一个元素添加到末尾,形成嵌套数组。
    • .concat() 不修改原数组,而是返回一个新数组
  • 查找更多方法: 永远的好朋友 MDN (Mozilla Developer Network)。

27-mutability

  • 可变性 (Mutability)
    • 在 JavaScript 中,数据类型分为两种:可变的和不可变的。
  • 数组是可变的 (Mutable)
    • 你可以改变数组的内容,例如替换一个元素 (myArray[1] = ...),添加一个元素 (.push()) 或删除一个元素 (.pop())。
    • 这些操作都是在原始数组上直接进行的。
  • 字符串是不可变的 (Immutable)
    • 不能改变一个已创建字符串的内部字符。
    • 示例: let myString = "abc"; myString[1] = "d";
    • 这行代码不会报错,但它也不会做任何事myString 的值仍然是 "abc"
    • JavaScript 对这种无效操作采取了“默默忽略”的态度。
    • 所有原始数据类型都是不可变的。

28-mutable-immutable-data-exercise

  • 练习: 比较 .push().concat() 的行为,理解可变性的影响。
  • .push() 的行为:
    let numbers1 = [1, 2, 3];
    let result1 = numbers1.push(4);
    // numbers1 现在是:[1, 2, 3, 4] 原数组被修改
    // result1 是:4 (新的数组长度)
    
  • .concat() 的行为:
    let numbers2 = [1, 2, 3];
    let result2 = numbers2.concat([4]);
    // numbers2 仍然是:[1, 2, 3] 原数组未变
    // result2 是:[1, 2, 3, 4] (全新的数组)
    
  • 可变性与不可变性总结:
    • 修改 (Mutating) 操作: 直接改变原始数据,如 .push()
    • 非修改 (Non-mutating) 操作: 不改变原始数据,而是返回一个新数据,如 .concat()
    • MDN 文档中,通常会明确指出一个方法是否会修改原数组/对象。
  • 可变变量 (let) vs. 不可变变量 (const)
    • let 声明的变量,可以被重新赋值,让它指向一个新的值 (let myVar = 10; myVar = 20;)。
    • const 声明的变量,一旦赋值就不能再改变它的指向,尝试重新赋值会报错。

29-immutable-variables-values

  • const 遇到可变值 (如数组)
    • 这是一个非常重要的概念:const 的“不可变”指的是 变量的指向 (pointer) 不可变,而不是它所 指向的值 (value) 不可变。
  • 示例:
    const operands = [4, 6];
    operands[0] = 5; // 这行代码是有效的!
    console.log(operands); // 输出 [5, 6]
    
    • 发生了什么?
      1. const operands 创建了一个不可变的指向,这个指向永远指向那个初始的数组对象。
      2. operands[0] = 5 没有改变 operands 的指向。它仍然指向同一个数组。
      3. 我们只是修改了那个数组内部的内容。因为数组本身是可变的
    • 什么是不允许的?
      • operands = [10, 20]; 这会报错,因为你试图改变 operands 的指向。
  • 为什么推荐不可变性?
    • 使用不可变的数据和 const 变量可以使代码更可预测,更安全。
    • 它能防止你在程序的其他地方无意中修改了数据,从而导致难以追踪的 bug。
    • 最佳实践: 默认使用 const。只有当你明确知道一个变量的指向需要被改变时,才使用 let

30-variables-arrays

  • 当一个变量赋值给另一个变量时
    let array1 = [1, 2, 3];
    let array2 = array1; // 关键在这里
    
    • array2 = array1 这行代码,并没有创建一个新的数组
    • 它只是创建了一个新的变量 array2,并让它指向与 array1 完全相同的那个数组对象
    • 现在,array1array2 都指向了内存中的同一个数组。
  • 修改的后果
    array1[1] = 4;
    console.log(array1); // 输出 [1, 4, 3]
    console.log(array2); // 输出 [1, 4, 3] 也会改变!
    
    • 因为两个变量指向同一个可变的数组,所以通过任何一个变量去修改这个数组,都会影响到通过另一个变量看到的结果。
    • 这就是可变数据可能带来的“危险”,如果你不清楚多个变量正引用着同一个数据,就可能产生意想不到的副作用。
  • let vs. const 在此场景下的表现
    • 即使你用 const 声明 array1array2,上述行为完全相同
    • const 只是锁定了 array1array2 的指向,让它们不能再指向其他数组。但你依然可以通过这两个变量去修改它们共同指向的那个数组的内容。
  • 核心建议:
    • 除非有明确的理由需要重新赋值,否则默认使用 const。这与你处理的是数组、对象还是原始值无关。const 关心的是变量的指向是否应该被改变。

31-objects-property-access

  • 对象 (Objects)
    • 对象是 JavaScript 中用于存储相关数据和功能的 键值对 (key-value pairs) 集合。
    • 示例:
      const js = {
        name: "JavaScript",
        abbreviation: "JS",
        isAwesome: true,
        birthYear: 1995,
      };
      
  • 对象字面量语法 (Object Literal Syntax)
    • 花括号 {}:用于定义一个对象。
    • 属性 (Properties):对象内部的 "键" 或 "名称",例如 name, isAwesome
    • 值 (Values):与属性相关联的数据,可以是任何 JavaScript 数据类型(字符串、数字、布尔值、甚至是数组或其他对象)。
    • 冒号 ::用于分隔属性名和其对应的值。
    • 逗号 ,:用于分隔对象中的多个属性-值对。
    • 代码格式: 换行和缩进不是必需的,但能大大提高代码的可读性。
  • 访问对象属性 (Property Access)
    • 点表示法 (Dot Notation): objectName.propertyName
    • 示例:
      • js.name"JavaScript"
      • js.isAwesometrue
  • 使用属性值
    • 访问到的属性值可以像任何普通值一样使用。
    • 你可以对它调用方法 (js.name.startsWith('Java')),或者用它进行计算 (2022 - js.birthYear)。
  • 修改对象属性
    • 对象是可变的 (mutable)
    • 重新赋值现有属性:
      • indecisive.lunch = 'tacos'; 会将 lunch 属性的值从 'sandwich' 改为 'tacos'
    • 添加新属性:
      • indecisive.snack = 'chips'; 会在 indecisive 对象上动态添加一个新的 snack 属性。

32-visualizing-object-access

  • 对象访问的可视化模型
    1. 创建对象: let indecisive = { lunch: 'sandwich' };
      • JavaScript 创建一个名为 indecisive 的变量。
      • 创建一个新的对象值 {}
      • indecisive 变量指向这个对象。
      • 在对象内部创建一个名为 lunch 的“指针”,它指向字符串值 'sandwich'
    2. 访问属性: indecisive.lunch
      • JavaScript 沿着 indecisive 的指针找到对象,然后沿着对象内部 lunch 的指针找到值 'sandwich'
    3. 重新赋值属性: indecisive.lunch = 'tacos';
      • JavaScript 找到 indecisive 指向的对象,然后改变对象内部 lunch 指针的指向,使其指向新的值 'tacos'
    4. 添加新属性: indecisive.snack = 'chips';
      • JavaScript 找到 indecisive 指向的对象,然后在对象内部创建一个新的名为 snack 的指针,并让它指向值 'chips'
  • 数组本质上也是对象 - (重要问答)
    • typeof []"object"
    • 数组是一种特殊的对象,它的属性名是数字字符串"0", "1", "2"...),这些就是我们所说的索引
    • 当你使用 myArray[0] 时,你实际上是在访问 myArray 这个对象上名为 "0" 的属性。
    • 数组还有一些内置的属性和方法,比如 .length, .push(), .sort() 等。
  • 方括号表示法 (Bracket Notation)
    • 除了点表示法,也可以用方括号来访问对象属性:objectName['propertyName']
    • 注意: 属性名需要以字符串形式放在括号里。
    • indecisive['lunch']indecisive.lunch 是等价的。
    • 这种方法在属性名包含特殊字符或者是由变量决定时非常有用。

33-objects-exercise

  • 练习: 创建代表你自己的对象。
  • 要点回顾:
    • 对象是组织复杂数据的强大工具。
    • 你可以自由定义属性名和值的类型,以最合理的方式来描述一个事物。
    • 属性的值可以是任何数据类型,包括数组和其他对象 (嵌套)
    • pet: null 是一个很好的例子,用 null 表示“故意没有宠物”。
  • 关于 Freeze (问答)
    • Object.freeze(obj) 是一个方法,可以“冻结”一个对象,使其浅层不可变
    • 冻结后,你不能再添加、删除或修改该对象的直接属性。
    • 这是一个确保对象不被意外修改的工具。

34-object-methods

  • 对象方法 (Object Methods)
    • 当对象的属性值是一个函数 (function) 时,我们称这个属性为一个方法
    • 方法定义了对象可以执行的动作
    • 示例:
      const dog = {
        name: "Ein",
        speak: function () {
          console.log("woof woof");
        },
      };
      
    • 调用方法: 使用圆括号 ()
      • dog.speak(); // 会在控制台打印 "woof woof"
      • 不带括号 dog.speak 只会返回函数本身,而不会执行它。
  • 我们已经见过的方法
    • 'hello'.indexOf('e')
    • [1, 2, 3].sort()
    • 这些都是内置对象(String, Array)上的方法。
  • 关键字 this
    • this 是一个特殊的关键字,通常在方法内部使用,它指向调用该方法的对象本身
    • 它允许你在方法内部访问该对象的其他属性。
    • this 在 JavaScript 中行为复杂,容易出错,初学者应谨慎使用。
  • 嵌套对象和数组
    • JavaScript 的数据结构可以无限嵌套。
    • 你可以有对象中的对象,数组中的对象,对象中的数组,等等。
    • 这使得构建复杂的数据模型成为可能,例如一个菜单对象,其属性是代表午餐和晚餐的对象,而这些对象内部又有更详细的菜品。

35-object-methods-exercise

  • 练习: 从一个复杂的嵌套对象 spiceGirls 中提取数据。
    • scavenger hunt 解决方案**:
      • 获取 "Girl Power": spiceGirls.motto (直接访问属性)
      • 获取 Ginger Spice 对象: spiceGirls.members[1] (先访问 members 数组,再用索引获取第二个元素)
      • 获取 "Spiceworld": spiceGirls.albums[1] (先访问 albums 数组,再用索引获取第二个元素)
      • 获取 "Victoria": spiceGirls.members[4].name (先访问 members 数组,用索引 [4] 获取 Posh Spice 对象,然后用 .name 访问该对象的 name 属性)
  • 核心要点: 通过混合使用点表示法 .方括号表示法 [],你可以深入任何复杂的嵌套数据结构来获取你需要的值。

36-built-in-objects

  • JavaScript 的内置对象
    • JavaScript 提供了许多开箱即用的内置对象,它们带有有用的属性和方法。
  • document 对象
    • 我们在浏览器环境中操作网页时使用的核心对象。
    • 它充满了各种属性(如 .title)和方法(如 .querySelector())。
    • 你可以在浏览器控制台输入 document 并回车,展开查看其复杂的内部结构。
  • console 对象
    • 提供与浏览器控制台交互的方法。
    • console.log(): 打印普通信息。
    • console.warn(): 打印警告信息(通常是黄色的)。
    • console.error(): 打印错误信息(通常是红色的)。
    • console.clear(): 清空控制台。
  • Math 对象
    • 提供数学相关的常量和函数。
    • Math.PI: 返回圆周率 π。
    • Math.random(): 返回一个 0 到 1 之间的伪随机数。
  • 字符串作为对象 (String Wrapper Object) - (重要概念)
    • 困惑: 如果字符串是原始值,为什么我们可以像对象一样调用方法,如 'hello'.toUpperCase()
    • 解释:
      1. 字符串本身是不可变的原始值
      2. 当你试图在字符串上调用一个方法时,JavaScript 会临时地将这个原始字符串包装成一个String 对象
      3. 这个临时的 String 对象拥有所有字符串方法(如 .toUpperCase(), .indexOf())。
      4. 方法执行完毕后,这个临时对象就会被丢弃。
      5. 重要:这些方法不会修改原始字符串(因为它是不可变的),而是返回一个新的字符串
  • 总结: 在 JavaScript 中,除了少数几个原始类型,几乎所有东西都是对象。这个模型统一了我们与不同数据类型交互的方式。

37-tic-tac-toe-demo

核心目标:使用对象和数组技能操作 DOM

  • 任务:通过 JavaScript 的对象和数组知识,来操作内置的document对象,实现一个井字棋(Tic-Tac-Toe)游戏。
  • 思路转变:从直接处理 HTML 中的字符串,转变为用 JavaScript 的数据结构(对象和数组)来表示游戏数据,再用这些数据来驱动页面变化。

步骤一:用数据结构表示玩家信息

创建玩家数组

  • 需求:游戏有两名玩家,且玩家信息在单次游戏中不会改变。
  • 实现:使用 const 声明一个名为 players 的数组,因为该数组的引用不会被重新赋值。
  • 结构:数组中包含两个对象,每个对象代表一位玩家。

定义玩家对象

  • 每个玩家对象包含两个属性(Property):
    • name: 玩家姓名(字符串)。
    • symbol: 玩家使用的棋子符号(例如 'X' 或 'O')。
// 初始化包含第一个玩家的数组
const players = [{ name: "Anjana", symbol: "X" }];

// 使用 .push() 方法添加第二个玩家
players.push({ name: "Everyone else", symbol: "O" });

步骤二:用 JavaScript 数据更新网页内容

动态设置玩家名称

  • 目标:将网页上显示的玩家 2 的名称更新为players数组中存储的数据。
  • 实现
    1. 通过 document.getElementById('p2-name') 获取目标元素。
    2. 访问其 textContent 属性。
    3. 通过索引访问 players 数组中的第二个玩家对象,并获取其 name 属性。
    4. 将获取到的名字赋值给元素的 textContent
// 获取第二个玩家(索引为1)的名称并更新到页面上
document.getElementById("p2-name").textContent = players[1].name;

步骤三:获取并操作棋盘方格

选择所有的棋盘方格

  • 目标:获取页面上代表棋盘的所有九个方格元素。
  • 提供了三种方法
    1. getElementsByClassName:
      • document.getElementsByClassName('square')
      • 根据 CSS 类名选择元素。
    2. querySelectorAll:
      • document.querySelectorAll('.square')
      • 使用 CSS 选择器语法(.代表类)来选择所有匹配的元素。
    3. 访问子元素:
      • 先获取棋盘的父容器(div,ID 为board)。
      • 然后使用其 .children 属性获取所有的子元素(即方格)。

理解“类数组”对象

  • 上述方法返回的集合(如 HTMLCollectionNodeList)虽然不是真正的数组,但其行为类似数组
  • 关键特性:它具有索引,我们可以像操作数组一样通过 squares[0]squares[4] 等方式访问其中的单个元素。

步骤四:实现下棋动作

玩家在指定方格下棋

  • 玩家 1 下在中间 (第五个方格,索引为 4):

    • 选中方格: squares[4]
    • 设置内容: squares[4].textContent = players[0].symbol;
  • 玩家 2 下在左上角 (第一个方格,索引为 0):

    • 选中方格: squares[0]
    • 设置内容: squares[0].textContent = players[1].symbol;

强调最佳实践

  • 不要硬编码:直接在代码中写入 'X''O' 是不好的做法(the lazy way)。
  • 从数据源获取:应该从 players 对象中动态地拉取玩家的 symbol 属性,这样代码更灵活、更易于维护。

总结

  • 我们现在拥有的对象和数组知识是强大的“超能力” 。
  • 利用这些能力,可以与 document 对象进行交互,对网页元素进行增删改查。
  • 最终目标是“告诉计算机我们想让它做什么”,而数据结构和 DOM 操作就是实现这一目标的强大工具。

38-javascript-pop-quiz-project-setup

  • 项目介绍:JavaScript 小测验
    • 我们将构建一个简单的网页测验游戏。
    • 页面上会显示一个关于 JavaScript 的陈述。
    • 用户点击 "True" 或 "False" 按钮来作答。
    • 页面会根据用户的答案反馈正确(绿色)或错误(红色),并显示一个解释。
  • 项目设置
    • 从控制台到文件: 我们将不再只在浏览器控制台中编写代码,而是将 JavaScript 代码写入一个本地的 .html 文件中。
    • 文本编辑器:
      • 任何文本编辑器都可以,如系统自带的 TextEdit(macOS) 或
      • 专业的代码编辑器,如 VS Code (Visual Studio Code),它提供了语法高亮、代码提示等便利功能。
    • 获取起始文件:
      • 从课程网站下载 JS-Quiz-starter.html 文件。
      • 将其保存在本地,并用你选择的文本编辑器打开。
  • 在 HTML 中编写 JavaScript
    • JavaScript 代码需要放在 HTML 文档的 <script></script> 标签之间。
    • <script> 标签可以放在 <head><body> 中,通常做法是放在 <body> 的末尾,以确保在 JS 执行前,页面所有元素都已加载。
  • 代码注释 (Code Comments)
    • 在 JavaScript 中,以 // 开头的行是单行注释
    • 注释中的内容会被 JavaScript 引擎忽略,它不会被执行。
    • 作用:
      • 为自己或他人解释代码的意图。
      • 记录待办事项 (TODOs)。
      • 临时禁用某行代码进行调试。
    • 注释对于代码的可读性和可维护性至关重要。
  • 关于项目文件结构 (问答)
    • 问题: 为什么 CSS 和 JavaScript 都在同一个 HTML 文件里?
    • 回答:
      • 为了简化本次学习项目,我们将所有代码放在一个文件中,便于管理和理解。
      • 真实的大型项目中,通常会将 HTML、CSS 和 JavaScript 分离到不同的文件中,以实现更好的组织和模块化。

39-dom-exercise

  • TODO #1: 声明变量以引用 DOM 元素
    • 关键词选择: 使用 const 而不是 let 是一个好选择,因为这些 DOM 元素的引用通常在整个程序运行期间都不会改变。
    • statement 变量:
      const statement = document.getElementById("statement");
      
    • optionButtons 变量 (多种方法):
      1. const optionButtons = document.querySelectorAll('button'); (获取页面上所有的 <button> 元素)。
      2. const optionButtons = document.querySelector('#options').children; (先获取 ID 为 options 的 div,再获取其所有子元素)。
      • 两种方法在此项目中结果相同,但第二种更精确地符合“获取 options div 内的元素”这一描述。
    • explanation 变量:
      const explanation = document.getElementById("explanation");
      
  • 迭代开发流程:
    1. 在你的本地文本编辑器中修改 .html 文件。
    2. 保存文件。
    3. 在浏览器中打开这个本地 html 文件(而不是课程网站上的链接)。
    4. 刷新浏览器页面,查看你代码的最新效果。
    5. 使用浏览器的开发者工具(特别是控制台)来检查变量和调试代码。

40-declaring-assigning-a-variable

  • TODO #2: 创建代表一个事实的 fact 对象
    • 这个对象应该包含三个属性:statement (陈述文本)、answer (正确答案) 和 explanation (解释文本)。
    • 示例:
      const fact = {
        statement: "Arrays are just objects.",
        answer: true, // 可以是布尔值 true/false
        // answer: "True", 也可以是字符串 "True"/"False"
        explanation:
          "Arrays are a kind of object with special numeric properties.",
      };
      
      • answer 属性的值使用布尔值 (true/false) 是更常见的做法,因为它能更直接地代表真假。
  • 关于对象属性名加引号 (问答)
    • 问题: 对象的属性名(键)是否需要加引号?
    • 回答:
      • 通常不需要:如果属性名是一个有效的 JavaScript 标识符(即不含空格、特殊字符,不以数字开头),你可以不加引号。
      • 需要加引号: 如果属性名包含空格或特殊字符,或者你希望使用一个保留关键字作为属性名,那么必须用引号把它包起来。
      • 访问: 对于带引号的、不规范的属性名,你只能使用方括号表示法 (obj['my-long-property name']) 来访问,而不能使用点表示法。
  • 语句结束的分号: 即使对象声明跨越多行,它仍然是一个单一的声明语句。在语句的末尾(即最后的 } 之后)加上分号是一个好习惯。

41-setting-statement-element

  • TODO #3: 将事实陈述显示在页面上
    • 我们需要将 fact 对象中的 statement 属性的值,设置为 statement 页面元素的文本内容。
    • 解决方案:
      statement.textContent = fact.statement;
      
      • statement: 我们在 TODO #1 中声明的、指向页面上 <div id="statement"> 的变量。
      • .textContent: 这是 DOM 元素的一个属性,用于获取或设置其内部的纯文本内容。
      • fact.statement: 从我们的 fact 对象中获取事实陈述的字符串。
  • 属性名的大小写敏感 (Case Sensitivity)
    • textContenttextcontent 是两个完全不同的属性名。JavaScript 是大小写敏感的。
    • 使用 VS Code 这样的 IDE (集成开发环境) 有助于避免这类拼写错误,因为它会提供代码自动补全和提示。

42-functions-parameters-arguments

  • 函数 (Functions)
    • 函数是 JavaScript 中用于执行特定任务或计算值的可重用代码块。它们是 "做事情" 的部分。
  • 声明函数 (Declaring a Function)
    • 语法:
      function functionName(parameter1, parameter2) {
        // code to be executed
        return someValue;
      }
      
    • function 关键字:表示这是一个函数声明。
    • functionName:函数的名称。
    • ():括号内是**参数 (parameters)**列表,它们是函数期望接收的输入的占位符。
    • {}:花括号内是函数体 (function body),包含了函数的具体代码。
    • return 关键字:指定函数执行后应返回的值。
  • 调用函数 (Calling a Function)
    • 使用函数名和括号来调用它,括号内提供实际的值,这些值称为参数 (arguments)
    • let result = functionName(argument1, argument2);
  • 参数 vs. 实参 (Parameters vs. Arguments)
    • 参数 (Parameters):在声明函数时,定义的占位符变量名,如 x, y
    • 实参 (Arguments):在调用函数时,传递给函数的实际值,如 2, 3
  • 无参数函数
    • 函数可以不需要任何输入来完成工作。这种情况下,参数列表为空 ()
    • 示例: 一个生成随机数的函数可以不接收任何参数。
  • 函数参数的灵活性 (太灵活了!)
    • 参数太少: 如果调用函数时提供的实参数量少于参数数量,未被赋值的参数将得到 undefined 值。
    • 参数太多: 如果提供的实参数量多于参数数量,多余的实参会被忽略
    • JavaScript 不会因为参数数量错误而报错。这要求程序员自己去处理可能出问题的参数。

43-function-return-values

  • 返回值 (Return Values)
    • return 语句用于指定一个函数调用结束后应该“产出”或“返回”的值。
    • 这个返回值可以被赋给一个变量,或者在其他表达式中使用。
  • console.log vs. return
    • console.log(value): 只是将一个值打印到控制台,方便调试和查看。它本身不产出值
    • return value: 将一个值作为函数的最终结果返回,使其可以在程序的其他地方被使用。
  • return 语句的函数
    • 如果一个函数没有 return 语句,或者有一个空的 return;,那么它在被调用后会隐式地返回 undefined
    • 这类函数通常用于执行一些“副作用 (side effects)”,比如修改 DOM 元素、打印到控制台等,而不是为了计算一个值。
  • return 的终止作用
    • 一旦函数执行到 return 语句,它会立即停止执行并退出,return 后面的任何代码都不会被运行。

44-creating-functions-exercise

  • 练习: 声明三个函数:multiply, yell, 和 longerThan
  • 解决方案:
    function multiply(a, b) {
      return a * b;
    }
    
    function yell(saying) {
      console.log(saying.toUpperCase());
      // 注意: 这个函数没有明确的 return 语句,所以它会返回 undefined
    }
    
    function longerThan(a1, a2) {
      return a1.length > a2.length;
      // a1.length > a2.length 这个表达式会计算出一个布尔值 (true 或 false)
    }
    
  • 匿名函数表达式:
    • 你也可以不给函数命名,而是将一个函数表达式赋值给一个变量:const myFunc = function(params) { ... };。这在功能上与命名函数声明等价。

45-arrow-functions

  • 箭头函数 (Arrow Functions)
    • 一种更简洁的函数声明语法,特别适合于写小而简单的函数。
    • 语法: (param1, param2) => expression
  • 箭头函数的特点:
    • 简洁: 省略了 functionreturn 关键字。
    • 隐式返回 (Implicit Return): 如果函数体只有一个表达式,箭头函数会自动返回这个表达式的结果。
      • const add = (x, y) => x + y; 等价于 function add(x, y) { return x + y; }
    • 单个参数可省略括号: 如果函数只有一个参数,可以省略参数外面的括号:x => x * 2
    • 多行函数体: 如果函数体需要包含多行代码,你需要用花括号 {} 包裹起来。在这种情况下,必须显式地使用 return 关键字来返回值。
      const complexFunc = (a, b) => {
        console.log("Doing something...");
        return a + b;
      };
      
    • this 关键字的行为: 箭头函数和普通函数在 this 的处理上存在重要区别,这是高级话题,但值得注意。

46-arrow-functions-exercise

  • 练习: 使用箭头函数语法重写之前的练习,创建 divide, whisper, 和 shorterThan
  • 解决方案:
    • const divide = (x, y) => x / y;
    • const whisper = text => console.log(text.toLowerCase()); (只打印,隐式返回 undefined)
      • 如果既要打印又要返回,则需使用多行函数体。
    • const shorterThan = (a1, a2) => a1.length < a2.length;
      • 可以直接比较数组的 .length 属性。

47-quiz-project-functions-exercise

  • 任务: 回到测验项目,完成 TODO #4 和 #5。
  • TODO #4: disable/enable 函数
    • 目标是创建两个函数,一个用于禁用按钮,一个用于启用按钮。
    • 这需要操作 HTML 元素的 disabled 属性。
    • 调查: 发现可以通过 element.setAttribute()element.removeAttribute() 方法来完成。
  • TODO #5: isCorrect 函数
    • 目标是创建一个函数,它接收一个用户的猜测(字符串),并将其与 fact 对象中的正确答案进行比较,返回 truefalse

48-quiz-project-disable-enable-solution

  • 解决方案: disable/enable 函数

  • 关键知识点: 操作属性

    • element.setAttribute('attributeName', 'value'): 设置或更新一个属性。对于 disabled 属性,value 可以是任何字符串(通常用空字符串 '')。
    • element.removeAttribute('attributeName'): 完全移除一个属性。
    • disabled 属性的特性: 只要 disabled 属性存在于元素上(不管其值是什么),该元素就会被禁用。要启用它,必须移除这个属性。
  • 函数实现:

    const disable = (button) => {
      button.setAttribute("disabled", "");
    };
    
    const enable = (button) => {
      button.removeAttribute("disabled");
    };
    
  • 测试: 创建了函数后,可以立即在控制台进行测试,例如 disable(optionButtons[0]),以验证其功能是否正确。

49-quiz-project-iscorrect-solution

  • 解决方案: isCorrect 函数
  • 函数签名 (Function Signature):
    • 这个函数需要接收一个参数,代表用户的猜测。
    • function isCorrect(guess) { ... }
  • 函数体 (Function Body):
    • 核心逻辑是比较传入的 guess 和储存在 fact 对象里的 answer
    • return guess === fact.answer;
  • 类型问题:
    • 用户点击按钮得到的值通常是一个字符串 (e.g., "true")。
    • 而你在 fact 对象中存储的答案可能是布尔值 (e.g., true)。
    • 使用 === 进行比较时,"true" === true 会返回 false,因为它们的类型不同。

50-boolean-q-a

  • 问答: 如何解决 isCorrect 函数中的类型不匹配问题?
  • 解决方案: 在比较之前,确保两边的类型一致。
    • 一个好的方法是将布尔值的答案转换为字符串,再进行比较。
    • .toString() 方法可以将布尔值 true 转换为字符串 "true"
  • 修正后的 isCorrect 函数:
    const isCorrect = (guessString) => {
      // 确保我们比较的是两个字符串
      return guessString === fact.answer.toString();
    };
    

51-scope

  • 作用域 (Scope)
    • 作用域决定了在代码的什么位置可以访问哪些变量。
  • 全局作用域 (Global Scope)
    • 在代码的最外层定义的变量处于全局作用域。它们在程序的任何地方都可以被访问。
  • 函数作用域 (Function Scope)
    • 每当声明一个函数时,就会创建一个新的函数作用域
    • 在函数内部用 letconst 声明的变量,只在该函数内部可见,外部无法访问。
  • 作用域链 (Scope Chain) / “豪华轿车”模型
    • 从外看不到内: 你不能从一个外部作用域(如全局作用域)“看到”或访问一个内部作用域(如函数作用域)中定义的变量。就像你无法看透豪华轿车的有色车窗
    • 从内能看到外: 你可以从一个内部作用域“看到”或访问外部作用域中定义的变量。就像坐在轿车里的人可以向外看。
    • 当在内部作用域中引用一个变量时,JavaScript 会先在当前作用域查找。如果没找到,它会沿着作用域链向外层作用域继续查找,直到找到为止,或者到达全局作用-域仍未找到,则抛出 ReferenceError

52-let-scope

  • 在内部作用域修改外部变量
    • 由于内部作用域可以“看到”外部作用域的变量,所以如果外部变量是用 let 声明的(即可重新赋值的),那么在内部作用域可以修改这个外部变量的指向。
    • 示例: trap 函数修改了全局变量 feeling 的值。
  • 为什么 const 更安全:
    • 如果外部变量是用 const 声明的,由于其指向不可变,尝试在任何地方(包括内部作用域)重新赋值都会导致错误。
    • 这可以防止函数产生意料之外的“副作用 (side effects)”,即修改了不属于它自己的变量。
  • 不可变性回顾 (问答)
    • 问题: 修改 let 变量的指向不是与字符串的不可变性冲突吗?
    • 回答: 不冲突。
      • 字符串是不可变的,意味着 "free" 这个值本身不能被改变。
      • let 变量是可重新赋值的,意味着 feeling 这个变量的指向可以从 "free" 改变为 "boxed in"
      • 我们没有改变字符串值,而是改变了变量指向哪个字符串值。

53-var-vs-let

  • var 关键字

    • 是 ES6 之前声明变量的唯一方式。
    • 它与 letconst 在作用域规则上存在重要差异。
  • 函数作用域 vs. 块级作用域 (Function Scope vs. Block Scope)

    • var: 只有函数作用域var 声明的变量只在它所在的函数内部有效,它会忽略{}(如 if 语句或 for 循环的 {})创建的块级作用域
    • let/const: 具有块级作用域。它们声明的变量只在它们所在的 {} 块内部有效。这是更直观和可预测的行为。
  • 示例:

    // var 忽略块级作用域
    var x = 10;
    if (true) {
      var x = 20; // 这里修改的是外层的 x
    }
    console.log(x); // 输出 20
    
    // let 遵守块级作用域
    let y = 10;
    if (true) {
      let y = 20; // 这里创建了一个新的、只在 if 块内有效的 y
    }
    console.log(y); // 输出 10
    
  • 结论:

    • 应当优先使用 letconst,因为它们的块级作用域行为更符合预期,能减少很多潜在的 bug。
    • var 应被视为历史遗留产物。了解它的行为有助于阅读老代码,但在新代码中应避免使用。

54-event-listeners

  • 回顾项目: 我们的目标是完成一个交互式的 JavaScript 测验游戏。当用户点击答案时,页面会给出反馈(颜色变化、显示解释)。
  • 事件 (Events)
    • 事件是让网页变得交互的关键。
    • 它是用户在网页上的行为(如点击、悬停、按键)或者浏览器自身发生的状态变化(如页面加载完成)。
    • 事件处理流程:
      1. 用户执行一个动作(例如,点击一个按钮)。
      2. 浏览器触发 (fires) 一个对应的事件(例如,一个 click 事件)。
      3. 我们的 JavaScript 代码需要监听 (listen for) 这个事件。
      4. 当监听到事件时,执行一个我们预先定义好的函数,这个函数被称为事件处理程序 (event handler)回调函数 (callback function)
  • addEventListener() 方法
    • 这是我们将事件处理程序附加到特定 DOM 元素上的核心方法。
    • 语法: element.addEventListener(eventName, handlerFunction);
    • 参数:
      1. eventName (字符串): 我们想要监听的事件的名称,如 'click', 'mouseover' 等。
      2. handlerFunction (函数): 当事件发生时,我们希望执行的函数。这个函数通常是匿名的(例如,一个箭头函数),因为它只在这里被使用。
  • 示例:
    document.addEventListener("click", () => {
      console.log("Clicked!");
    });
    
    • 这段代码会给整个 document 对象添加一个点击事件监听器。
    • 现在,无论你在页面的任何地方点击,控制台都会打印出 "Clicked!"。

55-event-object

  • 在事件处理程序中执行操作
    • 事件处理程序就是一个普通的 JavaScript 函数,你可以在里面做任何你想做的事情:
      • 修改 DOM 元素的文本内容。
      • 添加或移除 CSS 类来改变样式。
      • 调用其他函数。
  • 事件对象 (The Event Object)
    • 当浏览器调用我们的事件处理程序时,它会自动向该函数传递一个参数,这个参数就是事件对象
    • 我们可以在函数定义中通过一个参数名(通常简写为 e 或全写为 event)来捕获它。
      element.addEventListener("click", (event) => {
        // 现在我们可以使用 event 对象了
        console.log(event);
      });
      
    • 这个对象包含了关于刚刚发生的事件的所有详细信息
  • event.target
    • 事件对象上一个极其重要的属性。
    • event.target 指向最初触发事件的那个 DOM 元素
    • 例如,即使你把事件监听器加在了整个 document 上,event.target 也会告诉你用户具体点击的是哪个按钮、哪个段落或哪个标题。这对于事件委托等高级技巧至关重要。
  • 其他常见事件类型
    • dblclick: 双击事件。
    • mouseover: 鼠标指针移入一个元素时触发。
    • mouseout: 鼠标指针移出一个元素时触发。
    • focus: 元素获得焦点时触发(例如,通过点击或 Tab 键)。
    • 可以在 MDN 上找到完整的事件列表。

56-events-exercise

  • 练习: 在控制台中为测验页面添加临时事件监听器。

  • 任务 1: 点击 true 按钮时,将其文本大写

    // 1. 获取按钮
    const trueButton = optionButtons[0];
    
    // 2. 添加事件监听器
    trueButton.addEventListener("click", (event) => {
      // 3. 在处理程序中修改文本
      trueButton.textContent = trueButton.textContent.toUpperCase();
    });
    
  • 任务 2: 鼠标悬停在 H1 标题上时改变文本,移开时改回来

    // 1. 获取 H1 元素
    const h1 = document.querySelector("h1"); // querySelector 更简洁
    
    // 2. 添加 mouseover 监听器
    h1.addEventListener("mouseover", () => {
      h1.textContent = "Hovering!";
    });
    
    // 3. 添加 mouseout 监听器
    h1.addEventListener("mouseout", () => {
      h1.textContent = "Quiz.js";
    });
    
  • 要点: 在控制台中添加的事件监听器是临时的,刷新页面后就会消失。要让它们永久生效,必须将代码写入 .html 文件中的 <script> 标签内。

57-conditionals

  • 条件语句 (Conditionals)
    • 允许我们的代码根据某个条件的真假来做出决策,决定执行哪一段代码。
  • if 语句
    • 语法: if (condition) { ... }
    • 如果 condition 的计算结果为 true,则执行花括号 {} 内的代码块。
  • if...else 语句
    • 语法: if (condition) { ... } else { ... }
    • 如果 conditiontrue,执行 if 块的代码。
    • 如果 conditionfalse,执行 else 块的代码。
  • if...else if...else
    • 用于处理多种可能性。
    • JavaScript 会按顺序检查每个 ifelse if 的条件,一旦找到一个为 true 的,就执行其代码块,然后跳出整个条件链。如果所有条件都为 false,则执行最后的 else 块(如果存在)。
  • Truthy 和 Falsy
    • 当在条件语句中使用的不是一个纯粹的布尔值时,JavaScript 会自动将其转换为布尔值,这个过程遵循 "truthy" (真值) 和 "falsy" (假值) 的规则。
    • Falsy 值 (6 个):
      • false
      • 0 (数字零)
      • "" (空字符串)
      • null
      • undefined
      • NaN (Not a Number)
    • Truthy 值: 除了上面 6 个 falsy 值以外的所有其他值都是 truthy,包括:
      • 所有非空字符串 (如 "hello", "false")
      • 所有非零数字 (如 1, 10)
      • 所有对象 (包括 {}, [])

58-conditionals-exercise

  • 练习: 练习使用条件语句。
  • 任务 1: 比较名字长度
    • 核心是比较 firstName.lengthlastName.length
  • 任务 2: isEmpty 函数
    • 简洁实现: const isEmpty = (arr) => arr.length === 0;
  • 任务 3: 空数组的 Truthiness
    • 测试: if ([]) { console.log('truthy'); } else { console.log('falsy'); }
    • 结果: 空数组 []truthy 的。因为在 JavaScript 中,所有对象(包括数组)都是 truthy。
    • 对比: 空字符串 ""falsy 的。
  • 关键点: 不要混淆一个值本身和它的 truthiness。[] 是 truthy,但 [].length0,而 0 是 falsy。

59-logical-ternary-operators

  • 逻辑运算符 (Logical Operators)
    • 用于组合或反转布尔值。
  • ! (逻辑非 - NOT)
    • 反转一个布尔值:!true 结果是 false!false 结果是 true
    • 它会返回其操作数的 truthiness 的相反布尔值。
  • && (逻辑与 - AND)
    • 如果两个操作数都为 truthy,则结果为 truthy。只要有一个是 falsy,结果就是 falsy。
    • true && true -> true
    • true && false -> false
  • || (逻辑或 - OR)
    • 只要有一个操作数为 truthy,结果就为 truthy。只有两个都为 falsy 时,结果才是 falsy。
    • true || false -> true
    • false || false -> false
  • 三元运算符 (Ternary Operator)
    • if...else 语句的简洁写法,也是 JavaScript 中唯一一个需要三个操作数的运算符。
    • 语法: condition ? value_if_true : value_if_false
    • 示例: const mood = (forecast === 'sunny') ? 'happy' : 'sad';
    • 如果 condition 为真,整个表达式的值就是 value_if_true;否则就是 value_if_false

60-loops

  • 循环 (Loops)
    • 允许我们重复执行同一段代码多次。这个过程也叫迭代 (iteration)
  • 传统 for 循环
    • 语法: for (initialization; condition; final-expression) { ... }
      1. initialization: 初始化一个计数器变量(只在循环开始时执行一次)。
      2. condition: 每次循环开始前检查的条件。如果为 true,则执行循环体;如果为 false,则退出循环。
      3. final-expression: 每次循环结束后执行的表达式,通常用于更新计数器。
    • 虽然功能强大,但写法相对繁琐。
  • for...of 循环 (推荐)
    • 一种更现代、更简洁的循环语法,专门用于遍历可迭代对象 (iterables),如数组字符串
    • 语法: for (let item of collection) { ... }
    • 它会自动遍历 collection 中的每一个 item,你不需要手动管理索引和条件。
    • 示例:
      const numbers = [10, 20, 30];
      for (let number of numbers) {
        console.log(number); // 会依次打印 10, 20, 30
      }
      
  • 注意: for...offor...in 是不同的。for...of 遍历,而 for...in 遍历对象的键(属性名)。在遍历数组时,几乎总是应该使用 for...of

61-explanation-loop-project-exercise

  • 任务: 完成 TODO #6。
  • TODO #6: 为所有选项按钮添加点击事件监听器
    • 思路: 我们需要遍历 optionButtons 集合,并为其中的每一个按钮调用 addEventListener
    • for...of 循环是完成这个任务的完美工具。
  • 代码实现:
    // 1. 开始循环
    for (const button of optionButtons) {
      // 2. 在循环内部为当前按钮添加监听器
      button.addEventListener("click", (event) => {
        // 3. 在事件处理程序中,执行点击后需要发生的事
        // 例如:显示解释文本
        explanation.textContent = fact.explanation;
      });
    }
    
  • 执行时机:
    • for 循环本身是在页面加载、脚本运行时立即执行的。它的任务是“设置”好所有的事件监听器。
    • 而事件处理程序(addEventListener 的第二个参数,那个箭头函数)中的代码,只有在用户未来某个时间点点击按钮时才会被执行。

62-disable-loop-project-exercise

  • 任务: 完成 TODO #7。

  • TODO #7: 点击任一按钮后,禁用所有按钮

    • 思路: 这个“禁用”的动作需要在事件处理程序内部完成。当用户点击后,我们需要再次遍历所有的按钮,并调用我们之前写的 disable() 函数。
  • 代码实现 (在事件处理程序内部):

    // ... 在 addEventListener 的回调函数内部 ...
    
    // 再次循环,禁用所有按钮
    for (const otherButton of optionButtons) {
      disable(otherButton); //调用我们之前写的 helper function
    }
    
  • 作用域和闭包 (问答):

    • 这是一个高级话题,但关键点在于:事件处理程序函数会“记住”它被创建时所在的作用域。
    • 即使外层的 for 循环已经结束,每个按钮的事件处理程序仍然能够访问到它自己被创建时对应的那个 button 变量。这个现象叫做闭包 (closure)

63-iscorrect-project-exercise

  • 任务: 完成 TODO #8。

  • TODO #8: 根据答案的正确性,为被点击的按钮添加样式

    • 思路:
      1. 在事件处理程序中,获取被点击按钮的值。
      2. 使用我们之前写的 isCorrect() 函数来判断这个值是否正确。
      3. 使用 if...else 条件语句,根据 isCorrect() 的返回结果,为被点击的按钮添加 "correct""incorrect" 的 CSS 类。
  • 获取被点击的按钮:

    • 方法 1 (闭包): 直接使用外层 for 循环中的 button 变量。由于闭包,每个事件处理程序都“知道”自己是属于哪个按钮的。
    • 方法 2 (event.target): const clickedButton = event.target; 也可以获取到被点击的那个按钮。
  • 操作 CSS 类:

    • element.classList.add('className'): 为元素添加一个 CSS 类。
  • 代码实现 (在事件处理程序内部):

    // ... 在 addEventListener 的回调函数内部 ...
    
    const guess = button.value; // 或者 event.target.value
    
    if (isCorrect(guess)) {
      button.classList.add("correct");
    } else {
      button.classList.add("incorrect");
    }
    
  • 项目完成: 至此,我们已经成功构建了一个功能完整的、交互式的 JavaScript 测验游戏!你可以尝试扩展它,比如加入多道题目和“下一题”功能。

64-map-filter

  • 回顾: 我们已经完成了测验项目,现在将学习一些高级的数组方法,为下一个项目做准备。

  • 高阶数组方法: map, filter (以及 reduce) 是强大的函数式编程工具,用于处理数组。

  • map() 方法:

    • 作用: 遍历数组中的每一个元素,并对每个元素执行一个你提供的函数。然后,它会返回一个新的数组,这个新数组包含了每次函数调用的返回值。
    • 核心特性:
      • 不会修改原始数组 (非破坏性)。
      • 返回的新数组长度和原始数组长度相同
    • 示例: 从一个包含 Spice Girls 对象的数组中,提取出每个人的昵称,形成一个新的字符串数组。
      const nicknames = spices.map((s) => s.nickname + " Spice");
      // 结果: ['Baby Spice', 'Ginger Spice', ...]
      
    • map 非常适合用于转换数据,例如从对象数组中提取特定属性,或对每个数字进行计算。
  • 模板字面量 (Template Literals)

    • 一种更强大的字符串声明方式,使用反引号 (``)

    • 特点 (字符串插值): 允许你在字符串中直接嵌入变量或表达式。

    • 语法: ${expression}

    • 示例:

      const name = "Anjana";
      const greeting = `Hello, ${name}!`; // 结果: "Hello, Anjana!"
      
      const sum = `1 + 2 is ${1 + 2}`; // 结果: "1 + 2 is 3"
      
    • 这比使用 + 来拼接字符串要更简洁、更可读。

  • filter() 方法:

    • 作用: 遍历数组中的每一个元素,并对每个元素执行一个“predicate”函数。这个函数需要返回 truefalsefilter 会返回一个新的数组,只包含那些让测试函数返回 true 的原始元素。
    • 核心特性:
      • 不会修改原始数组
      • 返回的新数组长度通常小于或等于原始数组长度。
    • 示例: 从 Spice Girls 数组中,只筛选出名字包含 "Mel" 的成员。
      const mels = spices.filter((s) => s.name.includes("Mel"));
      // 结果: 一个只包含 Mel B 和 Mel C 对象的新数组
      
    • filter 非常适合用于根据某些条件筛选数据

65-map-filter-exercise

  • 练习: 使用 mapfilter 操作 Spice Girls 数组。
  • 任务 1 (map): 创建一个只包含她们真实姓名的新数组 names
    const names = spices.map((spice) => spice.name);
    // 结果: ['Emma', 'Geri', 'Melanie', 'Melanie', 'Victoria']
    
  • 任务 2 (filter): 创建一个新数组 endInY,只包含昵称以 "y" 结尾的成员对象。
    const endInY = spices.filter((s) => s.nickname.endsWith("y"));
    // 结果: 一个包含 Baby, Scary, Sporty Spice 对象的新数组
    

66-spread

  • 展开语法 (Spread Syntax)
    • 使用三个点 ...,可以将一个可迭代对象(如数组或字符串)“展开”成其独立的元素。
  • 在数组字面量中使用:
    • 可以轻松地将一个数组的元素合并到另一个新数组中。
    • 这是数组拼接 (concat) 的一种现代化、更简洁的替代方案。
    • 示例:
      const oldBurns = ["square", "wack"];
      const newBurns = ["basic", "dusty", "sus"];
      const burnBook = [...oldBurns, ...newBurns, "fetch"];
      // 结果: ['square', 'wack', 'basic', 'dusty', 'sus', 'fetch']
      
  • 在函数调用中使用:
    • 可以将一个数组的元素作为独立的参数传递给一个函数。
    • 示例:
      const newSkills = ["React", "TypeScript"];
      skills.push(...newSkills); // 等价于 skills.push('React', 'TypeScript');
      

67-doggos-quiz-game-setup

  • 新项目: Doggos 猜谜游戏
    • 玩法:
      1. 页面显示一张狗狗的图片。
      2. 提供几个狗的品种作为选项。
      3. 用户点击一个选项进行猜测。
      4. 页面反馈正确(绿色)或错误(红色),并高亮正确答案。
      5. 每次刷新页面,都会加载一张新的狗狗图片和新的选项。
    • 技术升级:
      • 我们将动态地从外部获取数据(狗狗图片和品种)。
      • 我们将动态地创建和添加 DOM 元素(选项按钮)。
  • 项目设置:
    1. 从课程网站下载 Doggo-Fetch-starter.html 文件。
    2. 保存到本地。
    3. 用你的代码编辑器打开。
    4. 检查文件内容,你会看到一些预设的 CSS、HTML 结构和一些需要我们填充的 JavaScript 函数。

68-while-loops

  • while 循环
    • 一种基于条件的循环。
    • 语法: while (condition) { ... }
    • 只要 condition 持续为 true,循环体内的代码就会一直重复执行
    • 应用场景: 当你不知道具体要循环多少次,但知道循环应该在什么条件下停止时,while 循环非常有用。
  • 示例: 生成一个包含 5 个随机数的数组。
    const randomNumbers = [];
    while (randomNumbers.length < 5) {
      randomNumbers.push(Math.random());
    }
    
    • 循环会一直运行,直到 randomNumbers 数组的长度达到 5 为止。
  • 危险: 无限循环 (Infinite Loop)
    • 如果 while 循环的条件永远为真 (例如 while (true)),它将永远不会停止。
    • 这会导致浏览器标签页卡死,甚至可能使整个浏览器崩溃。务必小心,确保你的循环条件最终会变为 false

69-doggo-quiz-while-exercise

  • 任务: 完成 getMultipleChoices 函数。
  • 目标:
    1. 从一个大的 possibleChoices 数组中,生成一个包含 n 个选项的新数组。
    2. 确保 correctAnswer 必须是这 n 个选项之一。
    3. 确保所有 n 个选项都是独一无二的(没有重复)。
    4. 最终返回的选项数组的顺序是随机的。
  • 思路分解:
    1. 创建一个空的 choices 数组用于存放结果。
    2. 首先,将 correctAnswer 直接 pushchoices 数组中,保证它一定在内。
    3. 使用 while 循环,条件是 choices.length < n,继续添加选项直到数量足够。
    4. while 循环内部: a. 使用 getRandomElement()possibleChoices 中随机获取一个“候选”选项。 b. 使用 if (!choices.includes(candidate)) 来检查这个候选选项是否已经存在choices 数组中。 c. 只有当它不存在时,才将其 pushchoices 数组。
    5. 循环结束后,使用预置的 shuffleArray(choices) 函数打乱数组顺序。
    6. 返回被打乱后的 choices 数组。

70-doggo-quiz-while-solution

  • 代码实现
    function getMultipleChoices(possibleChoices, correctAnswer, n) {
      const choices = [];
      choices.push(correctAnswer);
      
      while (choices.length < n) {
        const candidate = getRandomElement(possibleChoices);
        if (!choices.includes(candidate)) {
          choices.push(candidate);
        }
      }
      
      shuffleArray(choices);
      return choices;
    }
    
  • 测试: 通过在控制台中调用这个函数并传入不同的参数(不同的 n,不同的 correctAnswer 等),来验证其功能是否符合预期。

71-doggo-quiz-while-review

  • 总结: 这个 getMultipleChoices 函数综合运用了我们学到的许多概念:
    • 数组的创建和操作 (push)。
    • while 循环和条件逻辑。
    • if 语句和逻辑运算符 (!)。
    • 内置方法 (.includes())。
    • 调用自定义的辅助函数 (getRandomElement, shuffleArray)。
    • 这展示了如何通过组合简单的构建块来解决更复杂的问题,这是编程的核心思想。

72-settimeout

  • 同步 vs. 异步 (Synchronous vs. Asynchronous)
    • 同步 (Synchronous): 代码一行一行地、按顺序执行。后一行代码必须等待前一行代码执行完毕才能开始。这是我们到目前为止接触到的大部分代码的执行方式。
    • 异步 (Asynchronous): 某些操作(特别是耗时的操作)不会阻塞后续代码的执行。JavaScript 会“启动”这个操作,然后立即继续执行后面的代码。当那个耗时操作完成后,它会通过某种机制(如回调函数)来通知我们。
  • 为什么需要异步?
    • JavaScript 是单线程的,意味着它在同一时间只能做一件事。
    • 如果一个耗时的操作(如等待网络请求返回、等待用户点击)是同步的,它会阻塞整个程序,导致网页界面卡死,无法响应任何用户操作。
    • 异步模型让 JavaScript 在等待这些耗时操作的同时,仍然能够保持界面的响应性。
  • setTimeout()
    • 一个典型的异步函数。
    • 作用: 在指定的毫秒数之后,执行一个回调函数。
    • 语法: setTimeout(callbackFunction, delayInMilliseconds);
    • 执行流程:
      1. JavaScript 遇到 setTimeout
      2. 它将 callbackFunction 注册到一个“待办事项”列表,并设置一个定时器。
      3. JavaScript 不会等待,而是立即继续执行 setTimeout 后面的代码。
      4. delayInMilliseconds 过去后,callbackFunction 会被放入一个任务队列,等待主线程空闲时被执行。
  • 常见的异步操作:
    • 用户事件(我们不知道用户何时会点击按钮)。
    • 网络请求(从服务器加载数据)。
    • 文件操作(在 Node.js 中)。
    • 定时器(setTimeout, setInterval)。
  • 事件循环 (Event Loop)
    • 是 JavaScript 实现异步的核心机制。它是一个在幕后不断运行的进程,负责监视任务队列,并在主线程空闲时将队列中的任务取出并执行。
    • 这是一个复杂但非常重要的概念,推荐观看 Philip Roberts 的演讲 "What the heck is the event loop anyway?" 来深入理解。

73-apis-fetch

  • 回顾: 我们之前一直在使用硬编码(写死在代码里)的数据。现在,我们要学习如何从互联网上获取 (fetch) 动态数据。
  • API (Application Programming Interface)
    • API 是一种服务,它通过特定的 URL (称为端点 - endpoint) 暴露数据,让其他程序可以请求和使用。
    • Web 开发中,我们经常需要与各种 API 交互来获取数据,例如天气信息、股票价格、或者我们需要的狗狗图片。
    • 示例: dog.ceo 网站提供了一个 Dog API。访问其端点 https://dog.ceo/api/breed/hound/list 会返回一个包含所有猎犬品种列表的数据。
    • JSON (JavaScript Object Notation): API 返回的数据通常是 JSON 格式。这是一种轻量级的数据交换格式,其语法与 JavaScript 对象字面量非常相似,所以 JavaScript 可以很轻松地解析和使用它。
  • fetch() API
    • 是浏览器内置的一个强大函数,用于发起网络请求,从 URL 加载数据。
    • 它是我们与外部 API 通信的主要工具。
    • fetch() 是一个异步操作,因为它需要时间去等待网络响应。

74-working-with-promises

  • fetch() 的返回值
    • 当你调用 fetch(url) 时,它不会立即返回你想要的数据(如狗狗列表)。
    • 相反,它会立即返回一个特殊的对象,叫做 Promise
  • Promise (承诺)
    • Promise 是一个占位符,代表一个未来才会知道结果的异步操作。
    • 你可以把它想象成一张“欠条 (IOU)”。fetch 给了你一张欠条,说:“我保证会去拿数据,但现在还没拿到,你先拿着这张条。”
    • Promise 的三种状态:
      1. Pending (进行中): 初始状态。操作正在进行,结果未知。
      2. Fulfilled (已成功 / Resolved): 操作成功完成。Promise 现在持有了最终的
      3. Rejected (已失败): 操作失败。Promise 持有了一个错误 (error) 对象。
  • 处理 Promise:
    • 由于 fetch 返回的是 Promise,我们需要一种方式来处理这个 Promise,以便在它完成后(无论是成功还是失败)获取其结果。

75-using-await-with-promises

  • await 关键字
    • 是处理 Promise 的一种现代、更直观的方式。
    • 作用: await暂停当前函数的执行,等待它后面的 Promise 完成(进入 fulfilled 或 rejected 状态)。
    • 一旦 Promise 完成:
      • 如果 Promise 成功 (fulfilled)await 表达式会返回 Promise 持有的
      • 如果 Promise 失败 (rejected)await 会抛出 Promise 持有的错误
  • await 处理 fetch
    • 第一步: 获取响应 (Response)
      const response = await fetch(url);
      
      • await fetch(url) 会等待网络请求完成,然后返回一个 Response 对象。
      • 这个 Response 对象本身还不是我们最终想要的数据,它包含了关于 HTTP 响应的元信息(如状态码 200 OK)。
    • 第二步: 解析响应体 (Body)
      • 我们真正想要的数据在 response 对象的 body 里。
      • response 对象提供了一些方法来解析 body,最常用的是 .json(),它会将 body 解析为 JSON 对象。
      • 注意: .json() 方法本身也是一个异步操作,因为它需要时间来读取和解析数据流。因此,它也返回一个 Promise
    • 第三步: 获取最终数据
      const body = await response.json();
      
      • await response.json() 会等待 body 解析完成,并最终返回我们想要的 JavaScript 对象或数组。
    • 总结: 获取 JSON 数据的标准流程是两步 await
      1. await fetch() 获取 Response 对象。
      2. await response.json() 获取最终的 JSON 数据。
  • Promise.then(): 是另一种处理 Promise 的方法,属于更早期的回调风格。async/await 通常被认为更易读、更像同步代码。
  • JavaScript 不直接做网络请求
    • JavaScript 不会亲自去网络上获取数据。
    • 它委托给浏览器的其他组件(C++ 写的网络模块)。
    • 这些组件不在 JavaScript 的单线程中运行。

76-destructuring-objects-arrays

  • 解构赋值 (Destructuring Assignment)
    • 一种简洁的语法,允许你从数组或对象中“提取”值并赋给独立的变量。
  • 对象解构 (Object Destructuring)
    • 语法: const { property1, property2 } = someObject;
    • 它会创建两个新变量 property1property2,并将 someObject.property1someObject.property2 的值分别赋给它们。
    • 顺序不重要: 你可以按任何顺序提取属性,因为对象属性是无序的。
    • 在处理从 API 返回的、具有多个属性的大对象时非常有用。
  • 数组解构 (Array Destructuring)
    • 语法: const [firstItem, secondItem] = someArray;
    • 它会根据 位置(顺序) 来赋值。 firstItem 会得到 someArray[0] 的值,secondItem 会得到 someArray[1] 的值。
    • 顺序很重要
    • 可以跳过元素: const [ , , thirdItem] = arr; (使用逗号占位)。
    • 与展开语法结合 (Rest Pattern): const [first, ...rest] = arr; first 会得到第一个元素,rest 会得到一个包含其余所有元素的新数组。

77-destructuring-exercise

  • 任务: 完成 getBreedFromURL 函数。
  • 目标: 从一个类似 .../breeds/poodle-standard/n02093754_3933.jpg 的 URL 中,提取并格式化出品种名称,如 "standard poodle"
  • 挑战:
    • 品种部分可能是一个单词 (beagle) 或两个单词由连字符连接 (poodle-standard)。
    • 如果是两个单词,需要反转顺序。
  • 关键工具: String.prototype.split()
    • 'a-b-c'.split('-') 会返回 ['a', 'b', 'c']
    • 这是解析这种格式化字符串的核心。
  • 思路:
    1. / 分割整个 URL 字符串,获取包含品种的部分(例如,索引为 4 的部分)。
    2. 再用 分割这个品种部分。

78-destructuring-solution-return-breed

  • 解决方案 (续):
  • 获取品种部分:
    const unsplitBreed = url.split("/")[4];
    
    • 这会得到 poodle-standardbeagle
  • 进一步处理:
    // 方法一:使用 reverse() 和 join()
    const breedParts = unsplitBreed.split("-");
    breedParts.reverse();
    const formattedBreed = breedParts.join(' ').trim();
    return formattedBreed;
    
  • 另一种使用解构的思路:
    // 方法二:使用解构和条件判断
    const [breed, subBreed] = unsplitBreed.split('-');
    
    if (subBreed) {
      return subBreed + ' ' + breed;
    } else {
      return breed;
    }
    
    • 这种方法更明确,可能比依赖 .reverse().trim() 更健壮。

79-destructuring-solution-format-string

  • 方案整合与测试: 在实现功能后,用不同的输入(单品种 URL 和双品种 URL)来测试函数,确保它在所有情况下都能正确工作。
  • 处理 undefined: 在数组解构时,如果没有足够的元素来赋值,未被赋值的变量会得到 undefined。例如 const [a, b] = ['one']b 会是 undefined
  • 健壮性: 在实际编程中,选择更清晰、更能处理边界情况的方案非常重要。使用条件判断(如检查数组长度)通常比依赖一系列链式调用(如 .reverse().join().trim())更健-壮。

80-async-functions

  • async 函数
    • 问题: await 关键字不能在普通函数的顶层使用。它只能在特定的上下文中使用。
    • 解决方案: 为了能够使用 await,我们需要将函数声明为异步函数 (async function)
    • 语法: 在 function 关键字前加上 async
      async function fetchData() {
        // 现在可以在这里使用 await 了
        const response = await fetch(url);
        // ...
      }
      
    • 箭头函数语法: const fetchData = async () => { ... };
  • async 函数的返回值
    • 一个 async 函数总是隐式地返回一个 Promise
    • 如果函数体中 return 了一个普通值(如一个对象或数组),JavaScript 会将这个值包装在一个 fulfilled Promise 中返回。
    • 如果函数体中抛出了一个错误,它会被包装在一个 rejected Promise 中返回。
    • 这意味着: 调用一个 async 函数本身又是一个异步操作!要获取它的结果,你需要在调用它的地方也使用 await

81-async-function-exercise

  • 任务: 完成 fetchMessageasync 函数。

  • 目标: 将我们之前在控制台中分步执行的数据获取流程,封装到一个可重用的 async 函数中。

  • 代码实现:

    async function fetchMessage(url) {
      // 1. await fetch
      const response = await fetch(url);
    
      // 2. await .json()
      const body = await response.json();
    
      // 3. (可选) 解构
      const { message } = body;
    
      // 4. 返回最终数据
      return message;
    }
    
  • 调用:

    const dogData = await fetchMessage(someUrl);
    

82-adding-choice-buttons-exercise

  • 回顾项目结构:
    • 函数式风格: 我们将代码组织成多个独立的函数,每个函数负责一个特定的任务(获取数据、处理数据、渲染 UI)。这比将所有逻辑混在一起更清晰、更易于维护。
    • 数据流: 获取数据 -> 处理数据 -> 渲染数据。这是一个常见的前端开发模式。
  • 动态创建 DOM 元素:
    • 我们的 HTML 中没有预先写好的按钮。我们需要用 JavaScript 来创建它们。
    • 核心方法: document.createElement('tagName')
    • 示例: const myButton = document.createElement('button');
  • 任务: 完成 renderButtons 函数。
  • 目标: 接收一个选项数组 (choicesArray),并为数组中的每个选项创建一个按钮,设置其属性,并将其添加到页面上。

83-adding-choice-buttons-solution

  • 解决方案: renderButtons 函数

    function renderButtons(choicesArray) {
      // 获取容器元素
      const options = document.getElementById('options');
      
      // 遍历选择数组
      for (const choice of choicesArray) {
        // 创建按钮元素
        const button = document.createElement('button');
        
        // 设置按钮属性
        button.textContent = choice;
        button.value = choice;
        button.name = choice;
        
        // 添加点击事件监听器
        button.addEventListener('click', buttonHandler);
        
        // 将按钮添加到容器中
        options.appendChild(button);
      }
    }
    
  • 关键步骤说明:

    1. 遍历选项: 使用 for...of 循环遍历 choicesArray 数组
    2. 创建元素: 用 document.createElement('button') 创建按钮
    3. 设置属性: 为按钮设置文本内容、值和名称
    4. 绑定事件: 使用 addEventListener('click', buttonHandler) 添加点击处理
    5. 添加到DOM: 用 appendChild() 将按钮添加到页面容器中

84-render-quiz-exercise

  • 整合与执行:

    • 到目前为止,我们只定义了一堆函数,但还没有调用它们来启动整个程序。
    • 我们需要一个主执行流程。
  • 任务: 在脚本的最后,完成主逻辑。

  • 思路:

    1. 调用 loadQuizData() 函数来异步获取所有需要的数据(图片 URL、正确答案、选项)。因为它是 async 函数,所以我们必须 await 它。

    2. 使用数组解构来方便地从 loadQuizData 返回的数组中获取各个部分:

      const [imageUrl, correctAnswer, choices] = await loadQuizData();
      
    3. 将获取到的数据传递给 renderQuiz() 函数,让它来负责更新 UI。

      renderQuiz(imageUrl, correctAnswer, choices);
      
  • 项目完成:

    • 当这些代码被执行时,整个应用就会启动:获取数据 -> 处理数据 -> 创建图片 -> 创建按钮 -> 页面完全渲染 -> 等待用户交互。
    • 我们成功构建了一个动态的、数据驱动的 Web 应用!

85-modules

  • 回顾: 到目前为止,我们所有的 JavaScript 代码都写在一个文件里。随着项目变大,这会变得难以管理。
  • 模块 (Modules)
    • 作用: 是将大型 JavaScript 代码库分割成多个独立、可重用的文件的一种机制。
    • 每个文件都是一个独立的模块
  • 模块 vs. 普通脚本 (Script)
    • 作用域 (Scope): 这是最大的区别。
      • 普通脚本: 在顶层声明的变量和函数都属于全局作用域,容易产生命名冲突。
      • 模块: 每个模块都有自己的独立作用域。在一个模块中声明的变量和函数默认是私有的,不会污染全局作用域。
    • await 在顶层:
      • 普通脚本中,await 只能在 async 函数中使用。
      • 模块支持顶层 await (top-level await),允许你在模块的最外层直接使用 await,这在初始化数据时非常方便。
  • 如何使用模块:
    • <script> 标签中添加 type="module" 属性。
      <script type="module" src="my-app.js"></script>
      
  • importexport
    • 由于模块是独立作用域,我们需要一种方式在模块之间共享代码。
    • export: 从一个模块中导出(暴露)变量、函数或类,使其可以被其他模块使用。
      // in utilities.js
      export function shuffleArray(array) {
        /* ... */
      }
      export const PI = 3.14;
      
    • import: 在一个模块中导入(引入)另一个模块导出的内容。
      // in my-app.js
      import { shuffleArray, PI } from "./utilities.js";
      
      • 语法非常类似于对象解构。
  • 本地开发注意事项:
    • 由于浏览器的 CORS(跨源资源共享)安全策略,你不能通过 file:// 协议直接在本地打开一个使用模块的 HTML 文件。
    • 你需要一个本地开发服务器 (local development server) 来通过 http://localhost 提供文件服务。工具如 Vite11tyNext.js 或简单的 Python/Node.js 服务器都可以实现这一点。

86-modules-q-a

  • 问答总结:
    • 可以导入什么: 任何值都可以被 exportimport,包括函数、变量、常量、对象、类等。不仅仅是工具函数。
    • 本地开发服务器: 是使用 ES 模块进行本地开发的必需品
    • React 中的模块: React 和其他现代框架严重依赖模块系统。npm install 会将第三方库(它们本身也是模块)下载到 node_modules 文件夹,然后你可以通过 import 使用它们。
    • 推荐入门工具: 对于想开始构建多页面、模块化网站的初学者,11ty (Eleventy) 是一个很好的静态站点生成器。

87-debugging

  • 调试 (Debugging)
    • 是找出并修复代码中错误(bug)的过程。这是每个开发者日常工作中不可或缺的一部分。
  • 方法 1: console.log()
    • 最简单、最直接的调试方法。
    • 通过在代码的关键位置打印变量的值、消息或对象,来检查程序的执行流程和状态是否符合预期。
    • 优点: 简单易用。
    • 缺点: 会让代码变得混乱,发布前需要手动移除,对于复杂问题可能不够用。

88-browser-debugger

  • 方法 2: 浏览器调试器 (Browser Debugger)
    • 一个集成在浏览器开发者工具中的强大工具。
    • 核心功能: 断点 (Breakpoints)
      • 断点是你在代码中设置的一个“暂停点”。当代码执行到断点时,程序会暂停
      • 设置断点的方法:
        1. 在代码中写入 debugger; 关键字。
        2. 在开发者工具的 "Debugger" 或 "Sources" 面板中,点击代码行号来设置。
    • 当程序暂停时,你可以:
      • 检查作用域 (Scope): 查看当前作用域内所有变量的值。这对于理解为什么一个变量的值不是你所期望的非常有帮助。
      • 查看调用栈 (Call Stack): 了解函数是如何相互调用的,帮你追溯代码的执行路径。
      • 单步执行 (Step through):
        • Step Over (F10): 执行当前行,并移动到下一行。如果当前行是一个函数调用,它会执行完整个函数再停在下一行。
        • Step Into (F11): 如果当前行是一个函数调用,它会进入该函数内部,让你逐行调试函数体。
        • Resume (F8): 继续执行代码,直到遇到下一个断点或程序结束。
      • 在控制台中交互: 在暂停状态下,你可以在控制台中输入变量名来查看其值,甚至执行代码来测试某些操作。

89-try-catch-error-handling

  • 错误处理 (Error Handling)
    • 在某些操作(如网络请求)中,错误是可能发生的。健壮的程序需要能够预见并优雅地处理这些错误,而不是直接崩溃。
  • throw 关键字
    • 用于手动抛出 (throw) 一个错误。throw new Error('Something went wrong!');
  • try...catch 语句
    • 是 JavaScript 中捕获和处理错误的主要机制。
    • 语法:
      try {
        // 尝试执行可能会出错的代码
        dangerousOperation();
      } catch (error) {
        // 如果 try 块中发生错误,代码会立即跳转到这里
        // "error" 是一个包含错误信息的对象
        console.error("Oops, an error occurred:", error);
        // 在这里可以执行备用逻辑,如显示错误消息给用户
      }
      
    • 它允许你的程序在遇到可预料的错误时,不会停止运行,而是执行你指定的“B 计划”。

90-frameworks-vs-vanilla-javascript

  • 框架 vs. 原生 JavaScript (Vanilla JS)
    • 现状: 大部分公司和大型项目都在使用前端框架,如 React (最流行), Vue, Angular, Svelte 等。
    • 为什么学习原生 JS 很重要:
      1. 基础: 所有框架都构建在原生 JavaScript 之上。深刻理解原生 JS 的原理(如作用域、异步、DOM API)会让你学习任何框架都事半功倍。
      2. 适应性: 框架来来去去,但 JavaScript 的核心概念是持久的。扎实的基础让你能够轻松适应未来的技术变迁。
      3. 问题解决: 遇到问题时,你能分辨出是框架的问题还是底层 JavaScript 的问题,从而更快地定位和解决。
    • 学习路径: 建议先学好原生 JavaScript,打下坚实的基础,然后再去学习 React 等框架。不要跳过基础。

91-wrapping-up

  • 课程回顾:
    • 基础: 我们学习了 JavaScript 的数据类型、变量、作用域。
    • 核心: 掌握了函数、条件语句、循环、事件处理和 DOM 操作。
    • 进阶: 接触了异步编程 (Promise, async/await)、模块系统 (import/export) 和现代语法(解构、展开语法、模板字面量)。
    • 实践: 通过构建项目,将理论知识应用到实际中,并学习了调试和错误处理等实用的开发技能。
  • 后续学习:
    • 深入学习: Frontend Masters 提供了深入的 JavaScript、Node.js、React、Vue、TypeScript 等学习路径。
    • 实践: 不断地编写代码、做项目、尝试新事物是最好的学习方式。
    • 社区: 参与社区,阅读文档 (MDN),保持对技术生态系统的好奇心。
  • 核心思想: 编程是一个持续学习的旅程。我们已经迈出了坚实的第一步,现在拥有了继续探索这个广阔世界所需的工具和信心。继续前进,构建出色的软件!