JavaScript框架设计

978-7-115-34358-1
作者: 司徒正美
译者:
编辑: 张涛

图书目录:

详情

主要包括浏览器大战与JavaScript框架的发展,三次浏览器大战,JavaScript框架发展的四个阶段,HTML5对JavaScript框架的冲击,框架的设计探讨,应该有什么功能,API接口的设计,各种JS奇技与只有动态语言才有的设计模式等。

图书摘要

JavaScript框架设计
司徒正美◎编著
人民邮电出版社

北京

前言

首先说明一下,本书虽是讲解框架设计,但写个框架不是很深奥的事情,重点是建立更为完整的前端知识树。只有自己尝试写个框架,才有机会接触像原型、作用域、事件代理、缓存系统、定时器等深层知识,也才有机会了解applyElement、swapNode、importNode、removeNode、replaceNode、insertAdjacentHTML、createContextualFragment、runtimeStyle 等偏门 API ,也才会知晓像getElementById、getElementsByTagName、setAttribute、innerHTML 存在大量的 Bug,当然你也可以尝试最近几年浏览器带来的新API(包括ECMA262v5、v6、HTML5或大量误认为是HTML5的新模块),如Object.defineProperty、CSS.supports、WebKitShadowRoot、getDefaultComputedStyle……

虽然这难免落入“造轮子”的怪圈中,但“造轮子”在这世界却是出奇普遍。一般创造性的活动,一开始都是临摹他人的作品。就算不“造轮子”,也要收集一大堆“轮子”,作家有他的素材集,设计师有大量icon与笔刷,普通的“码农”也有个commonjs存放着一些常用的函数。以前的程序员们,经常会为了做一个数据处理程序而自己开发一门编程语言。如Charls Moore,他在美国国家天文台做射电望远镜数据提取程序时开发了 Forth;高德纳为了让自己写的书排版漂亮些,写了TeX;DHH为了做网站写了Rails……如果连写一个控件都要百度或Google查找答案,那水平不容易提高。

当前很少有技术书教你写框架的,即便是众多的 Java 类图书,大多数也是教你如何深入了解SHH的运作机理。

如果你是这两三年才接触JavaScript,那恭喜你了。现在JavaScript的应用洪荒时代已经过去, Portotype.js的幕府“统治”也已结果,且已迎来非常强势的jQuery纪元,有大量现成的插件可用,许多公司都用jQuery,意味着我们的技术有了用武之地。

但事实上还是要通过调试程序获得经验,只从 JavaScript 书上学习的那些知识点没法明白jQuery的源代码。

许多大公司的架构师根据技术发展的情况,他们都有自己一套或几套JavaScript底层库,各个部门视情况还发展针对于自己业务的UI库。而企业开发中,UI库就像流水线那样重要。而底层库只是一个好用的“锤子”或“胶钳”。要想迅速上手这么多公司框架,基础知识无疑是非常重要的。假若之前自己写过框架,那就有了经验。道理是一样的,框架设计的一些“套路”肯定存在的。本书就是把这些“潜规则”公开出来,迅速让自己成长为技术达人。

1.框架与库

下面稍微说一下框架与库的区别。

库是解决某个问题而拼凑出来的一大堆函数与类的集合。例如,盖一个房子,需要有测量的方法、砌砖的方法、安装门窗的方法等。每个方法之间都没什么关联。至于怎么盖房子都由自己决定。

框架则是一个半成品的应用,直接给出一个骨架,还例如盖房子,照着图纸砌砖、铺地板与涂漆就行了。在后端Java的三大框架中,程序员基本上就是与XML打交道,用配置就可以处理80%的编程问题。

从上面描述来看,框架带来的便利性无疑比库好许多。但前端JavaScript由于存在一个远程加载的问题,对JavaScript文件的体积限制很大,因此,框架在几年前都不怎么吃香。现在网速快多了,设计师在网页制造的地位(UED)也不比昔日,因此,集成程度更高的MVC、MVVM框架也相继面世。

不过,无论是框架还是库,只要在浏览器中运行,就要与DOM打交道。如果不用jQuery,就要发明一个半成品jQuery或一个半成品Prototype。对想提升自己能力的人来说,答案其实很明显,写框架还能提升自己的架构能力。

2.JavaScript发展历程

第1时期,洪荒时代。从1995年到2005年,就是从JavaScript发明到Ajax概念 [1]的提出。其间打了第一场浏览器战争,IE VS Netscape。这两者的DOM API出入很大,前端开发人员被迫改进技术,为了不想兼容某一个浏览器,发明UA(navigator.userAgent)嗅探技术。

这个时期的杰出代表是Bindows [2],2003年发布,它提供了一个完整的Windows桌面系统,支持能在EXT看到的各种控件,如菜单、树、表格、滑动条、切换卡、弹出层、测量仪表(使用VML实现,现在又支持SVG)。现在版本号是4.x,如下图所示。

其他比较著名的还有Dojo(2004年)、Sarissa(2003年)、JavaScript Remote Scripting(2000年)。

Dojo 有 IBM 做后台,有庞大的开发团队在做,质量有保证,被广泛整合到各大 Java 框架内(struct2、Tapestry、Eclipse ATF、MyFaces)。特点是功能无所不包,主要分为Core、Dijit、DojoX 3大块。Core 提供Ajax、events、packaging、CSS-based querying、animations、JSON 等相关操作API。Dijit 是一个可更换皮肤、基于模板的 Web UI 控件库。DojoX 包括一些新颖的代码和控件,如DateGrid、charts、离线应用和跨浏览器矢量绘图等,如下图所示。

JavaScript Remote Scripting 是较经典的远程脚本访问组件,支持将客户端数据通过服务器做代理进行远程的数据/操作交互。

Sarissa封装了在浏览器端独立调用XML的功能。

第2时期,Prototype“王朝”,2005 年~2008 年。其间打了第2次浏览器战争,交战双方是 IE6、IE7、IE8 VS Firefox 1、Firefox 2、Firefox 3,最后Firefox 3 大胜。浏览器战争中,Prototype积极进行升级,加入诸多 DOM 功能,因此,Jser(JavaScript 程序员)比之前好过多了。加之有 rails、script.aculo.us(一流的特效库)、Rico等助阵,迅速占领了市场。

Prototype时期,面向对象技术发展到极致,许多组件成套推出。DOM特征发掘也有序进行,再也不依靠浏览器嗅探去刻意屏蔽某一个浏览器了。无侵入式 JavaScript 开发得到推崇,所有JavaScript代码都抽离到JavaScript文件,不在标签内“兴风作浪”了。

Prototype的发展无可限量,直到1.5版本对DOM进行处理,这是一个错误 [3]。如它一个很好用的API-getElementsByClassName,由于W3C的标准化,Prototype升级慢了,它对DOM的扩展成为了它的“地雷”。

第3时期,jQuery纪元,2008年到现在(如下图所示)。

jQuery则以包裹方式来处理DOM,而且配合它的选择器引擎,若一下子选到N个元素,那么就处理 N 个元素,是集化操作,与主流的方式完全不一样。此外,它的方法名都起得很特别,人们一时很难接受。

2007年7月1日,jQuery发布了1.1.3版本,它的宣传是。

(1)速度改良:DOM的遍历比1.1.2版本快了大概800%。

(2)重写了事件系统:对键盘事件用更优雅的方法进行了处理。

(3)重写了effects系统:提高了处理速度。

停滞不前的Prototype已经跟不上时代的节奏,jQuery在1.3x版本时更换Sizzle,更纯净的CSS选择器引擎,易用性与性能大大提高,被程序员一致看好的mouseenter、mouseleave及事件代理也被整合进去,jQuery就占据了市场。

3.JavaScript框架分类

如果是从内部架构与理念划分,目前JavaScript框架可以划分为5类。

第1种出现的是以命名空间为导向的类库或框架,如创建一个数组用 new Array(),生成一个对象用new Object(),完全的Java 风格,因此我们就可以以某一对象为根,不断为它添加对象属性或二级对象属性来组织代码,金字塔般地垒叠起来。代表作如早期的YUI与EXT。

第2种出现的是以类工厂为导向的框架,如著名的Prototype,还有mootools、Base2、Ten。它们基本上除了最基本的命名空间,其他模块都是一个由类工厂衍生出来的类对象。尤其是mootoos 1.3把所有类型都封装成Type类型。

第3种就是以jQuery为代表的以选择器为导向的框架,整个框架或库主体是一个特殊类数组对象,方便集化操作——因为选择器通常是一下子选择到N个元素节点,于是便一并处理了。JQuery包含了几样了不起的东西:“无new实例化”技术,$(expr)就是返回一个实例,不需要显式地new出来;get first set all 访问规则;数据缓存系统。这样就可以复制节点的事件了。此外,IIFE (Immediately-Invoked Function Expression)也被发掘出来。

第4种就是以加载器串联起来的框架,它们都有复数个JavaScript文件,每个JavaScript文件都以固定规则编写。其中最著名的莫过于AMD。模块化是JavaScript走向工业化的标志。《Unix 编程艺术》列举的众多“金科玉律”的第一条就是模块,里面有言——“要编写复杂软件又不至于一败涂地的唯一方法,就是用定义清晰的接口把若干简单模块组合起来,如此一来,多数问题只会出现在局部,那么还有希望对局部进行改进或优化,而不至于牵动全身”。许多企业内部框架都基本采取这种架构,如Dojo、YUI、kissy、qwrap和mass等。

第5种就是具有明确分层构架的 MV*框架。首先是 JavaScript MVC(现在叫 CanJS)、backbonejs和spinejs,然后更符合前端实际的MVVM框架,如knockout、ember、angular、avalon、winjs。在MVVM框架中,原有DOM操作被声明式绑定取代了,由框架自行处理,用户只需专注于业务代码。

4.JavaScript框架的主要功能

下面先看看主流框架有什么功能。这里面包含jQuery这个自称为库的东西,但它接近9000行,功能比Prototype还齐备。这些框架类库的模块划分主要依据它们在github中的源代码,基本上都是一个模块一个JavaScript文件。

jQuery

jQuery强在它专注于DOM操作的思路一开始就是对的,以后就是不断在兼容性、性能上进行改进。

jQuery 经过多年的发展,拥有庞大的插件与完善的 Bug 提交渠道,因此,可以通过社区的力量不断完善自身。

Prototype.js

早期的王者,它分为4大部分。

语言扩展。

DOM 扩展。

Ajax 部分。

废弃部分(新版本使用其他方法实现原有功能)。

Prototype.js的语言扩展覆盖面非常广,包括所有基本数据类型及从语言借鉴过来的“类”。其中Enumerable 只是一个普通的方法包,ObjectRange、PeriodicalExecuter、Templat 则是用Class 类工厂生产出来的。Class类工厂来自社区贡献。

mootools

它由于 API 设计得非常优雅,其官方网站上有许多优质插件,因此才没有在原型扩展的反对浪潮中没落。

RightJS

又一个在原型上进行扩展的框架。

MochiKit

一个Python风格的框架,以前能进世界前十名的。

Ten

日本著名博客社区Hatena的JavaScript框架,由amachang开发,受Prototype.js影响,是最早以命名空间为导向的框架的典范。

mass Framework

它是一个模块化,以大模块开发为目标,jQuery式的框架。

经过细节比较,我们很容易得出以下框架特征的结论。

对基本数据类型的操作是基础,如jQuery 就提供了trim、camelCase、each、map 等方法, Prototype.js等侵入式框架则在原型上添加camelize等方法。

类型的判定必不可少,常见形式是isXXX 系列。

选择器、domReady、Ajax 是现代框架的标配。

DOM操作是重中之重,节点的遍历、样式操作、属性操作也属于它的范畴,是否细分就看框架的规模了。

brower sniff 已过时,feature detect 正被应用。不过特性侦测还是有局限性,如果针对于某个浏览器版本的渲染 Bug、安全策略或某些 Bug 的修正,还是要用到浏览器嗅探。但它应该独立成一个模块或插件,移出框架的核心。

现在主流的事件系统都支持事件代理。

数据的缓存与处理,目前浏览器也提供data-*属性进行这面的工作,但不太好用,需要框架的进一步封装。

动画引擎,除非你的框架像Prototype.js 那样拥有像script.aculo.us这样顶级的动画框架做后盾,最好也加上。

插件的易开发和扩展性。

提供诸如Deferred 这样处理异步的解决方案。

即使不专门提供一个类工厂,也应该存在一个名为extend 或mixin 的方法对对象进行扩展。jQuery虽然没有类工厂,但在jQuery UI 中也不得不增加一个,可见其重要性。

自从jQuery出来一个名为noConflict的方法,新兴的框架都带此方法,以求狭缝中生存。

许多框架非常重视Cookie 操作。

最后感谢一下业内一些朋友的帮忙,要不是他们,书不会这么顺利地写出来。以下排名不分先后:玉伯、汤姆大叔、弹窗教主、貘大、linxz 、正则帝 abcd。这些都是专家级人物,在业界早已闻名遐迩。由于本人水平有限,书中难免存有不妥之处,请读者批评指正,源程序和答疑网址:https://github.com/RubyLouvre/jsbook/issues。编辑联系邮箱:zhangtao@ptpress.com.cn。

注释

[1]. http://www.adaptivepath.com/ideas/ajax-new-approach-web-applications

[2]. http://www.bindows.net/

[3]. 详见Prototype核心成员的反思:http://perfectionkills.com/whats-wrong-with-extending-the-dom/

第1章 种子模块

种子模块也叫核心模块,是框架的最先执行的部分。即便像 jQuery 那样的单文件函数库,它的内部也分许多模块,必然有一些模块冲在前面立即执行,有一些模块只有用到才执行,也有一些模块可有可无,存在感比较弱,只在特定浏览器下才运行。

种子模块就是其中的急先锋,它里面的方法不一定要求个个神通广大,设计优良,但一定极具扩展性,常用,稳定。扩展性是指通过它们能将其他模块的方法包进来,让种子像大树一样成长;常用是指绝大多数模块都用到它们,防止做重复工作;稳定是指不能轻易在以后版本就给去掉,要信守承诺。

参照许多框架与库的实现,我认为种子模块应该包含如下功能:对象扩展,数组化,类型判定,简单的事件绑定与卸载,无冲突处理,模块加载与domReady。本章的讲解内容以mass Framework的种子模块为范本,可以到以下地址下载:

https://github.com/RubyLouvre/mass-Framework/blob/1.4/mass.js

 

1.1 命名空间

种子模块作为一个框架的最开始部分,除了负责辅建全局用的基础设施外,你有没有想到给读者一个震撼的开场呢?俗话说,好的开头是成功的一半。

时下“霸主”jQuery 就有一个很好的开头——IIFE(立即调用函数表达式),一下子吸引住读者,让读者吃一颗定心丸——既然作者的水平如此高超,还怕什么啊,直接拿来用。

IIFE是现代JavaScript框架最主要的基础设施,它像细胞膜一样包裹自身,防止变量污染。但我们总得在 Windows 里设置一个立足点,这个就是命名空间。基本上我们可以把命名空间等同于框架的名字,不过对于某些框架,它们是没有统一的命名空间,如Prototype.js , mootools。它们就是不想让你感觉到框架的存在,它的意义深透到 JavaScript、DOM、COM 等整个执行环境的每个角落,对原生对象的原型进行扩展。由于道格拉斯(JSON作者)的极力反对,新兴的框架都在命名空间上构建了。

命名空间是干什么用呢?这里就不多说了。我们看怎么在JavaScript模拟命名空间。JavaScript一切基于对象,但只有复合类型的对象才符合这要求,比如function、regexp、object……不过最常用的还是object与function。我们往一个对象上添加一个属性,而这个属性又是一个对象,这个对象我们又可以为它添加一个对象,通过这种方法,我们就可以有条不紊地构建我们的框架。用户想调用某个方法,它就以XXX.YYY.ZZZ()的形式调用。

if (typeof(Ten) === "undefined") {

Ten = {};

Ten.Function = {/*略*/}

Ten.Array = {/*略*/}

Ten.Class = {/*略*/}

Ten.JSONP = new Ten.Class(/*略*/)

Ten.XHR = new Ten.Class(/*略*/)

}

纵观各大类库的实现,一开始基本都是定义一个全局变量作为命名空间,然后对它进行扩展,如Base2的Base、Ext的Ext、jQuery的jQuery、YUI的YUI、dojo的dojo、MochiKit的MochiKit等。从全局变量的污染程度来看,分为两大类。

Prototype、mootools与Base2归为一类。Prototype的哲学是对JavaScript原生对象进行扩展。早些年,Ptototype差点成为事实的标准,因此基本没有考虑到与其他库的共存问题。基于Prototype,也发展出诸如script.aculo.us、rico、Plotr、ProtoChart、Scripty 2等非常优秀的类库及一大堆收费插件,非jQuery那一大堆垃圾插件所能比拟的。而且,有点渊源的插件几乎都与Prototype有关,如著名的lightbox。mootools是Prototype的升级版,更加OO,全面复制其API。Base则是想修复IE的Bug,让IE拥有标准浏览器的API,因此也把所有原生对象污染一遍。

第二类是jQuery、YUI、EXT这些框架。YUI与EXT就是像上面给出的代码那样,以叠罗汉方式构建的。jQuery则另辟蹊径,它是以选择器为导向的,因此它的命名空间是一个函数,方便用户把CSS表达器字符串传进来,然后通过选择器引擎进行查找,最后返回一个jQuery实例。另外,像jQuery最初也是非常弱小的,它想让人家试用自己的框架,但也想像Prototype那样使用美元符号作为它的命名空间。因此它特意实现了多库共存机制,在$、jQuery与用户指定的新命名空间中任意切换。

jQuery的多库共存原理很简单,因此后来也成为许多小库的标配。首先把命名空间保存到一个临时变量中,注意这时这个对象并不是自己框架的东西,可能是 Prototype.js 等巨头的,然后再搞个noConflict放回去。

//jQuery1.2

var _jQuery = window.jQuery, _$ = window.$;//先把可能存在的同名变量保存起来

jQuery.extend({

noConflict: function(deep) {

window.$ = _$;//这时再放回去

if (deep)

window.jQuery = _jQuery;

returnjQuery;

}

})

但jQuery的noConflict只对单文件的类库框架有用,像EXT就不能复制了。因此把命名空间改名后,将EXT置为null,然后又通过动态加载方式引入新的JavaScript文件,该文件再以EXT调用,将会导致报错。

mass Framework 对jQuery的多库共存进行改进,它与jQuery一样拥有两个命名空间,一个是美元符号的短命名空间,另一个是根据URL动态生成的长命名空间(jQuery就是jQuery!)。

namespace = DOC.URL.replace( /(#.+|\W)/g,'');

短的命名空间随便用户改名,长的命名空间则是加载新的模块时用的,虽然用户在模块中使用$做命名空间,但当JavaScript文件加载下来时,我们会对里面的内容再包一层,将$指向正确的对象,具体实现见define方法。

 

1.2 对象扩展

我们需要一种机制,将新功能添加到我们的命名空间上。这方法在JavaScript通常被称做extend或mixin。JavaScript对象在属性描述符(Property Descriptor)没有诞生之前,是可以随意添加、更改、删除其成员的,因此扩展一个对象非常便捷。一个简单的扩展方法实现是这样。

function extend(destination, source) {

for (var property in source)

destination[property] = source[property];

return destination;

}

不过,旧版本IE在这里有个问题,它认为像Object的原型方法就是不应该被遍历出来,因此for in 循环是无法遍历名为valueOf、toString 的属性名。这导致,后来人们模拟Object.keys 方法实现时也遇到了这个问题。

Object.keys = Object.keys || function(obj){

var a = [];

for(a[a.length] in obj);

return a ;

}

在不同的框架,这个方法还有不同的实现,如EXT分为apply与applyIf两个方法,前者会覆盖目标对象的同名属性,而后者不会。dojo允许多个对象合并在一起。jQuery还支持深拷贝。下面是mass Framework 的mix 方法,支持多对象合并与选择是否覆写。

function mix(target, source) {   //如果最后参数是布尔,判定是否覆写同名属性

var args = [].slice.call(arguments), i = 1, key,

ride = typeof args[args.length - 1] == "boolean" ? args.pop() : true;

if (args.length === 1) {   //处理$.mix(hash)的情形

target = !this.window ? this : {};

i = 0;

}

while ((source = args[i++])) {

for (key in source) {   //允许对象糅杂,用户保证都是对象

if (ride || !(key in target)) {

target[ key ] = source[ key ];

}

}

}

return target;

}

 

}

}

1.3 数组化

浏览器下存在许多类数组对象,如function内的arguments,通过document.forms、form. elements、doucment.links、select.options、document.getElementsByName、document.getElementsBy TagName、childNodes、children 等方式获取的节点集合(HTMLCollection、NodeList),或依照某些特殊写法的自定义对象。

var arrayLike = {

0: "a",

1: "1",

2: "2",

length: 3

}

类数组对象是一个很好的存储结构,不过功能太弱了,为了享受纯数组的那些便捷方法,我们在处理它们前都会做一下转换。

通常来说,只要[].slice.call就能转换了,但旧版本IE下的HTMLCollection、NodeList不是Object的子类,采用如上方法将导致 IE 执行异常。我们看一下各大库怎么处理的。

//jQuery的makeArray

var makeArray = function(array) {

var ret = [];

if (array != null) {

var i = array.length;

// The window, strings (and functions) also have 'length'

if (i == null || typeof array === "string" || jQuery.isFunction(array) ||array.setInterval)

ret[0] = array;

else

while (i)

ret[--i] = array[i];

}

return ret;

}

jQuery对象是用来储存与处理dom元素的,它主要依赖于setArray方法来设置和维护长度与索引,而setArray的参数要求是一个数组,因此makeArray的地位非常重要。这方法保证就算没有参数也要返回一个空数组。

Prototype.js的$A方法:

function $A(iterable) {

if (!iterable)

return [];

if (iterable.toArray)

return iterable.toArray();

var length = iterable.length || 0, results = new Array(length);

while (length--)

results[length] = iterable[length];

return results;

};

mootools的$A方法:

function $A(iterable) {

if (iterable.item) {

var l = iterable.length, array = new Array(l);

while (l--)

array[l] = iterable[l];

return array;

}

return Array.prototype.slice.call(iterable);

};

Ext的toArray方法:

var toArray = function() {

returnisIE ?

function(a, i, j, res) {

res = [];

Ext.each(a, function(v) {

res.push(v);

});

return res.slice(i || 0, j || res.length);

} :

function(a, i, j) {

return Array.prototype.slice.call(a, i || 0, j || a.length);

}()

Ext的设计比较巧妙,功能也比较强大。它一开始就自动执行自身,以后就不用判定浏览器了。它还有两个可选参数,对生成的纯数组进行操作。

dojo的_toArray和Ext一样,后面两个参数是可选的,只不过第二个是偏移量,最后一个是已有的数组,用于把新生的新组元素合并过去。

(function() {

var efficient = function(obj, offset, startWith) {

return (startWith || []).concat(Array.prototype.slice.call(obj, offset || 0));

};

var slow = function(obj, offset, startWith) {

var arr = startWith || [];

for (var x = offset || 0; x > obj.length; x++) {

arr.push(obj[x]);

returnarr;

};

dojo._toArray =

dojo.isIE ? function(obj) {

return ((obj.item) ? slow : efficient).apply(this, arguments);

} :

efficient;

})();

最后是mass的实现,与dojo一样,一开始就进行区分,W3C方直接[].slice.call,IE自己手动实现一个slice方法。

$.slice = window.dispatchEvent ? function(nodes, start, end) {

return [].slice.call(nodes, start, end);

} : function(nodes, start, end) {

var ret = [],

n = nodes.length;

if (end === void 0 || typeof end === "number" && isFinite(end)) {

start = parseInt(start, 10) || 0;

end = end == void 0 ? n : parseInt(end, 10);

if (start < 0) {

start += n;

}

if (end > n) {

end = n;

}

if (end < 0) {

end += n;

}

for (var i = start; i < end; ++i) {

ret[i - start] = nodes[i];

}

}

return ret;

}

 

1.4 类型的判定

JavaScript 存在两套类型系统,一套是基本数据类型,另一套是对象类型系统。基本数据类型包括6种,分别是undefined、string、null、boolean、function、object。基本数据类型是通过typeof来检测的。对象类型系统是以基础类型系统为基础的,通过 instanceof 来检测。然而,JavaScript自带的这两套识别机制非常不靠谱,于是催生了isXXX系列。就拿typeof来说,它只能粗略识别出 string、number、boolean、function、undefined、object 这 6 种数据类型,无法识别 Null、RegExpAragument 等细分对象类型。

让我们看一下这里面究竟有多少陷阱。

typeof null// "object"

typeof document.childNodes //safari "function"

typeof document.createElement('embed')//ff3-10 "function"

typeof document.createElement('object')//ff3-10 "function"

typeof document.createElement('applet')//ff3-10 "function"

typeof /\d/i //在实现了ecma262v4的浏览器返回 "function"

typeof window.alert //IE678 "object""

var iframe = document.createElement('iframe');

document.body.appendChild(iframe);

xArray = window.frames[window.frames.length - 1].Array;

var arr = new xArray(1, 2, 3); // [1,2,3]

arr instanceof Array; // false

arr.constructor === Array; // false

window.onload = function() {

alert(window.constructor);// IE67 undefined

alert(document.constructor);// IE67 undefined

alert(document.body.constructor);// IE67 undefined

alert((new ActiveXObject('Microsoft.XMLHTTP')).constructor);// IE6789 undefined

}

isNaN("aaa") //true

上面分4组,第一组是typeof 的坑。第二组是instanceof 的陷阱,只要原型上存在此对象的构造器它就返回true,但如果跨文档比较,iframe里面的数组实例就不是父窗口的Array的实例。第三组有关constructor的陷阱,在旧版本IE下DOM与BOM对象的constructor属性是没有暴露出来的。最后有关NaN,NaN对象与null、undefined一样,在序列化时是原样输出的,但isNaN这方法非常不靠谱,把字符串、对象放进去也返回true,这对我们序列化非常不利。

另外,在IE下typeof还会返回unknow的情况。

if (typeof window.ActiveXObject != "undefined") {

var xhr = new ActiveXObject("Msxml2.XMLHTTP");

alert(typeof xhr.abort);

}

基于这IE的特性,我们可以用它来判定某个VBscript方法是否存在。

<script type="text/VBScript">

function VBMethod(a,b)

VBMethod = a + b

end function

</script>

<script>

if(typeof VBMethod === "unknown"){//看这个

alert(VBMethod(10,34))

}

</script>

另外,以前人们总是以 document.all [1]是否存在来判定 IE ,这其实是很危险的。因为用document.all 来取得页面中的所有元素是不错的主意,这个方法 Firefox、Chrome 觊觎好久了,不过人们都这样判定,于是有了在Chrome下的这出闹剧。

typeof document.all // undefined

document.all // HTMLAllCollection[728] (728为元素总数)

在判定undefined、null、string、number、boolean、function这6个还算简单,前面两个可以分别与void(0)、null比较,后面4个直接typeof也可满足90%的情形。这样说是因为string、number、boolean可以包装成“伪对象”,typeof无法按照我们的意愿工作了,虽然它严格执行了 Ecmascript 的标准。

typeof new Boolean(1);//"object"

typeof new Number(1);//"object"

typeof new String("aa");//"object"

这些还是最简单的,难点在于 RegExp 与 Array。判定 RegExp 类型的情形很少,不多讲了, Array则不一样。有关isArray的实现不下二十种,都是因为JavaScript的鸭子类型 [2]被攻破了。直到Prototype.js把Object.prototype.toString发掘出来,此方法是直接输出对象内部的[[Class]],绝对精准。有了它,可以跳过95%的陷阱了。

isArray早些年的探索:

function isArray(arr) {

return arr instanceof Array;

}

function isArray(arr) {

return !!arr && arr.constructor == Array;

}

function isArray(arr) {//Prototype.js1.6.0.3

return arr != null && typeof arr === "object" &&

'splice' in arr && 'join' in arr;

}

function isArray(arr) {//Douglas Crockford

return typeof arr.sort == 'function'

}

function isArray(array) {//kriszyp

var result = false;

try {

new array.constructor(Math.pow(2, 32))

} catch (e) {

result = /Array/.test(e.message)

}

return result;

};

function isArray(o) {// kangax

try {

Array.prototype.toString.call(o);

return true;

} catch (e) {

}

return false;

};

function isArray(o) {//kangax

if (o && typeof o == 'object' && typeof o.length == 'number' && isFinite(o.length))

{

var _origLength = o.length;

o[o.length] = '__test__';

var _newLength = o.length;

o.length = _origLength;

return _newLength == _origLength + 1;

}

return false;

}

至于null、undefined、NaN直接这样:

function isNaN(obj) {

return obj !== obj

}

function isNull(obj) {

return obj === null;

}

function isUndefined(obj) {

return obj === void 0;

}

最后要判定的对象是 window,由于 ECMA 是不规范 Host 对象,window 对象属于 Host ,所以也没有被约定,就算Object.prototype.toString也对它无可奈何。

[object Object]IE6

[object Object]IE7

[object Object]IE8

[object Window]IE9

[object Window]firefox3.6

[object Window]opera10

[object DOMWindow]safai4.04

[object global]chrome5.0.3.22

不过根据window.window和window.setInterval去判定更加不够谱,用一个技巧我们可以完美识别IE6、IE7、IE8的window对象,其他还是用toString,这个神奇的hack(技巧)就是,window与document互相比较,如果顺序不一样,其结果是不一样的!

window == document // IE678 true;

document == window // IE678 false;

当然,如果细数起来,JavaScript匪夷所思的事比比都是。

存在a !== a 的情况;

存在a == b && b != a的情况;

存在 a == !a 的情况;

存在a === a+100 的情况;

1 < 2 < 3 为true, 3 > 2 > 1 为false;

0/0为NaN;

……

好了,至此,所有重要的 isXXX 问题都解决了,剩下的就把它们表达出来。经典做法就是直接罗列。

在Prototype.js中,拥有isElement、isArray、isHash、isFunction、isString、isNumber、isDate、isUndefined方法。

mootools搞了个typeOf判定基本类型,instanceOf判定自定义“类”。

RightJS 有isFunction 、isHash、isString、isNumber、isArray 、isElement 、isNode。

EXT有isEmpty、isArray、isDate、isObject、isSimpleObject、isPrimitive、isPrimitive、isFunction、isNumber、isNumeric、isString、isBoolean、isElement、isTextNode、isDefined、isIterable,应有尽有。最后,还有typeOf判定基本类型。

Underscore.js 有 isElement、isEmpty、isArray、isArguments、isObject、isFunction、isString、isNumber、isFinite、isNaN、isBoolean、isDate、isRegExp、isNull、isUndefined。

isXXX系列就像恶性肿瘤一样不断膨胀,其实你多弄几个isXXX也不能满足用户的全部需求。就像isDate、isRegExp会用到的机率有多高呢?

jQuery 就不与其他框架一样了,在 jQuery 1.4 中只有 isFunction、isArray、isPlainObject、isEmptyObject。IsFunction、isArray 肯定是用户用得最多,isPlainObject 则是用来判定是否为纯净的JavaScript对象,既不是DOM、BOM对象,也不是自定义“类”的实例对象,制造它的最初目的是用于深拷贝,避开像window那样自己引用自己的对象。isEmptyObject是用于数据缓存系统,当此对象为空时,就可以删除它。

//jquery2.0

jQuery.isPlainObject = function(obj) {

//首先排除基础类型不为Object的类型,然后是DOM节点与window对象

if (jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow(obj)) {

return false;

}

//然后回溯它的最近的原型对象是否有isPrototypeOf,

//旧版本IE的一些原生对象没有暴露constructor、prototype,因此会在这里过滤

try {

if (obj.constructor &&

!hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {

return false;

}

} catch (e) {

return false;

}

return true;

}

在avalon.mobile中有一个更精简的版本,由于它只支持IE10等非常新的浏览器,就没有干扰因素了,可以大胆使用ecma262v5的新API。

avalon.isPlainObject = function(obj) {

return obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype

}

isArrayLike也是一个常用的方法,但判定一个类数组太难了,唯一的辨识手段是它应该有一个大于或等于零的整型length属性。此外还有一些“共识”,如window与函数和元素节点(如form元素)不算类数组,虽然它们都满足前面的条件。因此至今jQuery没有把它暴露出来。

//jquery2.0

function isArraylike(obj) {

var length = obj.length, type = jQuery.type(obj);

if (jQuery.isWindow(obj)) {

return false;

}

if (obj.nodeType === 1 && length) {

return true;

}

return type === "array" || type !== "function" &&

(length === 0 ||

typeof length === "number" && length > 0 && (length - 1) in obj);

}

//avalon 0.9

function isArrayLike(obj) {

if (obj && typeof obj === "object") {

var n = obj.length

if (+n === n && !(n % 1) && n >= 0) { //检测length属性是否为非负整数

try {//像Argument、Array、NodeList等原生对象的length属性是不可遍历的

if ({}.propertyIsEnumerable.call(obj, 'length') === false) {

return Array.isArray(obj) || /^\s?function/.test(obj.item || obj. callee)

}

return true;

} catch (e) { //IE的NodeList直接抛错

return true

}

}

}

return false

}

//avalon.mobile更倚重Object.prototoype.toString来判定

function isArrayLike(obj) {

if (obj && typeof obj === "object") {

var n = obj.length,

str = Object.prototype.toString.call(obj)

if (/Array|NodeList|Arguments|CSSRuleList/.test(str)) {

return true

} else if (str === "[object Object]" && (+n === n && !(n % 1) && n >= 0)) {

return true //由于 ecma262v5 能修改对象属性的 enumerable,因此不能用 propertyIs

//Enumerable来判定了

}

}

return false

}

补充一句,1.3版本中,Prototype.js的研究成果(Object.prototype.toString.call)就应用于jQuery了。在1.2版本中,jQuery判定一个变量是否为函数非常复杂。

isFunction: function( fn ) {

return !!fn&&typeoffn != "string" && !fn.nodeName&&

fn.constructor != Array && /^[\s[]?function/.test( fn + "" );

}

jQuery1.43引入isWindow来处理makeArray中对window的判定,引入isNaN用于确保样式赋值的安全。同时引入type代替typeof关键字,用于获取数据的基本类型。

class2type = {}

jQuery.each("Boolean Number String Function Array Date RegExpObject".split(" "),

function(i, name) {

class2type[ "[object " + name + "]" ] = name.toLowerCase();

});

jQuery.type = function(obj) {

return obj == null ?

String(obj) :

class2type[toString.call(obj) ] || "object";

})

jQuery1.7中添加isNumeric代替isNaN。这是个不同于其他框架的isNumber,它可以是字符串,只要外观上像数字就行了。但 jQuery1.7 还做了一件违背之前提到稳定性的事情,贸然去掉jQuery.isNaN ,因此导致基于旧版本 jQuery 的一大批插件失效。

//jquery1.43~1.64

jQuery.isNaN = function(obj) {

return obj == null || !rdigit.test(obj) || isNaN(obj);

})

//jquery1.7 就是isNaN的取反版

jQuery.isNumeric = function(obj) {

return obj != null && rdigit.test(obj) && !isNaN(obj);

})

//jquery1.71~1.72

jQuery.isNumeric = function(obj) {

return !isNaN(parseFloat(obj)) && isFinite(obj);

}

//jquery2.1

jQuery.isNumeric = function(obj) {

return obj - parseFloat(obj) >= 0;

}

mass Framework 的思路与jQuery一致,尽量减少isXXX 系列的数量,把isWindow、isNaN、nodeName等方法都整进去了。这是个野心勃勃的方法,代码比较长,它既可以获取类型,也可以传入第二参数进行类型比较。

var class2type = {

"[objectHTMLDocument]": "Document",

"[objectHTMLCollection]": "NodeList",

"[objectStaticNodeList]": "NodeList",

"[objectIXMLDOMNodeList]": "NodeList",

"[objectDOMWindow]": "Window",

"[object global]": "Window",

"null": "Null",

"NaN": "NaN",

"undefined": "Undefined"

},

toString = class2type.toString;

"Boolean,Number,String,Function,Array,Date,RegExp,Window,Document,Arguments,NodeList"

.replace($.rword, function(name) {

class2type[ "[object " + name + "]" ] = name;

});

//class2type这个映射几乎把所有常用判定对象“一网打尽”了

mass.type = function(obj, str) {

var result = class2type[ (obj == null || obj !== obj) ? obj : toString.call(obj) ]

|| obj.nodeName || "#";

if (result.charAt(0) === "#") { //兼容旧版本浏览器与处理个别情况,如window.opera

//利用IE6、IE7、IE8 window == document为true,document == window竟然为false的神奇特性

if (obj == obj.document && obj.document != obj) {

result = 'Window';  //返回构造器名字

} else if (obj.nodeType === 9) {

result = 'Document';  //返回构造器名字

} else if (obj.callee) {

result = 'Arguments'; //返回构造器名字

} else if (isFinite(obj.length) && obj.item) {

result = 'NodeList';  //处理节点集合

} else {

result = toString.call(obj).slice(8, -1);

}

}

if (str) {

return str === result;

}

return result;

}

然后type方法就轻松了,用toString.call(obj)得出的值作键,直接从映射中取。只有在IE6、IE7、IE8中,我们才费一些周折处理window、document、arguments、nodeList等对象。当然,这只是在种子模块的情形,在语言模块,mass Framework还是会添加isArray、isFunction 这两个著名API,此外还有isPlainObject、isNative、isEmptyObject、isArrayLike这4个方法,在选择器模块,还追加isXML方法。

基于实用主义,我们有时不得不妥协。百度的tangram就是典型, 与EXT 一样,能想到的都写上,而且判定非常严谨。

baidu.isDate = function(o) {

return {}.toString.call(o) === "[object Date]" && o.toString() !== 'Invalid Date' && !isNaN(o);

}

baidu.isNumber = function(o) {

return '[object Number]' == {}.toString.call(o) && isFinite(o);

}

 

}

1.5 主流框架引入的机制——domReady

domReady其实是一种名为“DOMContentLoaded”事件的别称,不过由于框架的需要,它与真正的DOMContentLoaded有一点区别。在许多旧的JavaScript书藉中,它们都会教导我们把JavaScript逻辑写在window.onload回调中,以防DOM树还没有建完就开始对节点进行操作,导致出错。而对于框架来说,越早介入对DOM的干涉就越好,如要进行什么特征侦测之类的。domReady还可以满足用户提前绑定事件的需求,因为有时页面图片等资源过多,window.onload 就迟迟不能触发,这时若还没有绑定事件,用户点哪个按钮都没反应(除了跳转外)。因此主流框架都引入domReady机制,并且费了很大劲兼容所有浏览器,具体策略如下。

(1)对于支持DOMContentLoaded事件的使用DOMContentLoaded事件。

(2)旧版本IE 使用Diego Perini发现的著名hack!

//http://javascript.nwbox.com/IEContentLoaded/

//by Diego Perini 2007.10.5

function IEContentLoaded(w, fn) {

var d = w.document, done = false,

init = function() {

if (!done) {//只执行一次

done = true;

fn();

}

};

(function() {

try {//在DOM未建完之前调用元素doScroll抛出错误

d.documentElement.doScroll('left');

} catch (e) {//延迟再试

setTimeout(arguments.callee, 50);

return;

}

init();//没有错误则执行用户回调

})();

// 如果用户是在domReady之后绑定这个函数呢?立即执行它

d.onreadystatechange = function() {

if (d.readyState == 'complete') {

d.onreadystatechange = null;

init();

}

};

}

此外,IE 还可以通过script defer hack 进行判定。

//http://webreflection.blogspot.com/search?q=onContent

//by Andrea Giammarchi 2006.9.24

document.write("<script id=__ie_onload defer src=//0><\/scr" + "ipt>");

script = document.getElementById("__ie_onload");

script.onreadystatechange = function() {//IE即使是死链也能触发事件

if (this.readyState == "complete"){

init(); // 指定了defer的script会在DOM树建完才触发

};

不过有个问题是,如果我们的种子模块是动态加载的,在它插入DOM树时,DOM树已经建完呢?这该怎么触发我们的 ready 回调?jQuery 给出的方案是,连 onload 也监听了,但如果连onload也没赶上,就判定document.readyState是否等于complete!这样完美了吧,可惜Firefox3.6之前没有这属性!看mass给出的方案。

var readyList = [];

mass.ready = function(fn) {

if (readyList) {

fn.push(fn);

} else {

fn();

}

}

var readyFn, ready = W3C ? "DOMContentLoaded" : "readystatechange";

function fireReady() {

for (var i = 0, fn; fn = readyList[i++]; ) {

fn();

}

readyList = null;

fireReady = $.noop; //惰性函数,防止IE9二次调用_checkDeps

}

function doScrollCheck() {

try { //IE下通过doScrollCheck检测DOM树是否建完

html.doScroll("left");

fireReady();

} catch (e) {

setTimeout(doScrollCheck);

}

}

//在Firefox3.6之前,不存在readyState属性

//http://www.cnblogs.com/rubylouvre/archive/2012/12/18/2822912.html

if (!DOC.readyState) {

var readyState = DOC.readyState = DOC.body ? "complete" : "loading";

}

if (DOC.readyState === "complete") {

fireReady(); //如果在domReady之外加载

} else {

$.bind(DOC, ready, readyFn = function() {

if (W3C || DOC.readyState === "complete") {

fireReady();

if (readyState) { //IE下不能改写DOC.readyState

DOC.readyState = "complete";

}

}

});

if (html.doScroll) {

try { //如果跨域会报错,那时肯定证明是存在两个窗口的

if (self.eval === parent.eval) {

doScrollCheck();

}

} catch (e) {

doScrollCheck();

}

}

 

1.6 无冲突处理

无冲突处理也叫多库共存。不得不说,$是最重要的函数名,这么多框架都爱用它做自己的命名空间。在jQuery还比较弱小时,如何让人们试用它呢?当时Prototype是主流,jQuery于是发明了noConflict函数,下面是源代码:

var

window = this,

undefined,

_jQuery = window.jQuery,

_$ = window.$,

//把window存入闭包中的同名变量,方便内部函数在调用window时不用费大力气查找它

//_jQuery与_$用于以后重写

jQuery = window.jQuery = window.$ = function(selector, context) {

//用于返回一个jQuery对象

return new jQuery.fn.init(selector, context);

}

jQuery.extend({

noConflict: function(deep) {

//引入jQuery类库后,闭包外面的window.$与window.jQuery都储存着一个函数

//它是用来生成jQuery对象或在domReady后执行里面的函数的

//回顾最上面的代码,在还没有把function赋给它们时,_jQuery与_$已经被赋值了

//因此它们俩的值必然是undefined

//因此这种放弃控制权的技术很简单,就是用undefined把window.$里面的jQuery系的函数清除掉

//这时Prototype或mootools的$就可以“明媒正娶”了

window.$ = _$;//相当于window.$ = undefined

//如果连你的程序也有一个叫jQuery的东西,jQuery可以大方地连这个也让渡出去

//这时就要为noConflict添加一个布尔值,为true

if (deep)

//但我们必须用一个东西接纳jQuery对象与jQuery的入口函数

//闭包里面的东西除非被window等宿主对象引用,否则就是不可见的

//因此我们把闭包里面的jQuery return出去,外面用一个变量接纳就可以

window.jQuery = _jQuery;//相当window.jQuery = undefined

return jQuery;

}

});

使用时,先引入别人的库,然后引入jQuery,使用调用$.noConflict()进行改名,这样就不影响别人的$运作了。

mass Framework 更进一步,在引入种子模块的script标签上定义一个nick 属性,那么释放出来的命名空间就是你的那个属性值。里面也偷偷实现了jQuery那种机制。

<script nick="AAA" src="mass.js"></script>

<script>

AAA.log("xxxxx")

</script>

如果你不改,默认还是$——我说过了,大家都对它“垂涎三尺”。

注释

[1]. 事实上是非IE浏览器均实现了叫做“伪装为undefined”特性,当采用逻辑运算或类型判断时,会特意输出 undefined 值。http://fremycompany.com/BG/2013/Internet-Explorer-11-9385-new-features-771/

[2]. 在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”在鸭子类型中,关注的不是对象的类型本身,而是它如何被使用的。

第2章 模块加载系统

任何语言一到大规模应用阶段,必然要经历拆分模块的过程,以有利于维护与团队协作。与Java 走得最近的 dojo 率先引入了加载器,早期的加载器都是同步的,使用 document.write 与同步Ajax请求实现。后来dojo开始以JSONP的方法设计它的每个模块的结构,以script节点为主体加载它的模块,这个就是目前主流的加载器方式。不得不提的是,dojo的加载器与AMD规范的发明者都是James Burke,dojo加载器独立出来就是著名的require 加载器。本章将为你深入理解加载器的原理,讲授样本为mass并行加载器。你可以到这里下载:

https://github.com/RubyLouvre/mass-Framework/blob/master/mass.js

2.1 AMD规范

AMD是“Asynchronous Module Definition”的缩写,意为“异步模块定义”。重点有两个。异步——有效避免了采用同步加载方式中导致的页面假死现象。模块定义——每个模块必须按照一定的格式编写。主要接口有两个,define与require。define是模块开发者关注的方法,require是模块使用者关注的方法。

define的参数情况为define(id?, deps?, factory)。第一个为模块ID,第2个为依赖列表,第3个是工厂方法。前两个都是可选,如果不定义 ID,则是匿名模块,加载器运用一些“魔术”能让它辩识自己叫什么。通常情况,模块ID约等于模块在工程中的路径(放到线上,表现为URL)。在开发过程,许多情况未确定,一些JavaScript文件会移来移去的,因此匿名模块就大发所长。deps与factory有个约定,deps有多少个元素,factory就有多少个传参,位置一一对应。传参为其他模块的返回值。

define("xxx", ["aaa","bbb"], function (aaa,bbb){

});

通常情况下define中还有一个amd对象,里面储存着模块的相关信息。

require的参数情况为require(deps, callback),第一个为依赖列表,第2 个为回调。deps有多少个元素,callback就有多少个传参,情况与define方法一致。因此在内部,define方法会调用require来加载依赖模块。一直这样递归下去。

require(["aaa","bbb"], function(aaa,bbb){

})

接口就是这么简单,但 require 本身还包含许多特性比如使用“!”来引入插件机制,通过requirejs.config进行各种配置。模块只是整合的一部分,你要拆得开,也要合得拢,因此合并脚本的地位在加载器中非常重要。但前端JavaScript没有这功能,requirejs利用node.js写了个r.js帮你进行合并。

2.2 加载器所在路径的探知

要加载一个模块,我们需要一个 URL 作为加载地址,一个 script 作为加载媒介。但用户在require时都用ID,因此我们需要一个将ID转换为URL的方法。思路很简单,强加个约定,URL的合成规则为。

basePath + 模块ID + ".js"

由于浏览器自上而下分析DOM,当浏览器在解析我们的JavaScript文件(这个JavaScript文件是指加载器)时,它就肯定DOM树中最后加入的script标签。因此,我们有下面这个方法。

function getBasePath() {

var nodes = document.getElementsByTagName("script");

var node = nodes[nodes.length - 1];

var src = document.querySelector ? node.src : node.getAttribute("src", 4);

return src;

}

这个能满足99%的需求。但如果我们不得不动态加载我们的加载器呢?在旧版本下IE就会折戟沉沙,这个不奇怪,许多常规方法在IE6、IE7、IE8都失效,除了API的差异性,还有它本身的各种Bug。这个我很难指出是什么,总之要解决。如下面的这个JavaScript片断。

<script>

document.write('<script src="avalon.js"><\/script>');

document.write('<script src="mass.js"><\/script>');

document.write('<script

src="http://common.cnblogs.com/script/jquery.js"><\/script>');

</script>

mass.js为我们的加载器,里面执行getBasePath方法,预期得到http://192.168.1.32/mass.js,但在IE7却返回http://192.168.1.32/jquery.js。

这时就需要用readyChange属性,微软在document、image、xhr、script等东西都拥有了这个属性,用来查看加载情况。

function getBasePath() {

var nodes = document.getElementsByTagName("script");

if (window.VBArray) {//如果是IE

for (var i = 0, node; node = nodes[i++]; ) {

if (node.readyState === "interactive") {

break;

}

}

} else {

node = nodes[nodes.length - 1];

}

var src = document.querySelector ? node.src : node.getAttribute("src", 4);

return src;

}

这样就搞定了。现在想一下能否优化。访问DOM比一般的JavaScript代码消耗高许多。这时我们可以利用Error对象。

function getBasePath() {

try {

a.b.c()

} catch (e) {

if (e.fileName) {//firefox

return e.fileName;

} else if (e.sourceURL) {//safari

return e.sourceURL;

}

}

var nodes = document.getElementsByTagName("script");

if (window.VBArray) {//倒序查找更快

for (var i = nodes.length, node; node = nodes[--i]; ) {

if (node.readyState === "interactive") {

break;

}

}

} else {

node = nodes[nodes.length - 1];

}

var src = document.querySelector ? node.src : node.getAttribute("src", 4);

return src;

}

当然Opera与Chrome也有一些属性可以供我们提取,但比较复杂,这里就略去了。有兴趣可以到这里看一下:

http://www.cnblogs.com/rubylouvre/archive/2010/04/06/1705817.html

为了方便以后使用,我们先将mass.js这个去掉吧。在现实中,为了防止缓存,这个后面可能带版本号、时间戮什么的,也要去掉。

url = url.replace(/[?#].*/, "").slice(0, url.lastIndexOf("/") + 1);

在mass并行加载器中,有一个getCurrentScript方法,它用于取得正在被解析的JavaScript文件的src,而不局限于加载器的JavaScript地址。这个对实现匿名模块非常有用。想象一下,我们有个require(["a","b","c","d","e"], callback),这些模块在define里面都没有ID,识别哪一个模块是对应哪个JavaScript文件加载非常麻烦。或者使用iframe,将各个模块的加载独立于一个沙箱环境中,或者使用一些变量做标识(这存在被覆盖的危险),因此mass再次发掘Error对象的私有属性。

function getCurrentScript(base) {//为true时相当于getBasePath

var stack;

try { //Firefox可以直接 var e = new Error("test"),但其他浏览器只会生成一个空Error

a.b.c(); //强制报错,以便捕获e.stack

} catch (e) { //Safari的错误对象只有line、sourceId、sourceURL

stack = e.stack;

if (!stack && window.opera) {

//Opera 9没有e.stack,但有e.Backtrace,不能直接取得,需要对e对象转字符串进行抽取

stack = (String(e).match(/of linked script \S+/g) || []).join(" ");

}

}

if (stack) {

/**e.stack最后一行在所有支持的浏览器大致如下。

*Chrome23:

* at http://113.93.50.63/data.js:4:1

*Firefox17:

*@http://113.93.50.63/query.js:4

*@http://113.93.50.63/data.js:4

*IE10:

* at Global code (http://113.93.50.63/data.js:4:1)

* //firefox4+ 可以用document.currentScript

*/

stack = stack.split(/[@ ]/g).pop();  //取得最后一行,最后一个空格或@之后的部分

stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, "");//去掉换行符

return stack.replace(/(:\d+)?:\d+$/i, ""); //去掉行号与或许存在的出错字符起始位置

}

///我们在动态加载模块时,节点都插入head中,因此只在head标签中寻找

var nodes = (base ? document : head).getElementsByTagName("script");

for (var i = nodes.length, node; node = nodes[--i]; ) {

if ((base || node.className === moduleClass) && node.readyState === "interactive")

{

return node.className = node.src;

}//如果此模块加载过就重写className

}

}

2.3 require方法

require方法的作用是当依赖列表都加载完毕,执行用户回调。因此这里有个加载的过程,整个加载过程分为以下几步。

(1)取得依赖列表的第一个 ID,转换为 URL。无论是通过 basePath+ID+".js",还是以映射的方式直接得到。

(2)检测此模块有没有加载过,或正在被加载。因此我们需要一个对象来保持所有模块的加载情况。当用户从来没有加载过此节点时,就进入加载流程。

(3)创建script节点,绑定onerror、onload、onreadychange等事件判定加载成功与否,然后添加href并插入DOM树,开始加载。

(4)将模块的 URL,依赖列表等构建成一个对象,放到检测队列中,在上面的事件触发时进行检测。

在mass的加载器,它支持允许第一个参数为字符串,然后内部按空格或逗号切分为ID数组,以及做去重处理,其他都一样。

我们看一下模块ID的转换规则:

http://wiki.commonjs.org/wiki/Modules/1.1.1。

mass在这基础上做了扩展。

(1)模块ID本来就是URL的简体,因此可以包含斜线(/),并以/划分为多项。

(2)模块ID应该是以符合变量的规则的字符串组成,第一个项可以是“.”或“..”。这些都是URL的基本的规则,表示当前目录与父目录。

(3)模块ID的未尾可以包含“.js”,但如果它是指向一个CSS文件,那么必须以CSS结尾。

(4)如果以“/”或“./”开头,表示它与加载它的模块在同一目录。

(5)如果以“..”开头,表示它在加载它的模块的上一级目录,如果存在多个“..”,就要向上找。

(6)如果模块ID是“mass”,不做转换与加载,这表示mass框架的种子模块,也就是加载器的所在模块。

(7)如果模块ID是“ready”,不做转换与加载,这用于延迟用户回调到DOM树建完后执行。避免出现domReady的回调函数与模块的回调函数出现套嵌。

(8)如果情况就直接在前面按上basePath——加载器所在的目录!

除了这些情况外,我们通常还用到映射,就是允许用户在事前用一个方法,把ID与完整的URL对应好,这样就直接拿。mass称之为别名机制。ID只是给用户用的,框架还是URL做加载或其他检测。此外,AMD还发展一种shim技术,shim就是垫片的意思,目的是让不符合AMD定义的JavaScript文件也能无缝切入我们的加载器系统。

如这个是普通的别名机制:

require.config({

alias: {

'lang': 'http://common.cnblogs.com/script/mass/lang.js',

'css': 'http://common.cnblogs.com/script/mass/css.js'

}

});

而对于jQuery或其插件,我们需要shim机制:

require.config({

alias: {

'jquery': {

src: 'http://common.cnblogs.com/scriptjquery.js',

exports: "$"

},

'jquery.tooltip': {

src: 'http://common.cnblogs.com/script/ui/tooltip.js',

exports: "$",

deps: ["jquery"]

}

}

});

下面是require的源码:

window.require = $.require = function(list, factory, parent) {

// 用于检测它的依赖是否都为2

var deps = {},

// 用于保存依赖模块的返回值

args = [],

// 需要安装的模块数

dn = 0,

// 已安装完的模块数

cn = 0,

id = parent || "callback" + setTimeout("1");

parent = parent || basepath;//basepath为加载器的路径

String(list).replace($.rword, function(el) {

var url = loadJSCSS(el, parent)

if (url) {

dn++;

if (modules[url] && modules[url].state === 2) {

cn++;

}

if (!deps[url]) {

args.push(url);

deps[url] = "司徒正美"; //去重

}

}

});

modules[id] = {//创建一个对象,记录模块的加载情况与其他信息

id: id,

factory: factory,

deps: deps,

args: args,

state: 1

};

if (dn === cn) { //如果需要安装的等于已安装好的

fireFactory(id, args, factory); //安装到框架中

} else {

//放到检测队列中,等待checkDeps处理

loadings.unshift(id);

}

checkDeps();

};

每require一次,相当于把当前的用户回调当成一个不用加载的匿名模块,ID是随机生成,回调是否执行,要待到deps对象里面所有值都为2。

require里有三个重要方法:loadJSCSS,它用于转换ID为URL,后面再调用loadJS, loadCSS,或再调用 require 方法;fireFactory,就是执行用户回调,我们的最终目的;checkDeps,检测依赖是否都安装好,安装好就执行fireFactory。

function loadJSCSS(url, parent, ret, shim) {

//1. 特别处理mass|ready标识符

if (/^(mass|ready)$/.test(url)) {

return url;

}

//2. 转化为完整路径

if ($.config.alias[url]) {//别名机制

ret = $.config.alias[url];

if (typeof ret === "object") {

shim = ret;

ret = ret.src;

}

} else {

if (/^(\w+)(\d)?:.*/.test(url)) { //如果本来就是完整路径

ret = url;

} else {

parent = parent.substr(0, parent.lastIndexOf('/'));

var tmp = url.charAt(0);

if (tmp !== "." && tmp !== "/") { //相对于根路径

ret = basepath + url;

} else if (url.slice(0, 2) === "./") { //相对于兄弟路径

ret = parent + url.slice(1);

} else if (url.slice(0, 2) === "..") { //相对于父路径

var arr = parent.replace(/\/$/, "").split("/");

tmp = url.replace(/\.\.\//g, function() {

arr.pop();

return "";

});

ret = arr.join("/") + "/" + tmp;

} else if (tmp === "/") {

ret = parent + url;//相对于兄弟路径

} else {

$.error("不符合模块标识规则: " + url);

}

}

}

var src = ret.replace(/[?#].*/, ""),

ext;

if (/\.(css|js)$/.test(src)) {

ext = RegExp.$1;

}

if (!ext) { //如果没有后缀名,加上后缀名

src += ".js";

ext = "js";

}

//3. 开始加载JS或CSS

if (ext === "js") {

if (!modules[src]) { //如果之前没有加载过

modules[src] = {

id: src,

parent: parent,

exports: {}

};

if (shim) {//shim机制

require(shim.deps || "", function() {

loadJS(src, function() {

modules[src].state = 2;

modules[src].exports = typeof shim.exports === "function" ?

shim.exports() : window[shim.exports];

checkDeps();

});

});

} else {

loadJS(src);

}

}

return src;

} else {

loadCSS(src);

}

}

注意,上面的modules[src]是以完整路径做ID的,它对应的对象没有state属性,表示其正在加载中。

loadJS与loadCSS方法就比较纯粹了,不过loadJS会做个死链检测checkFail。

function loadJS(url, callback) {

//通过script节点加载目标模块

var node = DOC.createElement("script");

node.className = moduleClass; //让getCurrentScript只处理类名为moduleClass的script节点

node[W3C ? "onload" : "onreadystatechange"] = function() {

if (W3C || /loaded|complete/i.test(node.readyState)) {

//factorys里面装着define方法的工厂函数(define(id?,deps?, factory))

var factory = factorys.pop();

factory && factory.delay(node.src);

if (callback) {

callback();

}

if (checkFail(node, false, !W3C)) {

$.log("已成功加载 " + node.src, 7);

}

}

};

node.onerror = function() {

checkFail(node, true);

};

//插入到head的第一个节点前,防止IE6下head标签没闭合前使用appendChild抛错

node.src = url;

head.insertBefore(node, head.firstChild);

}

checkFail方法主要是用于开发调试。有3个参数。node——script节点,onError——是否为onerror触发,fuckIE——对应旧版本IE的hack。思路是,JavaScript文件从加载到解析到执行需要一个过程,在interact阶段,我们的JavaScript代码已经有些部分可以执行了,这时我们将模块对象的state改为1,如果还是undefined,我们就可识别它为死链。不过,此hack对不是以AMD定义的JavaScript模块无效,因为将state改1的逻辑是由define方法执行的。如果判定是死链,我们就把此节点移除。

function checkFail(node, onError, fuckIE) {

var id = node.src;//检测是否死链

node.onload = node.onreadystatechange = node.onerror = null;

if (onError || (fuckIE && !modules[id].state)) {

setTimeout(function() {

head.removeChild(node);

});

$.log("加载 " + id + " 失败" + onError + " " + (!modules[id].state), 7);

} else {

return true;

}

}

checkDeps方法会在用户加载模块之前及script.onload后各执行一次,检测模块的依赖情况,如果模块没有任何依赖或state都为2了,我们调用fireFactory方法。

function checkDeps() {

loop: for (var i = loadings.length, id; id = loadings[--i]; ) {

var obj = modules[id], deps = obj.deps;

for (var key in deps) {

if (hasOwn.call(deps, key) && modules[key].state !== 2) {

continue loop;

}

}

//如果deps是空对象或者其依赖的模块的状态都是2

if (obj.state !== 2) {

loadings.splice(i, 1);//必须先移除再安装,防止在IE下DOM树建完后手动刷新页面,会多次执行它

fireFactory(obj.id, obj.args, obj.factory);

checkDeps();//如果成功,则再执行一次,以防有些模块就差本模块没有安装好

}

}

}

历经千辛万苦,我们终于到达fireFactory方法。它的工作是从modules中收集各模块的返回值,执行factory, 完成模块的安装。

function fireFactory(id, deps, factory) {

for (var i = 0, array = [], d; d = deps[i++]; ) {

array.push(modules[d].exports);

}

var module = Object(modules[id]),

ret = factory.apply(global, array);

module.state = 2;

if (ret !== void 0) {

modules[id].exports = ret;

}

return ret;

}

2.4 define方法

我们再来看 define 方法,打个比方,它与 require 的关系就是内应外合。define 是应,require是合。require拥有加载器90%的调度资源,以围城姿态攻打我们封闭的JavaScript模块。JavaScript模块则由一个“内鬼”define来看守城门。当require发出请求,define就打开城门,模块被兼并到mass Framework 的“庞大帝国内”!

define有 3 个参数,前面两个为可选,事实上这里的 ID没什么用,就是给开发者看的,它还是用getCurrentScript方法得到script节点路径做ID,deps没有就补上一个空数组。在mass中,先根据浏览器的情况对模块进行分级,一些模块是专门用于打补丁的,只对IE6、IE7、IE8有用,但对Chrome没有用,这个我们在require时就自动排除它。但合并后,我们不得不把它们都加上(因为不知道面对的是什么浏览器),mass 设置一个参数,用于忽略执行这个补丁模块的工厂函数,保证框架用浏览器的原生API执行。这个参数就设置在define中,为一个布尔,如果为true就跳过。

此外,define还要考虑循环依赖的问题。比如说加载A,要依赖B与C,加载B,要依赖A与C,这时A与B就循环依赖了。A与B在判定各自的deps中的键值都是2时才执行,结果都无法执行了。

define的源码:

window.define = $.define = function(id, deps, factory) {

var args = $.slice(arguments);

if (typeof id === "string") {

var _id = args.shift();

}

if (typeof args[0] === "boolean") { //用于文件合并,在标准浏览器中跳过补丁模块

if (args[0]) {

return;

}

args.shift();

}

if (typeof args[0] === "function") {

args.unshift([]);

} //上线合并后能直接得到模块ID,否则寻找当前正在解析中的script节点的src作为模块ID

//现在除了Safari外,我们都能直接通过getCurrentScript一步到位得到当前执行的script节点

//Safari可通过onload+delay闭包组合解决

id = modules[id] && modules[id].state >= 1 ? _id : getCurrentScript();

factory = args[1];

factory.id = _id; //用于调试

factory.delay = function(id) {

args.push(id);

var isCycle = true;

try {

isCycle = checkCycle(modules[id].deps, id);

} catch (e) {

}

if (isCycle) {

$.error(id + "模块与之前的某些模块存在循环依赖");

}

delete factory.delay; //释放内存

require.apply(null, args); //0,1,2 --> 1,2,0

};

if (id) {

factory.delay(id, args);

} else { //先进先出

factorys.push(factory);

}

};

checkCycle方法:

function checkCycle(deps, nick) {

//检测是否存在循环依赖

for (var id in deps) {

if (deps[id] === " 司 徒 正 美" && modules[id].state !== 2 && (id === nick || checkCycle(modules[id].deps, nick))) {

return true;

}

}

}

define与require互相调用的示意如图2.1所示。

▲图2.1

至此整个加载器就完成了。现在的难点转为我们如何判定当前模块的加载情况,如何知道它的模块名,如何排查它的对应的链接是否有效。为此mass动用一个modules对象,两个数组(loadings与factory)。小小一个加载器,里面的注释就提及到许多兼容性问题,真正与DOM打交道还没有开始呢!

最后看一下mass加载器,加载自己框架的ajax、node模块一共做了多少事吧!

require("ajax, node", function($) {

$.log("加载完成!")

})

Firefox20会加载这么多模块,如图2.2所示。

▲图2.2

IE10在IE7模式下会加载如下模块,如图2.3所示。

▲图2.3

由于使用了模块化,我们就可以分级,对旧版本IE使用体积更庞大的query模块,其他则使用query_neo,并且多了许多补丁模块。

此外,mass加载器会把加载过程全部记录下来,大家可以直接在Firefox看到这些消息,如图2.4所示。

▲图2.4

模块加载器会让我们的前端开发变得更为工业化,维护调试都非常方便,强烈建立大家在工作时使用。现在国内主流的几个加载器seajs、requirejs、mass、oyejs都是比较好的选择。

相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
JavaScript核心原理:规范、逻辑与设计
JavaScript核心原理:规范、逻辑与设计
JavaScript入门经典(第7版)
JavaScript入门经典(第7版)
JavaScript函数式编程指南
JavaScript函数式编程指南
PHP、MySQL和JavaScript入门经典(第6版)
PHP、MySQL和JavaScript入门经典(第6版)
JavaScript学习指南(第3版)
JavaScript学习指南(第3版)

相关文章

相关课程