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
2
3
4
5
6
7
8
9
10
11
12
var Nüwa = {
head: 人首,
body: 蛇身,
specialPower: function() {
createHuman();
}
...
}

var Human001 = Object.create(Nüwa); // creating object based on prototype object
Human001.body = 人身; // modifying property dynamically
Human001.specialPower = undefined; // deleting property dynamically

那么能否在 JavaScript 中实现类似 class-based OOP 的行为呢?也是可以的。构造函数 (constructor) 可以一定程度上承担 class 所具备的功能。

女娲没日没夜地捏着泥巴,手终于开始发酸了;而且她也觉得这样效率实在是太低。她打算建造一个自动化人类生产工厂,自己只需要传入对应的参数,工厂就能生产出具备这些特点的人类:

1
2
3
4
5
6
7
8
9
10
function Human(name, gender, destiny) {
this.name = name;
this.gender = gender;
this.destiny = destiny;
this.speak = function() { ... };
this.hear = function() { ... };
...
}

var Human999 = new Human('Huang', 'male', 'Emperor')


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

JavaScript only needs one friend in the world of HTML - the <script> element.

牢记一个原则:当 browser 在解析 HTML 文件时,如果它遇到了某个 <script> 元素,它将立刻执行其中的 JavaScript 代码,无论 HTML 文件是否已经解析完成。

<script> 有两个 attribute: asyncdefer,它们可以 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-driven programming is a paradigm in which the flow of the program is determined by events, usually user actions.

首先,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
2
3
4
5
6
<button id = "bttn">Press me</button>
<script>
var bttn = document.getElementById("bttn");
function bttnClick() { alert("World ends!") }
bttn.addEventListener('click', bttnCLick);
</script>

addEventListener() 函数为某个元素添加一个 event handler;其第一个参数是 event 的名称,第二个参数是当 event 触发时所需要执行的函数。同样的,我们有 removeEventListener() 来删除特定的 event handler。

addEventListener() 函数还有第三个参数,useCapture: false;它与 event propagation 有关。

Event Propagation

在某元素的范围内,一个事件的发生不仅会影响到该元素本身,还会影响到其所有的祖宗元素;因此,多个 event handlers 可能被触发。Event propagation 规定了这种情况下 event handlers 被执行的顺序。

1
2
3
4
5
6
7
8
9
10
<html>
...
<p id="p1">
<p id="p2">
...
<p id = "pn">
</p>
</p>
</p>
</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:上一进程不会阻塞下一进程的执行;两者可以并行。

Asynchrounous programming enables your program to start a potentially long-running task and still be able to be responsive to other events when that task runs, rather whan having to wait until that task has finished.

在 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 请求。
    • 使用 GETPOST
    • 异步请求 (asynchronously):无需等待服务器的 response,不会阻塞浏览器的控制。
  • [d] 当收到服务器的 response 后,执行之前定义的 callback function。
  • [e] 通常接下来 JavaScript 通过 DOM 来修改并渲染网页,这避免了网页的重载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var ajaxObj = new XMLHttpRequest();  // [a]
if (!ajaxObj)
alert('Cannot create XMLHttpRequest object!!')

// HTTP request function
function ajaxRequest() {
ajaxObj.onreadystatechange = ajaxResponse; // [b] inline style not using addEventListener
ajaxObj.open('GET', "checking.php?number="+snum.value, true); // [c]
ajaxObj.send();
}

snum.addEventListener('blur', ajaxRequest); // <=> snum.onblur = ajaxRequest

// callback function
function ajaxResponse() {
if (ajaxObj.readyState == 4 && ajaxObj.status == 200) {
// [e] use innerHTML/addElement to modify web page
document.getElementById('chkReg').innerHTML = ajaxObj.responseText;
}
}

Callback Hell

在不刷新浏览器的情况下执行 DOM 事件时 (e.g. 点击某链接, 回车等事件操作) browser 会向服务端发送若干 HTTP request,携带后台可识别的参数并等待服务器响应返回数据。

这个过程是异步回调的,当许多功能需要连续调用,环环相扣互相依赖时,将会产生庞大的 nested callback:一个 callback function 在函数体中调用另一个 callback,一层一层递归下去……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function clickOnLinksResponse(init, callback) {
...
callback(result);
}

...

function ajaxResponse() {
clickOnLinksResponse(0, (result1) => {
EnterResponse(result1, (result2) => {
SubmitResponse(result2, (result3) => {
...
})
})
})
}

这种庞大的嵌套回调函数被称为 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
2
3
4
5
6
7
8
9
10
11
fetch(url)
.then(function() {
// code for handling the returned data
})
.then(function() {
...
})
...
.catch(function() {
// code to be run when returns any networks errors
})

fetch() 函数返回的是一个 Promise 对象 (其最终将被解析为 Response 对象);这里的 .then(), .catch() 实际上都是 Promise 对象对应的语法:它们是“附着”在 Promise 对象后的 callback functions。

那么什么是 Promise 对象呢?对这个词的最初印象来自 Racket 中 delayed evaluation 的 promise 概念。在学习了 JavaScript 中的 Promise 对象之后确实感受到了一点很 subtle 的相似性。

Promise Object

A Promise is a proxy for a value not necessarily known when it is created.

This lets asynchoronous methods (e.g., fetch()) behave synchronously: instead of immediately returning the final value, it returns a promise to supply the value at some point in the future.

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])

  • initfetch() 函数的自定义 options,可以在其中指定 HTTP request 的:
    • HTTP method:默认为 GET
    • HTTP headers。
    • HTTP body。
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
snum.addEventListener('blur', fetchRequest);

function fetchRequest() {
fetch('checking.php?number=' + snum.value);
.then(response => {
if (response.status == 200) {
response.text().then(data => { // text() returns a promise
document.getElementById('chkReg').innerHTML = data;
});
} else {
console.log("HTTP return status: " + response.status);
}
})
.catch(err => { console.log("Fetch Error!"); });
}

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 the async function.

一言以蔽之,在某个 async 函数内,我们将所有 asynchronous function 前加上 await,就可以省去 .then(), .catch() 等冗长的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
snum.addEventListener('blur', fetchRequest);

async function fetchRequest() {
try {
let response = await fetch('checking?php?number=' + snum.value);
if (response.status == 200) {
let data = await response.text();
document.getElementById('chkReg').innerHTML = data;
} else {
console.log("HTTP return status: " + response.status);
}
} catch(err) {
console.log("Fetch Error!");
}
}


JSON

JSON (JavaScript Object Notation) is a standard text-based format for representing structured data based on JavaScript object syntax.

关于 JSON 没什么要讲的,理解为每个对象的 toString() 方法就好。它主要用于网络数据的交换;此外,不仅仅是 JavaScript,现在很多语言都能生成或解析 JSON 格式的数据。

JSON object 由一系列 name-value pairs 组成:

  • name:一定是双引号括起来的字符串。
  • value:strings (双引号), numbers, objects (大括号), arrays (中括号), booleans, null/empty.

注意,JSON 是纯文本格式:它只包含 properties,不包含 methods。

1
2
3
// trick: 使用 stringify() 与 parse() 实现 JavaScript 中对象的深拷贝
let stringObj = JSON.stringify(object);
let copyObj = JSON.parse(stringObj);


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:

-----------------------------------そして、次の曲が始まるのです。-----------------------------------