web自学3——JavaScript
在简单学习了 HTML 和 CSS 之后,接下来是 JavaScript 的学习,学习的重点不是 JavaScript 作为一个脚本语言的语法内容,而是 JavaScript 对于网页的控制,参考教程:https://developer.mozilla.org。
- HTML 定义了网页的内容
- CSS 描述了网页的布局
- JavaScript 控制了网页的行为
1. 在 HTML 中使用 JS
在 HTML 和 CSS 一起组装成一个网页之后,浏览器的 JavaScript 引擎将执行 JavaScript 代码,这保证了当 JavaScript 开始运行之前,网页的结构和样式已经就位,然后 JS 可以更新这个网页的具体内容等。
对于浏览器,它的每一个标签页使用了一个独立的运行环境,不同标签页之间没有任何联系,JS 代码也相互独立运行,一定程度上保证了信息安全。
script 标签
在 HTML 的 head 或者 body 部分都可以使用,例如:
1 |
|
建议在 body 部分的最后使用(也就是</body>之前),否则加载时可能遇到 HTML 元素还没出现等错误,通常 JS 需要等待 HTML/CSS 加载完成后进行执行,对网页进行进一步的处理。
链接 JS 文件
导入外部的 js 文件,在 HTML 的 head 或者 body 部分都可以使用,例如:
1 |
|
加载逻辑
对于外部脚本的加载执行,默认情况下,浏览器处理 HTML/CSS 生成页面时,遇到<script>元素时会中断页面的加载,立刻加载脚本并执行,然后加载页面的剩余部分。
可以使用 async 进行异步化加载,它告知浏览器:在遇到<script>元素时不要中断后续页面的加载,异步地进行脚本加载,等到 JS 脚本下载完成时中断页面的处理,执行脚本,执行完成后继续加载页面。使用 async 引入的脚本建议不要修改页面内容,并且不能依赖其他脚本,因为对于多个 async,加载的进度不可控,因此执行的顺序也不可控。
1 | <script async src="abc.js"></script> |
可以使用 defer 进行,它明确告知浏览器:遇到<script>元素时异步进行脚本加载,等到整个页面加载结束,才会执行脚本,脚本会在 DOMContentLoaded 事件触发之前执行完,并且对于多个脚本按照出现的顺序依次执行,例如
1 | <script defer src="js/script1.js"></script> |
2. JS 基础语法
作为脚本语言,JS 和 Python 非常类似,首先过一遍语法。
- JS 的语句使用大括号划分语句块的结构层次等。
- JS 对于大小写敏感!
- JS 语句可以不使用结束符,但是建议使用分号作为语句的结束,没有分号的语句出现在行末总是默认语句结束了。
- JS 支持注释,包括
/**/和//都可以。
基本字面量
- 数字 Number 包括整数和小数;(整数和小数根本不分开,特别注意)
- 字符串 String 例如
a="John",支持索引访问(从 0 开始),可以直接获取长度a.length; - 布尔类型 Boolean 例如 true 和 false;
- 数组 Array 例如
[1,2,3]; - 对象 Object 例如
{name:"abc",length:20};(JSON 语法就是由此衍生的,最后一个不要加逗号) - 函数 Function 例如
function fa(a,b){ return a*b; }
数字字符串可以与数字互相转换类型:
1 | (123).toString(); // 得到 "123" |
变量
和 Python 一样,JS 变量是没有固定类型的,变量可以赋任何类型的值。
1 | var a, b; // 变量声明(创建了变量,但是没有值,未定义undefined) |
可以使用 typeof 获取当前变量的类型,例如
1 | var a = 1; |
不声明直接使用变量也是可以的,这时会自动视为 window 的一个属性,例如
1 | a = "abc"; |
对于var a等变量声明语句,JS 解析时采用了声明提升的规则,也就是会自动把变量声明语句(不含初值)移动到语句块的最前方。
函数
见下例:
1 | // 无参数 |
对象
对象的定义以及属性的获取见下例,格式就是 JSON 那样:
1 | var a = { |
对象除了属性,还可以定义方法,也就是对象可调用的函数,例如
1 | var a={ |
流程控制
条件语句,例如
1 | if (a > 0) { |
循环语句,基本上也和 c 风格一样,例如:
1 | // c风格的for循环 |
也支持 break 和 continue 跳出循环。
严格模式
JS 的语法较为宽松,但是可以在脚本或函数的开头使用下面一行代码开启严格模式,
1 | ; |
严格模式会用更严格的标准来检查语法:
- 不允许使用未声明的变量;
- 不允许使用
delete a;删除变量或对象或函数; - 不允许函数重名等。
- …
3. JS 脚本如何执行
JS 脚本在网页中何时会被执行?主要有两类:
- 网页加载时,或者网页加载完成时;
- 特定的事件触发,例如点击按钮,在文本框输入内容,关闭页面等,主要是通过调用函数来执行。
加载时执行
例一,加载页面时 demo 元素会被 JS 修改内容,变成”段落已修改。”
1 | <p id="demo">我的第一个段落</p> |
效果为:
我的第一个段落
例二,加载页面时直接在页面中添加内容,注意不要在文档加载完成之后使用document.write(),这会覆盖该文档。
1 | <script> |
效果为:
例三,加载页面时会自动触发弹窗,还会向浏览器控制台写入信息(未使用)
1 | <script> |
由事件触发
例一,点击按钮会把<p>元素的内容设置为当前时间,因为把按钮的 onclick 绑定到了 JS 的 myFunction 函数,会触发执行 myFunction 函数。
1 | <p> |
效果为:
当前时间:
除了在 HTML 元素中绑定,也可以在 JS 脚本中对元素的特定事件绑定相应函数。例如下面通过 JS 脚本对文本绑定了很多函数,对应行为会分别触发相应函数。
源代码为:
1 | <p id="d0">与这段文字交互</p> |
效果为:
与这段文字交互
常见的事件为:
- 当前 HTML 元素被改变
onchange; - 当前 HTML 元素被单击(似乎左右键都行)
onclick; - 当前 HTML 元素被右键点击
oncontextmenu; - 鼠标移动到 HTML 元素上
onmouseover; - 鼠标从 HTML 元素上移开
onmouseout;
4. JS 如何修改页面
接下来考虑:JS 脚本如何修改网页的内容,包括 HTML 元素的内容,属性和样式等。
接入 HTML 输入流
使用 document.write 在页面当前位置中添加内容,注意不要在文档加载完成之后使用 document.write(),这会覆盖该文档。
源代码如下(未使用)
1 | <script> |
改变元素内容
使用document.getElementById等方法可以获取元素,对元素的 innerHTML 属性赋值,就是直接修改元素的内容。
源代码如下
1 | <ul> |
效果如下:
- Hello World!
- Hello World!
- Hello World!
改变元素属性/样式
使用document.getElementById等方法可以获取元素,对元素的 style 属性的子属性赋值,就是直接修改元素的样式。
源代码如下:
1 | <ul> |
效果如下:
- Hello World! 改蓝色
- Hello World! 改字体
- Hello World! 改字号
直接弹窗
浏览器支持三种弹窗:警告框、确认框、提示框。
- 警告框向用户弹出警告,用户只能关闭,
- 确认框需要用户选择确认或取消,返回一个布尔值,
- 提示框需要用户输入一个值(可以给默认值),然后点击确认或直接取消,如果取消返回值为 null,
可以分别点击按钮试一下,源代码为:
1 | <script> |
计时事件
可以实现每秒获取时间,写入元素中: 也就是每秒钟运行一次代码,点击时间则可以停止这个计时事件。
源代码如下,似乎函数使用可以放在定义之前?
1 | <script> |
5. JS 模块化
前面的例子中,JavaScript 要么直接写在 HTML 的 <script> 标签中,要么通过:
1 | <script src="abc.js"></script> |
引入一个外部 JS 文件。这种脚本可以称为普通脚本(classic script)。它只是把 JS 代码从 HTML 中移到了另一个文件里,本质上仍然像一段直接插入页面的脚本,实际差异不大。
这种原始的脚本写法会把所有的顶层变量暴露到全局作用域中。多个普通脚本之间也共享同一个全局环境,因此后面的脚本可以使用前面脚本定义的变量和函数:
1 | <script src="a.js"></script> |
这种处理方式简单,但问题也很明显:
- 多个脚本共享全局作用域,名字容易冲突;
- 脚本之间依赖关系不清楚,必须靠 HTML 中的加载顺序保证;
- 一个文件到底向外提供了什么功能、依赖了什么功能,不够明确;
- 文件拆多以后,维护的重点不只是“文件变短”,而是“依赖关系是否清楚”。
ES Module 解决的就是这个问题。模块不是简单地把代码塞进另一个文件,而是让每个文件有自己的作用域,并且用 export 明确导出功能,用 import 明确导入依赖。
在 HTML 中,模块脚本需要写成:
1 | <script type="module" src="./main.js"></script> |
普通脚本和模块脚本的主要区别可以先记成:
| 普通脚本 | 模块脚本 |
|---|---|
<script src="abc.js"></script> |
<script type="module" src="./main.js"></script> |
| 顶层变量可能进入全局作用域 | 每个模块有自己的作用域 |
| 依赖关系主要靠 HTML 中的脚本顺序 | 依赖关系写在 import 中 |
不能使用顶层 import/export |
可以使用 import/export |
| 默认不是严格模式 | 默认使用严格模式 |
| 默认会阻塞后续 HTML 解析 | 默认类似 defer,等 HTML 解析后执行 |
对于现代编程语言来说,模块化的重点不是“因为代码太长所以拆文件”,而是解耦——每个文件明确声明自己依赖什么、向外提供什么。
还有一个实践上的差异:普通 HTML + 普通脚本的例子,很多时候可以直接双击用浏览器打开 index.html,也就是通过 file:// 访问;但是使用 ES Module 之后,直接双击用浏览器打开可能无法正常工作,通常需要开启本地 HTTP server,从 localhost 的指定端口访问。
这是浏览器的安全检查导致的:模块脚本会按照模块加载规则继续请求 import 的其它文件,浏览器对 module 的跨源检查也更严格。file:// 的 origin 比较特殊,直接打开时可能遇到模块导入失败或 CORS 相关错误。
例如一个最简单的 TodoList,所有逻辑都写在一个 HTML 文件中:
1 |
|
注意:
- 这个简单的 Todolist 其实并没有保存 Todo 数据。每个任务只是运行时创建出来的 DOM 节点,数据只存在于当前页面中;关闭或刷新页面后,浏览器会重新加载最初的 HTML,列表也会恢复为空。
- 数据的存储有很多种实现,与具体情景有关。例如只是本地使用的小工具,一般可以把 Todo 数组转换成 JSON 后保存到浏览器的
localStorage,页面加载时再读取并恢复。如果需要登录、多设备同步或多人共享时,通常会把数据发送给后端,由服务器保存到数据库。
下面用模块化的方式改写,得到的是同一个目录下的三个文件:todo.js、main.js 和 index.html。
1 | export function createTodoItem(text) { |
这里的 export 关键词表示函数 createTodoItem 可以被其它模块使用。
然后在入口模块 main.js 中导入并使用:
1 | import { createTodoItem } from "./todo.js"; |
这里的 import 语句表示当前模块依赖 ./todo.js 中导出的 createTodoItem。
对应的 index.html 可以简化为:
1 |
|
