JavaScript入门笔记-(2)
Written by SJTU-XHW
Reference: 《Professional JavaScript for Web Developers》 3rd Edition
本人学识有限,笔记难免有错,恳请读者能够批评指正,本人将不胜感激!
Chapter 7. 简单使用 DOM
假设你学习了初级数据结构,那么把 HTML 页面想象成一个以元素为结点的一般树。JavaScript DOM 操作就是操作这个文档树,达到改变前端页面的目的。
在浏览器运行的 JavaScript 引擎中,全局运行环境中会自动设置一个变量 document
(DOM 对象),操作它就是在动态操作 HTML 页面。
7.1 基本操作
假设你学习了 CSS 的基础用法。
document.querySelector(<selectorStr>)
:JavaScript DOM 中的重要方法,document 对象的 按选择器查找 HTML 中的元素(CSS 中有效的所有选择器,除了伪类选择器都行);如果有多个符合选择器的对象,那么只会返回一个。想要返回全部,请使用
querySelectorAll() -> NodeList (or Array)
;注:还有一些老方法
getElementById
、getElementByTagName
不建议使用,因为太多太繁了,有这个方法足够了。<Node>.textContent
:很多元素,例如div
、button
、p
、a
等等,可以通过对 DOM 结点设置这个属性来达到更改文本的目的。前提是这个元素能够显示文本;<Node>.href
:很多含有href
属性的元素可以用这种方法更改链接,类似地,src
等属性也可以如此更改;<Node>.classList
:获取结点的class
属性列表,相当于<Node>.className.split(" ")
再转为DOMTokenList
;对于
DOMTokenList
,与 JavaScript 原生数组不一样,它有自己的属性、方法:DOMTokenList.length
(只读);DOMTokenList.item(idx)
;DOMTokenList.contains(token)
;DOMTokenList.add(token[, token2, ...])
;DOMTokenList.remove(...)
;DOMTokenList.toggle(token)
:修改属性非常方便的选择!!,若token
存在其中,则删除并返回 false;若不存在,则添加并返回 true;DOMTokenList.forEach(<callback>)
;
网页刷新:
document.location.reload()
;
7.2 创建新结点
一般分三步走:
- 创建结点 DOM 对象:
document.createElement(<element-type>)
; - 设置结点对象属性:
<Node>.attr = ...
; - 向指定结点对象追加新的对象:
<Node>.appendChild(<Node>)
;
7.3 移动、删除结点
注意一个问题:DOM 对象引用与 HTML 元素一一对应。因此除非你使用 <Node>.cloneNode()
,否则,之前新建的结点对象如果再一次 appendChild
,那么它会移动到父结点的底部,例如:
1 | const sect = document.querySelector("section"); |
删除结点就使用:<parentNode>.removeChild(<Node>)
,如果不知道父元素,那么可以借助 parentNode
属性:
1 | <Node>.parentNode.removeChild(<Node>); |
7.4 改变结点样式
JavaScript 可以通过修改 DOM 对象的属性来修改其 CSS 样式。
有两种方法可以实现:
<Node>.style
属性本身是个对象:1
2
3
4
5para.style.color = "white";
para.style.backgroundColor = "black";
para.style.padding = "10px";
para.style.width = "250px";
para.style.textAlign = "center";<Node>.setAttribute(<attrName>, <attrValue>)
;对应有
getAttribute
;它们不止能改 CSS 样式(style
属性),其他属性也能改;
个人建议使用后一种,因为不需要额外记忆内置变量名。
Chapter 8. 事件
事件是 JavaScript 能够响应用户行为的重要手段之一。
事件可能有以下几种:
- 用户选择、点击或将光标悬停在某一元素上。
- 用户在键盘中按下某个按键。
- 用户调整浏览器窗口的大小或者关闭浏览器窗口。
- 网页结束加载。
- 表单提交。
- 视频播放、暂停或结束。
- 发生错误。
为了对一个事件做出反应,就要在 JavaScript 中附加一个事件处理器。它通常是自己创建的、已注册的一个函数。具体使用方法举个例子:
8.1 注册、移除事件处理器
假设 HTML 上有个按钮:
1 | <button class="sbtn groupt"> |
那么:
1 | const btn = document.querySelector("button.sBtn"); |
知识补充: JavaScript 模板字符串(反引号引起的字符串)
相当于 shell 中的单引号字符串、python 的
f
格式化字符串。可以向其中嵌入变量(
${js_variable}
)而无需加法运算符;
于是我们认识了注册事件处理器的函数:<DOMObj>.addEventListener(<event-type>, <func>)
;
除了上面提到的 click
事件,button
元素常用的还有:
"mouseover" / "mouseout"
(即 hover in / hover out);"focus" / "blur"
(即按钮聚焦、失焦,常见用 tab 的情况);"dblclick"
(即双击);"submit"
(对表单中的按钮而言,触发了"click"
就接连触发了"submit"
);
其实 "click"
事件几乎对其他所有元素都可用;
还有一些事件只有特定元素有,例如 "play"
事件只有 <video>
、<audio>
元素有;
相对地,我们可以移除指定的事件处理器:removeEventListener(<event-type>, <func>)
;
这就像 Qt 的信号-槽机制中的 connect
和 disconnect
,就连 “一对多”、“多对一”、“一对一” 等原则也与 Qt 一致,不再赘述。
⚠⚠ 友情提醒 ⚠⚠
不要使用内联事件处理器(写在 HTML 中的事件处理器),它们已经被 deprecated 了,例如:
1 | <button onclick="alert('It was deprecated!');"> |
8.2 事件对象
JavaScript 解释器在回调某个事件处理器时,会自动向参数列表中传入 event / evt / e
(三个都行,开发时为了可读性请选择一个),
事件对象有个常用属性:
event.target
:相当于 Qt 中信号-槽机制的event.sender
,返回发送方对象;event.key
:相当于 QtQKeyEvent::key()
,就是触发键击信号的键;
对常见的 DOM 元素,例如
div
、p
,可以设置<DOMObj>.textContent
改变文本值;
那么我们使用事件对象有什么用?
和 Qt 一样,答案是自定义默认的事件处理。有些默认的事件处理器(例如表单按钮的 "submit"
动作),开发者不好取消,我们可以通过事件对象的方法 event.preventDefault()
来阻止默认动作、进行自定义:
例如对这个表单,我们想让用户必须填写 First name
和 Last name
,缺一不可,
1 | <form> |
我们就能这样阻止表单的默认提交动作:
1 | const form = document.querySelector("form"); |
8.3 事件传递
我们类比 Qt 的信号-槽机制,在 Qt 中,一个事件 / 信号被捕获后,进入事件 / 信号处理函数,我们以 bool eventFilter(QEvent* event, QObject* obj)
为例,如果该函数返回 true
,表示事件处理结束,事件不会再向父控件传递,反之则会。
在 JavaScript 中,事件传递行为和 Qt 相近,只是收到并处理后,开发者无法自定义事件的传递行为,一定会继续向父元素传递。
在一些情况下,由于父子元素需要控制事件比较复杂,如果不控制事件的传递,很有可能引发一些问题,例如我们想做个页面包含一个视频,最初它为隐藏状态;还有一个标记为“显示视频”的按钮。我们希望有如下交互:
- 当用户单击“显示视频”按钮时,显示包含视频的盒子,但不要开始播放视频。
- 当用户在视频上单击时,开始播放视频。
- 当用户单击盒子内视频以外的任何区域时,隐藏盒子。
如果这么写:
1 | <button> |
1 | const btn = document.querySelector("button"); |
知识补充 1: JavaScript DOM 对象
classList
属性;知识补充 2: JavaScript
video / audio
元素对应的 BOM 方法play()
;
我们点击显示视频是没问题的,但是有个问题是,当我们点击视频开始播放的时候,容器会被隐藏!这是因为 video
空间在 div
内部,点击操作会传递到 div
中,相当于同时触发了两个事件处理器。
所以 JavaScript 中的解决方案是 event.stopPropagation()
,在事件处理器中,可以停止事件自动地向父元素传播。
上面的 JavaScript 改成这样就能实现目的了:
1 | const btn = document.querySelector("button"); |
⚠⚠注意:event.target
在事件传递中保持不变,始终代表最内层的元素(事件传递的第一个收到的元素、事件捕获的最后一个捕获到的元素)⚠⚠
8.4 JavaScript 中的 “事件捕获”
JavaScript 中,除了事件传递,还有一种术语叫 “事件捕获”,它和事件传递 唯一区别 是,“事件捕获” 的事件传递方向是从父元素到子元素,与一般事件传递方向正好相反。
你只有在 addEventListener
注册事件处理器时加上第三参数 { capture: true }
才会启用。
你问为什么有这两种传递方式?唉,早期厂商的浏览器互不兼容(Netscape 只使用事件捕捉,而 Internet Explorer 只使用事件冒泡)。
现在的 W3C 指定标准后,将这两种方式都保留下来了,不过默认都是普通的事件传递。
因此这里强烈建议,在编写代码时,尽量使用一种传递方式,增强可读性和可维护性。
Chapter 9. JavaScript 异步
网页端程序的响应速度极其重要,因此网页端应用采用异步的重要性不言而喻。
9.1 认识旧式异步实现:基于事件处理器 和 回调函数
上一章我们已经接触到了 JavaScript 异步编程的一种形式:事件处理器。它不会立即被调用,而是在事件发生时才被调用。只要我们把事件换成某个对象的状态变化,那么就变成了异步程序。
于是 早期的异步 API 就是采用这种方式:通过给 XMLHttpRequest
对象附加事件监听器来让程序在请求进展和最终完成时获得通知。
XMLHttpRequest
有loadend
事件;
1 | <button id="xhr">click to send request</button> |
1 | const log = document.querySelector(".event-log"); |
还有一种异步实现的方式:回调函数。它是一个被传递到另一个函数中的、会在适当的时候被调用的函数。
我们看到之前很多参数名 <callback>
都是回调函数。
那么为什么这种方法被舍弃了呢?考虑这样的场景:假设有 N 个独立的步骤,需要异步执行,它们必须按照一定的执行次序执行。那么当使用回调函数的方式实现的时候,它们就会深度嵌套。例如:
1 | function doStep1(init, callback) { |
这样不仅不美观,而且极大影响代码可读性、可维护性(尤其是 JavaScript 语言本身就难以调试);
所以,大多数现代的异步 API 都不使用上述的方法。现在的 JavaScript 异步编程的基础是 Promise;
9.2 新式异步:JavaScript Promise
JavaScript 的 Promise 对象是 一个由异步函数返回的、可以向我们指示当前操作所处的状态的 对象。
JavaScript 中有很多异步 API 都是基于 Promise 的,它们在被调用后会进行一些操作并立即返回 Promise 对象。开发者可以通过将处理函数附加到返回的 Promise 对象上实现回调。
9.2.1 API fetch()
[global.]fetch()
方法就是一个基于 Promise 的、替代 XMLHttpRequest
的 API,我们直接看一个使用示例:
1 | const fetchPromise = fetch( |
上面的程序分成几个步骤:
- 使用
fetch
(默认)向指定网页发送 GET 请求,返回 Promise 对象存放在fetchPromise
中; - 输出 Promise 对象,能看到
Promise { <state>: "pending" }
,说明 Promise 对象有属性state
来查看异步任务是否完成; - 使用
Promise.then(<callback>)
向 Promise 对象加入回调函数,callback
函数传入参数就是之前使用fetch
API 规定的返回内容(Response
对象);
总结:
fetch(<url>, <options>) -> Promise ->> Response
;
9.2.1-Ex 知识补充:Response 对象
常见实例属性(几乎都是只读,想要自定义请从构造函数传入):body
、headers
、status
、url
(响应的 URL)、redirected
(可以在 fetch
第二参数中禁止重定向:{redirect: "error"}
)等;
常见的实例方法:
Response.blob() -> Promise ->> stream
:这是个异步 API,可以读取响应体中的二进制数据;Response.clone()
:完全复制 Response 对象,唯一区别是内存地址和引用不同;Response.formData() -> Promise ->> FormData
:以表单数据的方式读取响应体,最终返回FormData
对象(和之前的Array
、DOMTokenList
又不一样,自己查 API);Response.json() -> Promise ->> JSON
:以 JSON 数据的方式读取响应体,最终返回原生JSON
对象;Response.text() -> Promise ->> USVString
:以字符串的方式读取响应体,最终返回类似String
的对象,总是 UTF-8;
9.2.2 链式 Promise
Promise 更加优雅的原因是,它的 then
实例方法也会返回 Promise 对象,不过这个对象指示 then
回调函数执行的情况。这样,我们就可以链式地使用 Promise:
1 | const fetchPromise = fetch( |
但我们在上一节中,回调实现遇到 “深层嵌套” 的问题,Promise 好像没有解决?它只不过换成了 then
的嵌套?
实际上不是这样,我们可以借助返回 Promise 的机制多返回几次,把上面的代码改写成:
1 | const fetchPromise = fetch( |
9.2.3 Promise 异常处理
众所周知,网络就是不稳定的,不可避免会出现一些问题。Promise 作为异步的基石,必然要进行异常处理。
以 fetch()
为例,它本身可能因为各种原因出现错误(如没有网络连接、或 URL 本身存在问题),而我们也需要自己在特定的业务逻辑中主动抛出错误。
对此,Promise.catch(<callback>)
就能解决问题:
1 | const fetchPromise = fetch( |
⚠⚠请注意,只要异步操作中接收到了服务器的响应(不管是什么,例如服务器返回的 404),除非开发者自己抛出错误,否则 Promise 认为 fulfilled⚠⚠
9.2.4 Promise state
之前我们在 9.2.1 中看到 Promise 对象有个属性 state
指示异步任务的完成情况。
它有 3 种状态:
- pending:挂起等待,异步任务进行中;
- fulfilled:异步任务成功完成,此时会执行
then
传入的回调函数; - rejected:异步任务执行失败,此时会执行
catch
传入的回调函数,如果没有就向上抛出错误;
fulfilled 和 rejected 统称 settled;
9.2.5 合并 Promise
除了上面提到的链式使用 Promise,可能还会遇到一些情况,需要合并使用多个 Promise。
Promise.all(Array[Promise]) -> Promise ->> Array[...]
,等待所有的 Promise 全部 settled,并且只有全部的 Promise 都 fulfilled,这个 Promise 才会 fulfilled,否则 rejected;Promise.any(Array[Promise]) -> Promise ->> ...
,等待任意一个 Promise settled,该 Promise 最终状态取决于这个等到的 Promise;
9.2.6 显式等待 Promise
如果在一些业务逻辑中必须使用同步编程(例如没有最终返回值就进行不下去),那么就可以对 Promise 进行等待处理。对 Promise 的等待必须放在 异步函数(用关键字 async
修饰,箭头函数也是这样) 中,在返回 Promise 的 API 前加上 await
关键字,这样程序能够等待在这里并且返回最终对象;同时错误也会成为普通的 JavaScript 异常向上抛出。
⚠⚠请注意,异步函数只能返回 Promise 对象。⚠⚠
为什么必须要在异步函数中?因为异步函数有等待,也允许开发者在外面等待 / 继续使用 Promise 处理这个异步函数。
await
和 Promise 链一样,强制异步操作串行完成(一般应用在 “前一个步骤是后一个步骤的参数“ 的情况)。如果不需要串行,Promise.all
可以有更好的性能。
9.2.7 自定义基于 Promise 的异步 API
这里需要了解 Promise 的构造函数 Promise(<executor>)
,executor
的需求参数如下:
1 | function executor(resolve, reject) { /*...*/ } |
resolve
、reject
都是函数,分别对应传入 then
、catch
中的函数。
当一个新的 Promise 对象被创建(new
)后,它立即执行 executor
中的内容(通常是耗时任务)。
如果 executor
内调用了 resolve
(也就是 then
传入的函数),那么 Promise 对象自动进入 fulfilled 状态;反之如果调用了 reject
或者抛出错误(自动调用 reject
),就相当于调用了 catch 传入的函数,或向上抛出了错误。
9.2.8 异步的补充:JavaScript 多线程
这里只是介绍一下,JavaScript 中存在 workers
机制,例如 dedicated workers
,创建线程,但不能访问 DOM 等等。有需要的话自行查询文档。
Chapter 10. JavaScript 简单保存会话状态
基本上有 2 种简单的方法:localStorage
、sessionStorage
。
二者唯一的区别是,前者是 cookie,可以在用户关闭页面后,仍然保留数据,直到下一次打开就能还原数据;后者是 session,数据只保留到用户关闭此会话(即浏览器的标签页)。
使用方法是一样的:
- 定义对象:直接赋值。
myStorage = [window.]localStorage;
- 异常:
SecurityError
,用户拒绝使用 cookie / session; - 设置键值:
localStorage.setItem("<key>", "<value>");
- 获取键值:
localStorage.getItem("<key>", "<value>");
- 移除键值:
localStorage.removeItem("<key>");
- 清空:
locaStorage.clear();