JavaScript进阶篇(一)
注:此篇文章适合有一定JavaScript基础的同学食用,非零基础
生命周期概览
典型客户端Web应用的生命周期从用户在浏览器地址栏输入一串URL,或单击一个链接开始。
从用户的角度来说,浏览器构建了发送至服务器(序号2)的请求,该服务器处理了请求(序号3)并形成了一个通常由HTML、CSS和JavaScript代码所组成的响应。当浏览器接收了响应(序号4)时,我们的客户端应用开始了它的生命周期。由于客户端Web应用是图形用户界面(GUI)应用,其生命周期与其他的GUI应用相似(例如标准的桌面应用或移动应用),其执行步骤:
- 页面构建——创建用户界面;
- 事件处理——进入循环(序号5)从而等待事件(序号6)的发生,发生后调用事件处理器
应用的生命周期随着用户关掉或离开页面(序号7)而结束。
页面构建阶段
当Web应用能被展示或交互之前,其页面必须根据服务器获取的响应(通常是HTML、CSS和JavaScript代码)来构建。页面构建阶段的目标是建立Web应用的UI,其主要包括两个步骤:
- 解析HTML代码并构建文档对象模型(DOM)
- 执行JavaScript代码
步骤1会在浏览器处理HTML节点的过程中执行,步骤二会在HTML解析到一种特殊节点——脚本节点(包含或引用JavaScript代码的节点)时执行。页面构建阶段中,这两个步骤会交替执行多次
页面构建阶段始于浏览器接收HTML代码时,该阶段为浏览器构建页面UI的基础。通过解析收到的HTML代码,构建一个个HTML元素,构建DOM。在这种对HTML结构化表示的形式中,每个HTML元素都被当作一个节点。
当浏览器遇到第一个脚本元素时,它已经用多个HTML元素创建了一个简单的DOM树。
附:
HTML5规范:https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5
DOM3规范:https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
需要注意的是,每当解析到脚本元素时,浏览器就会停止从HTML构建DOM,并开始执行JavaScript代码,并不是一起进行构建,因为JavaScript中可能会有对DOM进行操作,如果同时进行构建可能会引发一些不必要的错误。
所有包含在脚本元素中的JavaScript代码由浏览器的JavaScript引擎执行,例如,Firefox的Spidermonkey引擎,Chrome和Opera和V8引擎和Edge的(IE的)Chakra引擎。由于代码的主要目的是提供动态页面,故而浏览器通过全局对象提供了一个API使JavaScript引擎可以与之交互并改变页面内容。
页面构建一小时,适配就搞一天。
好消息是微软即将停止对IE的支持,IE将退出历史舞台,不必再费心费力的去考虑IE用户
JavaScript中的全局对象
浏览器暴露给JavaScript引擎的主要全局对象是window对象,它代表了包含着一个页面的窗口。window对象是获取所有其他全局对象、全局变量(甚至包含用户定义对象)和浏览器API的访问途径。全局window对象最重要的属性是document,它代表了当前页面的DOM。通过使用这个对象,JavaScript代码就能在任何程度上改变DOM,包括修改或移除现存的节点,以及创建和插入新的节点。
浏览器API:
JavaScript代码的不同类型
- 全局代码
- 函数代码
比如:
包含在函数内的代码叫作函数代码,而在所有函数以外的代码叫作全局代码,全局代码由JavaScript引擎以一种直接的方式自动执行,每当遇到这样的代码就一行接一行地执行
若想执行函数代码,则必须被其他代码调用:既可以是全局代码,也可以是其他函数,还可以由浏览器调用。
事件处理
客户端Web应用是一种GUI应用,也就是说这种应用会对不同类型的事件作响应
浏览器执行环境的核心思想基于:同一时刻只能执行一个代码片段,即所谓的单线程执行模型
当一个事件抵达后,浏览器需要执行相应的事件处理函数。这里不保证用户总会极富耐心地等待很长时间,直到下一个事件触发。所以,浏览器需要一种方式来跟踪已经发生但尚未处理的事件。为实现这个目标,浏览器使用了事件队列
所有已生成的事件(无论是用户生成的,例如鼠标移动或键盘按压,还是服务器生成的,例如Ajax事件)都会放在同一个事件队列中,以它们被浏览器检测到的顺序排列
事件处理的过程:
- 浏览器检查事件队列头
- 如果浏览器没有在队列中检测到事件,则继续检查
- 如果浏览器在队列头中检测到了事件,则取出该事件并执行相应的事件处理器(如果存在)。在这个过程中,余下的事件在事件队列中耐心等待,直到轮到它们被处理
由于一次只能处理一个事件,所以我们必须格外注意处理所有事件的总时间。执行需要花费大量时间执行的事件处理函数会导致Web应用无响应
重点注意浏览器在这个过程中的机制,其放置事件的队列是在页面构建阶段和事件处理阶段以外的。这个过程对于决定事件何时发生并将其推入事件队列很重要,这个过程不会参与事件处理线程
事件是异步的
事件可能会以难以预计的时间和顺序发生(强制用户以某个顺序按键或单击是非常奇怪的)
我们对事件的处理,以及处理函数的调用是异步的
如下类型的事件会在其他类型事件中发生
- 浏览器事件,例如当页面加载完成后或无法加载时
- 网络事件,例如来自服务器的响应(Ajax事件和服务器端事件)
- 用户事件,例如鼠标单击、鼠标移动和键盘事件
- 计时器事件,当
timeout
时间到期或又触发了一次时间间隔。
事件处理的概念是Web应用的核心,除了全局代码,页面中的大部分代码都将作为某个事件的结果执行
在事件能被处理之前,代码必须要告知浏览器我们要处理特定事件
注册事件
事件处理器是当某个特定事件发生后我们希望执行的函数,为了达到这个目标,我们必须告知浏览器我们要处理哪个事件,这个过程叫作注册事件处理器
在客户端Web应用中,有两种方式注册事件:
- 通过把函数赋给某个特殊属性
- 通过使用内置
addEventListener
方法
例如:
1 | window.onload = function () {}; |
通过这种方式,事件处理器就会注册到load事件上(当DOM已经就绪并全部构建完成,就会触发这个事件)
把函数赋值给特殊属性是一种简单而直接的注册事件处理器方式,但是致命的缺点是对于某个事件只能注册一个事件处理器
addEventListener
方法让我们能够注册尽可能多的事件
例如:
1 | document.body.addEventListener ("mousemove", function () { |
当鼠标移动或者单击body时,两个事件就会被触发,添加一条消息到id为second的元素上
处理事件
事件处理背后的主要思想是当事件发生时,浏览器调用相应的事件处理器
由于单线程执行模型,所以同一时刻只能处理一个事件,任何事件只能等待当前事件被处理完毕之后才会被执行
在上面的例子中,浏览器的动作是这样的:
小结
浏览器接收的HTML代码用作创建DOM的蓝图,它是客户端Web应用结构的内部展示阶段
我们使用JavaScript代码来动态地修改DOM以便给Web应用带来动态行为
客户端Web应用的执行分为两个阶段:
页面构建代码是用于创建DOM的,而全局JavaScript代码是遇到script节点时执行的。在这个执行过程中,JavaScript代码能够以任意程度改变当前的DOM,还能够注册事件处理器——事件处理器是一种函数,当某个特定事件(例如,一次鼠标单击或键盘按压)发生后会被执行。注册事件处理器很容易:使用内置的addEventListener方法
事件处理——在同一时刻,只能处理多个不同事件中的一个,处理顺序是事件生成的顺序。事件处理阶段大量依赖事件队列,所有的事件都以其出现的顺序存储在事件队列中。事件循环会检查实践队列的队头,如果检测到了一个事件,那么相应的事件处理器就会被调用。
函数:定义与参数
像普通人一样编写代码和像“忍者”一样编写代码的最大差别在于是否把JavaScript作为函数式语言(functional language)来理解。对这一点的认知水平决定了你编写的代码水平。
——《JavaScript忍者秘籍》
JavaScript中最关键的概念是:函数是第一类对象(first-class objects),或者说它们被称作一等公民(first-class citizens)
函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象,函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递
函数及函数式概念之所以如此重要,其原因之一在于函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,我们编写的所有的脚本代码都将在一个函数内执行
JavaScript中对象有以下几种常用功能:
对象可通过字面量来创建{}
对象可以赋值给变量、数组项,或其他对象的属性
对象可以作为参数传递给函数
对象可以作为函数的返回值
对象能够具有动态创建和分配的属性
不同于很多其他编程语言,在JavaScript中,我们几乎能够用函数来实现同样的事
函数是第一类对象
JavaScript中函数拥有对象的所有能力,也因此函数可被作为任意其他类型对象来对待。当我们说函数是第一类对象的时候,就是说函数也能够实现以下功能:
通过字面量创建
1
function ninjaFunction () {}
赋值给变量,数组项或其他对象的属性
作为函数的参数来传递
作为函数的返回值
具有动态创建和分配的属性
对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作
把函数作为第一类对象是函数式编程(functional programming)的第一步,函数式编程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块化
第一类对象的特点之一是,它能够作为参数传入函数。对于函数而言,这项特性也表明:如果我们将某个函数作为参数传入另一个函数,传入函数会在应用程序执行的未来某个时间点才执行,说的一般一点就是回调函数(callbackfunction)
回调函数
每当我们建立了一个将在随后调用的函数时,无论是在事件处理阶段通过浏览器还是通过其他代码,我们都是在建立一个回调(callback)
回调就是在执行过程中,我们建立的函数会被其他函数在稍后的某个合适时间点“再回来调用”
有效运用JavaScript的关键在于如何使用回调函数
下面这个函数完全没什么实际用处,它的参数接收另一个函数的引用,并作为回调调用该函数:
1 | function useless (ninjaCallBack) { |
这个例子只是告诉我们,这个函数完全没什么实际用处,它的参数接收另一个函数的引用,并作为回调调用该函数,
再来看一个例子:
1 | function getText () { |
断言函数assert通常使用两个参数。第一个参数是用于断言的表达式,本例中,我们需要确定使用参数getText调用useless函数返回的值与变量text是否相等 useless(getText) === text
,若第一个参数的执行结果为true,断言通过;反之,断言失败。第二个参数是与断言相关联的信息,通常会根据通过/失败来输出到日志上
断言函数实现方法:
1 | <!-- |
自定义函数 report()
1 | function report(text) { |
现在来一个小例子测试一下吧:
1 | var text = 'test test'; |
执行结果:
getText参数调用useless回调函数后,得到了期望的返回值,代码执行过程:
JavaScript的函数式本质让我们能把函数作为第一类对象,所以上述例子中的代码可以被简化为:
JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)
当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间
使用比较器排序
一般情况下只要我们拿到了一组数据集,就很可能需要对它进行排序。假如有一组随机序列的数字数组:0, 3, 2, 5, 7, 4, 8, 1
JavaScript数组可以用sort方法。利用该方法可以只定义一个比较算法,比较算法用于指示按什么顺序排列,这才是回调函数所要介入的
不同于让排序算法来决定哪个值在前哪个值在后,我们将会提供一个函数来执行比较。我们会让排序算法能够获取这个比较函数作为回调,使算法在其需要比较的时候,每次都能够调用回调
该回调函数的期望返回值为:
- 如果传入值的顺序需要被调换,返回正数
- 不需要调换,返回负数
- 两个值相等,返回0
对于排序上述数组,我们对比较值做减法就能得到我们所需要的值:
1 | var values = [0, 3, 2, 5, 7, 4, 8, 1] |
没有必要思考排序算法的底层细节(甚至是选择了什么算法)。JavaScript引擎每次需要比较两个值的时候都会调用我们提供的回调函数
函数式方式让我们能把函数作为一个单独实体来创建,正像我们对待其他类型一样,创建它、作为参数传入一个方法并将它作为一个参数来接收,函数就这样显示了它一等公民的地位
函数作为对象的乐趣
可以给函数添加属性:
1 | // 创建一个新的对象并赋予一个新的属性 |
这种特性所能做的更有趣的事:
- 在集合中存储函数使我们轻易管理相关联的函数,例如,某些特定情况下必须调用的回调函数
- 记忆让函数能记住上次计算得到的值,从而提高后续调用的性能
今天就到这里,有时间再更新~