JavaScript高级技巧

前端开发
2018年12月17日
444

高级函数

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

安全的类型检测

JavaScript内置的类型检测机制并非完全可靠。事实上,发生错误否定及错误肯定的情况也不在少数。比如说typeof操作符,由于它有一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果。Safari(直至第4版)在对正则表达式应用typeof操作符时会返回function,因此很难确定某个值到底是不是函数。

再比如,intanceof操作符在存在多个全局域(像一个页面包含多个frame)的情况下,也是问题多多。一个经典的例子,就是像下面这样将对象标识为数组。

js
var isArray = value instanceof Array;

以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。如果value是在另个frame中定义的数组,那么以上代码就会返回false。

在检测某个对象到度是原生对象还是开发人员自定义的对象的时候,也会有问题。出现这个问题的原因是浏览器开始原生支持JSON对象了。因为很多人一直在使用Douglas Crockford的JSON库,而该库定义了一个全局JSON对象。于是开发人员很难确定页面中的JSON到底是不是原生的。

解决上述问题的办法都一样。大家知道,在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性中就指定了上述字符串中的构造函数名。

js
alert(Object.prototype.toString.call(value)); // '[object Array]'

由于原生数组的构造函数名与全局作用域无关,因为使用toString()就能保证返回一致的值。利用这一样,可以创建如下函数:

js
function isArray (value) { return Object.prototype.toString.call(value) === '[object Array]'; }

同样,也可以基于这一思路来测试某个值是不是原生函数或正则表达式:

js
function isFunction (value) { return Object.prototype.toString.call(value) === '[object Function]'; } function isRegExp (value) { return Object.prototype.toString.call(value) === '[object RegExp]'; }

不过要注意,对于在IE中以COM对象形式实现的任何函数,isFunction()函数都将返回false。

这一技巧也广泛应用于检测原生JSON对象。Object的toString()方法不能检测非原生构造函数的构造函数名。因此,开发人员定义的任何构造函数都将返回[object Object]。有些JavaScript库会包含与下面类似的代码。

js
var isNativeJSON = window.JSON && object.prototype.toString.call(JSON) === '[object JSON]';

在Web开发中能够区分原生与非原生JavaScript对象非常重要。只有这样才能确切知道某个对象到底有哪些功能。这个技巧可以对任何对象给出正确的结论。

作用域安全的构造函数

构造函数其实就是一个使用new操作符调用的函数。当使用new调用时,构造函数内用到的this对象就会指向新创建的对象实例。

js
function Person (name, age, job) { this.name = name; this.age = age; this.job = job; } var person = new Person('Nicholas', 29, 'SE');

当没有使用new操作符来调用该构造函数的情况下。由于该this对象是在运行时绑定的,所以直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。

js
var person = Person('Ni', 29, 'SE'); alert(window.name); // 'Ni' alert(window.age); // 29 alert(window.job); // 'SE'

这个问题是由this对象的晚绑定造成的,在这里this被解析成了window对象。由于window的name属性是用于识别链接目标和frame的,所以这里对该属性的偶然覆盖可能会导致该页面出现其他错误。这个问题的解决方法就是创建一个作用域安全的构造函数

作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。如果不是,那么会创建新的实例并返回。

js
function Person (name, age, job) { if (this instanceof Person) { this.name = name; this.age = age; this.job = job; } else { return new Person(name, age, job); } }

关于作用域安全的构造函数的贴心提示。实现这个模式后,你就锁定了可以调用构造函数的环境。如果你使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。这里有个例子:

js
function Polygon (sides) { if (this instanceof Polygon) { this.sides = sides; this.getArea = function () { return 0; } } else { return new Polygon(sides); } } function Rectangle (width, height) { Polygon.call(this, 2); this.width = width; this.height = eheight; this.getArea = function () { return this.width * this.height; } } var rect = new Rectangle(5, 10); alert(rect.sides); // undefined

如果构造函数窃取结合使用原型链或者寄生组合则可以解决这个问题:

js
function Polygon (sides) { if (this instanceof Polygon) { this.sides = sides; this.getArea = function () { return 0; } } else { return new Polygon(sides); } } function Rectangle (width, height) { Polygon.call(this, 2); this.width = width; this.height = eheight; this.getArea = function () { return this.width * this.height; } } Rectangle.prototype = new Polygon(); var rect = new Rectangle(5, 10); alert(rect.sides); // 2

多个程序员在同一个页面上写JavaScript的环境中,作用域安全构造函数就很有用了。届时,对全局对象意外的更改可能会导致一些常常难以追踪的错误。除非你单纯基于构造函数窃取来实现继承,推荐作用域安全构造函数作为最佳实践。

惰性载入函数

因为浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,将执行引导到正确的代码中。

js
function createXHR () { if (typeof XMLHttpRequest !== 'undefined') { return new XMLHttpRequest(); } else if (typeof ActiveXObject !== 'undefined') { if (typeof arguments.callee.activeXString !== 'string') { var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'], i, len; for (i = 0, len = versions.length; i < len; i++) { try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (e) {} } } return new ActiveXObject(arguments.callee.activeXString); } else { throw new Error('No XHR object available.'); } }

每次调用createXHR()时,它都要对浏览器所支持的能力仔细检查。即使每次调用时分支的结果不变:如果浏览器支持内置XHR,那么它就一直支持了,那么这种测试就变得没必要了。即使只有一个if语句的代码,也肯定要比没有if语句的慢,所以如果if语句不必每次执行,那么代码就可以运行地更快一些。

解决方案就是称之为惰性载入的技巧。惰性载入表示函数执行的仅会发生一次。有两种实现惰性载入的方式。

第一种就是在函数被调用时再处理函数。

js
function createXHR () { if (typeof XMLHttpRequest !== 'undefined') { createXHR = function () { return new XMLHttpRequest(); } } else if (typeof ActiveXObject !== 'undefined') { createXHR = function () { if (typeof arguments.callee.activeXString !== 'string') { var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'], i, len; for (i = 0, len = versions.length; i < len; i++) { try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (e) {} } } return new ActiveXObject(arguments.callee.activeXString); } } else { createXHR = function () { throw new Error('No XHR object available.'); } } return createXHR(); }

第二种实现惰性载入的方式就是在声明函数时就指定适当的函数。

js
var createXHR = (function () { if (typeof XMLHttpRequest !== 'undefined') { return function () { return new XMLHttpRequest(); } } else if (typeof ActiveXObject !== 'undefined') { return function () { if (typeof arguments.callee.activeXString !== 'string') { var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'], i, len; for (i = 0, len = versions.length; i < len; i++) { try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (e) {} } } return new ActiveXObject(arguments.callee.activeXString); } } else { return function () { throw new Error('No XHR object available.'); } } })();

惰性载入函数的优点是只在执行分支代码时牺牲一点属性。至于哪个方式更合适,就要看具体需求而定了。不过这两种方式都能避免执行不必要的代码。

函数绑定

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

js
var handler = { message: 'Event Handled', hadleClick: function (event) { alert(this.message); } } var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', handler.handleClick);

在上面这个例子中,创建了一个叫handler的对象。handler.handleClick()被分配为一个DOM按钮的事件处理程序。当按下该按钮时,就调用该函数,显示一个警告框。虽然貌似警告框应该显示Event handled,然而实际上显示的是undefined。这个问题在于没有保存handler.handleClick()的环境,所以this对象最后最指向了DOM按钮而非handler。可以使用一个闭包来修正这个问题。

js
var handler = { message: 'Event Handled', hadleClick: function (event) { alert(this.message); } } var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', function (event) { handler.handleClick(event); });

这个解决方案在click事件处理程序内使用了一个闭包直接调用handler.handleClick()。当然,这是特定于这段代码的解决方案。创建多个闭包可能会令代码变得难于理解和调试。因此,很多JavaScript实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫bind()

一个简单的bind()函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。

js
function bind (fn, context) { return function () { return fn.apply(context, arguments); } }

这个函数似乎简单,但其功能是非常强大的。在bind()中创建了一个闭包,闭包使用apply()调用传入的函数,并给apply()传递context对象和参数。注意,这里使用的arguments对象是内部函数的而非bind()的。当调用返回的函数时,它会在给定环境中执行被传入的函数并给出所有参数。

js
var handler = { message: 'Event Handled', hadleClick: function (event) { alert(this.message); } } var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', bind(handler.handleClick, handler));

ES5为所有函数定义了一个原生的bind()方法,进一步简单了操作。

js
var handler = { message: 'Event handled', handleClick: function (event) { alert(this.message + ':' + event.type); } }; var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', handler.handleClick.bind(handler));

函数柯里化

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

js
function add (num1, num2) { return num1 + num2; } function curriedAdd (num2) { return add(5, num2); } alert(add(2, 3)); // 5 alert(curriedAdd(3)); // 8

这段代码定义了两个函数:add()和curriedAdd()。后者本质上是任何情况下第一个参数为5的add()版本,尽管从技术上来说curriedAdd()并非柯里化的函数,但它很好地展示了其概念。

柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入柯里化的函数和必要的参数。下面是创建柯里化函数的通用方式。

js
function curry (fn) { var args = Array.prototype.slice.call(arguments, 1); return function () { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null, finalArgs); } }

curry()函数的主要工作就是将返回函数的参数进行排序。curry()的第一个参数是要进行柯里化的函数,其他参数是要传入的值。为了获取第一个参数之后的所有参数,在arguments对象上调用了slice()方法,并传入参数1表示被返回的数组包含从第二个参数开始的所有参数。然后args数组包含了来自外部函数的参数。在内部函数中,创建了innerArgs数组用来存放所有传入的函数。有了存放来自外部函数和内部函数的参数数组后,就可以使用concat()方法将它们组合为finalArgs,然后使用apply()将结果传递给该函数。注意这个函数并没有考虑到执行环境,所以调用apply()时第一个参数是null。curry()函数可以按以下方式应用。

js
function add (num1, num2) { return num1 + num2; } var curriedAdd = curry(add, 5); alert(curriedAdd(3)); // 8 var curriedAdd2 = curry(add, 5, 12); alert(curriedAdd()); // 17

函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的bind()函数。

js
function bind (fn, context) { var args = Array.prototype.slice.call(arguments, 2); return function () { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context, finalArgs); } } var handler = { message : 'Event handled', handleClick: function (name, event) { alert(this.message + ':' + name + ':' + event.type); } }; var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', bind(handler.handleClick, handler, 'myBtn'));

ES5的bind()方法也实现函数柯里化,只要在this的值之后再传入另一个参数即可:

js
var handler = { message : 'Event handled', handleClick: function (name, event) { alert(this.message + ':' + name + ':' + event.type); } }; var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', handler.handleClick.bind(handler, 'myBtn'));

JavaScript中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。它们都用于创建复杂的算法和功能。

防篡改对象

JavaScript共享的本质一直是开发人员心头的痛。因为任何对象都可以被在同一环境中运行的代码修改。ES5致力于解决这个问题,可以让开发人员定义防篡改对象(tamper-proof object)。

需要注意的是:一旦把对象定义为防篡改,就无法撤销了。

不可扩展对象

默认情况下,所有对象都是可以扩展的。使用Object.preventExtensions()方法可以让对象不可扩展。

js
var person = { name: 'Ni' }; Object.preventExtensions(person); person.age = 29; alert(person.age); // undefined

在严格模式下,尝试给不可扩展的对象添加新成员会导致抛出错误。

js
var person = { name: 'Ni' }; alert(Object.isExtensible(person)); // true Object.preventExtensions(person); alert(Object.isExtensible(person)); // false

密封的对象

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

要密封对象,可以使用Object.seal()方法。

js
var person = { name: 'Ni' }; Object.seal(person); person.age = 29; // 在严格模式下这行代码会抛出错误 alert(person.age); // undefined delete person.name; // 在严格模式下这行代码会抛出错误 alert(person.name); // 'Ni'
js
var person = { name: 'Ni' }; alert(Object.isExtensible(person)); // true alert(Object.isSealed(person)); // false Object.seal(person); alert(Object.isExtensible(person)); // false alert(Object.isSealed(person)); // true

冻结的对象

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

ES5定义了Object.freeze()方法可以用来冻结对象。

js
var person = { name: 'Ni' }; Object.freeze(person); person.age = 29; // 在严格模式下这行代码会抛出错误 alert(person.age); // undefined delete person.name; // 在严格模式下这行代码会抛出错误 alert(person.name); // 'Ni' person.name = 'Greg'; // 在严格模式下这行代码会抛出错误 alert(person.name); // 'Ni'
js
var person = { name: 'Ni' }; alert(Object.isExtensible(person)); // true alert(Object.isSealed(person)); // false alert(Object.isFrozen(person)); // false Object.freeze(person); alert(Object.isExtensible(person)); // false alert(Object.isSealed(person)); // true alert(Object.isFrozen(person)); // true

定级定时器

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

可以把JavaScript想象成在时间线上运行的。当页面载入时,首先执行是任何包含在<script>元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,不过有时候也包含一些初始数据的处理。在这之后,JavaScript进程将等待更多代码执行。当进程空闲的时候,下一个代码会被立刻触发并立刻执行。

除了主JavaScript执行进程外,还有一个需要在进程下一次空闲时执行的代码队友。随着页面在其生命周期的推移,代码会按照执行顺序添加入队列。

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

实时器对队列的工作方式是,当特定时间过去后将代码插入。如果这个时间点上,队列中没有其他东西,那么这段代码就会被执行,表面上看上去好像代码就在精确指定的时间点上执行了。其他情况下,代码可能明显地等待更长时间才执行。

请看以下代码:

js
var btn = document.getElementById('myBtn'); btn.onclick = function () { setTimeout(function () { document.getElementById('message').style.visibility = 'visible'; }, 250); // 其他代码 }

在这里给一个按钮设置了一个事件处理程序。事件处理程序设置了一个250ms后调用的定时器。点击该按钮后,首先将onclick事件处理程序加入队友。该程序执行后才设置定时器,再有250ms后,指定的代码才被添加到队列中等待执行。实际上,对setTimeout()的调用表示要晚点执行某些代码。

关于定时器要记住的最重要的事情是,指定的时间间隔表示何时将定时器代码添加到队友,而不是何时实际执行代码

比如前面例子中onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置之后的300ms后才会被执行。队列中所有的代码都要等到JavaScript进程空闲之后才能执行,而不管它们是如何添加到队列中的。

实际上Firefox中定时器的实现还能让你确定定时器过了多久才执行,这需要传递一个实际执行的时间与指定时间的间隔的差值。

js
// 仅Firefox中 setTimeout(function (diff) { if (diff > 0) { // 晚调用 } else if (diff < 0) { // 早调用 } else { // 调用及时 } }, 250)

执行完一套代码后,JavaScript进程会返回一段很短的时间,这样页面上的其他处理就可以进行了。由于JavaScript进程会阻塞其他页面处理,所以必须有这些小间隔来防止用户界面被锁定(代码长时间运行中还有可能出现)。这样设置一个定时器,可以确保在定时器代码执行前至少有一个进程间隔。

重复的定时器

使用setIntervale()创建的定时器确保了定时器代码规则地插入队友中。这个方式的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。幸好,JavaScript引擎够聪明,能避免这个问题。当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中最小时间间隔为指定间隔。

这种重复定时器的规则有两个问题:一是某些间隔会被跳过;二是多个定时器的代码执行之间的间隔可能会比预期的小。为了避免这两个缺点,可以用如下模式使用链式setTimeout()调用。

js
setTimeout(function () { // 处理中 setTimeout(arguments.callee, interavl); }, interval);

这个模式主要用于重复定时器,如下例所示:

js
setTimeout(function () { var div = document.getElementById('myDiv'); var left = parseInt(div.style.left) + 5; div.style.left = left + 'px'; if (left < 200) { setTimeout(arguments.callee, 50); } }, 50);

这段定时器代码每次执行的时候将一个<div>元素向右移动,当左坐标在200像素的时候停止。

Yielding Processes

脚本长时间运行的问题通常是由两个原因之一造成的:过长的、过深嵌套和函数调用或者是进行大量处理的循环。这两者中,后者是较为容易解决的问题。长时间运行的循环通常遵循如下模式:

js
for (var i = 0, len = data.length; i < len; i++) { process(data[i]); }

这个模式的问题在于要处理的项目的数量在运行前是不可知的。数组中的项目数量直接关系到执行完该循环的时间长度。同时由于JavaScript的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间就越久。

在展开循环之前,你需要回答以下两个重要的问题:

  • 该处理是否必须同步完成?如果不是,那么将某些处理推迟到以后是个不错的备选项。
  • 数据是否必须按顺序处理?如果不是,那么可能可以将某些处理推迟到以后。

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

js
// 把这个模式封装到一个函数里面 // array,要循环的数组 // process,要执行的操作 // context,上下文环境 function chunk (array, process, context) { // 模式 setTimeout(function () { // 取出下一个条目并处理 var item = array.shift(); process.call(context, item); // 若还有条目,再设置另一个定时器 if (array.length > 0) { setTimeout(arguments.callee, 100); } }, 100); }

使用示例:

js
var data = [12, 123, 1234, 456, 23, 23, 5, 4123, 45, 345, 5646, 2265, 3214, 123]; function printValue (item) { var div = document.getElementById('myDiv'); div.innserHTML += item + '<br>'; } chunk(data, printValue); // 因为函数处于全局作用域中,所以无需传入context对象

函数节流

浏览器中某些计算和处理要比其他的昂贵很多。高频率的使用可能会让浏览器崩溃。为了绕开这个问题,你可以使用定时器对该函数进行节流

函数节流背后的思想是指,某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器,并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。以下是该模式的基本形式:

js
var processor = { timeoutId: null, // 实际进行处理的方法 performProcessing: function () { // 实际执行的代码 }, // 初始处理调用的方法 process: function () { clearTimeout(this.timeoutId); var that = this; this.timeoutId = setTimeout(function () { that.performProcessing(); }, 100); } }

这个模式可以使用以下函数来简化,这个可以自动进行定时器的设置和清除:

js
function throttle (method, context) { cleatTimeout(method.tId); method.tId = setTimeout(function () { method.call(context); }, 100); }

节流在resize事件中最常用。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极短的时间内进行过多的计算。例如,假设有一个<div>元素需要保持它的高度始终等于宽度:

js
window.onresize = function () { var div = document.getElementById('myDiv'); div.style.height = div.offsetWidth + 'px'; }

这段非常简单的例子有两个问题可能会造成浏览器运行缓慢。首先,要计算offsetWidth属性,如果该元素或者页面上其他元素有非常复杂的CSS样式,那么这个过程会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的CSS的话,这又需要很多计算。这就可以用到throttle()函数,如下所示:

js
function resizeDiv () { var div = document.getElementById('myDiv'); div.style.height = div.offsetWidth + 'px'; } window.onresize = function () { throttle(resizeDiv); }

自定义事件

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

**观察者模式由两类对象组成:主体和观察者。**主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。

该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。

从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。

事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中——通过自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:

js
function EventTarget () { this.handlers = {}; // 用于储存事件处理程序 } EventTarget.prototype = { constructor: EventTarget, // 注册给定类型事件的事件处理程序 addHandler: function (type, handler) { if (typeof this.handlers[type] === 'undefined') { this.handlers[type] = []; } this.handlers[type].push(handler); }, // 用于触发一个事件 fire: function (event) { if (!event.target) { event.target = this; } if (this.handlers[event.type] instanceof Array) { var handlers = this.handlers[event.type]; for (var i = 0, len = handlers.length; i < len; i++) { handlers[i](event); } } }, // 注销某个事件类型的事件处理程序 removeHandlers: function (type, handler) { if (this.handlers[type] instanceof Array) { var handlers = this.handlers[type]; for (var i = 0, len = handlers.length; i < len; i++) { if (handlers[i] === handler) { break; } } handlers.splice(i, 1); } } };

使用EventTarget类型的自定义事件可以如下使用:

js
function handlerMessage (event) { console.log('Message received: ' + event.message); } // 创建一个新对象 var target = new EventTarget(); // 添加一个事件处理程序 target.addHandler('message', handlerMessage); // 触发事件 target.fire({ type: 'message', message: 'Hello world' }); // Message received: Hello world // 删除事件处理程序 target.removeHandlers('message', handlerMessage); // 再次触发,应没有事件事件程序 target.fire({ type: 'message', message: 'Hello world' });

因为这种功能是封闭在一种自定义类型中的,其他对象可以继承EventTarget并获得这个行为,如下所示:

js
// 寄生组合继承 function object (o) { function F () {} F.prototype = o; return new F(); } function inheritPrototype (subType, superType) { var prototype = object(superType.prototype); // 创建对象 prototype.constructor = subType; // 增强对象 subType.prototype = prototype; // 指定对象 } function Person (name, age) { EventTarget.call(this); this.name = name; this.age = age; } inheritPrototype(Person, EventTarget); Person.prototype.say = function (message) { this.fire({ type: 'message', message: message }); }; function handlerMessage (event) { console.log(event.target.name + ' says: ' + event.message); } // 创建新person var person = new Person('Ni', 29); // 添加一个事件处理程序 person.addHandler('message', handlerMessage); // 在该对象上调用1个方法,它触发消息事件 person.say('Hi there.'); // Ni says: Hi there.

拖放

拖放是一种非常流行的用户界面模式。它的概念很简单:点击某个对象,并按住鼠标不放,将鼠标移动到另一个区域,然后释放鼠标将对象“放”在这里。拖放功能也流行到了Web上,成为一些更传统的配置界面的一种候选方案。

拖放的基本概念很简单:创建一个绝对定位的元素,使其可以用鼠标移动。这个技术源自一种叫做“鼠标拖尾”的经典网页技巧。鼠标拖尾是一个或多个图片在页面上跟着鼠标指针移动。单元素鼠标拖尾的基本代码需要为文档设置一个onmousemove事件处理程序,它总是将指定元素移动到鼠标指针的位置:

js
EventUtil.addHandler(document, 'mousemove', function (event) { var myDiv = document.getElementById('myDiv'); myDiv.style.left = event.clientX + 'px'; myDiv.style.top = event.clientY + 'px'; })

拖放界面可用以下代码实现:

js
var DragDrop = function () { var dragdrop = new EventTarget(), // 之前的自定义事件 dragging = null, diffX = 0, diffY = 0; function handleEvent (event) { // 获取事件和目标 event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); // 确定事件类型 switch (event.type) { case 'mousedown': if (target.className.indexOf('draggable') > -1) { dragging = target; diffX = event.clientX - target.offsetLeft; diffY = event.clientY - target.offsetTop; dragdrop.fire({type: 'dragstart', target: dragging, x: event.clientX, y: event.clientY }); } break; case 'mousemove': if (dragging !== null) { // 指定位置 dragging.style.left = (event.clientX - diffX) + 'px'; dragging.style.top = (event.clientY - diffY) + 'px'; // 触发自定义事件 dragdrop.fire({type: 'drag', target: dragging, x: event.clientX, y: event.clientY }); } break; case 'mouseup': dragdrop.fire({type: 'dragend', target: dragging, x: event.clientX, y: event.clientY }); dragging = null; break; } } // 公共接口 dragdrop.enable = function () { EventUtil.addHandler(document, 'mousedown', handleEvent); EventUtil.addHandler(document, 'mousemove', handleEvent); EventUtil.addHandler(document, 'mouseup', handleEvent); }; dragdrop.diable = function () { EventUtil.removeHandler(document, 'mousedown', handleEvent); EventUtil.removeHandler(document, 'mousemove', handleEvent); EventUtil.removeHandler(document, 'mouseup', handleEvent); }; return dragdrop; }