网络版报告四之JavaScript基础

JavaScript 基础学习。

什么是JavaScript?

高层定义

JavaScript 是一种脚本编程语言,它可以在网页上实现复杂的功能,网页展现给你的不再是简单的静态信息,而是实时的内容更新——交互式的地图、2D/3D 动画、滚动播放的视频等等——JavaScript 就在其中。它是标准 Web 技术蛋糕的第三层,其中 HTML 和 CSS 我们已经在学习区的其他部分进行了详细的讲解。

  • HTML 是一种标记语言,用来结构化我们的网页内容并赋予内容含义,例如定义段落、标题和数据表,或在页面中嵌入图片和视频。
  • CSS 是一种样式规则语言,可将样式应用于 HTML 内容,例如设置背景颜色和字体,在多个列中布局内容。
  • JavaScript 是一种脚本语言,可以用来创建动态更新的内容,控制多媒体,制作图像动画,还有很多。(好吧,虽然它不是万能的,但可以通过简短的代码来实现神奇的功能。)

这三层依次建立,秩序井然。以简单文本标签作为示例。首先用 HTML 将文本标记起来,从而赋予它结构和目的:

1
<button type="button">Player 1: Chris</button>

然后我们可以为它加一点 CSS 让它更好看:

1
2
3
4
5
6
7
8
9
10
11
12
button {
font-family: "helvetica neue", helvetica, sans-serif;
letter-spacing: 1px;
text-transform: uppercase;
border: 2px solid rgb(200 200 0 / 60%);
background-color: rgb(0 217 217 / 60%);
color: rgb(100 0 0 / 100%);
box-shadow: 1px 1px 2px rgb(0 0 200 / 40%);
border-radius: 10px;
padding: 3px 10px;
cursor: pointer;
}

最后,我们可以再加上一些 JavaScript 来实现动态行为:

1
2
3
4
5
6
7
8
const button = document.querySelector("button");

button.addEventListener("click", updateName);

function updateName() {
const name = prompt("请输入新的名字");
button.textContent = `Player 1: ${name}`;
}

尝试点击最后一个版本的文本标签,观察会发生什么

它到底可以做什么?

客户端 JavaScript 语言的核心包含一些普遍的编程特性,以让你可以做到如下的事情:

  • 在变量中储存有用的值。比如上文的示例中,我们请求客户输入一个新名字,然后将其储存到 name 变量中。
  • 操作一段文本(在编程中称为“字符串”(string))。上文的示例中,我们取字符串“玩家 1:”,然后把它和 name 变量拼接起来,创造出完整的文本标签,比如“玩家 1: 小明”。
  • 运行代码以响应网页中发生的特定事件。上文的示例中,我们用一个 click 事件来检测按钮什么时候被点击,然后运行代码更新文本标签。
  • 以及更多!

JavaScript 语言核心之上所构建的功能更令人兴奋。**应用程序接口(Application Programming Interface,API)**将为你的代码提供额外的超能力。

API 是已经建立好的一套代码组件,可以让开发者实现原本很难甚至无法实现的程序。就像现成的家具套件之于家居建设,用一些已经切好的木板组装一个书柜,显然比自己设计,寻找合适的木材,裁切至合适的尺寸和形状,找到正确尺寸的螺钉,然后再组装成书柜要简单得多。

API 通常分为两类。

浏览器 API 内建于 web 浏览器中,它们可以将数据从周边计算机环境中筛选出来,还可以做实用的复杂工作。例如:

  • 文档对象模型 API 能通过创建、移除和修改 HTML,为页面动态应用新样式等手段来操作 HTML 和 CSS。比如当某个页面出现了一个弹窗,或者显示了一些新内容(像上文小演示中看到那样),这就是 DOM 在运行。
  • 地理位置 API 获取地理信息。这就是为什么谷歌地图可以找到你的位置,而且标示在地图上。
  • 画布 (Canvas)WebGL API 可以创建生动的 2D 和 3D 图像。人们正运用这些 web 技术制作令人惊叹的作品。参见 Chrome Experiments 以及 webglsamples
  • 诸如 HTMLMediaElementWebRTC 等影音类 API(英语)让你可以利用多媒体做一些非常有趣的事,比如在网页中直接播放音乐和影片,或用自己的网络摄像头获取录像,然后在其他人的电脑上展示(试用简易版截图演示以理解这个概念)。

备注: 上述很多演示都不能在旧浏览器中运行。推荐你在测试代码时使用诸如 Firefox、Chrome、Edge 或者 Opera 等现代浏览器。当代码即将交付生产环境时(也就是真实的客户即将使用真实的代码时),你还需要深入考虑跨平台测试

第三方 API 并没有默认嵌入浏览器中,一般要从网上取得它们的代码和信息。比如:

  • Twitter API新浪微博 API 可以在网站上展示最新推文之类。
  • 谷歌地图 APIOpenStreetMap API高德地图 API 可以在网站嵌入定制的地图等等。

备注: 这些 API 为进阶内容,本模块中不会涉及,更多信息请参考:客户端 web API 模块

先稳住!你看到的只是冰山一角。你不可能仅靠学一天 JavaScript 就能构建下一个 Facebook、谷歌地图、或 Instagram——还有很多基础需要了解,这也是为什么你会在这里,让我们继续吧!

JavaScript在页面上做了什么?

现在我们实实在在的学习一些代码,与此同时,探索 JavaScript 运行时背后发生的事情。

让我们简单回顾一下,浏览器在读到一个网页时发生什么(CSS 如何工作一文中首次谈及)。浏览器在读到一个网页时,代码(HTML、CSS 和 JavaScript)将在一个运行环境(浏览器标签页)中得到执行。就像一间工厂,将原材料(代码)加工为一件产品(网页)。

JavaScript 的一个非常常见的用途是通过文档对象模型 API(如上所述)动态修改 HTML 和 CSS,以更新用户界面。

浏览器安全

每个浏览器标签页就是其自身用来运行代码的独立容器(这些容器用专业术语称为“运行环境”)。大多数情况下,每个标签页中的代码完全独立运行,而且一个标签页中的代码不能直接影响另一个标签页(或者另一个网站)中的代码。这是一个好的安全措施,如果不这样,黑客就可以从其他网站盗取信息,或做一些其他坏事。

**备注:**以安全的方式在不同网站或标签页中传送代码和数据的方法是存在的,但它们属于进阶技术,本课程不会涉及。

JavaScript 运行次序

当浏览器执行到一段 JavaScript 代码时,通常会按从上往下的顺序执行这段代码。这意味着你需要注意代码书写的顺序。比如,我们回到第一个例子中的 JavaScript 代码:

1
2
3
4
5
6
7
8
const button = document.querySelector("button");

button.addEventListener("click", updateName);

function updateName() {
const name = prompt("输入一个新的名字:");
button.textContent = `玩家 1:${name}`;
}

首先使用 document.querySelector选定一个按钮,然后使用 addEventListener给它附上一个事件监听器(第 3 行),使得在它被点击时,updateName()代码块(5 – 8 行)便会运行。updateName()代码块(这类可以重复使用的代码块称为“函数”)向用户请求一个新名字,然后把这个名字插入到段落中以更新显示。

如果互换了代码里最初两行的顺序,会导致问题。浏览器开发者控制台将返回一个错误:Uncaught ReferenceError: Cannot access 'button' before initialization。这意味着 button 对象还未初始化,所以我们不能为它增添事件监听器。

备注: 这是一个很常见的错误,在引用对象之前必须确保该对象已经存在。

解释代码 vs 编译代码

作为程序员,你或许听说过这两个术语:解释(interpret)和编译(compile)。在解释型语言中,代码自上而下运行,且实时返回运行结果。代码在由浏览器执行前,不需要将其转化为其他形式。代码将直接以文本格式被接收和处理。

相对的,编译型语言需要先将代码转化(编译)成另一种形式才能运行。比如 C/C++ 先被编译成机器码,然后才能由计算机运行。程序将以二进制的格式运行,这些二进制内容是由程序源代码产生的。

JavaScript 是轻量级解释型语言。浏览器接受到 JavaScript 代码,并以代码自身的文本格式运行它。技术上,几乎所有 JavaScript 转换器都运用了一种叫做即时编译(just-in-time compiling)的技术;当 JavaScript 源代码被执行时,它会被编译成二进制的格式,使代码运行速度更快。尽管如此,JavaScript 仍然是一门解释型语言,因为编译过程发生在代码运行中,而非之前。

两种类型的语言各有优势,这个问题我们暂且不谈。

服务端代码 vs 客户端代码

你或许还听说过服务器端(server-side)和客户端(client-side)代码这两个术语,尤其是在 web 开发时。客户端代码是在用户的电脑上运行的代码,在浏览一个网页时,它的客户端代码就会被下载,然后由浏览器来运行并展示。在本模块中我们讨论的主要是客户端 JavaScript

而服务器端代码在服务器上运行,然后运行结果才由浏览器下载并展示出来。流行的服务器端 web 语言包括:PHP、Python、Ruby、ASP.NET,甚至有 JavaScript!JavaScript 也可用作服务器端语言,比如现在流行的 Node.js 环境,你可以在我们的动态网页——服务器端编程主题中找到更多关于服务器端 JavaScript 的知识。

怎样向页面添加 JavaScript

可以像添加 CSS 那样将 JavaScript 添加到 HTML 页面中。CSS 使用 <link> 元素链接外部样式表,使用 <style> 元素向 HTML 嵌入内部样式表,而 JavaScript 这里只需一个元素——<script>。我们来看看它是怎么工作的。

内部 JavaScript

  1. 首先,下载示例文件 apply-javascript.html 。放在一个方便的文件夹里。

  2. 分别在浏览器和文本编辑器中打开这个文件。你会看到这个 HTML 文件创建了一个简单的网页,其中有一个可点击按钮。

  3. 接下来,打开文本编辑器,在正文底部——就在结束标签 </body> 之前——添加以下内容:

    1
    2
    3
    <script>
    // 在此编写 JavaScript 代码
    </script>
  4. 下面,在 <script> 元素中添加一些 JavaScript 代码,这个页面就能做一些更有趣的事。在“// 在此编写 JavaScript 代码”一行下方添加以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    document.addEventListener("DOMContentLoaded", () => {
    function createParagraph() {
    const para = document.createElement("p");
    para.textContent = "你点击了按钮!";
    document.body.appendChild(para);
    }

    const buttons = document.querySelectorAll("button");

    for (const button of buttons) {
    button.addEventListener("click", createParagraph);
    }
    });
  5. 保存文件并刷新浏览器,然后你会发现,点击按钮文档下方将会添加一个新段落。

备注: 如果示例不能正常工作,请依次检查所有步骤,并保证没有纰漏。原始文件是否以 .html为扩展名保存到本地了? </body>标签前是否添加了 <script>元素?JavaScript 代码输入是否正确?JavaScript 是区分大小写的,而且非常精确,所以你需要准确无误地输入所示的句法,否则可能会出错。

外部 JavaScript

这很不错,但是能不能把 JavaScript 代码放置在一个外部文件中呢?现在我们来研究一下。

  1. 首先,在刚才的 HTML 文件所在的目录下创建一个名为 script.js的新文件。请确保扩展名为 .js,只有这样才能被识别为 JavaScript 代码。

  2. 移除当前位于 </body>底部的 <script>元素,并且在结束标签 </head>之前添加以下内容(这样浏览器就能比在底部时更快开始加载文件):

    1
    <script type="module" src="script.js"></script>
  3. script.js 文件中,添加下面的脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function createParagraph() {
    const para = document.createElement("p");
    para.textContent = "你点击了按钮!";
    document.body.appendChild(para);
    }

    const buttons = document.querySelectorAll("button");

    for (const button of buttons) {
    button.addEventListener("click", createParagraph);
    }
  4. 保存并刷新浏览器。就会发现点击按钮不起作用,如果检查浏览器控制台,会看见类似 Cross-origin request blocked 的错误。这是因为与许多外部资源一样,JavaScript 模块需要从与 HTML 同源的地方加载,并且 file://URL 不符合条件。有两个解决方案可以解决这个问题:(好像意思是说 <script type="module"> 比较严格,得用互联网服务器 ;我用了这第二种方式)

    • 我们推荐的解决方案是按照指南设置本地测试服务器。运行服务器程序并且在 8000 端口提供文件 apply-javascript-external.htmlscript.js,打开浏览器并访问 http://localhost:8000

    • 如果无法运行本地服务器,也可以使用 <script defer src="script.js"></script>代替 <script type="module" src="script.js"></script>。了解更多信息请参阅下面的脚本加载策略。但是注意,本教程其他部分使用的特性可能需要本地 HTTP 服务器。

  5. 现在网站和之前一样了,但是我们的 JavaScript 放在了一个外部文件。一般来说,这对组织代码并在多个 HTML 文件中复用来说是一件好事。此外,没有大段脚本的 HTML 更容易阅读。

内联 JavaScript 处理器

注意,有时候你会遇到在 HTML 中存在着一丝真实的 JavaScript 代码。它或许看上去像这样:

1
2
3
4
5
function createParagraph() {
const para = document.createElement("p");
para.textContent = "你点击了按钮!";
document.body.appendChild(para);
}
1
<button onclick="createParagraph()">点我!</button>

这个演示与之前的两个功能完全一致,只是在 <button>元素中包含了一个内联的 onclick处理器,使得函数在按钮被按下时运行。

然而请不要这样做。这将使 JavaScript 污染了 HTML,而且效率低下。对于每个需要应用 JavaScript 的按钮,你都得手动添加 onClick="createParagraph()"属性。

请使用 addEventListener

与其在 HTML 中包含 JavaScript,不如使用纯 JavaScript 构造。通过 querySelectorAll() 函数,可以选择页面上的所有按钮。然后可以循环遍历这些按钮,使用 addEventListener() 为每个按钮分配一个处理器。代码如下所示:

1
2
3
4
5
const buttons = document.querySelectorAll("button");

for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", createParagraph);
}

这样写乍看去比 onclick 属性要长一些,但是这样写会对页面上所有按钮生效,无论多少个,或添加或删除,完全无需修改 JavaScript 代码。

**备注:**请尝试修改 apply-javascript.html 以添加更多按钮。刷新后可发现按下任一按钮时都会创建一个段落。这样很高效吧?

脚本加载策略

页面上的所有 HTML 代码都按其出现的顺序加载。如果使用 JavaScript 去操作页面上的元素(更准确的说,是文档对象模型),那么如果 JavaScript 在 HTML 之前就被加载和解析了,代码将无法运行。

有几种不同的策略来确保 JavaScript 只在 HTML 解析之后运行:

  • 在上面的内部 JavaScript 示例中,脚本元素放在文档正文(底部),因此只能在 HTML 正文的其他部分被解析以后运行。

  • 在上面的外部 JavaScript 实例中,脚本元素放在文档的头部,在解析 HTML 正文之前解析。但是由于我们使用了 <script type="module">,代码被视为一个模块,并且浏览器在执行 JavaScript 模块之前会等待所有的 HTML 代码都处理完毕(也可以把外部脚本放在正文的底部,但是如果 HTML 内容较多且网络较慢,在浏览器开始获取并加载脚本之前可能需要大量的时间,因此将外部脚本放在头部通常会更好一些)。

  • 如果仍然想在文档头部使用非模块脚本,可能阻塞整个页面的显示,并且可能出现错误,因为脚本在文档解析之前执行:

    • 对于外部脚本,应该在 <script>元素上添加 defer(或者如果不需要 HTML 解析完成,则可以使用 async)属性。
    • 对于内部脚本,应该将代码封装在 DOMContentLoaded事件监听器中。

    这超出了本教程的范围,除非你需要支持非常老的浏览器,否则不要这样做,使用 <script type="module">代替即可。

注释

就像 HTML 和 CSS,JavaScript 代码中也可以添加注释,浏览器会忽略它们,注释只是为你的同事(还有你,如果半年后再看自己写的代码,还会记得其中的含义吗)提供关于代码如何工作的指引。注释非常有用,而且应该经常使用,尤其在大型应用中。注释分为两类:

  • 在双斜杠(//)后添加单行注释,比如:

    1
    // 我是一条注释
  • /**/ 之间添加多行注释,比如:

    1
    2
    3
    4
    /*
    我也是
    一条注释
    */

比如说,我们可以这样为上一个演示添加注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 函数:创建一个新的段落并添加至 HTML body 底部。
function createParagraph() {
const para = document.createElement("p");
para.textContent = "你点了这个按钮!";
document.body.appendChild(para);
}

/*
1. 取得页面上所有按钮的引用并将它们置于一个数组中。
2. 通过一个循环为每个按钮添加一个点击事件的监听器。
当按钮被点击时,调用 createParagraph() 函数。
*/

const buttons = document.querySelectorAll("button");

for (const button of buttons) {
button.addEventListener("click", createParagraph);
}

**备注:**一般来说,注释越多越好,但如果你发现自己添加了大量注释来解释变量是什么(变量名也许应该更直观),或者解释非常简单的操作(也许代码过于复杂),那么就应该小心了。

JavaScript 初体验

现在,你已经学到了一些 JavaScript 的理论知识,以及用 JavaScript 能够做些什么。下面我们会提供一个创建简单的 Javascript 程序的实践的教程——循序渐进地构建一个简易版“猜数字”游戏。

我们并不要求你立刻完整理解所有代码:你不需要借此学会 JavaScript,甚至不需要理解我们要求你编写的全部代码。当前只是概括地介绍一些抽象的概念,让你了解 JavaScript 的特性是如何协同工作的,以及获得编写 JavaScript 的一些感受。所有具体特性将在后续文章中详细介绍,如果你没有很快地全部理解它们,请不要担心!

**备注:**可以看到,JavaScript 中许多代码特性和其他编程语言是一致的(函数、循环,等等)。尽管代码语法不尽相同,但概念基本类似。

像程序员一样思考

学习编程,语法本身并不难,真正困难的是如何应用它来解决现实世界的问题。你要开始像程序员那样思考。一般来讲,这种思考包括了解你程序运行的目的,为达到该目的应选定的代码类型,以及如何使这些代码协同运行。

为达成这一点,我们需要努力编程,获取语法经验,注重实践,再加一点创造力,几项缺一不可。代码写的越多,就会完成得越优秀。虽然我们不能保证你在 5 分钟内拥有“程序员大脑”,但是整个课程中你将得到大量机会来训练程序员思维。

请牢记这一点,然后开始观察本文的示例,体会一下将其分解为可操作任务的大体过程。

示例——猜数字游戏

本文将向你演示如何构建下面的小游戏:

先玩上几盘,在继续之前先熟悉一下这个游戏。

假设你的老板给你布置了以下游戏设计任务要求:

我想让你开发一个猜数字游戏。游戏应随机选择一个 1 到 100 之间的自然数,然后邀请玩家在 10 轮以内猜出这个数字。每轮后都应告知玩家的答案正确与否,如果出错了,则告诉他数字是低了还是高了。并且应显示出玩家前一轮所猜的数字。一旦玩家猜对,或者用尽所有机会,游戏将结束。游戏结束后,可以让玩家选择重新开始。

看到这个要求,首先我们要做的是将其分解成简单的可操作的任务,尽可能从程序员的思维去思考:

  1. 随机生成一个 1 到 100 之间的自然数。
  2. 记录玩家当前的轮数。从 1 开始。
  3. 为玩家提供一种猜测数字的方法。
  4. 一旦有结果提交,先将其记录下来,以便用户可以看到他们先前的猜测。
  5. 然后检查它是否正确。
  6. 如果正确:
    1. 显示祝贺消息。
    2. 阻止玩家继续猜测(这会使游戏混乱)。
    3. 显示控件允许玩家重新开始游戏。
  7. 如果出错,并且玩家有剩余轮次:
    1. 告诉玩家他们错了。
    2. 允许他们输入另一个猜测。
    3. 轮数加 1。
  8. 如果出错,并且玩家没有剩余轮次:
    1. 告诉玩家游戏结束。
    2. 阻止玩家继续猜测(这会使游戏混乱)。
    3. 显示控件允许玩家重新开始游戏。
  9. 一旦游戏重启,确保游戏的逻辑和 UI 完全重置,然后返回步骤 1。

让我们继续,看看我们如何将这些步骤转换为代码,构建这个示例,从而探索 JavaScript 的特性。

初始位置

本教程开始前,请将 number-guessing-game-start.html 文件保存下来。同时在文本编辑器和 Web 浏览器中将其打开,可以看到一个简单的标题、一段游戏说明和一个用于输入猜测的表单,此时表单不会执行任何操作。

我们将在 HTML 底部的 <script>元素中添加新的代码:

1
2
3
<script>
// Your JavaScript goes here
</script>

添加变量以保存数据

让我们开始吧。首先,在<script> 元素中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
let randomNumber = Math.floor(Math.random() * 100) + 1;

const guesses = document.querySelector(".guesses");
const lastResult = document.querySelector(".lastResult");
const lowOrHi = document.querySelector(".lowOrHi");

const guessSubmit = document.querySelector(".guessSubmit");
const guessField = document.querySelector(".guessField");

let guessCount = 1;
let resetButton;

这段代码设置了存储数据的变量和常量以供程序使用。

变量本质上是值(例如数字或字符串)的名称。你可以使用关键字 let 和一个名字来创建变量。

常量也用于对值进行命名,但其不像变量,在创建后将无法修改这个值。本例中用常量来保存对用户界面元素的引用。界面元素的文字可能会改变,但引用是不变的。你可以使用关键字 const 和一个名字来创建常量。

可以使用等号(=)和一个值来为变量或常量赋值。

在我们的示例中:

  • 我们用数学算法得出一个 1 到 100 之间的随机数,并赋值给第一个变量(randomNumber)。

  • 接下来的三个常量均存储着一个引用,分别指向 HTML 结果段落中某个元素(注意它们是如何放置在 <div> 元素内的),用于在代码后面段落中插入值:

    1
    2
    3
    4
    5
    <div class="resultParas">
    <p class="guesses"></p>
    <p class="lastResult"></p>
    <p class="lowOrHi"></p>
    </div>
  • 接下来的两个常量存储对表单文本输入和提交按钮的引用,并用于控制以后提交猜测:

    1
    2
    3
    <label for="guessField">Enter a guess: </label>
    <input type="number" id="guessField" class="guessField" />
    <input type="submit" value="Submit guess" class="guessSubmit" />
  • 倒数第二个变量存储一个计数器并初始化为 1(用于跟踪玩家猜测的次数),最后一个变量存储对重置按钮的引用,这个按钮尚不存在(但稍后就有了)。

函数 (Function)

下面,在之前的代码中添加以下内容:

1
2
3
function checkGuess() {
alert("I am a placeholder");
}

函数是可复用的代码块,可以一次编写,反复运行,从而节省了大量的重复代码。它们真的很有用。定义函数的方法很多,但现在我们先集中考虑当前这个简单的方式。这里我们使用关键字 function、一个函数名、一对小括号定义了一个函数。随后是一对花括号({ })。花括号内部是调用函数时要运行的所有代码。

要运行一个函数代码时,可以输入函数名加一对小括号。

让我们尝试一下。保存你的代码并刷新浏览器页面。然后进入 开发者工具 JavaScript 控制台,并输入以下代码:

1
checkGuess();

在按下 Return/Enter 之后,你应该会看到一个告警窗口,显示 I am a placeholder;我们在代码中定义了一个函数,当我们调用它时,其都会创建一个告警窗口。

运算符

JavaScript 运算符允许我们执行比较、做数学运算、连接字符串,以及其他类似的事情。

请保存代码以免丢失,然后刷新浏览器页面,打开 开发者工具 JavaScript 控制台。然后我们就可以尝试下文中的示例了:把下表中“示例”一列中的每一项都原封不动输入进来,每次输入完毕后都按下 Return/Enter ,可以看到返回的结果。

首先让我们来看看算术运算符,例如:

运算符 名称 示例
+ 6 + 9
- 20 - 15
* 3 * 7
/ 10 / 5

你也可以使用 + 运算符将文本字符串连接在一起(术语“串联”(concatenation))。尝试依次输入以下几行:

1
2
3
4
5
6
const name = "Bingo";
name;
const hello = " says hello!";
hello;
const greeting = name + hello;
greeting;

还有一些快捷操作符可用,称为复合赋值运算符。例如,如果你只希望在现有字符串末尾添加一个新串,可以这样做:

1
2
let name1 = "Bingo";
name1 += " says hello!";

这等价于:

1
2
let name2 = "Bingo";
name2 = name2 + " says hello!";

在执行真/假比较时(例如在条件语句中,见下表),我们使用比较运算符,例如:

运算符 名称
=== 全等(它们是否完全一样?)
!== 不相等(它们是否不一样?)
< 小于
> 大于

条件语句(Conditional)

回到我们的 checkGuess() 函数,我们希望它不仅能够给出一个占位符消息,同时还能检查玩家是否猜对,并做出适当的反应。

现在,将当前的 checkGuess() 函数替换为此版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function checkGuess() {
const userGuess = Number(guessField.value);
if (guessCount === 1) {
guesses.textContent = "Previous guesses: ";
}
guesses.textContent += `${userGuess} `;

if (userGuess === randomNumber) {
lastResult.textContent = "Congratulations! You got it right!";
lastResult.style.backgroundColor = "green";
lowOrHi.textContent = "";
setGameOver();
} else if (guessCount === 10) {
lastResult.textContent = "!!!GAME OVER!!!";
lowOrHi.textContent = "";
setGameOver();
} else {
lastResult.textContent = "Wrong!";
lastResult.style.backgroundColor = "red";
if (userGuess < randomNumber) {
lowOrHi.textContent = "Last guess was too low!";
} else if (userGuess > randomNumber) {
lowOrHi.textContent = "Last guess was too high!";
}
}

guessCount++;
guessField.value = "";
guessField.focus();
}

呀——好多的代码!让我们来逐段探究。

  • 第一行声明了一个名为 userGuess 的变量,并将其设置为在文本字段中输入的值。我们还对这个值应用了内置的 Number() 方法,只是为了确保该值是一个数字。由于我们没有更改此变量,因此我们使用 const 声明。

  • 接下来,我们遇到我们的第一个条件代码块。条件代码块让你能够根据某个条件的真假来选择性地运行代码。虽然看起来有点像一个函数,但它不是。条件块的最简单形式是从关键字 if 开始,然后是一些括号,然后是一些花括号。括号内包含一个比较。如果比较结果为 true,就会执行花括号内的代码。反之,花括号中的代码就会被跳过,从而执行下面的代码。本文的示例中,比较测试的是 guessCount 变量是否等于 1,即玩家是不是第一次猜数字:

    1
    guessCount === 1;

    如果是,我们让 guesses 段落的文本内容等于 Previous guesses:。如果不是就不用了。

  • 第 6 行将当前 userGuess 值附加到 guesses 段落的末尾,并加上一个空格,以使每两个猜测值之间有一个空格。

  • 下一个代码块中做了几个检查:

    • 第一个 if(){ } 检查用户的猜测是否等于在代码顶端设置的 randomNumber 值。如果是,则玩家猜对了,游戏胜利,我们将向玩家显示一个漂亮的绿色的祝贺信息,并清除“高了 / 低了”信息框的内容,调用 setGameOver() 方法。
    • 紧接着是一个 else if(){ } 结构。它会检查这个回合是否是玩家的最后一个回合。如果是,程序将做与前一个程序块相同的事情,只是这次它显示的是 Game Over 而不是祝贺消息。
    • 最后的一个块是 else { },前两个比较都不返回 true 时(也就是玩家尚未猜对,但是还有机会)才会执行这里的代码。在这个情况下,我们会告诉玩家他们猜错了,并执行另一个条件测试,判断并告诉玩家猜测的数字是高了还是低了。
  • 函数最后三行(26 - 28 行)是为下次猜测值提交做准备的。我们把 guessCount 变量的值加 1,以使玩家消耗一次机会(++ 是自增操作符,为自身加 1),然后我们把表单中文本域的值清空,重新聚焦于此,准备下一轮游戏。

事件(Event)

现在,我们有一个实现比较不错的 checkGuess() 函数了,但它现在什么事情也做不了,因为我们还没有调用它。理想中,我们希望在点击“Submit guess”按钮时调用它,为此,我们需要使用事件。事件就是浏览器中发生的事儿,比如点击按钮、加载页面、播放视频,等等,我们可以通过调用代码来响应事件。侦听事件发生的结构称为事件监听器(Event Listener),响应事件触发而运行的代码块被称为事件处理器(Event Handler)。

checkGuess() 函数后添加以下代码:

1
guessSubmit.addEventListener("click", checkGuess);

这里为 guessSubmit 按钮添加了一个事件监听器。addEventListener() 方法包含两个可输入值(称为“参数”(argument)),监听事件的类型(本例中为 click),和当事件发生时我们想要执行的代码(本例中为 checkGuess() 函数)。注意,addEventListener() 中作为参数的函数名不加括号。

现在,保存代码并刷新页面,示例应该能够工作了,但还不够完善。现在唯一的问题是,如果玩家猜对或游戏次数用完,游戏将出错,因为我们尚未定义游戏结束时应运行的 setGameOver() 函数。现在,让我们补全所缺代码,并完善示例功能。

补全游戏功能

在代码最后添加一个 setGameOver() 函数,然后我们一起来看看它:

1
2
3
4
5
6
7
8
function setGameOver() {
guessField.disabled = true;
guessSubmit.disabled = true;
resetButton = document.createElement("button");
resetButton.textContent = "Start new game";
document.body.append(resetButton);
resetButton.addEventListener("click", resetGame);
}
  • 前两行通过将 disable 属性设置为 true 来禁用表单文本输入和按钮。这样做是必须的,否则用户就可以在游戏结束后提交更多的猜测,游戏的规则将遭到破坏。
  • 接下来的三行创建一个新的 <button>元素,设置它的文本为“Start new game”,并把它添加到当前 HTML 的底部。
  • 最后一行在新按钮上设置了一个事件监听器,当它被点击时,一个名为 resetGame() 的函数被将被调用。

现在我们需要定义 resetGame() 这个函数,依然放到代码底部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function resetGame() {
guessCount = 1;

const resetParas = document.querySelectorAll(".resultParas p");
for (const resetPara of resetParas) {
resetPara.textContent = "";
}

resetButton.parentNode.removeChild(resetButton);

guessField.disabled = false;
guessSubmit.disabled = false;
guessField.value = "";
guessField.focus();

lastResult.style.backgroundColor = "white";

randomNumber = Math.floor(Math.random() * 100) + 1;
}

这段较长的代码将游戏中的一切重置为初始状态,然后玩家就可以开始新一轮的游戏了。此段代码:

  • guessCount 重置为 1。
  • 清除所有信息段落。这里,我们选择 <div class="resultParas"></div> 内的所有段落,然后通过循环迭代,将它们的 textContent 设置为 ''(一个空字符串)。
  • 删除重置按钮。
  • 启用表单元素,清空文本域并聚焦于此,准备接受新猜测的数字。
  • 删除 lastResult 段落的背景颜色。
  • 生成一个新的随机数,这样就可以猜测新的数字了!

此刻一个能功能完善的(简易版)游戏就完成了。恭喜!

我们现在来讨论下其他很重要的代码功能,你可能已经看到过,但是你可能没有意识到这一点。

循环(Loop)

上面代码中有一部分需要我们仔细研读,那就是 for...of循环。循环是一个非常重要的编程概念,它让你能够重复运行一段代码,直到满足某个条件为止。

首先,请再次转到 浏览器开发工具 JavaScript 控制台然后输入以下内容:

1
2
3
4
const fruits = ["apples", "bananas", "cherries"];
for (const fruit of fruits) {
console.log(fruit);
}

发生了什么?控制台中打印出了字符串 'apples'、'bananas'、'cherries'

这正是循环所为。const fruits = ['apples', 'bananas', 'cherries']; 这一行创建了一个数组。我们在本章稍后的完整的数组指南中会作深入探究。就目前而言,数组是元素(本例中为字符串)的集合。

for...of 循环为你提供了一种获取数组中的每一个元素的方法,并在元素的基础上运行 JavaScript 代码。for (const fruit of fruits) 这一行的意思是:

  1. 获取 fruits 中的第一个元素。
  2. fruit 变量设置为这个元素,然后运行花括号 {} 间的代码。
  3. 获取 fruits 中的下一个元素,然后重复步骤 2,直至到达 fruits 的末尾。

现在让我们来看一下猜数字游戏中的循环——resetGame() 函数中可以找到以下内容:

1
2
3
4
const resetParas = document.querySelectorAll(".resultParas p");
for (const resetPara of resetParas) {
resetPara.textContent = "";
}

这段代码通过 querySelectorAll() 方法创建了一个包含 <div class="resultParas"> 内所有段落的变量,然后通过循环迭代,删除每个段落的文本内容。

请注意,即使 resetParas 是一个常量,我们也可以更改其内部属性,例如 textContent

浅谈对象(Object)

在讨论前最后再改进一波。在 let resetButton;(脚本顶端部分)下方添加下面一行内容,然后保存文件:

1
guessField.focus();

这一行通过 focus()方法让光标在页面加载完毕时自动放置于 <input> 输入框内,这意味着玩家可以马上开始第一次猜测,而无需点击输入框。这只是一个小的改进,却提高了可用性——为使用户能投入游戏提供一个良好的视觉线索。

深入分析一下。JavaScript 中一切都是对象。对象是存储在单个分组中的相关功能的集合。可以创建自己的对象,但这是较高阶的知识,我们今后才会谈及。现在,仅需简要讨论浏览器内置的对象,它们已经能够做许多有用的事情。

在本示例的特定情况下,我们首先创建一个 guessField 常量来存储对 HTML 中的文本输入表单域的引用,在文档顶部的声明区域中可以找到以下行:

1
const guessField = document.querySelector(".guessField");

使用 document 对象的 querySelector()方法可以获得这个引用。querySelector() 需要一个信息——用一个 CSS 选择器 可以选中需要引用的元素。

因为 guessField 现在包含一个指向 <input> 元素的引用,它现在就能够访问一系列的属性(存储于对象内部的基础变量,其中一些的值无法改变)和方法(存储在对象内部的基础函数)。focus()input 元素可用方法之一,因此我们可以使用这行代码将光标聚焦于此文本框上︰

1
guessField.focus();

不包含对表单元素引用的变量不提供 focus() 方法。例如,引用 <p> 元素的 guesses 常量,包含一个数字的 guessCount 变量。

操作浏览器对象

浏览器对象如何使用呢,下面我们来小试牛刀。

  1. 首先在浏览器中打开你的程序。

  2. 接下来打开浏览器开发者工具,并且切换到 JavaScript 控制台的标签页。

  3. 输入 guessField,控制台将会显示此变量包含一个 <p> 元素。同时控制台还能自动补全运行环境中对象的名字,包括你的变量!

  4. 现在输入下面的代码:

    1
    guessField.value = 2;

    value 属性表示当前文本区域中输入的值。在输入这条指令后,你将看到文本域区中的文本被我们修改了!

  5. 现在试试输入 guesses 然后回车。控制台会显示一个包含 <p> 元素的变量。

  6. 现在试试输入下面这一行:

    1
    guesses.value;

    浏览器会返回 undefined,因为段落中并不存在 value 属性。

  7. 为了改变段落中的文本内容,你需要用 textContent属性来代替 value。试试这个:

    1
    guesses.textContent = "Where is my paragraph?";
  8. 下面是一些有趣的东西。尝试依次输入下面几行:

    1
    2
    3
    4
    guesses.style.backgroundColor = "yellow";
    guesses.style.fontSize = "200%";
    guesses.style.padding = "10px";
    guesses.style.boxShadow = "3px 3px 6px black";

    页面上的每个元素都有一个 style 属性,它本身包含一个对象,其属性包含应用于该元素的所有内联 CSS 样式。让我们可以使用 JavaScript 在元素上动态设置新的 CSS 样式。

查找并解决 JavaScript 代码的错误

错误类型

一般来说,代码错误主要分为两种:

  • 语法错误:代码中存在拼写错误,将导致程序完全或部分不能运行,通常你会收到一些出错信息。只要熟悉语言并了解出错信息的含义,你就能够顺利修复它们。
  • 逻辑错误:有些代码语法虽正确,但执行结果和预期相悖,这里便存在着逻辑错误。这意味着程序虽能运行,但会给出错误的结果。由于一般你不会收到来自这些错误的提示,它们通常比语法错误更难修复。

事情远没有你想的那么简单,随着探究的深入,会有更多差异因素浮出水面。但在编程生涯的初级阶段上述分类方法已足矣。下面我们将依次分析。

一个出错的示例

让我们重回猜数字游戏,这次我们将故意引入一些错误。请到 Github 下载一份 number-game-errors.html

  1. 请分别在你的文本编辑器和浏览器中打开刚下载的文件。
  2. 先试玩游戏,你会发现在点击“确定”按钮时,游戏并没有响应。

首先查看开发者控制台,看是否存在语法错误,然后尝试修复。详见下文。

修复语法错误

以前的课程中,你学会了在 开发工具 JavaScript 控制台 中输入一些简单的 JavaScript 命令。(如果你忘记了如何在浏览器中打开它,可以直接打开上面的链接)。更实用的是,当 JavaScript 代码进入浏览器的 JavaScript 引擎时,如果存在语法错误,控制台会提供出错信息。现在我们去看一看。

  1. 打开 number-game-errors.html 所在的标签页,然后打开 JavaScript 控制台。你将看到以下出错信息:

  2. 这个错误很容易跟踪,浏览器为你提供了几条有用的信息(截图来自 Firefox,其他浏览器也提供类似信息)。从左到右依次为:

    • 红色的×表示这是一个错误。
    • 一条出错信息,表示问题出在哪儿:“TypeError:guessSubmit.addeventListener is not a function”(类型错误:guessSubmit.addeventListener 不是函数)
    • 点击 [详细了解] 将跳转到一个 MDN 页面,其中包含了此类错误超详细的解释。
    • JavaScript 文件名,点击将跳转到开发者工具的“调试器”标签页。如果你按照这个链接,你会看到错误突出显示的确切行。
    • 出错的行,以及导致错误的首个字符号。这里错误来自 85 行,第 15 个字符。
  3. 我们在代码编辑器中找到第 85 行:

    1
    guessSubmit.addeventListener("click", checkGuess);
  4. 出错信息显示“guessSubmit.addeventListener 不是一个函数”,说明这里可能存在拼写错误。如果你不确定某语法的拼写是否正确,可以到 MDN 上去查找,目前最简便的方法就是去你喜欢的搜索引擎搜索“MDN + 语言特性”。就本文当前内容你可以点击addEventListener()

  5. 因此这里错误显然是我们把函数名写错造成的。请记住,JavaScript 区分大小写,所以任何轻微的不同或大小写问题都会导致出错。将 addeventListener 改为 addEventListener 便可解决。

语法错误:第二轮

  1. 保存页面并刷新,可以看到出错信息不见了。
  2. 现在,如果尝试输入一个数字并按确定按钮,你会看到…另一个错误!

  1. 此次出错信息为“TypeError:lowOrHi is null”(“类型错误:lowOrHi 为 null”),在第 78 行。(我的提示信息不一样)

备注:Null是一个特殊值,意思是“什么也没有”,或者“没有值”。这表示 lowOrHi 已声明并初始化,但没有任何有意义的值,可以说:它没有类型没有值。

**备注:**这条错误没有在页面加载时立即发生,是因为它发生在函数内部(checkGuess() { ... }块中)。函数内部的代码运行于一个外部代码相互独立的域内,后面函数的文章中将更详细地讲解。此时此刻,只有当代码运行至 86 行并调用 checkGuess() 函数时,代码才会抛出出错信息。

  1. 请观察第 78 行代码:

    1
    lowOrHi.textContent = '你刚才猜高了!';
  2. 该行试图将 lowOrHi 变量中的 textContent 属性设置为一个字符串,但是失败了,这是因为 lowOrHi 并不包含预期的内容。为了一探究竟,你可以在代码中查找一下该变量的其他实例。lowOrHi 最早出现于第 48 行:

    1
    const lowOrHi = document.querySelector("lowOrHi");
  3. 此处,我们试图让该变量包含一个指向文档 HTML 中特定元素的引用。我们来检查一下在该行代码执行后变量的值是否为 null。在第 49 行添加以下代码:

    1
    console.log(lowOrHi);

备注:console.log() 是一个非常实用的调试功能,它可以把值打印到控制台。因此我们将其置于代码第 48 行时,它会将 lowOrHi 的值打印至控制台。

  1. 保存并刷新,你将在控制台看到 console.log() 的执行结果:

显然,此处 `lowOrHi` 的值为 `null`,所以第 48 行肯定有问题。
  1. 我们来思考问题有哪些可能。第 48 行使用 document.querySelector()方法和一个 CSS 选择器来取得一个元素的引用。进一步查看我们的文件,我们可以找到有问题的段落:

    1
    <p class="lowOrHi"></p>
  2. 这里我们需要一个类选择器,它应以一个点开头(.),但被传递到第 48 行的querySelector()方法中的选择器没有点。这可能是问题所在!尝试将第 48 行中的 lowOrHi 改成 .lowOrHi

  3. 再次保存并刷新,此时 console.log() 语句应该返回我们想要的 <p> 元素。终于把错误搞定了!此时你可以把 console.log() 一行删除,或保留它以便随后参考。选择权在你。

语法错误:第三轮

  1. 现在,如果你再次试玩,你离成功更进了一步。游戏过程按部就班,直到猜测正确或机会用完,游戏结束。
  2. 此时如果点击“开始新游戏”,游戏将再次出错,抛出与开始时同样的错误——“TypeError:resetButton.addeventListener is not a function”!这次它来自第 94 行。
  3. 查看第 94 行,很容易看到我们犯了同样的错误。我们只需要再次将 addeventListener 改为 addEventListener。现在就改吧。

逻辑错误

此时,游戏应该可以顺利进行了。但经过几次试玩后你一定会注意到要猜的随机数不是 0 就是 1。这可不是我们期望的!

游戏的逻辑肯定是哪里出现了问题,因为游戏并没有返回错误,只是不能正确运行。

  1. 寻找 randomNumber 变量和首次设定随机数的代码。保存着游戏开始时玩家要猜的随机数的实例大约在 44 行:

    1
    let randomNumber = Math.floor(Math.random()) + 1;

    重新开始游戏产生随机数的设定语句大约在 113 行:

    1
    randomNumber = Math.floor(Math.random()) + 1;
  2. 为了检查问题是否来自这两行,我们要再次转到我们的朋友 - 控制台:在两行代码之后各插入下面的代码:

    1
    console.log(randomNumber);
  3. 保存并刷新,然后试玩,你会看到在控制台显示的随机数总是等于 1。

修正逻辑错误

为了解决这个问题,让我们来思考这行代码如何工作。首先,我们调用 Math.random(),它生成一个在 0 和 1 之间的十进制随机数,例如 0.5675493843。

1
Math.random();

接下来,我们把调用 Math.random() 的结果作为参数传递给 Math.floor(),它会舍弃小数部分返回与之最接近的整数。然后我们给这个结果加上 1:

1
Math.floor(Math.random()) + 1;

由于将一个 0 和 1 之间的随机小数的小数部分舍弃,返回的整数一定为 0,因此在此基础上加 1 之后返回值一定为 1。要在舍弃小数部分之前将它乘以 100。便可得到 0 到 99 之间的随机数:

1
Math.floor(Math.random() * 100);

然后再加 1,便可得到一个 100 以内随机的自然数:

1
Math.floor(Math.random() * 100) + 1;

将上述两行内容替换为此,然后保存刷新,游戏终于如期运行了!

其他常见错误

代码中还会遇到其他常见错误。本节将指出其中的大部分。

不管输入什么程序总说“你猜对了!”

这是混淆赋值和严格等于运算符的又一症状。例如我们把 checkGuess() 里的:

1
if (userGuess === randomNumber) {

改成

1
if (userGuess = randomNumber) {

因为条件永远返回 true,使得程序报告你猜对了。小心哦!

SyntaxError: missing ) after argument list

这个很简单。通常意味着函数/方法调用后的结束括号忘写了。

SyntaxError: missing : after property id

JavaScript 对象的形式有错时通常会导致此类错误,如果把

1
function checkGuess() {

写成了

1
function checkGuess( {

浏览器会认为我们试图将函数的内容当作参数传回函数。写圆括号时要小心!

SyntaxError: missing } after function body

这个简单。通常意味着函数或条件结构中丢失了一个花括号。如果我们将 checkGuess() 函数末尾的花括号删除,就会得到这个错误。

SyntaxError: expected expression, got ‘string’ 或 SyntaxError: string literal contains an unescaped line break

这个错误通常意味着字符串两端的引号漏写了一个。如果你漏写了字符串开始的引号,将得到第一条出错信息,这里的 'string’ 将被替换为浏览器发现的意外字符。如果漏写了末尾的引号将得到第二条。

对于所有的这些错误,想想我们在实例中是如何逐步解决的。错误出现时,转到错误所在的行观察是否能发现问题所在。记住,错误不一定在那一行,错误的原因也可能和我们在上面所说的不同!

如何存储你需要的信息——变量

这一块的笔记由于 onedrive 崩了导致没有保存,见官网:https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Core/Scripting/Variables

使用 let 声明变量。

数字与运算符

人人都爱数学

与其他一些编程语言不同,JavaScript 只有一个数据类型用来表示数字(无论是整数还是小数)——就是 Number。这意味着,你在 JavaScript 中处理的任何类型的数字,都以完全相同的方式处理它们。

实用的Number方法

Number 对象是一个实例,它表示你在 JavaScript 中使用的所有标准数字。该对象提供了一系列有用的方法,供你对数字进行操作。本文并未详细介绍这些方法,因为我们希望将其作为入门指南,仅涵盖当前所需的真正基础内容;然而,一旦你反复阅读本模块后,建议查阅对象参考页面以进一步了解可用功能。

例如,要将数字四舍五入到指定的小数位数,可以使用 toFixed() 方法。在浏览器的控制台中输入以下代码:

1
2
3
4
const lotsOfDecimal = 1.7665849587;
lotsOfDecimal;
const twoDecimalPlaces = lotsOfDecimal.toFixed(2);
twoDecimalPlaces;

转换至 number 数据类型

有时,你可能会遇到一个以字符串类型存储的数字,这会使得对其进行计算变得困难。这种情况最常发生在数据被输入到表单的输入框中,且输入类型为文本时。解决此问题的方法是将字符串值传递给Number() 构造函数,以返回该值的数字版本。

例如,尝试在控制台下输入这几行:

1
2
let myNumber = "74";
myNumber += 3;

你会得到的结果为 743,而不是 77,因为 myNumber 实际上是一个字符串。你可以通过以下代码来测试这个行为:

1
typeof myNumber;

为了修正计算,可以这么做:

1
2
let myNumber = "74";
myNumber = Number(myNumber) + 3;

结果为 77,与最初预期一致。

算术运算符

算术运算符用于在 JavaScript 中进行数学计算:

运算符 名称 作用 示例
+ 加法 两个数相加。 6 + 9
- 减法 从左边的数减去右边的数。 20 - 15
* 乘法 两个数相乘。 3 * 7
/ 除法 将左侧的数除以右侧的数。 10 / 5
% 求余(有时候也叫取模) 在你将左边的数分成同右边数字相同的若干整数部分后,返回剩下的余数 8 % 3(返回 2,因为 3 可以被 8 整除两次,还剩余 2。)
** 取底数(base)的指数次(exponent)方,即指数所指定的底数相乘指数所指定的次数。 5 ** 2(返回 3125,相当于 5 * 5 。)

运算符优先级

在下面的例子中,浏览器会先进行乘法运算,再进行加法运算。

1
2
3
let num1 = 10;
let num2 = 50;
num2 + num1 / 8 + 2;

这是因为运算符优先级——一些运算符将在计算算式(在编程中称为表达式)的结果时先于其他运算符被执行。JavaScript 中的运算符优先级与学校的数学课程相同——乘法和除法总是先完成,然后是加法和减法(总是从左到右进行计算)。

如果想要改变计算优先级,可以把想要优先计算的部分用括号围住。所以要得到结果为 6,我们可以这样做:

1
(num2 + num1) / (8 + 2);

自增与自减运算符

有时候,你需要反复把一个变量加 1 或减 1。这可以方便地使用自增(++)和自减(--)运算符来完成。我们在 JavaScript 初体验 文章的“猜数字”游戏中,当我们添加 1 到我们的 guessCount 变量来跟踪用户在每次转动之后剩下的猜测时,我们使用了 ++ 运算符。

1
guessCount++;

让我们在控制台中尝试使用这些操作。首先需要注意的是,你无法直接将这些操作应用于一个数字,这可能看起来有些奇怪,但我们实际上是在为变量赋予一个新的更新值,而非直接对值本身进行操作。以下代码将返回错误:

1
3++;

所以,你只能增加一个现有的变量。尝试这个:

1
2
let num1 = 4;
num1++;

好的,第二个奇怪的东西!执行此操作时,你会看到返回值为 4,这是因为浏览器先返回当前值,然后增加变量。如果你再次返回变量值,则可以看到它已经递增:

1
num1;

自减运算符 -- 同样如此,尝试以下操作:

1
2
3
let num2 = 6;
num2--;
num2;

赋值运算符

赋值运算符是向变量分配值的运算符。我们已经使用了最基本的一个很多次了:=,它只是将右边的值赋给左边的变量,即:

1
2
3
let x = 3; // x 的值是 3
let y = 4; // y 的值是 4
x = y; // x 和 y 有相同的值,4

但是还有一些更复杂的类型,它们提供了有用的快捷方式,可以使你的代码更加整洁和高效。最常见的类型如下:

运算符 名称 作用 示例 等价形式
+= 加法赋值 右边的值加上左边的变量值,然后返回新的变量值 x += 4; x = x + 4;
-= 减法赋值 从左侧的变量值中减去右侧的值,然后返回新的变量值 x -= 3; x = x - 3;
*= 乘法赋值 左侧的变量值乘上右侧的值,然后返回新的变量值 x *= 3; x = x * 3;
/= 除法赋值 左边的变量值除以右边的数值,然后返回新的变量值 x /= 5; x = x / 5;

尝试在你的控制台中输入上面的一些示例,以了解它们的工作原理。在每种情况下,你是否可以猜出在输入第二行之前的值。

请注意,你可以在每个表达式的右侧使用其他变量,例如:

1
2
3
let x = 3; // x 包含值 3
let y = 4; // y 包含值 4
x *= y; // x 现在包含值 12

调整画布盒子的大小

在这个练习中,我们将让你填写一些数字和操作符来操纵一个盒子的大小。该盒子使用称为 Canvas API 的浏览器 API 绘制。没有必要担心这是如何工作的——现在只关注其数学部分。盒子的宽度和高度(以像素为单位)由变量 xy 定义,变量 xy 最初都被赋值为 50。

1
2
3
4
5
6
7
8
9
10
11
12
const canvas = document.getElementById("canvas");
const para = document.querySelector("p");
const ctx = canvas.getContext("2d");

// 仅编辑以下两行
let x = 50;
let y = 50;

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "green";
ctx.fillRect(10, 10, x, y);
para.textContent = `长方形的宽为 ${x}px,长为 ${y}px。`;

比较运算符

有时,我们将要运行真/假测试,然后根据该测试的结果进行相应的操作——为此,我们使用比较运算符

运算符 名称 作用 示例
=== 严格等于 测试左右值是否相同。 5 === 2 + 4
!== 严格不等于 测试左右值是否相同。 5 !== 2 + 3
< 小于 测试左值是否小于右值。 10 < 6
> 大于 测试左值是否大于右值。 10 > 20
<= 小于或等于 测试左值是否小于或等于右值。 3 <= 2
>= 大于或等于 测试左值是否大于或等于右值。 5 >= 4

**备注:**你可能会看到有些人在他们的代码中使用 ==!= 来判断相等和不相等,这些都是 JavaScript 中的有效运算符,但它们与 ===/!== 不同,前者测试值是否相同,但是数据类型可能不同,而后者严格测试值和数据类型是否相同。严格的测试往往导致更少的错误,所以我们建议你使用这些严格的版本。

如果你尝试在控制台中输入这些值,你将看到它们都返回 true/false 值——我们在上一篇文章中提到的那些布尔值。这些是非常有用的,因为它们允许我们在我们的代码中做出决定——每次我们想要选择某种类型时,都会使用这些代码,例如:

  • 根据功能是打开还是关闭,在按钮上显示正确的文本标签。
  • 如果游戏结束,则显示游戏消息,或者如果游戏已经获胜,则显示胜利消息。
  • 显示正确的季节性问候,取决于假期季节。
  • 根据选择的缩放级别缩小或放大地图。

当我们在以后的文章中查看条件语句时,我们将介绍如何编写这样的逻辑。现在,我们来看一个简单的例子:

1
2
<button>启动机器</button>
<p>机器已停止运行。</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const btn = document.querySelector("button");
const txt = document.querySelector("p");

btn.addEventListener("click", updateBtn);

function updateBtn() {
if (btn.textContent === "启动机器") {
btn.textContent = "停止机器";
txt.textContent = "机器已启动!";
} else {
btn.textContent = "启动机器";
txt.textContent = "机器已停止运行。";
}
}

你可以在 updateBtn() 函数内看到正在使用的相等运算符。在这种情况下,我们不会测试两个数学表达式是否具有相同的值——我们正在测试一个按钮的文本内容是否包含某个字符串——但它仍然是相同的工作原理。如果按钮当前为“启动机器”,则将其标签更改为“停止机器”,并根据需要更新标签。如果按钮上写着“停止机器”时被按下,我们将显示回原来的内容。

备注:这种在两个状态之间来回交换的行为通常被称为切换。它在一个状态和另一个状态之间切换——点亮,熄灭等。

文本处理——JavaScript中的字符串

接下来,我们将把注意力转向字符串——也就是编程中所说的文本片段。在本文中,我们将了解在学习 JavaScript 时,你应该了解的关于字符串的所有常见内容,例如创建字符串、在字符串中转义引号和连接字符串。

创建字符串

首先,输入以下几行:

1
2
const string = "这场革命将不会被电视转播。";
console.log(string);

就像我们处理数字一样,我们声明一个变量,并用一个字符串值初始化它,然后返回值。这里唯一的区别是,在编写字符串时,我们需要在字符串值的两边加上引号。

如果你不这样做,或者在书写过程中,漏掉其中一个引号,你就会得到一个错误。试着输入以下几行:

1
2
3
const badString1 = 这是一个测试;
const badString2 = '这是一个测试;
const badString3 = 这是一个测试';

这几行不起作用,因为没有用引号包裹的任何文本字符串都被假定为变量名、属性名、保留字,等等。如果浏览器不能找到它,那么将会引发错误(例如:“missing; before statement”)。如果浏览器能够识别字符串从哪里开始,但是不能找到字符串的结尾符,如第二行所示,那么它也会引发错误(带有“unterminated string literal”)。如果你写的程序目前也引发这样的错误,那么请回过头来仔细检查所有字符串,以确保没有漏写引号。

如果你之前定义了变量 string,下面的操作将会起作用——尝试一下:

1
2
const badString = string;
console.log(badString);

badString 的值现在被设置为了与 string 具有相同的值。

单引号、双引号和反引号

在 JavaScript 中,你可以选择单引号(')、双引号(")或反引号(```)来包裹字符串。以下所有方法都可以:

1
2
3
4
5
6
7
const single = '单引号';
const double = "双引号";
const backtick = `反引号`;

console.log(single);
console.log(double);
console.log(backtick);

字符串的开头和结尾必须使用相同的字符,否则会出现错误:

1
const badQuotes = '不允许这样写!";

使用单引号声明的字符串和使用双引号声明的字符串是相同的,你可以根据个人偏好来使用。但推荐选择一种样式并在代码中保持一致。

使用反引号声明的字符串是一种特殊字符串,被称为模板字面量。在大多数情况下,模板字面量与普通字符串类似,但它具有一些特殊的属性:

  • 你可以在其中嵌入 JavaScript
  • 你可以声明多行的模板字面量

嵌入 JavaScript

在模板字面量中,你可以在 ${ } 中包装 JavaScript 变量或表达式,其结果将被包含在字符串中:

1
2
3
const name = "克里斯";
const greeting = `你好,${name}`;
console.log(greeting); // "你好,克里斯"

你可以使用相同的技术来连接两个变量:

1
2
3
4
const one = "你好,";
const two = "请问最近如何?";
const joined = `${one}${two}`;
console.log(joined); // "你好,请问最近如何?"

像这样连接字符串被称为串联(concatenation)。

上下文中的串联

让我们看一下实际使用的串联:

1
2
<button>按这里</button>
<div id="greeting"></div>
1
2
3
4
5
6
7
8
9
const button = document.querySelector("button");

function greet() {
const name = prompt("你叫什么名字?");
const greeting = document.querySelector("#greeting");
greeting.textContent = `你好呀,${name}!很高兴见到你!`;
}

button.addEventListener("click", greet);

使用“+”连接字符串

你只能将 ${} 与模板字面量一起使用,而不能与不同字符串一起使用。你可以使用 + 运算符来连接普通字符串。

1
2
3
const greeting = "你好";
const name = "克里斯";
console.log(greeting + "," + name); // "你好,克里斯"

但是,模板字面量通常更具可读性:

1
2
3
const greeting = "你好";
const name = "克里斯";
console.log(`${greeting}${name}`); // "你好,克里斯"

在字符串中包含表达式

除了变量,你还可以在模板字面量中包含 JavaScript 表达式,表达式的结果将包含在最终的结果中:

1
2
3
4
5
6
7
const song = "青花瓷";
const score = 9;
const highestScore = 10;
const output = `我喜欢歌曲《${song}》。我给它打了 ${
(score / highestScore) * 100
} 分。`;
console.log(output); // "我喜欢歌曲《青花瓷》。我给它打了 90 分。"

多行字符串

模板字符串会保留源代码中的换行符,因此你可以编写跨越多行的字符串,如下所示:

1
2
3
4
5
6
7
8
const newline = `终于有一天,
你知道了必须做的事情,而且开始……`;
console.log(newline);

/*
终于有一天,
你知道了必须做的事情,而且开始……
*/

要使用普通字符串获得等效的输出,你必须在字符串中包含换行字符(\n):

1
2
3
4
5
6
7
const newline = "终于有一天,\n你知道了必须做的事情,而且开始……";
console.log(newline);

/*
终于有一天,
你知道了必须做的事情,而且开始……
*/

在字符串中包含引号

既然我们使用引号来表示字符串的开始和结束,那么我们如何在字符串中包含实际的引号呢?我们知道这样做是不行的:

1
const badQuotes = "She said "I think so!"";

一种常见的方法是换用其他字符来声明字符串:

1
2
const goodQuotes1 = 'She said "I think so!"';
const goodQuotes2 = `She said "I'm not going in there!"`;

另一种选择是转义存在问题的引号。转义字符意味着我们对它们做了一些处理,以确保它们被识别为文本,而不是代码的一部分。在 JavaScript 中,我们通过在字符之前加上反斜杠来实现这一点。试试这个:

1
2
const bigmouth = 'I\'ve got no right to take my place…';
console.log(bigmouth);

你可以使用相同的技术来插入其他特殊字符。

数字与字符串

当我们尝试连接一个字符串和数字时会发生什么?让我们在控制台中尝试一下:

1
2
3
const name = "Front ";
const number = 242;
console.log(name + number); // "Front 242"

你可能会认为这会返回一个错误,但它可以正常工作。数字应该如何显示为字符串是相当明确的,所以浏览器会自动将数字转换为字符串,并将两个字符串连接起来。

如果你有一个数字变量,你想将其转换为字符串,或者你想将一个字符串变量转换为数字,你可以使用以下两个结构:

  • 如果可以的话,Number() 函数会将其他参数转换成数字。试试这个

    1
    2
    3
    4
    const myString = "123";
    const myNum = Number(myString);
    console.log(typeof myNum);
    // number
  • 相反,String() 函数将其他参数转换为字符串。

    1
    2
    3
    4
    const myNum2 = 123;
    const myString2 = String(myNum2);
    console.log(typeof myString2);
    // string

有用的字符串方法

把字符串当作对象

在 JavaScript 中,大多数值都能够当做对象来使用。当你使用如下方法创建了一个字符串时:

1
const string = "这是我的字符串";

虽然变量本身不是一个对象,但由于在对其进行属性访问时它可被作为对象使用,所以其依然拥有大量可用的属性与方法。

查询字符串的长度

使用 length 属性。

1
2
const browserType = "mozilla";
browserType.length;

获取指定字符

值得一提的是,你可以使用方括号表示法获取字符串中的任意字符——意思是你可以在变量名的末尾添加方括号([])。在方括号内,你可以添加想要获取的字符的编号,例如,要获取第一个字母,你可以这样做:

1
browserType[0];

记住:计算机从 0 开始计数,而不是从 1 开始!

要获取任意字符串的最后一个字符,我们可以使用下面这行代码,将这个技巧与我们上面提到的 length 属性相结合起来:

1
browserType[browserType.length - 1];

字符串 “mozilla” 的长度为 7,但由于计数从 0 开始,所以最后一个字符的位置为 6,使用 length-1 即可获取最后一个字符。

检查一个字符串是否包含某个子字符串

有时候你会想要知道一个较小的字符串是否存在于一个较大的字符串中(我们通常会说一个子字符串是否存在于另一个字符串中)。这可以使用 includes() 方法来完成,该方法需要一个参数——你想要搜索的子字符串。

如果字符串中包含指定的子字符串,它会返回 true,否则返回 false

1
2
3
4
5
6
7
const browserType = "mozilla";

if (browserType.includes("zilla")) {
console.log("发现 zilla!");
} else {
console.log("没有 zilla!");
}

你经常会想知道一个字符串是否以某个具体的子字符串开头或结尾。这是一个十分常见的需求,有两个方法可以用来做这件事:startsWith()endsWith()

1
2
3
4
5
6
7
const browserType = "mozilla";

if (browserType.startsWith("zilla")) {
console.log("以 zilla 开头!");
} else {
console.log("**不**以 zilla 开头!");
}
1
2
3
4
5
6
7
const browserType = "mozilla";

if (browserType.endsWith("zilla")) {
console.log("以 zilla 结尾!");
} else {
console.log("**不**以 zilla 结尾!");
}

查找某个子字符串的位置

你可以使用 indexOf() 方法来在一个较大的字符串中查找一个子字符串的位置。这个方法接收两个参数——你想要搜索的子字符串、一个可选的参数指定搜索的起始位置。

如果字符串包含该子字符串,indexOf() 返回子字符串第一次出现的下标。如果字符串不包含该子字符串,indexOf() 返回 -1

1
2
const tagline = "MDN - Resources for developers, by developers";
console.log(tagline.indexOf("developers")); // 20

如果你从字符串的开头开始数字符(包括空白字符)的数量,从 0 开始,子字符串 "developers" 第一次出现是在下标 20

1
console.log(tagline.indexOf("x")); // -1

而这一段代码则返回 -1,因为字符 x 没有在字符串中出现。

现在你已经知道了如何查找子字符串第一次出现的位置,那么该如何继续查找子字符串出现的位置呢?你可以通过将一个大于上一次出现下标的值作为第二个参数传入方法来实现。

1
2
3
4
5
const firstOccurrence = tagline.indexOf("developers");
const secondOccurrence = tagline.indexOf("developers", firstOccurrence + 1);

console.log(firstOccurrence); // 20
console.log(secondOccurrence); // 35

在这里我们告诉方法在下标 21firstOccurrence + 1)开始搜索子字符串 "developers",然后它返回了下标 35

从字符串提取子字符串

你可以使用 slice() 方法来从一个字符串提取子字符串。你需要传入:

  • 提取的起始下标
  • 提取的终止下标,不含该下标,也即在这个下标处的字符不会包含在提取出的子字符串中。

示例:

1
2
const browserType = "mozilla";
console.log(browserType.slice(1, 4)); // "ozi"

在下标 1 处的字符是 "o",在下标 4 处的字符是 "l"。所以我们会提取从 "o" 开始到 "l" 前的所有字符,获得 "ozi"

如果你想要提取字符串中某个字符后余下的所有字符,你不需要添加第二个参数,只需要添加你想要从字符串中提取余下字符的那个字符的位置。尝试以下代码:

1
browserType.slice(2); // "zilla"

这段代码会返回 "zilla"——因为位于下标 2 处的字符是字母 "z",并且因为你没有添加第二个参数,返回的子字符串会是字符串中余下的所有字符。

转换大小写

字符串方法 toLowerCase()toUpperCase() 接收一个字符串并分别将所有字符转换为小写或大写。如果你要在将数据存储到数据库之前规范化用户输入的所有数据,这会非常有用。

1
2
3
const radData = "My NaMe Is MuD";
console.log(radData.toLowerCase());
console.log(radData.toUpperCase());

替换字符串的某部分

你可以使用 replace() 方法将字符串中的一个子字符串替换为另一个子字符串。

在这个例子中,我们会提供两个参数——我们想要替换的字符串和我们想要作为替代的字符串:

1
2
3
4
5
const browserType = "mozilla";
const updated = browserType.replace("moz", "van");

console.log(updated); // "vanilla"
console.log(browserType); // "mozilla"

注意,和许多字符串方法相同,replace() 不会修改调用它的那一个字符串,而是返回一个新的字符串。如果你想要更新原本的 browserType 变量,你需要像这样做:

1
2
3
4
let browserType = "mozilla";
browserType = browserType.replace("moz", "van");

console.log(browserType); // "vanilla"

同时注意,我们现在需要用 let 来声明 browserType,而不是用 const,因为我们要对它重新赋值。

谨记,replace() 在这种写法下只会更改子字符串第一次出现的位置。如果你想更改所有出现的位置,你可以使用 replaceAll()

1
2
3
4
let quote = "To be or not to be";
quote = quote.replaceAll("be", "code");

console.log(quote); // "To code or not to code"

数组

在本模块的最后一篇文章中,我们将看看数组——一种将一组数据存储在单个变量名下的优雅方式。现在我们看看它有什么用,然后探索如何来创建一个数组,检索、添加和删除存储在数组中的元素,以及其他更多的功能。

数组是什么?

数组通常被描述为“像列表一样的对象”; 简单来说,数组是一个包含了多个值的对象。数组对象可以存储在变量中,并且能用和其他任何类型的值完全相同的方式处理,区别在于我们可以单独访问列表中的每个值,并使用列表执行一些有用和高效的操作,如循环 - 它对数组中的每个元素都执行相同的操作。也许我们有一系列产品和价格存储在一个数组中,我们想循环遍历所有这些产品,并将它们打印在发票上,同时将所有产品的价格统计在一起,然后将总价格打印在底部。

如果我们没有数组,我们必须将每个产品存储在一个单独的变量中,然后调用打印的代码,并为每个产品单独添加。花费的时间要长得多,效率很低,而且也容易出错。如果我们有 10 个产品需要添加发票,那就只是有点麻烦而已,但是 100 个,或者 1000 个呢?我们稍后将在文章中使用这个例子。

像以前的文章一样,我们通过在 JavaScript 控制台中输入一些示例来了解数组的基础知识。

创建数组

数组由方括号构成,其中包含用逗号分隔的元素列表。

  1. 假设我们想在一个数组中存储一个购物清单 - 我们会做一些像下面这样的事情。在你的控制台中输入以下行:

    1
    2
    let shopping = ["bread", "milk", "cheese", "hummus", "noodles"];
    shopping;
  2. 在这种情况下,数组中的每个项目都是一个字符串,但请记住,你可以将任何类型的元素存储在数组中 - 字符串,数字,对象,另一个变量,甚至另一个数组。你也可以混合和匹配项目类型 - 它们并不都是数字,字符串等。尝试下面这些:

    1
    2
    let sequence = [1, 1, 2, 3, 5, 8, 13];
    let random = ["tree", 795, [0, 1, 2]];

访问和修改数组元素

然后,你可以使用括号表示法访问数组中的元素,与检索特定字符串字符的方法相同。

  1. 在你的控制台中输入以下内容:

    1
    2
    shopping[0];
    // returns "bread"
  2. 你还可以简单地为单个数组元素提供新值来修改数组中的元素。例如:

    1
    2
    3
    shopping[0] = "tahini";
    shopping;
    // shopping will now return [ "tahini", "milk", "cheese", "hummus", "noodles" ]
  3. 请注意,数组中包含数组的话称之为多维数组。你可以通过将两组方括号链接在一起来访问数组内的另一个数组。例如,要访问数组内部的一个项目,即 random 数组中的第三个项目(参见上一节),我们可以这样做:

    1
    random[2][2];

获取数组长度

你可以通过使用length 属性获取数组的长度(数组中有多少项元素),这与查找字符串的长度(以字符为单位)完全相同。尝试以下代码:

1
2
sequence.length;
// should return 7

虽然 length 属性也有其他用途,但最常用于循环(循环遍历数组中的所有项)。例如:

1
2
3
4
let sequence = [1, 1, 2, 3, 5, 8, 13];
for (let i = 0; i < sequence.length; i++) {
console.log(sequence[i]);
}

你将在以后的文章中正确地了解循环,但简而言之,这段代码的意思是:

  1. 在数组中的元素编号 0 开始循环。
  2. 在元素编号等于数组长度的时候停止循环。这适用于任何长度的数组,但在这种情况下,它将在编号 7 的时候终止循环(这很好,因为我们希望最后一位元素的编号是 6)。
  3. 对于每个元素,使用 console.log() 将其打印到浏览器控制台。

一些有用的数组方法

在本节中,我们将介绍一些相当有用的数组方法,这些方法允许我们将字符串拆分为字符串数组,反之亦然,以及添加或删除元素。

字符串和数组之间的转换

通常,你会看到一个包含在一个长长的字符串中的原始数据,你可能希望将有用的项目分成更有用的表单,然后对它们进行处理,例如将它们显示在数据表中。为此,我们可以使用 split() 方法。在其最简单的形式中,这需要一个参数,你要将字符串分隔的字符,并返回分隔符之间的子串,作为数组中的项。

**备注:**好吧,从技术上讲,这是一个字符串方法,而不是一个数组方法,但是我们把它放在数组中,因为它在这里很合适。

  1. 我们来玩一下这个方法,看看它是如何工作的。首先,在控制台中创建一个字符串:

    1
    let myData = "Manchester,London,Liverpool,Birmingham,Leeds,Carlisle";
  2. 现在我们用每个逗号分隔它:

    1
    2
    let myArray = myData.split(",");
    myArray;
  3. 最后,尝试找到新数组的长度,并从中检索一些项目:

    1
    2
    3
    4
    myArray.length;
    myArray[0]; // the first item in the array
    myArray[1]; // the second item in the array
    myArray[myArray.length - 1]; // the last item in the array
  4. 你也可以使用 join() 方法进行相反的操作。尝试以下:

    1
    2
    let myNewString = myArray.join(",");
    myNewString;
  5. 将数组转换为字符串的另一种方法是使用 toString() 方法。 toString() 可以比 join() 更简单,因为它不需要一个参数,但更有限制。使用 join() 可以指定不同的分隔符(尝试使用与逗号不同的字符运行步骤 4)。

    1
    2
    let dogNames = ["Rocket", "Flash", "Bella", "Slugger"];
    dogNames.toString(); //Rocket,Flash,Bella,Slugger

添加和删除数组项

我们还没有涵盖添加和删除数组元素,现在让我们来看看。我们将使用在上一节中最后提到的 myArray 数组。如果你尚未遵循该部分,请先在控制台中创建数组:

1
2
3
4
5
6
7
8
let myArray = [
"Manchester",
"London",
"Liverpool",
"Birmingham",
"Leeds",
"Carlisle",
];

首先,要在数组末尾添加或删除一个项目,我们可以使用 push()pop()

  1. 让我们先使用 push()——注意,你需要添加一个或多个要添加到数组末尾的元素。尝试下面的代码:

    1
    2
    3
    4
    myArray.push("Cardiff");
    myArray;
    myArray.push("Bradford", "Brighton");
    myArray;
  2. 当方法调用完成时,将返回数组的新长度。如果要将新数组长度存储在变量中。例如:

    1
    2
    3
    var newLength = myArray.push("Bristol");
    myArray;
    newLength;
  3. 从数组中删除最后一个元素的话直接使用 pop() 就可以。例如:

    1
    myArray.pop();
  4. 当方法调用完成时,将返回已删除的项目。你也可以这样做:

    1
    2
    3
    let removedItem = myArray.pop();
    myArray;
    removedItem;

unshift()shift() 从功能上与 push()pop() 完全相同,只是它们分别作用于数组的开始,而不是结尾。

  1. 首先 unshift()——尝试一下这个命令(在头部加入一个元素):

    1
    2
    myArray.unshift("Edinburgh");
    myArray;
  2. 现在 shift()——尝试一下!(在头部删除一个元素)

    1
    2
    3
    let removedItem = myArray.shift();
    myArray;
    removedItem;

条件语句

先pass

循环语句

先pass

函数——可复用的代码块

先pass

后面的内容先不看。

mathjax - 公式

网址链接方式

举例如下

1
2
3
4
5
6
7
8
9
<!-- MathJax 3 (CDN 保留,离线环境可自行替换为本地) -->
<script id="MathJax-script" defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
window.MathJax = {
tex: { inlineMath:[['$','$'],['\\(','\\)']], displayMath:[['$$','$$'],['\\[','\\]']], processEscapes:true },
options: { skipHtmlTags:['script','noscript','style','textarea','pre'] }
};
</script>

本地下载

要在无网络的环境下使用公式渲染,最可靠的方式就是下载一份完整的 MathJax 文件到本地。对于 3.2.2 版本,推荐直接从 GitHub 的 MathJax 仓库(而非 MathJax-src 源码库)下载预构建文件。这样做能避免复杂的编译过程,直接获得可在浏览器中使用的文件。

以下是具体的操作步骤:

📥 1. 下载与准备

  1. 访问下载页面:在浏览器中打开此链接:github.com/mathjax/MathJax/releases/tag/3.2.2

  2. 下载压缩包:在页面下方找到 “Assets” 区域,点击 Source code (zip)MathJax-3.2.2.zip 进行下载(我只)。

    • 注意:MathJax-src 是“源码版”,需要编译才能使用,请勿直接使用。(我下载的其中一个就是MathJax-src-3.2.2.zip 就是源码包,另一个 MathJax-3.2.2.tar.gz 才是编译后的 )

      解压后,查看根目录下是否有 es5 文件夹。若有,则正确;若只有 components.ts 等文件,则仍是源码。

  3. 解压文件:将下载的压缩包解压到您项目的 src/js/ 目录下。

项目结构最终会类似于:

1
2
3
4
5
6
7
8
9
10
your-project/
├── src/
│ ├── css/
│ ├── images/
│ └── js/ # JavaScript 文件存放目录
│ └── MathJax/ # <-- 将解压后的 MathJax 文件夹放在这里
│ ├── es5/
│ ├── ...
├── template.html
└── ...

⚙️ 2. 页面配置与引用

下载完成后,修改您的 template.html 文件,将 MathJax 的 CDN 链接替换为本地路径。

这里MathJax 3 的 CommonHTML 输出引擎默认从 CDN 加载字体(https://cdn.jsdelivr.net/npm/mathjax@3/es5/output/chtml/fonts)。如果你把 MathJax 整个目录放到了本地,就必须告诉它字体在哪,否则公式会无法正常显示(例如显示为缺失字符的方块)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 1. 配置 MathJax(必须在加载脚本之前) -->
<script>
window.MathJax = {
tex: {
/* 允许行内用 \(...\) 或 $...$(后者按需开;报告里建议只开 \(...\) 最稳) */
inlineMath: [['\\(', '\\)']],
displayMath: [['\\[', '\\]']], /* 区块公式用 \[ ... \] */
tags: 'none', /* 不需要公式自动编号就 none */
processEscapes: true
},
chtml: {
fontURL: 'src/js/MathJax-3.2.2/es5/output/chtml/fonts' // 本地字体路径
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
}
};
</script>

<!-- 2. 加载本地 MathJax(使用 defer 确保顺序) -->
<script defer src="src/js/MathJax-3.2.2/es5/tex-svg-full.js"></script>

效果太难看了,改成使用 SVG 渲染模式 (注:如果你的文件夹里还有 tex-svg-full.js,用那个更好,功能更全)。SVG 是把公式当成真正的矢量图形来画,线条锐利、粗细适中、比例完美,所以看起来高级很多

1
<script defer src="./MathJax-3.2.2/es5/tex-svg-full.js"></script>

此时可以删除本地字体路径的语句,用不上了。

复制到word中

Word 公式的底层格式叫 OMML(Office Math Markup Language),是微软自家的 XML 格式;

1
2
3
4
5
LaTeX      → 纯文本标记语言(学术界排版标准)
OMML → Word 内部的 XML 公式格式(Word 2007+ 的唯一原生存储)
MathML → W3C 网页标准(浏览器里用,Word 不直接存它)

你在 Word 里看到的公式 = OMML 渲染出来的

我试过了,word 公式里直接写 latex 代码不能正常展示。AI 需要 Word 2019/365 或更新版本,并且只支持部分 LaTeX 命令(我用的是 2016 版本,不行)。

单个公式可以在网页中选中公式 - 右键 - copy to clipboard - MathML Code ,这就是复制上了 MathML 格式的代码,直接复制黏贴到 word 中就能显示(MathML(数学置标语言) 是 Word 能够识别的标准数学格式之一)。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2026 Vincere Zhou
  • 访问人数: | 浏览次数:

请我喝杯茶吧~

支付宝
微信