JavaScript 杂记
COMP3322 课程 JavaScript, AJAX & JSON 相关内容拾遗。
接触到的第二种 dynamically typed OOP 语言 (第一种是 Ruby),但其语法又总是令人联想到 C 与 Java;学起来不算难,令人惊艳的点主要在于它是如何通过 DOM (Document Object Model) 与 HTML 交互的。
严格来说,HTML, CSS, JavaScript 三件套中只有 JavaScript 能够称之为是一种「编程语言」 (programming language)。前两者是静态的标记语言 (markup language),因此不归在 PL 类别中。
This article is a self-administered course note.
It will NOT cover any exam or assignment related content.
Prototype-based OOP
We don't need classes... Objects inherit from object. What could be more object-oriented than that?
关于语言的部分我不打算详细记录,JavaScript 的语法并非这门语言的精髓所在。
但值得注意的是,JavaScript 的 OOP 概念并非传统的 class-based programming,而是 prototype-based programming (基于原型编程)。
在传统的 class-based OOP 中,我们首先定义抽象的,(通常是) 静态的 类 (class),再在类所规定的框架下创造 对象 (object)。这一过程被称为 实例化 (instantiation)。
而在 JavaScript 为代表的 prototype-based OOP 中,类的概念并不存在;继承发生在对象与对象之间。若对象 \(a\) 继承了对象 \(b\),我们称对象 \(b\) 为对象 \(a\) 的原型 (prototype)。
很显然,只有在 dynamically typed 语言中 prototype-based OOP 才有生存的空间;对象间的继承依赖于对对象的动态更改,即,在运行时增添,删减或重写对象的性质与行为。
如果仔细想想的话,其实 prototype-based OOP 才是更符合直觉的 OOP
定义方式。女娲在造人之前并没有获得某种人类的制造蓝图
(class Human
),而是以自己为原型 (Object Nüwa
)
决定了人类的外形。如果我们使用 JavaScript 来描述女娲造人的过程:
1 | var Nüwa = { |
那么能否在 JavaScript 中实现类似 class-based OOP 的行为呢?也是可以的。构造函数 (constructor) 可以一定程度上承担 class 所具备的功能。
女娲没日没夜地捏着泥巴,手终于开始发酸了;而且她也觉得这样效率实在是太低。她打算建造一个自动化人类生产工厂,自己只需要传入对应的参数,工厂就能生产出具备这些特点的人类:
1 | function Human(name, gender, destiny) { |
Document Object Model
文档对象模型 (DOM, Document Object Model) 在 JavaScript 中被广泛使用,但需要注意的是,它并不是 JavaScript 或其他编程语言的一部分 (platfrom- & language-neutrality)!
DOM 是一种 programming interface:在实现了 DOM 接口 (implement DOM interface) 的编程语言中,我们能够调用一系列强大的 API,来对 DOM 及其所 model 的 HTML 文件进行动态操纵。
Element Node Object
在 DOM 中,每个 HTML element 都是一个结点 (node)。结点之间相互连接,从而形成了一颗树。这颗树的结构正是 HTML 文档的结构。DOM 中的结点共分为三种:
- element nodes.
- text nodes.
- attribute nodes.
很明显 text nodes 与 attribute nodes 一定是 DOM 树的叶子节点。
在实现了 DOM interface 的编程语言中,element object 自带一系列 properties 与 method:
attributes
: 返回 a list of attributes.className
id
innerHTML
: 以字符串的形式返回该元素的 markup content (tags 包围的所有内容)。使用这个 property 对元素进行外科手术式的修改。style
: 返回一个特殊的对象,该对象中存储了一系列 styling name-value pairs。tagName
addEventListener()
: [stay tuned for event handling]getAttribute()
,setAttribute()
,removeAttribute()
一个典型的 JavaScript workflow:使用 document object [stay tuned] 定位某个 element node object,访问它的 properties 以获取信息或调用它的 methods 对其进行修改。
Document Object
document
是一个特殊的对象;它代表整个 HTML
文档。通常我们将其作为 DOM crawling 的起点。除此之外,它还是
window
对象的孩子,因此我们也可以用
window.document
进行指代。
常用的 DOM crawling 方法:方法名中含有 Elements
的往往返回 a list of element nodes.
getElementById()
getElementsByClassName()
getElementsByTagName()
querySelector()
: 该方法返回与选择器匹配的 第一个 元素对应的结点。querySelectorAll()
动态修改 DOM 树的结构;添加新的结点:
createAttribute()
createElement()
createTextNode()
以上方法的参数均为字符串:注意字符串在 JavaScript 中是 immutable 的。
Executing JS in HTML
牢记一个原则:当 browser 在解析 HTML 文件时,如果它遇到了某个
<script>
元素,它将立刻执行其中的 JavaScript
代码,无论 HTML 文件是否已经解析完成。
<script>
有两个 attribute: async
与
defer
,它们可以 override 这一默认的,由位置决定的顺序。
different
<script> |
execution priority | details |
---|---|---|
<script> in
<head> |
very high - blocks parser | loaded & executed before parsing begins |
<script> |
high - interrupts parser | parser pauses until it is loaded & executed |
<script async> |
high - interrupts parser | parsing and loading go asynchronously; parser pauses when JS is ready to execute |
<script> at the end
of <body> |
low - waits parser end | loaded & executed after parsing ends |
<script defer> |
very low - executes after
<script> at the end of <body> |
parser does not wait for the script; it continues to build DOM |
注意,我们能够保证 <script defer>
一定在 DOM is
fully built 之后,DOMContentLoaded
事件之前被执行;而
<script async>
被执行的时刻是不可预测的。它可能在
DOMContentLoaded
事件之前被执行,也有可能在其之后被执行;一切取决于 script 完成 loading
所需的时间。
Event Handling
首先,event 也是 DOM interface 的一部分。因此我们可以在实现了 DOM interface 的 JavaScript 中直接使用一系列 pre-built event objects,调用各种 event-related APIs。
Event Handler
Event handler 是一段代码:当其侦测 (listen) 到某事件触发 (event fires) 时,其立刻执行所储存的代码 (通常是一个函数)。有三种 registering event handlers 的方法:
- As an HTML attribute: bad style.
- As a method attached to the element: only one handler per event.
- Using
addEventListener()
.
1 | <button id = "bttn">Press me</button> |
addEventListener()
函数为某个元素添加一个 event
handler;其第一个参数是 event 的名称,第二个参数是当 event
触发时所需要执行的函数。同样的,我们有
removeEventListener()
来删除特定的 event handler。
addEventListener()
函数还有第三个参数,useCapture: false
;它与 event
propagation 有关。
Event Propagation
在某元素的范围内,一个事件的发生不仅会影响到该元素本身,还会影响到其所有的祖宗元素;因此,多个 event handlers 可能被触发。Event propagation 规定了这种情况下 event handlers 被执行的顺序。
1 | <html> |
假设 pn
是事件发生处的最直接元素,event propagation
规定:
- 首先进行 capturing propagation:从
<html>
到pn
,自顶向下检测元素是否定义了处理对应事件的 event handler;如果有,并且其useCapture
为真,执行该 event handler。 - 然后进行 bubbling propagation:从
<pn>
到<html>
,自底向上检测元素是否定义了处理对应事件的 event handler;如果有,并且其useCapture
为假 (default),执行该 event handler。
Asynchrounous Programming
什么是同步/异步 (synchronous/asynchronous)?在中文语境下其实我们很容易将这两个词的意义弄混。
- synchronous:下一进程必须等待上一进程完成之后才能开始执行。
- asynchronous:上一进程不会阻塞下一进程的执行;两者可以并行。
在 web application 中,asynchronous programming 的应用使得 web page:
- faster:多个异步进程能够同时进行,大大节约了时间。
- more responsive:在异步进程执行期间 browser 将不会停摆,仍然能对用户触发的事件作出响应。
当异步进程执行完毕之后,将会有对应的函数处理该进程的结果。我们将这种函数称为 callback function (回调函数);callback function 仅仅会在对应的异步进程执行完毕后被调用。
仔细想想,这是不是和 event 与 event handler 的定义有点相似?event handler 只有在对应的 event 触发后才被调用;如果把异步进程的完成视作某个事件的话,其对应的 callback function 就是该事件的 event handler。
实际上,在 JavaScript 中最常见的 asynchronous paradigm - AJAX
中,就采用了通过添加 event handler 的方式实现异步的思想 [stay tuned for
AJAX with XMLHttpRequest
]。
AJAX with
XMLHttpRequest
XMLHttpRequest
Object
AJAX (Asynchronous JavaScript with XML,异步 Javascript 与 XML) 范式:在无需重载网页的情况下,使得 browser 能对网页进行快速的,增加式的动态更新。(本博客的全局音乐播放功能就应用了 AJAX)
为什么我们要采用 AJAX?想象这样一个场景:在注册账号时我们常常需要在输入手机号之后点击“发送验证码”按钮。在传统方式下,浏览器在收到服务器的响应后将会重载页面,于是我们之前输入的手机号被刷新掉了;这无疑大大降低了用户体验。而 AJAX 能够解决这个问题。
XMLHttpRequest
对象是 (传统) AJAX 范式的核心。在
client-side (JavaScript) 中:
- [a] 声明一个
XMLHttpRequest
对象。 - [b] 对该对象的
onreadystatechange
property 注册一个 callback function。 - [c] 调用该对象的
open()
与send()
方法向服务器发送 HTTP 请求。- 使用
GET
或POST
。 - 异步请求 (asynchronously):无需等待服务器的 response,不会阻塞浏览器的控制。
- 使用
- [d] 当收到服务器的 response 后,执行之前定义的 callback function。
- [e] 通常接下来 JavaScript 通过 DOM 来修改并渲染网页,这避免了网页的重载。
1 | var ajaxObj = new XMLHttpRequest(); // [a] |
Callback Hell
在不刷新浏览器的情况下执行 DOM 事件时 (e.g. 点击某链接, 回车等事件操作) browser 会向服务端发送若干 HTTP request,携带后台可识别的参数并等待服务器响应返回数据。
这个过程是异步回调的,当许多功能需要连续调用,环环相扣互相依赖时,将会产生庞大的 nested callback:一个 callback function 在函数体中调用另一个 callback,一层一层递归下去……
1 | function clickOnLinksResponse(init, callback) { |
这种庞大的嵌套回调函数被称为 callback hell (回调地狱),或是 pyramid of doom;它大大降低了代码的可读性,并且由于耦合度太高,debug 也十分困难。
为了避免 callback hell,许多现代的 asynchronous API 不再采用 callback
functions。[stay tuned for fetch()
and Promise
object]
AJAX with fetch()
fetch()
是一种更 modern style 的 AJAX
实现。基本语法是:
1 | fetch(url) |
fetch()
函数返回的是一个 Promise 对象 (其最终将被解析为
Response 对象);这里的 .then()
, .catch()
实际上都是 Promise 对象对应的语法:它们是“附着”在 Promise 对象后的
callback functions。
那么什么是 Promise 对象呢?对这个词的最初印象来自 Racket 中 delayed evaluation 的 promise 概念。在学习了 JavaScript 中的 Promise 对象之后确实感受到了一点很 subtle 的相似性。
Promise
Object
Promise 对象有三种状态:
- Pending: initial state, before the Promise is resolved or rejected.
- Resolved: Promise has been resolved with a value.
- Rejected: Promise has been rejected with a reason.
Promise 对象后附着的一系列 .then()
与
.catch()
可以视为该对象的 callback functions。
- 当 Promise 的状态为 resolved 时,以 Promise 的 resolved value
作为参数调用
.then()
。 - 当 Promise 的状态为 rejected 时,以 Promise 的 rejected reason
作为参数调用
.catch()
。
我们常常能看到 Promise 对象下附着了多个连续的 .then()
函数:我们把这一系列 .then()
函数称为 callback
chain。callback chain 中函数的调用是环环相扣的:如果
.then()
有返回值,那么它返回的是一个 Promise 对象;在被
resolved 后将对应的 resolved value 传入下一个 .then()
中进行处理。
Promise 对象的 callback chain 本质上是对传统方式下 callback hell 的解耦合 (nested \(\to\) chaining)。
Response
Object
.then()
函数将对应 Promise 对象的 resolved value
作为参数;也就是说,它能够剥开 Promise 的外壳获取到里面的东西。我们来看
fetch()
函数的定义:Promise<Response> fetch(input[, init])
。
init
是fetch()
函数的自定义 options,可以在其中指定 HTTP request 的:- HTTP method:默认为
GET
。 - HTTP headers。
- HTTP body。
- HTTP method:默认为
- Promise 对象所封装的是 Response
对象:它携带着服务器传回的信息并传入对应的
.then()
函数。
Response 对象常用的 properties 与方法:
status
: HTTP response code.statusText
: HTTP response text.ok
: True if status is HTTP 2xx.headers
: a Header object associated with the response header.text()
: a Promise contains the body as a string.json()
: a Promise contains the body text as JSON [stay tuned].
1 | snum.addEventListener('blur', fetchRequest); |
async
/await
sytatic sugar
使用 async
/await
写出的代码更近似传统的
synchronous style,因此可读性更强。
async
declaration defines an asynchronous function which returns an implicit Promise.- within the
async
function,await
expression 'pauses' the execution. await
is only valid inside theasync
function.
一言以蔽之,在某个 async
函数内,我们将所有 asynchronous
function 前加上 await
,就可以省去 .then()
,
.catch()
等冗长的定义。
1 | snum.addEventListener('blur', fetchRequest); |
JSON
关于 JSON 没什么要讲的,理解为每个对象的 toString()
方法就好。它主要用于网络数据的交换;此外,不仅仅是
JavaScript,现在很多语言都能生成或解析 JSON 格式的数据。
JSON object 由一系列 name-value pairs 组成:
- name:一定是双引号括起来的字符串。
- value:strings (双引号), numbers, objects (大括号), arrays (中括号), booleans, null/empty.
注意,JSON 是纯文本格式:它只包含 properties,不包含 methods。
1 | // trick: 使用 stringify() 与 parse() 实现 JavaScript 中对象的深拷贝 |
Reference
This article is a self-administered course note.
References in the article are from corresponding course materials if not specified.
Course info: Code, COMP3322. Lecturer, Dr. Tam Anthony Tat Chun.
Additional website: