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函数传入参数就是之前使用fetchAPI 规定的返回内容(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();













