[TOC]

1 序

一般来说,编写JavaScript要么使用过程方式,要么使用面向对象方式。然后,由于它天生的动态属性,这种语言还能使用更为复杂和有趣的模式。这些技巧要利用ECMAScript的语言特点、BOM扩展和DOM功能来获得强大的效果。

2 高级函数

函数时JavaScript中最有趣的部分之一。它们本质上是十分简单和过程化的,但也可以是非常复杂和动态的。一些额外的功能可以通过使用闭包来实现。由于所有的函数都是对象,所以使用函数指针非常简单。这些令JavaScript函数不仅有趣而且强大。

3 安全的类型检测

4 自定义isArray函数

高级技巧 - 图1
高级技巧 - 图2

5 作用域安全的构造函数

构造函数其实就是一个使用new操作符调用的函数。当使用new调用时,构造函数内用到的this对象会指向新创建的对象实例。如图:
高级技巧 - 图3
创建一个安全的作用域的构造函数。(避免函数中的this指向window)如图
高级技巧 - 图4

关于作用域安全的构造函数的贴心提示。实现这个模式后,你就锁定了可以调用构造函数的环境。如果你使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。(具体请参靠《JavaScript 高级程序设计3版》p599)

6 惰性载入函数

因为浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,解决方案就是称之为惰性载入的技巧。

7 惰性载入方式1

惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式:第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。例如,可以用下面的方式使用惰性载入重写createXHR()。
高级技巧 - 图5
高级技巧 - 图6
在这个惰性载入的createXHR()中,if语句的每一个分支都会为createXHR变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下一次调用createXHR()的时候,就会直接调用被分配的函数,这样就不用再次执行if语句了。

8 惰性载入方式2

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。以下就是按照这一思路重写前面例子的结果。

高级技巧 - 图7
高级技巧 - 图8
这个例子中使用的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。实际的逻辑都一样。不一样的地方就是第一行代码(使用var定义函数)、新增了自执行的匿名函数,另外每个分支都返回正确的函数定义,以便立即将其赋值给createXHR()。

9 惰性载入函数的优点

惰性载入函数的优点是只在执行分支代码时牺牲一点性能。
以上两种方式,至于那种方式更合适,就要看你的具体需求而定了。不过这两种方式都能避免执行不必要的代码。
小实例
高级技巧 - 图9

10 函数绑定

函数绑定要创建一个函数,可以在待定的this环境中以指定参数调用另一个函数。该技巧常常和回调与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。

一个简单的bind()函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。语法如下:
高级技巧 - 图10
高级技巧 - 图11

原生的bind()方法与前面介绍的自定义bind()方法类似,都是要传入作为this值的对象。支持原生bind()方法的浏览器有IE9+、Firefox4+ 和 Chrome。
只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。它们主要用于事件处理程序以及setTimeout()和setInterval()。然而,被绑定函数与普通函数相比有更多的开销,他们需要更多内容,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。

11 函数柯里化

与函数绑定紧密相关的主题是函数柯里化(function currying),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。

12 防篡改对象

JavaScript共享的本质一直是开发人员心头的痛。因为任何对象都可以被在同一环境中运行的代码修改。开发人员很可能会意外地修改别人的代码,甚至更糟糕地,用不兼容的功能重写原生对象。ECMAScript5致力于解决这个问题,可以让开发人员定义防篡改对象(tamper-proof object)。
高级技巧 - 图12
不过,请注意:一旦把对象定义为防篡改,就无法撤销了。

13 不可扩展对象

使用Object.preventExtensions()方法可以改变这个行为,让你不能再给对象添加属性和方法。
高级技巧 - 图13
虽然不能给对象添加新成员,但已有的成员则丝毫不受影响。你仍然还可以修改和删除已有的成员。另外,使用Object.istExtensible()方法还可以确定对象是否可以扩展。
高级技巧 - 图14

14 密封的对象

ES5为对象定义的第二个保护级别是密封对象(sealed object)。密封对象不可扩展,而且已有成员的[[Configurable]]特性将被设置为false。这就意味着不能删除属性和方法,因为不能使用Object.defineProperty()把数据属性修改为访问器属性,或者相反。属性值是可以修改的。
要密封对象,可以使用Object.seal()方法。
高级技巧 - 图15

使用Object.isSealed()方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以用Object.isExtensible()检测密封的对象也会返回false。
高级技巧 - 图16

15 冻结的对象

最严格的防篡改级别是冻结对象(frozen object)。冻结的对象既不可扩展,又是密封的,而且对象数据属性的[[Writable]]特性会被设置为false。如果定义[[Set]]函数,访问器属性仍然是可写的。ES5定义的Object.freeze()方法可以用来冻结对象。

高级技巧 - 图17
当然,也有一个Object.isFrozen()方法用于检测冻结对象。因为冻结对象既是密封的又是不可扩展的,所以用Object.isExtensible()和Object.isSealed()检测冻结对象将分别返回false和true。
高级技巧 - 图18

对JavaScript库的作者而言,冻结对象是很有用的。因为JavaScript库最怕有人意外(或有意)地修改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题的发送。

16 高级定时器

使用setTimeout()和setInterval()创建的定时器可以用于实现有趣且有用的功能。虽然人们对JavaScript的定时器存在普遍的误解,认为它们是线程,其实JavaScript是运行于单线程的环境中的,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机是不能保证的,因为在页面的生命周期中,不同时间可能有其他代码在控制JavaScript进程。在页面下载完后的代码运行、事件处理程序、Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。

可以把JavaScript想象成在时间线上运行的。当页面载入时,首先执行是任何包含在<script>元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,不过有时候也包含一些初始数据的处理。在这之后,JavaScript进程将等待更多代码执行。当进程空闲的时候,下一个代码会被触发并立刻执行。例如,当点击某个按钮时,onclick事件处理程序会立刻执行,只要JavaScript进程处于空闲状态。这样一个页面的时间线类似于图:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093867665-93cf0650-a139-48cb-8ee1-74061c30aca0.png#)

在JavaScript中没有任何代码时立刻执行的,但一旦进程空闲则尽快执行。

17 重复的定时器

使用setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个方式的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之前没有任何停顿。幸好,JavaScript引擎够聪明,能避免这个问题。当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
这种重复定时器的规则有2个问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会比预期的小。假设,某个onclick事件处理程序使用setInterval()设置了一个200ms间隔的重复定时器。如果事件处理程序花了300ms多一点的时间完成,同时定时器代码也花了差不多的时间,就会跳过一个间隔同时运行着一个定时器代码。如图:
高级技巧 - 图19
这个例子中的第1个定时器是在205ms处添加到队列中的,但是直到过了300ms处才能够执行。当执行这个定时器代码时,在405ms处又给队列添加了另外一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在5ms处添加的定时器代码结束之后,405处添加的定时器代码就离开执行。

为了避免setInterval()的重复定时器的这2个缺点,你可以用如下模式使用链式setTimeout()调用。<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093869160-fd4e3dfe-9c06-47e4-9ea1-96d703312b10.png#)

这个模式链式调用了setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用了arguments.callee来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。这个模式主要用于重复定时器,如下列所示。<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093869633-523e0b91-e9c2-4be1-b9ea-f364de643c2b.png#)<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093869975-b588a7a6-32f1-47de-8c13-11f456b04200.png#)<br />    这段定时器代码每次执行的时候将一根<div>元素向右移动,当左坐标在200像素的时候停止。JavaScript动画中使用这个模式很常见。<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093870942-6e74ef47-9e8b-4d05-b637-2d5a5bb5f724.png#)

18 Yielding Processes

运行在浏览器中的JavaScript都被分配了一个确定数量的资源。不同于桌面应用往往能够随意控制他们要的内存大小和处理器时间,JavaScript被严格限制了,以防止恶意的Web程序员把用户的计算机搞挂了。其中一个限制是长时间运行脚本的制约,如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长时间执行,询问是允许其继续执行还是停止它。所有JavaScript开发人员的目标就是,确保用户永远不会在浏览器中看到这个令人费解的对话框。定时器时绕开此限制的方法之一。<br />    脚本长时间运行的问题通常是由两个原因之一造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环。这两者中,后者是较为容易解决的问题。长时间运行的循环通常遵循以下模式:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093871254-98ec1ca5-b0c2-4699-b0a9-2f308b9207e0.png#)

这个模式的问题在于要处理的项目的数量在运行前是不可知的。如果完成process()要花100ms,只有2个项目的数组可能不会造成影响,但是10个的数组可能会导致脚本要运行一秒钟才能完成。数组中的项目数量直接关系到执行完成该循环的时间长度。同时由于JavaScript的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越久。<br />    在展开该循环之前,你需要回答以下两个重要的问题。
  • 该处理是否必须同步完成?如果这个数据的处理会造成其他运行的阻塞,那么最好不要改动它。不过,如果你对这个问题的回答确定为“否”,那么将某些处理推迟到以后是个不错的备选项。
  • 数据是否必须按顺序完成?通常,数组只是对项目的组合和迭代的一种简便的方法而无所谓顺序。如果项目的顺序不是非常重要,那么可能可以将某些处理推迟到以后。

当你发现某个循环占用了大量时间,同时对于上述两个问题,你的回答都是“否”,那么你就可以使用定时器分割这个循环。这是一种叫做数组分块(array chunking)的技术,小块小块地处理数组,通常每次一小块。基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。基本的模式如下。
高级技巧 - 图20

在数组分块模式中,array变量本质上就是一个“待办事宜”列表,它包含了要处理的项目。使用shift()方法可以获取队列中下一个要处理的项目,然后将其传递给某个函数。如果在队列中还有其他项目,则设置另一个定时器,并通过arguments.callee调用同一个匿名函数。要实现数组分块非常简单,可以使用以下函数。<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093871989-f621c766-7e1d-45e4-b2bf-7c22f0894e33.png#)

chunk()方法接受三个参数:要处理的项目的数组,用于处理项目的函数,以及可选的运行该函数的环境。函数内部用了之前描述过的基本模式,通过call()调用process()函数,这样可以设置一个合适的执行环境(如果必须)。定时器的时间间隔设置为了100ms,使得JavaScript进程有时间在处理项目的事件之间转入空闲。你可以根据你的需要更改这个间隔大小,不过100ms在大多数情况下效果不错。可以按如下所示使用该函数:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093872512-786be001-aebc-4229-a35f-1a3dba7df624.png#)<br />    这个例子使用printValue()函数将data数组中的每个值输出到一个<div>元素。由于函数处在全局作用域内,因此无需给chunk()传递一个context对象。

必须当心的地方是,传递给chunk()的数组是用作一个队列的,因此当处理数据的同时,数组中的条目也在改变。如果你想保持原数组不变,则应该将该数组的克隆传递给chunk(),如下例所示:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093872988-6f19d5cb-1ed6-4702-96e6-8cdd3556bd6f.png#)<br />    当不传递任何参数调用某个数组的concat()方法时,将返回和原来数组中项目一样的数组。这样你就可以保证原数组不会被该函数更改。<br />    数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理会运行,这样就可能避免长时间运行脚本的错误。<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093873509-be0b2e71-8e2b-4b87-a9d0-e177ca0e1e77.png#)

19 函数节流

浏览器中某些计算和处理要比其他的昂贵很多。例如,DOM操作比起非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在IE中使用onresize事件处理程序的时候容易发生,当调整浏览器大小的时候,该事件会连续触发。在onresize事件处理程序内部如果尝试进行DOM操作,其高频率的更改可能会让浏览器崩溃。为了绕开这个问题,你可以使用定时器对该函数进行节流。<br />    函数节流背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。以下是该模式的基本形式:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093874420-7680615b-07a0-4e34-8437-22bd14c7e40e.png#)<br />    在这段代码中,创建了一个叫做processor对象。这个对象还有2个方法:process()和performProcessing()。前者是初始化任何处理所必须调用的,后者则实际进行应完成的处理。当调用了process(),第一步是清除存好的timeoutId,来阻止之前的调用被执行。然后,创建一个新的定时器调用performProcessing()。由于setTimeout()中用到的函数的环境总是window,所以有必要保存this的引用以方便以后使用。

时间间隔设为了100ms,这表示最后一次调用process()之后至少100ms后才会调用performProcessing()。所有如果100ms之内调用了process()共20次,performProcessing()仍只会被调用一次。<br />    这个模式可以使用throttle()函数来简化,这个函数可以自动进行定时器的设置和清除,如下例所示:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093874650-d55be151-1326-43a0-8a75-3295ab6278cc.png#)<br />    throttle()函数接受两个参数:要执行的函数以及在哪个作用域中执行。上面这个函数首先清除之前设置的任何定时器。定时器ID是存储在函数的tId属性中的,第一次把方法传递给throttle()的时候,这个属性可能并不存在。接下来,创建一个新的定时器,并将其ID储存在方法的tId属性中。如果这是第一次对这个方法调用throttle()的话,那么这段代码会创建该属性。定时器代码使用call()来确保方法在适当的环境中执行。如果没有给出第二个参数,那么就在全局作用域执行该方法。<br />    前面提到过,节流在resize事件中是最常用的。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极短的时间内进行过多的计算。例如,假设有一个<div>元素需要保持它的高度始终等同于宽度。那么实现这一功能的JavaScript可以如下编写:<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093874978-1a4f7bf5-258f-4c73-a9ff-911b93995bed.png#)<br />    这段非常简单的例子有两个问题可能会造成浏览器运行缓慢。首先,要计算offsetWidth属性,如果该元素或者页面上其他元素有非常复杂的CSS样式,那么这个过程将会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的CSS的话,这又需要很多计算。这就可以用到throttle()函数,如下例所示:<br />    ![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093875651-f370c438-c3f3-47b4-a4ba-43e9d012405e.png#)<br />    这里,调整大小的功能被放入了一个叫做resizeDiv()的单独函数中。然后onresize事件处理程序调用throttle()并传入resizeDiv函数,而不是直接调用resizeDiv()。多数情况下,用户是感觉不到变化的,虽然给浏览器节省的计算可能会非常大。<br />    只要代码时周期性执行的,都应该使用节流,但是你不能控制请求执行的速率。这里展示的throttle()函数用了100ms作为间隔,你当然可以根据你的需要来修改它。

20 自定义事件

事件是JavaScript与浏览器交互的主要途径。事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。

观察者模式由两类对象组成:**主体和观察者。**主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。从另一个方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。<br />    事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中——通过实现自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以入下定义:<br />    ![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093876204-099c5af6-8f4a-46b2-b0eb-353c0407d411.png#)<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093876669-c0229bd2-1321-43ca-8d8e-0229e4f31cdd.png#)<br />![](https://cdn.nlark.com/yuque/0/2021/png/291746/1636093876884-6a97fc38-938c-421e-a74c-cd1267eb2cd9.png#)<br />    EventTarget 类型有一个单独的属性handlers,用于储存事件处理程序。还有三个方法:addHandler(),用于注册给定类型事件的事件处理程序;fire(),用于触发一个事件;removeHandler(),用于注销某个事件类型的事件处理程序。<br />    addHandler()方法接受两个参数:事件类型和用于处理该事件的函数。当调用该方法时,会进行一次检查,看看handlers属性中是否已经存在一个针对该事件类型的数组;如果没有,则创建一个新的。然后使用push()将该处理程序添加到数组的末尾。

如果要触发一个事件,要调用fire()函数。该方法接受一个单独的参数,是一个至少包含 type 属性的对象。fire()方法先给event对象设置一个target属性,如果它尚未被指定的话。然后它就查找对应该事件类型的一组处理程序,调用各个函数,并给出event对象。因为这些都是自定义事件,所以event对象上还需要的额外信息由你自己觉得。<br />    removeHandler()方法是 addHandler()的辅助,它们接受的参数一样:事件的类型和事件处理程序。这个方法搜索事件处理程序 的数组找到要删除的处理程序的位置。如果找到了,则使用break操作符退出for循环。然后使用splice()方法将该项目从数组中删除。

然后,使用EventTarget类型的自定义事件可以如下使用:
高级技巧 - 图21
在这段代码中,定义了handleMessage()函数用于处理message事件。它接受event对象并输出message属性。调用target对象的addHandler()方法并传给“message”以及handleMessage()函数。在接下来的一行上,调用了fire()函数,并传递了包含2个属性,即type和message的对象直接量。它会调用message事件处理程序,这样就会显示一个警告框(来自handleMessage())。然后删除了事件处理程序,这样即使事件再次触发,也不会显示任何警告框。
因为这种功能是封装在一种自定义类型中的,其他对象可以继承EventTarget并获得这个行为,如下列所示:
高级技巧 - 图22
高级技巧 - 图23
Person类型使用了寄生组合继承方法继承EventTarget。一旦调用了say()方法,便触发了事件,它包含了消息的细节。在某种类型的另外的方法中调用fire()方法时很常见的,同时它通常不是公开调用的。这段代码可以照如下方式使用:
高级技巧 - 图24
这个例子中的handleMessage()函数显示了某人名字(通过event.target.name获得)的一个警告框和消息正文。当调用say()方法并传递一个消息时,就会触发message事件。接下来,它又会调用handleMessage()函数并显示警告框。
当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码时完全分离的。

21 示例代码:

22 拖放

《JavaScript高级程序设计3》原生JS实现的拖拽功能。如果需要可以参考书中例子。

23 小结

JavaScript中的函数非常强大,因为它们是第一类对象。使用闭包和函数环境切换,还可以有很多使用函数的强大方法。可以创建作用域安全的构造函数,确保在缺少new操作符时调用构造不会改变错误的环境对象。
  • 可以使用惰性载入函数,将任何代码分支推迟到第一次调用函数的时候。
  • 函数绑定可以让你创建始终在指定环境中运行的函数,同时函数柯里化可以让你创建已经填了某些参数的函数。
  • 将绑定和柯里化组合起来,就能够给你一种在任意环境中以任意参数执行任意函数的方法。

ES5允许通过以下几种方式来创建防篡改对象。

  • 不可扩展的对象,不允许给新的对象添加新的属性或方法。
  • 密封的对象,也是不可扩展的对象,不允许删除已有的属性和方法。
  • 冻结的对象,也是密封的对象,不允许重写对象的成员。

JavaScript中可以使用setTimeout()和setInterval()如下创建定时器。

  • 定时器代码是放在一个等待区域,直到时间间隔到了之后,此时将代码添加到JavaScript的处理队列中,等待下一次JavaScript进程空闲时被执行。
  • 每次一段代码执行结束之后,都会有一小段空闲时间进行其他浏览器处理。
  • 这种行为意味着,可以使用定时器长时间运行的脚步切分为一小块一小块可以在以后运行的代码段,这种做法有助于Web应用对用户交互有更积极的响应。

JavaScript中经常以事件的形式应用观察者模式。虽然事件常常和DOM一起使用,但是你也可以通过实现自定义事件在自己的代码中应用。使用自定义事件有助于将不同部分的代码相互之间解耦,让维护更加容易,并减少引入错误的机会。
拖放对于桌面和Web应用都是一个非常流行的用户界面范例,它能够让用户非常方便地以一种直观的方式重写排列或者配置东西。在JavaScript中可以使用鼠标事件和一些简单的计算来实现这种功能类型。将拖放行为和自定义事件结合起来可以创建一个可重复使用的框架,它能应用于各种不同的情况下。