本文摘自:人民邮电出版社异步图书《JavaScript框架设计(第2版)》

试读本书:http://www.epubit.com.cn/book/details/4849

敲重点:
活动规则:试读样章,评论区留言说一下你对本书的一些感想,同时关注异步社区专栏,并留言你想要得到的图书。
活动时间:即日起-9月10日(活动奖项公告在9月11日)
赠书数量:1本 先到先得!
备注:可以选本书作为奖品也可以选择其他图书
更多好书可以来人邮社异步社区查看,申请下期活动:http://www.epubit.com.cn/

第2章 语言模块

1995年,Brendan Eich读完了在程序语言设计中曾经出现过的所有错误,自己又发现了一些更多的错误,然后用它们创造出了LiveScript。之后,为了紧跟Java语言的潮流,它被重新命名为JavaScript。再然后,为了追随一种皮肤病的时髦名字,这个语言又命名为ECMAScript。

上面一段话出自博文《编程语言伪简史》。可见,JavaScript受到了多么辛辣的嘲讽,它在当时是多么不受欢迎。抛开偏见,JavaScript的确有许多不足之处。由于互联网的传播性及浏览器厂商大战,JavaScript之父失去了对此门语言的掌控权。即便他想修复这些bug或推出某些新特性,也要所有浏览器厂商都点头才行。IE6的市场独占性,打破了他的奢望。这个局面直到Chrome诞生,才有所改善。

但在IE6时期,浏览器提供的原生API数量是极其贫乏的,因此各个框架都创造了许多方法来弥补这缺陷。视框架作者原来的语言背景不同,这些方法也是林林总总。其中最杰出的代表是王者Prototype.js,把ruby语言的那一套方式或范式搬过来,从底层促进了JavaScript的发展。ECMA262V6添加那一堆字符串、数组方法,差不多就是改个名字而已。

即便是浏览器的API也不能尽信,尤其是IE6、IE7、IE8到处是bug。早期出现的各种“JS库”,例如远古的prototype、中古的mootools,到近代的jQuery,再到大规模、紧封装的YUI和Extjs,很大的一个目标就是为了填“兼容性”这个“大坑”。

在avalon2中,就提供了许多带compact命名的模块,它们就是专门用于修复古老浏览器的兼容性问题。此外,本章也介绍了一些非常底层的知识点,能让读者更熟悉这门语言。

2.1 字符串的扩展与修复

笔者发现脚本语言都对字符串特别关注,有关它的方法特别多。笔者把这些方法分为三大类,如图2-1所示。

图2-1

显然以前,总是想着通过字符串生成标签,于是诞生了一些方法,如anchor、big、blink、bold、fixed、fontcolor、italics、link、small、strike、sub及sup。

剩下的就是charAt、charCodeAt、concat、indexOf、lastIndexOf、localeCompare、match、replace,search、slice、split、substr、substring、toLocaleLowerCase、toLocaleUpperCase、toLowerCase、toUpperCase及从Object继承回来的方法,如toString、valueOf。

鲜为人知的是,数值的toString有一个参数,通过它可以转换为进行进制的数值,如图 2-2所示。

图2-2

但相对于其他语言,JavaScript的字符串方法可以说是十分贫乏的,因此后来的ES5、ES6又加上了一堆方法。

即便这样,也很难满足开发需求,比如说新增的方法就远水救不了近火。因此各大名库都提供了一大堆操作字符串的方法。我综合一下Prototype、mootools、dojo、EXT、Tangram、RightJS的一些方法,进行比较去重,在mass Framework为字符串添加如下扩展:contains、startsWith、endsWith、repeat、camelize、underscored、capitalize、stripTags、stripScripts、escapeHTML、unescapeHTML、escapeRegExp、truncate、wbr、pad,写框架的读者可以视自己的情况进行增减,如图2-3所示。其中前4个是ECMA262V6的标准方法;接着9个发端于Prototype.js广受欢迎的工具方法;wbr则来自Tangram,用于软换行,这是出于汉语排版的需求。pad也是一个很常用的操作,已被收录,如图2-3所示。

图2-3

到了另一个框架avalon2,笔者的方法也有用武之地,或者改成avalon的静态方法,或者作为ECMA262V6的补丁模块,或者作为过滤器(如camelize、truncate)。

各种方法实现如下。

contains 方法:判定一个字符串是否包含另一个字符串。常规思维是使用正则表达式。但每次都要用new RegExp来构造,性能太差,转而使用原生字符串方法,如indexOf、lastIndexOf、search。

function contains(target, it) {//indexOf改成search,lastIndexOf也行得通return target.indexOf(it) != -1;
}

在Mootools版本中,笔者看到它支持更多参数,估计目的是判定一个元素的className是否包含某个特定的class。众所周知,元素可以添加多个class,中间以空格隔开,使用mootools的contains就能很方便地检测包含关系了。

function contains(target, str, separator) {return separator ?(separator + target + separator).indexOf(separator + str + separator) > -1 :target.indexOf(str) > -1;
}

startsWith方法:判定目标字符串是否位于原字符串的开始之处,可以说是contains方法的变种。

//最后一个参数是忽略大小写
function startsWith(target, str, ignorecase) {var start_str = target.substr(0, str.length);return ignorecase ? start_str.toLowerCase() === str.toLowerCase() :start_str === str;
}

endsWith方法:与startsWith方法相反。

//最后一个参数是忽略大小写
function endsWith(target, str, ignorecase) {var end_str = target.substring(target.length - str.length);return ignorecase ? end_str.toLowerCase() === str.toLowerCase() :end_str === str;
}

2.1.1 repeat

repeat方法:将一个字符串重复自身N次,如repeat(”ruby”, 2)得到rubyruby。

版本1:利用空数组的join方法。

function repeat(target, n) {return (new Array(n + 1)).join(target);
}

版本2:版本1的改良版。创建一个对象,使其拥有length属性,然后利用call方法去调用数组原型的join方法,省去创建数组这一步,性能大为提高。重复次数越多,两者对比越明显。另外,之所以要创建一个带length属性的对象,是因为要调用数组的原型方法,需要指定call的第一个参数为类数组对象,而类数组对象的必要条件是其length属性的值为非负整数。

function repeat(target, n) {return Array.prototype.join.call({length: n + 1}, target);
}

版本3:版本2的改良版。利用闭包将类数组对象与数组原型的join方法缓存起来,避免每次都重复创建与寻找方法。

var repeat = (function() {var join = Array.prototype.join, obj = {};return function(target, n) {obj.length = n + 1;return join.call(obj, target);}
})();

版本 4:从算法上着手,使用二分法,比如我们将ruby重复5次,其实我们在第二次已得到rubyruby,那么第3次直接用rubyruby进行操作,而不是用ruby。

function repeat(target, n) {var s = target, total = [];while (n > 0) {if (n % 2 == 1)total[total.length] = s;//如果是奇数if (n == 1)break;s += s;n = n >> 1;//相当于将n除以2取其商,或说开2二次方}return total.join('');
}

版本5:版本4的变种,免去创建数组与使用jion方法。它的短处在于它在循环中创建的字符串比要求的还长,需要回减一下。

function repeat(target, n) {var s = target, c = s.length * ndo {s += s;} while (n = n >> 1);s = s.substring(0, c);return s;
}

版本6:版本4的改良版。

function repeat(target, n) {var s = target, total = "";while (n > 0) {if (n % 2 == 1)total += s;if (n == 1)break;s += s;n = n >> 1;}return total;
}

版本7:与版本6相近。不过在浏览器下递归好像都做了优化(包括IE6),与其他版本相比,属于上乘方案之一。

function repeat(target, n) {if (n == 1) {return target;}var s = repeat(target, Math.floor(n / 2));s += s;if (n % 2) {s += target;}return s;
}

版本8:可以说是一个反例,很慢,不过实际上它还是可行的,因为实际上没有人将n设成上百成千。

function repeat(target, n) {return (n <= 0) ? "" : target.concat(repeat(target, --n));
}

经测试,版本6在各浏览器的得分是最高的。

2.1.2 byteLen

byteLen方法:取得一个字符串所有字节的长度。这是一个后端过来的方法,如果将一个英文字符插入数据库char、varchar、text类型的字段时占用一个字节,而将一个中文字符插入时占用两个字节。为了避免插入溢出,就需要事先判断字符串的字节长度。在前端,如果我们要用户填写文本,限制字节上的长短,比如发短信,也要用到此方法。随着浏览器普及对二进制的操作,该方法也越来越常用。

版本 1:假设当字符串每个字符的Unicode编码均小于或等于255时,byteLength为字符串长度;再遍历字符串,遇到Unicode编码大于255时,为byteLength补加1。

function byteLen(target) {var byteLength = target.length, i = 0;for (; i < target.length; i++) {if (target.charCodeAt(i) > 255) {byteLength++;}}return byteLength;
}

版本2:使用正则表达式,并支持设置汉字的存储字节数。比如用mysql存储汉字时,是3个字节数。

function byteLen(target, fix) {fix = fix ? fix : 2;var str = new Array(fix + 1).join("-")return target.replace(/[^\x00-\xff]/g, str).length;
}

版本3:来自腾讯的解决方案。腾讯通过多子域名+postMessage+manifest离线proxy页面的方式扩大localStorage的存储空间。在这个过程中,我们需要知道用户已经保存了多少内容,因此就必须编写一个严谨的byteLen方法。

/*** http://www.alloyteam.com/2013/12/js-calculate-the-number-of-bytes-occupied-by-a-string/* 计算字符串所占的内存字节数,默认使用UTF-8的编码方式计算,也可制定为UTF-16* UTF-8 是一种可变长度的 Unicode 编码格式,使用1~4个字节为每个字符编码* * 000000 - 00007F(128个代码)     0zzzzzzz(00-7F)                             1个字节* 000080 - 0007FF(1920个代码)    110yyyyy(C0-DF) 10zzzzzz(80-BF)             2个字节* 000800 - 00D7FF 00E000 - 00FFFF(61440个代码)   1110xxxx(E0-EF) 10yyyyyy 10zzzzzz           3个字节* 010000 - 10FFFF(1048576个代码) 11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz  4个字节* * 注: Unicode在范围 D800-DFFF 中不存在任何字符* {@link <a onclick="javascript:pageTracker._trackPageview('/outgoing/zh.wikipedia. org/wiki/UTF-8');" * href="http://zh.wikipedia.org/wiki/UTF-8">http://zh.wikipedia.org/wiki/UTF-8</a>}* * UTF-16 大部分使用2个字节编码,编码超出 65535 的使用4个字节* 000000 - 00FFFF  2个字节* 010000 - 10FFFF  4个字节* * {@link <a onclick="javascript:pageTracker._trackPageview('/outgoing/zh.wikipedia. org/wiki/UTF-16');" * href="http://zh.wikipedia.org/wiki/UTF-16">http://zh.wikipedia.org/wiki/UTF-16</a>}* @param  {String} str * @param  {String} charset utf-8, utf-16* @return {Number}*/
function byteLen(str, charset){var total = 0,charCode,i,len;charset = charset ? charset.toLowerCase() : '';if(charset === 'utf-16' || charset === 'utf16'){for(i = 0, len = str.length; i < len; i++){charCode = str.charCodeAt(i);if(charCode <= 0xffff){total += 2;}else{total += 4;}}}else{for(i = 0, len = str.length; i < len; i++){charCode = str.charCodeAt(i);if(charCode <= 0x007f) {total += 1;}else if(charCode <= 0x07ff){total += 2;}else if(charCode <= 0xffff){total += 3;}else{total += 4;}}}return total;
}

truncate方法:用于对字符串进行截断处理。当超过限定长度,默认添加3个点号。

function truncate(target, length, truncation) {length = length || 30;truncation = truncation === void(0) ? '...' : truncation;return target.length > length ?target.slice(0, length - truncation.length) + truncation : String(target);
}

camelize方法:转换为驼峰风格。

function camelize(target) {if (target.indexOf('-') < 0 && target.indexOf('_') < 0) {return target;//提前判断,提高getStyle等的效率}return target.replace(/[-_][^-_]/g, function(match) {return match.charAt(1).toUpperCase();});
}

underscored方法:转换为下划线风格。

function underscored(target) {return target.replace(/([a-z\d])([A-Z])/g, '$1_$2').replace(/\-/g, '_').toLowerCase();
}

dasherize方法:转换为连字符风格,即CSS变量的风格。

function dasherize(target) {return underscored(target).replace(/_/g, '-');
}

capitalize方法:首字母大写。

function capitalize(target) {return target.charAt(0).toUpperCase() + target.substring(1).toLowerCase();
}

stripTags 方法:移除字符串中的html标签。比如,我们需要实现一个HTMLParser,这时就要处理option元素的innerText问题。此元素的内部只能接受文本节点,如果用户在里面添加了span、strong等标签,我们就需要用此方法将这些标签移除。在Prototype.js中,它与strip、stripScripts是一组方法。

var rtag = /<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi
function stripTags(target) {return String(target || "").replace(rtag, '');
}

stripScripts 方法:移除字符串中所有的script标签。弥补stripTags方法的缺陷。此方法应在stripTags之前调用。

function stripScripts(target) {return String(target || "").replace(/<script[^>]*>([\S\s]*?)<\/script>/img, '')
}

escapeHTML 方法:将字符串经过html转义得到适合在页面中显示的内容,如将“<”替换为“&lt;”`。此方法用于防止XSS攻击。

    function escapeHTML(target) {return target.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, "&quot;").replace(/'/g, "'");
}

unescapeHTML方法:将字符串中的html实体字符还原为对应字符。

function unescapeHTML(target) {return String(target).replace(/'/g, '\'').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
}

注意一下escapeHTML和unescapeHTML这两个方法,它们不但在replace的参数是反过来的,replace的顺序也是反过来的。它们在做html parser非常有用的。但涉及浏览器,兼容性问题就一定会存在。

在citojs这个库中,有一个类似于escapeHTML的方法叫escapeContent,它是这样写的。

function escapeContent(value) {value = '' + value;if (isWebKit) {helperDiv.innerText = value;value = helperDiv.innerHTML;} else if (isFirefox) {value = value.split('&').join('&amp;').split('<').join('&lt;').split('>'). join('&gt;');} else {value = value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');}return value;}

看情况是处理&amp;时出了分歧。但它们这么做其实也不能处理所有html实体。因此Prototype.js是建议使用原生API innerHTML, innerText来处理。

var div = document.createElement('div')var escapeHTML = function (a) {div.data = areturn div.innerHTML
}var unescapeHTML = function (a) {div.innerHTML = areturn getText(div)//相当于innerText, textContent
}function getText(node) {if (node.nodeType !== 1) {return node.nodeValue} else if (node.nodeName !== 'SCRIPT') {var ret = ''for (var i = 0, el; el = node.childNodes[i++]; ) {ret += getText(el)}} else {return ''}
}

但这样一来,它们就不能运行于Node.js环境中,并且性能也不好,于是人们发展出下面这些库。

https://github.com/mathiasbynens/he
https://github.com/mdevils/node-html-entities

escapeRegExp方法:将字符串安全格式化为正则表达式的源码。

function escapeRegExp(target) {return target.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
}

2.1.3 pad

pad方法:与trim方法相反,pad可以为字符串的某一端添加字符串。常见的用法如日历在月份前补零,因此也被称之为fillZero。笔者在博客上收集许多版本的实现,在这里转换为静态方法一并写出。

版本1:数组法,创建数组来放置填充物,然后再在右边起截取。

function pad(target, n) {var zero = new Array(n).join('0');var str = zero + target;var result = str.substr(-n);return result;
}

版本2:版本1的变种。

function pad(target, n) {return Array((n + 1) - target.toString().split('').length).join('0') + target;
}

版本3:二进制法。前半部分是创建一个含有n个零的大数,如(1<<5).toString(2),生成100000,(1<<8).toString(2)生成100000000,然后再截短。

function pad(target, n) {return (Math.pow(10, n) + "" + target).slice(-n);
}

版本4:Math.pow法,思路同版本3。

function pad(target, n) {return ((1 << n).toString(2) + target).slice(-n);
}

版本5:toFixed法,思路与版本3差不多,创建一个拥有n个零的小数,然后再截短。

function pad(target, n) {return (0..toFixed(n) + target).slice(-n);
}

版本6:创建一个超大数,在常规情况下是截不完的。

function pad(target, n) {return (1e20 + "" + target).slice(-n);
}

版本7:质朴长存法,就是先求得长度,然后一个个地往左边补零,加到长度为n为止。

function pad(target, n) {var len = target.toString().length;while (len < n) {target = "0" + target;len++;}return target;
}

版本8:也就是现在mass Framework使用的版本,支持更多的参数,允许从左或从右填充,以及使用什么内容进行填充。

function pad(target, n, filling, right, radix) {var num = target.toString(radix || 10);filling = filling || "0";while (num.length < n) {if (!right) {num = filling + num;} else {num += filling;}}return num;
}

在ECMA262V7规范中,pad方法也有了对应的代替品——padStart,此外,还有从后面补零的方法——padEnd

https://github.com/es-shims/es7-shim

wbr方法:为目标字符串添加wbr软换行。不过需要注意的是,它并不是在每个字符之后都插入<wbr>字样,而是相当于在组成文本节点的部分中的每个字符后插入<wbr>字样。例如,aa<span> bb</span>cc,返回a<wbr>a<wbr><span>b<wbr>b<wbr></span>c<wbr>c<wbr>。另外,在Opera下,浏览器默认css不会为wbr加上样式,导致没有换行效果,可以在css中加上wbrafter { content: "\00200B" }解决此问题。

function wbr(target) {return String(target).replace(/(?:<[^>]+>)|(?:&#?[0-9a-z]{2,6};)|(.{1})/gi, '$&<wbr>').replace(/><wbr>/g, '>');
}

format方法:在C语言中,有一个叫printf的方法,我们可以在后面添加不同类型的参数嵌入到将要输出的字符串中。这是非常有用的方法,因为JavaScript涉及大量的字符串拼接工作。如果涉及逻辑,我们可以用模板;如果轻量点,我们可以用这个方法。它在不同框架中名字是不同的,Prototype.js叫interpolate;Base2叫format;mootools叫substitute

function format(str, object) {var array = Array.prototype.slice.call(arguments, 1);return str.replace(/\\?\#{([^{}]+)\}/gm, function(match, name) {if (match.charAt(0) == '\\')return match.slice(1);var index = Number(name)if (index >= 0)return array[index];if (object && object[name] !== void 0)return  object[name];return  '';});
}

format方法支持两种传参方法,如果字符串的占位符为0、1、2这样的非零整数形式,要求传入两个或两个以上的参数,否则就传入一个对象,键名为占位符。

var a = format("Result is #{0},#{1}", 22, 33);
alert(a);//"Result is 22,33"
var b = format("#{name} is a #{sex}", {name: "Jhon",sex: "man"
});
alert(b);//"Jhon is a man"

2.1.4 quote

quote 方法:在字符串两端添加双引号,然后内部需要转义的地方都要转义,用于接装JSON的键名或模板系统中。

版本1:来自JSON3。

//avalon2
//https://github.com/bestiejs/json3/blob/master/lib/json3.js
var Escapes = {92: "\\\\",34: '\\"',8: "\\b",12: "\\f",10: "\\n",13: "\\r",9: "\\t"
}// Internal: Converts 'value' into a zero-padded string such that its
// length is at least equal to 'width'. The 'width' must be <= 6.
var leadingZeroes = "000000"
var toPaddedString = function (width, value) {// The '|| 0' expression is necessary to work around a bug in// Opera <= 7.54u2 where '0 == -0', but 'String(-0) !== "0"'.return (leadingZeroes + (value || 0)).slice(-width)
};
var unicodePrefix = "\\u00"
var escapeChar = function (character) {var charCode = character.charCodeAt(0), escaped = Escapes[charCode]if (escaped) {return escaped}return unicodePrefix + toPaddedString(2, charCode.toString(16))
};
var reEscape = /[\x00-\x1f\x22\x5c]/g
function quote(value) {reEscape.lastIndex = 0return '"' + ( reEscape.test(value)? String(value).replace(reEscape, escapeChar) : value ) + '"'
}avalon.quote = typeof JSON !== 'undefined' ? JSON.stringify : quote

版本2:来自百度的etpl模板库。

//https://github.com/ecomfe/etpl/blob/2.1.0/src/main.js#L207
function stringLiteralize(source) {return '"'+ source.replace(/\x5C/g, '\\\\').replace(/"/g, '\\"').replace(/\x0A/g, '\\n').replace(/\x09/g, '\\t').replace(/\x0D/g, '\\r')+ '"';
}

当然,如果浏览器已经支持原生JSON,我们直接用JSON.stringify就行了。另外,FF在JSON发明之前,就支持String.prototype.quote与String.quote方法,我们在使用quote之前需要判定浏览器是否内置这些方法。

接下来,我们来修复字符串的一些bug。字符串相对其他基础类型,没有太多bug,主要是3个问题。

(1)IE6、IE7不支持用数组中括号取它的每一个字符,需要用charAt来取。

(2)IE6、IE7、IE8不支持垂直分表符,于是诞生了var isIE678= !+"\v1"这个伟大的判定hack。

(3)IE对空白的理解与其他浏览器不一样,因此实现trim方法会有一些不同。

前两个问题只能回避,我们重点研究第3个问题,也就是如何实现trim方法。由于太常用,所以相应的实现也非常多。我们可以一起看看,顺便学习一下正则。

2.1.5 trim与空白

版本1:虽然看起来不怎么样,但是动用了两次正则替换,实际速度非常惊人,这主要得益于浏览器的内部优化。base2类库使用这种实现。在Chrome刚出来的年代,这实现是异常快的,但chrome对字符串方法的疯狂优化,引起了其他浏览器的跟风。于是正则的实现再也比不了字符串方法了。一个著名的字符串拼接例子,直接相加比用Array做成的StringBuffer还快,而StringBuffer技术在早些年备受推崇!

function trim(str) {return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
……

版本2:和版本1很相似,但稍慢一点,主要原因是它最先是假设至少存在一个空白符。Prototype.js使用这种实现,不过其名字为strip,因为Prototype的方法都是力求与Ruby同名。

<div class="se-preview-section-delimiter"></div>…javascript
function trim(str) {return str.replace(/^\s+/, '').replace(/\s+$/, '');
}

版本 3:截取方式取得空白部分(当然允许中间存在空白符),总共调用了 4 个原生方法。设计非常巧妙,substring以两个数字作为参数。Math.max以两个数字作参数,search则返回一个数字。速度比上面两个慢一点,但基本比10之前的版本快!

function trim(str) {return str.substring(Math.max(str.search(/\S/), 0),str.search(/\S\s*$/) + 1);
}

版本4:这个可以称得上版本2的简化版,就是利用候选操作符连接两个正则。但这样做就失去了浏览器优化的机会,比不上版本3。由于看来很优雅,许多类库都使用它,如jQuery与Mootools。

function trim (str) {return str.replace(/^\s+|\s+$/g, '');
}

版本 5:match 如果能匹配到东西会返回一个类数组对象,原字符匹配部分与分组将成为它的元素。为了防止字符串中间的空白符被排除,我们需要动用到非捕获性分组(?:exp)。由于数组可能为空,我们在后面还要做进一步的判定。好像浏览器在处理分组上比较无力,一个字慢。所以不要迷信正则,虽然它基本上是万能的。

function trim(str) {str = str.match(/\S+(?:\s+\S+)*/);return str ? str[0] : '';
}

版本6:把符合要求的部分提供出来,放到一个空字符串中。不过效率很差,尤其是在IE6中。

function trim(str) {return str.replace(/^\s*(\S*(\s+\S+)*)\s*$/, '$1');
}

版本7:与版本6很相似,但用了非捕获分组进行了优点,性能较之有一点点提升。

function trim(str) {return str.replace(/^\s*(\S*(?:\s+\S+)*)\s*$/, '$1');
}

版本8:沿着上面两个的思路进行改进,动用了非捕获分组与字符集合,用“?”顶替了“*”,效果非常惊人。尤其在IE6中,可以用疯狂来形容这次性能的提升,直接秒杀FF3。

function trim(str) {return str.replace(/^\s*((?:[\S\s]*\S)?)\s*$/, '$1');
}

版本9:这次是用懒惰匹配顶替非捕获分组,在火狐中得到改善,IE没有上次那么疯狂。

function trim(str) {return str.replace(/^\s*([\S\s]*?)\s*$/, '$1');
}

版本 10:笔者只想说,搞出这个的人已经不能用厉害来形容,而是专家级别了。它先是把可能的空白符全部列出来,在第一次遍历中砍掉前面的空白,第二次砍掉后面的空白。全过程只用了indexOf与substring这个专门为处理字符串而生的原生方法,没有使用到正则。速度快得惊人,估计直逼内部的二进制实现,并且在IE与火狐(其他浏览器当然也毫无疑问)都有良好的表现,速度都是零毫秒级别的,PHP.js就收纳了这个方法。

Function trim(str) {
var whitespace = ' \n\r\t\f\x0b\xa0\u2000\u2001\u2002\u2003\n\\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000';
for (var I = 0; I < str.length; I++) {if (whitespace.indexOf(str.charAt(i)) === -1) {
str = str.substring(i);
break;}
}
for (I = str.length – 1; I >= 0; I--) {if (whitespace.indexOf(str.charAt(i)) === -1) {
str = str.substring(0, I + 1);
break;}
}
return whitespace.indexOf(str.charAt(0)) === -1 ? str : ‘’;
}

版本 11:实现10的字数压缩版,前面部分的空白由正则替换负责砍掉,后面用原生方法处理,效果不逊于原版,但速度都非常逆天。

Function trim(str) {
str = str.replace(/^\s+/, '');
for (var I = str.length – 1; I >= 0; I--) {if (/\S/.test(str.charAt(i))) {
str = str.substring(0, I + 1);
break;}
}
return str;
}

版本12:版本10更好的改进版,注意说的不是性能速度,而是易记与使用方面。

Function trim(str) {
var m = str.length;
for (var I = -1; str.charCodeAt(++I) <= 32; )
for (var j = m – 1; j > I && str.charCodeAt(j) <= 32; j--)
return str.slice(I, j + 1);
}

但这还没有完。如果你经常翻看jQuery的实现,你就会发现jQuery1.4之后的trim实现,多出了一个对xA0的特别处理。这是Prototype.js的核心成员·kangax的发现,IE或早期的标准浏览器在字符串的处理上都有bug,把许多本属于空白的字符没有列为\s,jQuery在1.42中也不过把常见的不断行空白xA0修复掉,并不完整,因此最佳方案还是版本10。

// Make sure we trim BOM and NBSP
var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
jQuery.trim = function( text ) {return text == null ?
"" :
( text + "" ).replace( rtrim, "" );
}

下面是一个比较晦涩的知识点——空白字符。根据屈屈的博文[1],浏览器会把WhiteSpace和LineTerminator都列入空白字符。Ecma262 v5文档规定的WhiteSpace,如表2-1所示。

表2-1

Unicode编码

说明

U+0020

” ” “\x20”, “\u0020”, <SP>半角空格符,键盘空格键

U+0009

“\t”, “\x09”, “\u0009”, <TAB>制表符,键盘tab键

U+000B

“\v”, “\x0B”, “\u000B”,<VT>垂直制表符

U+000C

“\f”, “\x0C”, “\u000C”,<FF>换页符

U+000D

“\r”, “\x0D”, “\u000D”,<CR>回车符

U+000A

“\n”, “\x0A”, “\u000A”,<LF>换行符

U+00A0

“\xA0”, “\u00A0”,<NBSP>禁止自动换行空格符

U+1680

OGHAM SPACE MARK,欧甘空格

U+180E

Mongolian Vowel Separator,蒙古文元音分隔符

U+2000

EN QUAD

U+2001

EM QUAD

U+2002

EN SPACE,En空格。与En同宽(Em的1/2)

U+2003

EM SPACE,Em空格。与Em同宽

U+2004

THREE-PER-EM SPACE,Em 1/3空格

U+2005

FOUR-PER-EM SPACE,Em 1/4空格

U+2006

SIX-PER-EM SPACE,Em 1/6空格

U+2007

FIGURE SPACE,数字空格。与单一数字同宽

U+2008

PUNCTUATION SPACE,标点空格。与同字体窄标点同宽

U+2009

THIN SPACE,窄空格。Em 1/6或1/5宽

U+200A

HAIR SPACE,更窄空格。比窄空格更窄

U+200B

Zero Width Space,<ZWSP>,零宽空格

U+200C

Zero Width Non Joiner,<ZWNJ>,零宽不连字空格

U+200D

Zero Width Joiner,<ZWJ>,零宽连字空格

U+202F

NARROW NO-BREAK SPACE,窄式不换行空格

U+2028

<LS>行分隔符

U+2029

<PS>段落分隔符

U+205F

中数学空格。用于数学方程式

U+2060

Word Joiner,同U+200B,但该处不换行。Unicode 3.2新增,代替U+FEFF

U+3000

IDEOGRAPHIC SPACE,<CJK>,表意文字空格,即全角空格

U+FEFF

Byte Order Mark,<BOM>,字节次序标记字符。不换行功能于Unicode 3.2起废止

2.2 数组的扩展与修复

得益于Prototype.js的ruby式数组方法的侵略,让Jser()前端工程师大开眼界,原来对数组的操作也如此丰富多彩。原来JavaScript的数组方法就是基于栈与队列的那一套,像splice还是很晚加入的。让我们回顾一下它们的用法,如图2-4所示。

图2-4

  • pop方法:出栈操作,删除并返回数组的最后一个元素。
  • push方法:入栈操作,向数组的末尾添加一个或更多元素,并返回新的长度。
  • shift方法:出队操作,删除并返回数组的第一个元素。
  • unshift方法:入队操作,向数组的开头添加一个或更多元素,并返回新的长度。
  • slice方法:切片操作,从数组中分离出一个子数组,功能类似于字符串的。

substring、slice和substr是“三兄弟”,常用于转换类数组对象为真正的数组。

  • sort方法:对数组的元素进行排序,有一个可选参数,为比较函数。
  • reverse方法:颠倒数组中元素的顺序。
  • splice方法:可以同时用于原数组的增删操作,数组的remove方法就是基于它写成的。
  • concat方法:用于把原数组与参数合并成一个新数组,如果参数为数组,那么它会把其第一维的元素放入新数组中。因此我们可以利用它实现数组的平坦化操作与克隆操作。
  • join方法:把数组的所有元素放入一个字符串,元素通过指定的分隔符进行分隔。你可以想象成字符串split的反操作。
  • indexOf方法:定位操作,返回数组中第一个等于给定参数的元素的索引值。
  • lastIndexOf方法:定位操作,同上,不过是从后遍历。索引操作可以说是字符串同名方法的翻版,存在就返回非负整数,不存在就返回−1。
  • forEach方法:迭代操作,将数组的元素依次传入一个函数中执行。Ptototype.js中对应的名字为each。
  • map方法:收集操作,将数组的元素依次传入一个函数中执行,然后把它们的返回值组成一个新数组返回。Ptototype.js中对应的名字为collect。
  • filter方法:过滤操作,将数组的元素依次传入一个函数中执行,然后把返回值为true的那个元素放入新数组返回。在Prototype.js中,它有3个名字,即select、filter和findAll。
  • some方法:只要数组中有一个元素满足条件(放进给定函数返回true),那么它就返回true。Ptototype.js中对应的名字为any。
  • every方法:只有数组中所有元素都满足条件(放进给定函数返回true),它才返回true。Ptototype.js中对应的名字为all。
  • reduce方法:归化操作,将数组中的元素归化为一个简单的数值。Ptototype.js中对应的名字为inject。
  • reduceRight方法:归化操作,同上,不过是从后遍历。

为了方便大家记忆,我们可以用图2-5搞懂数组的18种操作。

图2-5

由于许多扩展也基于这些新的标准化方法,因此笔者先给出IE6、IE7、IE8的兼容方案,全部在数组原型上修复它们。

[1, 2, , 4].forEach(function(e){console.log(e)
});
//依次打印出1,2,4,忽略第2、第3个逗号间的空元素

reduce与reduceRight是一组,我们可以利用reduce方法创建reduceRight方法。

ap.reduce = function(fn, lastResult, scope) {if (this.length == 0)return lastResult;var i = lastResult !== undefined ? 0 : 1;var result = lastResult !== undefined ? lastResult : this[0];for (var n = this.length; i < n; i++)result = fn.call(scope, result, this[i], i, this);return result;
}ap.reduceRight = function(fn, lastResult, scope) {var array = this.concat().reverse();return array.reduce(fn, lastResult, scope);
}

接下来,我们看看主流库为数组增加了哪些扩展吧。

Prototype.js的数组扩展:eachSlice、detect、grep、include、inGroupsOf、invoke、max、min、partition、pluck、reject、sortBy、zip、size、clear、first、last、compact、flatten、without、uniq、intersect、clone、inspect。

Rightjs的数组扩展:include、clean、clone、compact、empty、first、flatten、includes、last、max、merge、min、random、reject、shuffle、size、sortBy、sum、uniq、walk、without。

mootools的数组扩展:clean、invoke、associate、link、contains、append、getLast、getRandom、include、combine、erase、empty、flatten、pick、hexToRgb、rgbToHex。

EXT的数组扩展:contains、pluck、clean、unique、from、remove、include、clone、merge、intersect、difference、flatten、min、max、mean、sum、erase、insert。

Underscore.js的数组扩展:detect、reject、invoke、pluck、sortBy、groupBy、sortedIndex、first、last、compact、flatten、without、union、intersection、difference、uniq、zip。

qooxdoo的数组扩展:insertAfter、insertAt、insertBefore、max、min、remove、removeAll、removeAt、sum、unique。

Tangram的数组扩展:contains、empty、find、remove、removeAt、unique。

我们可以发现,Prototype.js那一套方法影响深远,许多库都有它的影子,全面而细节地囊括了各种操作,大家可以根据自己的需要与框架宗旨制订自己的数组扩展。笔者在这方面的考量如下,至少要包含平坦化、去重、乱序、移除这几个操作,其次是两个集合间的操作,如取并集、差集、交集。

下面是各种具体实现。

contains方法:判定数组是否包含指定目标。

function contains(target, item) {return target.indexOf(item) > -1
}

removeAt方法:移除数组中指定位置的元素,返回布尔值表示成功与否。

function removeAt(target, index) {return !!target.splice(index, 1).length
}

remove方法:移除数组中第一个匹配传参的那个元素,返回布尔值表示成功与否。

function remove(target, item) {var index = target.indexOf(item);if (~index)return removeAt(target, index);return false;
}

shuffle 方法:对数组进行洗牌。若不想影响原数组,可以先复制一份出来操作。有关洗牌算法的介绍,可见下面两篇博文。

《Fisher-Yates Shuffle》

《数组的完全随机排列》

function shuffle(target) {var j, x, i = target.length;for (; i > 0; j = parseInt(Math.random() * i),x = target[--i], target[i] = target[j], target[j] = x) {}return target;
}

random方法:从数组中随机抽选一个元素出来。

function random(target) {return  target[Math.floor(Math.random() * target.length)];
}

flatten方法:对数组进行平坦化处理,返回一个一维的新数组。

function flatten(target) {var result = [];target.forEach(function(item) {if (Array.isArray(item)) {result = result.concat(flatten(item));} else {result.push(item);}});return result;
}

unique方法:对数组进行去重操作,返回一个没有重复元素的新数组。

function unique(target) {var result = [];loop:  for (var i = 0, n = target.length; i < n; i++) {for (var x = i + 1; x < n; x++) {if (target[x] === target[i])continue loop;}result.push(target[i]);}return result;
}

compact方法:过滤数组中的null与undefined,但不影响原数组。

function compact(target) {return target.filter(function(el) {return el != null;});
}

pluck方法:取得对象数组的每个元素的指定属性,组成数组返回。

function pluck(target, name) {var result = [], prop;target.forEach(function(item) {prop = item[name];if (prop != null)result.push(prop);});return result;
}

groupBy方法:根据指定条件(如回调对象的某个属性)进行分组,构成对象返回。

function groupBy(target, val) {var result = {};var iterator = $.isFunction(val) ? val : function(obj) {return obj[val];};target.forEach(function(value, index) {var key = iterator(value, index);(result[key] || (result[key] = [])).push(value);});return result;
}

sortBy方法:根据指定条件进行排序,通常用于对象数组。

function sortBy(target, fn, scope) {var array = target.map(function(item, index) {return {el: item,re: fn.call(scope, item, index)};}).sort(function(left, right) {var a = left.re, b = right.re;return a < b ? -1 : a > b ? 1 : 0;});return pluck(array, 'el');
}

union方法:对两个数组取并集。

function union(target, array) {return unique(target.concat(array));
}

intersect方法:对两个数组取交集。

function intersect(target, array) {return target.filter(function(n) {return ~array.indexOf(n);});
}

diff方法:对两个数组取差集(补集)。

function diff(target, array) {var result = target.slice();for (var i = 0; i < result.length; i++) {for (var j = 0; j < array.length; j++) {if (result[i] === array[j]) {result.splice(i, 1);i--;break;}}}return result;
}

min方法:返回数组中的最小值,用于数字数组。

function min(target) {return Math.min.apply(0, target);
}

max方法:返回数组中的最大值,用于数字数组。

function max(target) {return Math.max.apply(0, target);
}

基本上就这么多了,如果你想实现sum方法,可以使用reduce方法。我们再来抹平Array原生方法在各浏览器的差异,一个是IE6、IE7下unshift不返回数组长度的问题,一个splice的参数问题。unshift的bug很容易修复,可以使用函数劫持方式搞定。

if ([].unshift(1) !== 1) {var _unshift = Array.prototype.unshift;Array.prototype.unshift = function() {_unshift.apply(this, arguments);return this.length; //返回新数组的长度}
}

splice在一个参数的情况下,IE6、IE7、IE8默认第二个参数为零,其他浏览器为数组的长度,当然我们要以标准浏览器为准!

下面是最简单的修复方法。

if ([1, 2, 3].splice(1).length == 0) {
//如果是IE6、IE7、IE8,则一个元素也没有删除var _splice = Array.prototype.splice;Array.prototype.splice = function(a) {if (arguments.length == 1) {return _splice.call(this, a, this.length)} else {return _splice.apply(this, arguments)}}
}

下面是不利用任何原生方法的修复方法。

Array.prototype.splice = function(s, d) {var max = Math.max, min = Math.min,a = [], i = max(arguments.length - 2, 0),k = 0, l = this.length, e, n, v, x;s = s || 0;if (s < 0) {s += l;}s = max(min(s, l), 0);d = max(min(isNumber(d) ? d : l, l - s), 0);v = i - d;n = l + v;while (k < d) {e = this[s + k];if (e !== void 0) {a[k] = e;}k += 1;}x = l - s - d;if (v < 0) {k = s + i;while (x) {this[k] = this[k - v];k += 1;x -= 1;}this.length = n;} else if (v > 0) {k = 1;while (x) {this[n - k] = this[l - k];k += 1;x -= 1;}}for (k = 0; k < i; ++k) {this[s + k] = arguments[k + 2];}return a;
}

一旦有了splice方法,我们也可以自行实现pop、push、shift、unshift方法,因此你明白为什么这几个方法是直接修改原数组了吧?浏览器厂商的思路与我们一样,大概也是用splice方法来实现它们!

var ap = Array.prototype
var _slice = sp.slice;
ap.pop = function() {return this.splice(this.length - 1, 1)[0];
}ap.push = function() {this.splice.apply(this,[this.length, 0].concat(_slice.call(arguments)));return this.length;
}ap.shift = function() {return this.splice(0, 1)[0];
}ap.unshift = function() {this.splice.apply(this,[0, 0].concat(_slice.call(arguments)));return this.length;
}

数组的空位

上面是一个forEach例子的演示,实质上我们通过修复原型方法的手段很难达到ecmascript规范的效果。缘故在于数组的空位,它在JavaScript的各个版本中都不一致。

数组的空位是指数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

Array(3) // [, , ,]

上面的代码中,Array(3)返回一个具有3个空位的数组。

注意,空位不是undefined,而是一个位置的值等于undefined,但依然是有值的。空位是没有任何值,in运算符可以说明这一点。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

上面的代码说明,第一个数组的0号位置是有值的,第二个数组的0号位置是没有值的。

ECMA262V5对空位的处理,已经很不一致了,大多数情况下会忽略空位。比如,forEach()、filter()、every()和some()都会跳过空位;map()会跳过空位,但会保留这个值;join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。

[,'a'].forEach((x,i) => log(i)); // 1
['a',,'b'].filter(x => true) // ['a','b']
[,'a'].every(x => x==='a') // true
[,'a'].some(x => x !== 'a') // false
[,'a'].map(x => 1) // [,1]
[,'a',undefined,null].join('#') // "#a##"
[,'a',undefined,null].toString() // ",a,,"

ECMA262V6则是明确将空位转为undefined。比如,Array.from方法会将数组的空位转为undefined,也就是说,这个方法不会忽略空位。

Array.from(['a',,'b']) // [ "a", undefined, "b" ]

扩展运算符(…)也会将空位转为undefined。

[...['a',,'b']] // [ "a", undefined, "b" ]

copyWithin()会连空位一起拷贝。

[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

fill()会将空位视为正常的数组位置。

new Array(3).fill('a') // ["a","a","a"]

for…of循环也会遍历空位。

let arr = [, ,];
for (let i of arr) {   console.log(1); }
// 1
// 1

上面的代码中,数组arr有两个空位,for…of并没有忽略它们。如果改成map方法遍历,那么空位是会跳过的。

entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。

[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
[...[,'a'].keys()] // [0,1]
[...[,'a'].values()] // [undefined,"a"]
[,'a'].find(x => true) // undefined
[,'a'].findIndex(x => true) // 0

由于空位的处理规则非常不统一,所以建议避免出现空位

2.3 数值的扩展与修复

数值没有什么好扩展的,而且JavaScript的数值精度问题未修复,要修复它们可不是一两行代码了事。先看扩展,我们只把目光集中于Prototype.js与mootools就行了。

Prototype.js为它添加8个原型方法:Succ是加1;times是将回调重复执行指定次数toPaddingString与上面提到字符串扩展方法pad作用一样;toColorPart是转十六进制;abs、ceil、floor和abs是从Math中偷来的。

mootools的情况:limit是从数值限定在一个闭开间中,如果大于或小于其边界,则等于其最大值或最小值;times与Prototype.js的用法相似;round是Math.round的增强版,添加了精度控制;toFloat、toInt是从window中偷来的;其他的则是从Math中偷来的。

在ES5_shim.js库中,它实现了ECMA262V5提到的一个内部方法toInteger。

// http://es5.github.com/#x9.4
// http://jsperf.com/to-integer
var toInteger = function(n) {n = +n;if (n !== n) { // isNaNn = 0;} else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {n = (n > 0 || -1) * Math.floor(Math.abs(n));}return n;
};

但依我看来都没什么意义,数值往往来自用户输入,我们一个正则就能判定它是不是一个“数”。如果是,则直接Number(n)!

基于同样的理由,mass Framework对数字的扩展也是很少的,3个独立的扩展。

limit 方法:确保数值在[n1,n2]闭区间之内,如果超出限界,则置换为离它最近的最大值或最小值。

function limit(target, n1, n2) {var a = [n1, n2].sort();if (target < a[0])target = a[0];if (target > a[1])target = a[1];return target;
}

nearer方法:求出距离指定数值最近的那个数。

function nearer(target, n1, n2) {var diff1 = Math.abs(target - n1),diff2 = Math.abs(target - n2);return diff1 < diff2 ? n1 : n2
}

Number下唯一需要修复的方法是toFixed,它是用于校正精确度,最后的数会做四舍五入操作,但在一些浏览器中并没有这样干。想简单修复的可以这样处理。

if (0.9.toFixed(0) !== '1') {Number.prototype.toFixed = function(n) {var power = Math.pow(10, n);var fixed = (Math.round(this * power) / power).toString();if (n == 0)return fixed;if (fixed.indexOf('.') < 0)fixed += '.';var padding = n + 1 - (fixed.length - fixed.indexOf('.'));for (var i = 0; i < padding; i++)fixed += '0';return fixed;};
}

追求完美的话,还存在这样一个版本,把里面的加、减、乘、除都重新实现了一遍。

https://github.com/es-shims/es5-shim/blob/master/es5-shim.js

toFixed方法实现得如此艰难其实也不能怪浏览器,计算机所理解的数字与我们是不一样的。众所周知,计算机的世界是二进制,数字也不例外。为了储存更复杂的结构,需要用到更高维的进制。而进制间的换算是存在误差的。虽然计算机在一定程度上反映了现实世界,但它提供的顶多只是一个“幻影”,经常与我们的常识产生偏差。比如,将1除以3,然后再乘以3,最后得到的值竟然不是1;10个0.1相加也不等于1;交换相加的几个数的顺序,却得到了不同的和。JavaScript不能免俗。

console.log(0.1 + 0.2)
console.log(Math.pow(2, 53) === Math.pow(2, 53) + 1) //true
console.log(Infinity > 100) //true
console.log(JSON.stringify(25001509088465005)) //25001509088465004
console.log(0.1000000000000000000000000001) //0.1
console.log(0.100000000000000000000000001) //0.1
console.log(0.1000000000000000000000000456) //0.1
console.log(0.09999999999999999999999) //0.1
console.log(1 / 3) //0.3333333333333333
console.log(23.53 + 5.88 + 17.64)// 47.05
console.log(23.53 + 17.64 + 5.88)// 47.050000000000004

这些其实不是bug,而是我们无法接受这事实。在JavaScript中,数值有3种保存方式。

(1)字符串形式的数值内容。

(2)IEEE 754标准双精度浮点数,它最多支持小数点后带15~17位小数,由于存在二进制和十进制的转换问题,具体的位数会发生变化。

(3)一种类似于C语言的int类型的32位整数,它由4个8 bit的字节构成,可以保存较小的整数。

当JavaScript遇到一个数值时,它会首先尝试按整数来处理该数值,如果行得通,则把数值保存为31 bit的整数;如果该数值不能视为整数,或超出31 bit的范围,则把数值保存为64位的IEEE 754浮点数。

聪明的读者一定想到了这样一个问题:什么时候规规矩矩的整数会突然变成捉摸不定的双精度浮点数?答案是:当它们的值变得非常庞大时,或者进入1和0之间时,规矩矩矩的整数就会变成捉摸不定的双精度浮点数。因此,我们需要注意以下数值。

首先是1和0;其次是最大的Unicode数值1114111(7位数字,相当于(/x41777777);最大的RGB颜色值16777215(8位数字,相当于#FFFFFF);最大的32 bit整数是147483647(10位数字,即Math.pow(2,31)-1``);最少的32位bit整数 -2147483648,因为JavaScript内部会以整数的形式保存所有Unicode值和RGB颜色;再次是2147483647,任何大于该值的数据将保存为双精度格式;最大的浮点数9007199254740992(16位数字,即Math.pow(2,53)),因为输出时类似整数,而所有Date对象(按毫秒计算)都小于该值,因此总是模拟整数的格式输出;最大的双精度数值1.7976931348623157e+308,超出这个范围就要算作无穷大了。

因此,我们就看出缘由了,大数相加出问题是由于精度的不足,小数相加出问题是进制转算时产生误差。第一个好理解,第二个,主要是我们常用的十进制转换为二进制时,变成循环小数及无理数等有无限多位小数的数,计算机要用有限位数的浮点数来表示是无法实现的,只能从某一位进行截短。而且,因为内部表示是二进制,十进制看起来是能除尽的数,往往在二进制是循环小数。

比如用二进制来表示十进制的0.1,就得写成2的幂(因为小于1,所以幂是负数)相加的形式。若一直持续下去,0.1就成了0.000110011001100110011…这种循环小数。在有效数字的范围内进行舍入,就会产生误差。

综上,我们就尽量避免小数操作与大数操作,或者转交后台去处理,实在避免不了就引入专业的库来处理。

2.4 函数的扩展与修复

ECMA262V5对函数唯一的扩展就是bind函数。众所周知,这是来自Prototype.js,此外,其他重要的函数都来自Prototype.js。

Prototype.js的函数扩展包括以下几种方法。

  • argumentNames:取得函数的形参,以字符串数组形式返回。未来的Angular.js也是通过此方法实现函数编译与DI(依赖注入)。
  • bind:劫持this,并预先添加更多参数。
  • bindAsEventListener:如bind相似,但强制返回函数的第一个参数为事件对象,这是用于修复IE的多投事件API与标准API的差异。
  • curry:函数柯里化,用于一个操作分成多步进行,并可以改变原函数的行为。
  • wrap:AOP的实现。
  • delay:setTimeout的“偷懒”写法。
  • defer:强制延迟0.01s才执行原函数。
  • methodize:将一个函数变成其调用对象的方法,这也是为其类工厂的方法链服务。

这些方法每一个都是别具匠心,影响深远。

我们先看bind方法,它用到了著名的闭包。所谓闭包,就是一个引用着外部变量的内部函数。比如下面这段代码。

var observable = function(val) {var cur = val;//一个内部变量function field(neo) {if (arguments.length) {//setterif (cur !== neo) {cur = neo;}} else {//getterreturn cur;}}field();return field;
}

上面代码里面的field函数将与外部的cur构成一个闭包。Prototype.js中的bind方法只要依仗原函数与经过切片化的args构成闭包,而让这方法名符其实的是curry,用户最初的传参,劫持到返回函数修正this的指向。

Function.prototype.bind = function(context) {if (arguments.length < 2 && context == void 0)return this;var __method = this, args = [].slice.call(arguments, 1);return function() {return __method.apply(context, args.concat.apply(args, arguments));}
}

正因为有这东西,我们才方便修复IE多投事件API和attachEvent回调中的this问题,它总是指向window对象,而标准浏览器的addEventListener中的this则为其调用对象。

var addEvent = document.addEventListener ?function(el, type, fn, capture) {el.addEventListener(type, fn, capture)} :function(el, type, fn) {el.attachEvent("on" + type, fn.bind(el, event))}

ECMA262V5对其认证后,唯一的增强是对调用者进行检测,确保它是一个函数。顺便总结一下。

(1)call是obj.method(a,b,c)到method(obj,a,b,c)的变换。

(2)apply是obj.method(a,b,c)到method(obj, [a,b,c])的变换,它要求第2个参数必须存在,一定是数组或Arguments这样的类数组,NodeList这样具有争议性的内容就不要乱传进去了。因此jQuery对两个数组或类数组的合并是使用jQuery.merge,放弃使用Array.prototype.push.apply。

(3)bind就是apply的变种,它可以劫持this对象,并且预先注入参数,返回后续执行方法。

这3个方法是非常有用,我们可以设法将它们“偷”出来。

var bind = function(bind) {return{bind: bind.bind(bind),call: bind.bind(bind.call),apply: bind.bind(bind.apply)}
}(Function.prototype.bind)

那怎么用它们呢?比如我们想合并两个数组,直接调用concat,方法如下。

var a = [1, [2, 3], 4];
var b = [5,6];
console.log(b.concat(a)); //[5,6,1,[2,3],4]

使用bind.bind方法则能将它们进一步平坦化。

var concat = bind.apply([].concat);
console.log(concat(b, a)); //[1,3,1,2,3,4]

又如切片化操作,它经常用于转换类数组对象为纯数组的。

var slice = bind([].slice)
var array = slice({0: "aaa",1: "bbb",2: "ccc",length: 3
});
console.log(array)//[ "aaa", "bbb", "ccc"]

更常用的操作是转换arguments对象,目的是为了使用数组的一系列方法。

function test() {var args = slice(arguments)console.log(args)//[1,2,3,4,5]
}
test(1, 2, 3, 4, 5)

我们可以将hasOwnProperty提取出来,判定对象是否在本地就拥有某属性。

var hasOwn = bind.call(Object.prototype.hasOwnProperty);
hasOwn({a:1}, "a") // true
hasOwn({a:1}, "b") // false

使用bind.bind就需要多执行一次。

var hasOwn2 = bind.bind(Object.prototype.hasOwnProperty);
hasOwn2({a:1}, "b")() // false

上面bind.bind的行为其实就是一种curry,它给了你再一次传参的机会,这样你就可以在内部判定参数的个数,决定继续返回函数还是结果。这在设计计算器的连续运算上非常有用。从这个角度来看,我们可以得到一个信息,bind着重于作用域的劫持,curry在于参数的不断补充。

我们可以编写一个 curry,当所有步骤输入的参数个数等于最初定义的函数的形参个数时,就执行它。

function curry(fn) {function inner(len, arg) {if (len == 0)return fn.apply(null, arg);return function(x) {return inner(len - 1, arg.concat(x));};}return inner(fn.length, []);
}function sum(x, y, z, w) {return x + y + z + w;
}
curry(sum)('a')('b')('c')('d'); // => 'abcd'

不过这里我们假定用户每次都只传入一个参数,所以我们可以改进一下。

function curry2(fn) {function inner(len, arg) {if (len <= 0)return fn.apply(null, arg);return function() {return inner(len - arguments.length,arg.concat(Array.apply([], arguments)));};}return inner(fn.length, []);
}

这样就可以在中途传递多个参数,或不传递参数。

curry2(sum)('a')('b', 'c')('d'); // => 'abcd'
curry2(sum)('a')()('b', 'c')()('d'); // => 'abcd'

不过,上面的函数形式有个更帅气的名称,叫self-curryrecurry。它强调的是递归调用自身来补全参数。

与curry相似的是partial。curry的不足是参数总是通过push的方式来补全,而partial则是在定义时所有参数已经都有了,但某些位置上的参数只是个占位符,我们接下来的传参只是替换掉它们。博客上有篇文章《Partial Application in JavaScript》专门介绍了这个内容。

Function.prototype.partial = function() {var fn = this, args = Array.prototype.slice.call(arguments);return function() {var arg = 0;for (var i = 0; i < args.length && arg < arguments.length; i++)if (args[i] === undefined)args[i] = arguments[arg++];return fn.apply(this, args);};
}

它是使用undefined作为占位符。

var delay = setTimeout.partial(undefined, 10);
//接下来的工作就是代替掉第一个参数
delay(function() {alert("this call to will be temporarily delayed.");
})

有关这个占位符,该博客的评论列表中也有大量的讨论,最后确定下来是使用_作为变量名,内部还是指向undefined。笔者认为这样做还是比较危险的,框架应该提供一个特殊的对象,比如Prototype在内部使用$break = {}作为断点的标识。我们可以用一个纯空对象作为partial的占位符。

var _ = Object.create(null)

纯空对象没有原型,没有toString、valueOf等继承自Object的方法,很特别。在IE下我们可以这样模拟它。

var _ = (function() {var doc = new ActiveXObject('htmlfile')doc.write('<script><\/script>')doc.close()var Obj = doc.parentWindow.Objectif (!Obj || Obj === Object)returnvar name, names =['constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf']while (name = names.pop())delete Obj.prototype[name]return Obj
}())

我们继续回来讲partial。

function partial(fn) {var A = [].slice.call(arguments, 1);return A.length < 1 ? fn : function() {var a = Array.apply([], arguments);var c = A.concat();//复制一份for (var i = 0; i < c.length; i++) {if (c[i] === _) {//替换占位符c[i] = a.shift();}}return fn.apply(this, c.concat(a));}
}
function test(a, b, c, d) {return "a = " + a + " b = " + b + " c = " + c + " d = " + d
}
var fn = partail(test, 1, _, 2, _);
fn(44, 55)// "a = 1 b = 44 c = 2 d = 55"

curry、partial的应用场景在前端世界[2]真心不多,前端讲究的是即时显示,许多API都是同步的,后端由于IO操作等耗时长,像Node.js提供了大量的异步函数来提高性能,防止堵塞。但是过多异步函数也必然带来回调嵌套的问题,因此我们需要通过curry等函数变换,将套嵌减少到可以接受的程度。这个我会在第13章讲述它们的使用方法。

函数的修复涉及apply与call两个方法。这两个方法的本质就是生成一个新的函数,将原函数与用户传参放到里面执行而已。在JavaScript创建一个函数有很多办法,常见的有函数声明和函数表达式,次之是函数构造器,再次是eval、setTimeout……

Function.prototype.apply || (Function.prototype.apply = function (x, y) {x = x || window;y = y ||[];x.__apply = this;if (!x.__apply)x.constructor.prototype.__apply = this;var r, j = y.length;switch (j) {case 0: r = x.__apply(); break;case 1: r = x.__apply(y[0]); break;case 2: r = x.__apply(y[0], y[1]); break;case 3: r = x.__apply(y[0], y[1], y[2]); break;case 4: r = x.__apply(y[0], y[1], y[2], y[3]); break;default:var a = [];for (var i = 0; i < j; ++i)a[i] = "y[" + i + "]";r = eval("x.__apply(" + a.join(",") + ")");break;}try {delete x.__apply ? x.__apply : x.constructor.prototype.__apply;}catch (e) {}return r;
});Function.prototype.call || (Function.prototype.call = function () {var a = arguments, x = a[0], y = [];for (var i = 1, j = a.length; i < j; ++i)y[i - 1] = a[i]return this.apply(x, y);
});

2.5 日期的扩展与修复

Date构造器是JavaScript中传参形式最丰富的构造器,大致分为4种。

new Date();
new Date(value);//传入毫秒数
new Date(dateString);
new Date(year, month, day /*, hour, minute, second, millisecond*/);

其中第3种可以玩多种花样,个人建议只使用“2009/07/12 12:34:56”,后面的时分秒可省略。这个所有浏览器都支持。此构造器的兼容列表可见下文。

http://dygraphs.com/date-formats.html

若要修正它的传参,这恐怕是个大工程,要整个对象替换掉,并且影响Object.prototype.toString的类型判定,因此不建议修正。ES5.js中有相关源码,大家可以看这里。

https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js

JavaScript的日期是抄自Java的java.util.Date,但是Date这个类中的很多方法对时区等支持不够,且不少都是已过时的。Java程序员也推荐使用calnedar类代替Date类。JavaScript可选择的余地比较少,只能凑合继续用。比如:对属性使用了前后矛盾的偏移量,月份与小时都是基于0,月份中的天数则是基于1,而年则是从1900开始的。

接下来,我们为旧版本浏览器添加几个ECMA262标准化的日期方法吧。

if (!Date.now) {Date.now = function() {return +new Date;}
}
if (!Date.prototype.toISOString) {void function() {function pad(number) {var r = String(number);if (r.length === 1) {r = '0' + r;}return r;}Date.prototype.toJSON = Date.prototype.toISOString = function() {return this.getUTCFullYear()+ '-' + pad(this.getUTCMonth() + 1)+ '-' + pad(this.getUTCDate())+ 'T' + pad(this.getUTCHours())+ ':' + pad(this.getUTCMinutes())+ ':' + pad(this.getUTCSeconds())+ '.' + String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5)+ 'Z';};}();
}

IE6和IE7中,getYear与setYear方法都存在bug,不过这个修复起来比较简单。

if ((new Date).getYear() > 1900) {Date.prototype.getYear = function() {return this.getFullYear() - 1900;};Date.prototype.setYear = function(year) {return this.setFullYear(year); //+ 1900};
}

至于扩展,由于涉及本地化,许多日期库都需要改一改才能用,其中以dataFormat这个很有用的方法较为特别。笔者先给一些常用的扩展吧。

传入两个Date类型的日期,求出它们相隔多少天。

function getDatePeriod(start, finish) {return Math.abs(start * 1 - finish * 1) / 60 / 60 / 1000 / 24;
}

传入一个Date类型的日期,求出它所在月的第一天。

function getFirstDateInMonth(date) {return new Date(date.getFullYear(), date.getMonth(), 1);
}

传入一个Date类型的日期,求出它所在月的最后一天。

function getLastDateInMonth(date) {return new Date(date.getFullYear(), date.getMonth() + 1, 0);
}

传入一个Date类型的日期,求出它所在季度的第一天。

function getFirstDateInQuarter(date) {return new Date(date.getFullYear(), ~~(date.getMonth() / 3) * 3, 1);
}

传入一个Date类型的日期,求出它所在季度的最后一天。

function getFirstDateInQuarter(date) {return new Date(date.getFullYear(), ~~(date.getMonth() / 3) * 3 + 3, 0);
}

判断是否为闰年。

function isLeapYear(date) {return new Date(this.getFullYear(), 2, 0).getDate() == 29;
}
//EXT
function isLeapYear2(date) {var year = data.getFullYear();return !!((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year)));
}

取得当前月份的天数。

function getDaysInMonth1(date) {switch (date.getMonth()) {case 0:case 2:case 4:case 6:case 7:case 9:case 11:return 31;case 1:var y = date.getFullYear();return y % 4 == 0 && y % 100 != 0 || y % 400 == 0 ? 29 : 28;default:return 30;}
}var getDaysInMonth2 = (function() {var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];function isLeapYear(date) {var y = date.getFullYear();return y % 4 == 0 && y % 100 != 0 || y % 400 == 0;}return function(date) { // return a closure for efficiencyvar m = date.getMonth();return m == 1 && isLeapYear(date) ? 29 : daysInMonth[m];};
})();function getDaysInMonth3(date) {return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
}

[1] http://imququ.com/post/bom-and-javascript-trim.html

[2] 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由Christopher Strachey以逻辑学家Haskell Curry命名的,尽管它是Moses Schnfinkel和Gottlob Frege发明的。patial,bind只是其一种变体。其用处有3:1.参数复用;2.提前返回;3.延迟计算/运行。

《JavaScript框架设计(第2版)》之语言模块相关推荐

  1. javascript框架设计之种子模块

    javascript框架设计之种子模块 本文给大家介绍的是司徒正美的javascript框架设计的第二章种子模块的相关内容,算是一个小小的读后感,小伙伴们可以参考下. 种子模块也叫核心模块,是框架中最 ...

  2. 《JavaScript框架设计》——1.3 数组化

    本节书摘来自异步社区<JavaScript框架设计>一书中的第1章,第1.3节,作者:司徒正美 更多章节内容可以访问云栖社区"异步社区"公众号查看. 1.3 数组化 浏 ...

  3. javascript——从「最被误解的语言」到「最流行的语言」

    JavaScript曾是"世界上最被误解的语言",因为它担负太多的特性,包括糟糕的交互和失败的设计,但随着Ajax的到来,JavaScript"从最受误解的编程语言演变为 ...

  4. 【读书笔记】《框架设计(第2版)CLR Via C#》中两个比较有趣的知识点

    本季度公司要求阅读<框架设计(第2版)CLR Via C#>这本书,由于前两个月一直很忙,也没有时间阅读,偶尔阅读也是很晚回家以后临睡前拿起这经典之作读那么一个小节.最近利用周末可以说一鼓 ...

  5. 《框架设计 CLR Via C# (第2版)》 - 学习笔记

    <框架设计 CLR Via C#> (第2版) ========== ========== ========== [作者] (美) Jeffrey Richter [译者] (中) 周靖 ...

  6. HTML5期末大作业:京东网站设计——仿2016版京东首页(1页) HTML+CSS+JavaScript 大学生网页作品 电商网页设计作业模板 学生网页制作源代码下载

    HTML5期末大作业:京东网站设计--仿2016版京东首页(1页) HTML+CSS+JavaScript 大学生网页作品 电商网页设计作业模板 学生网页制作源代码下载 常见网页设计作业题材有 个人. ...

  7. [Java版]selenium关键字驱动框架设计实战(二)

    [Java版]selenium关键字驱动框架设计实战(一)本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力.引言使用和学习selen ...

  8. 火车时刻表C语言报告,终稿火车订票系统的设计与实现课程设计.doc最终版(范文1)...

    <火车订票系统的设计与实现课程设计.doc>由会员分享,可免费在线阅读全文,更多与<(终稿)火车订票系统的设计与实现课程设计.doc(最终版)>相关文档资源请在帮帮文库(www ...

  9. C语言大型工程框架设计之设备管理

    好久没上来写写了,突然间手痒了整点有意思的东西以便日后回顾.C语言框架设计主要就是以面向对象的思想来进行底层的设计,参考Linux的内核和驱动层设计,设计完成后后续迭代只需在应用层进行添加修改即可,可 ...

最新文章

  1. Exception loading sessions from persistent storage
  2. 超详细:常用的设计模式汇总
  3. 简单介绍Linux下安装Tomcat的步骤
  4. java基础学不会_java基础
  5. Centos安装后的一些必要处理工作
  6. 轻松构建Tomcat源码
  7. mysql 热切换_热璞HotDB学习篇—如何基于MySQL进行数据高可用
  8. 排序算法之——冒泡排序分析
  9. 数组/指针,字符数组/字符串/指向字符的指针
  10. 樱桃OTC前工程师感恩自白
  11. mysql嵌套查询方式
  12. 微信网页扫码登录的实现
  13. matlab蚁群算法解决vrp过程,蚁群算法MATLAB解VRP问题
  14. 近似算法之旅行商问题
  15. 运营可能是一种思维方式
  16. Convolution Arithmetic
  17. 易基因|综合DNA甲基化测序揭示前列腺癌死亡率的预后表观遗传生物标志物 | 文献速递
  18. centos7升级系统内核
  19. 【转载】抽象语法树简介(有图)
  20. 老男孩视频学习知识点总结之系统优化初步讲解

热门文章

  1. ChatGPT 快速阅读论文:3 个步骤让你轻松阅读论文文档
  2. JavaScript 复制浏览器地址
  3. Redis可视化工具Redis Desktop Manager的使用
  4. 主流智能家居产品,主要面临哪些问题?
  5. Android、Java 获取时间戳、年月日、时间戳转换为具体时间
  6. 10 个超实用 Excel 快捷键
  7. wxss中图片路径不正确,无法使用本地图片
  8. css给图片底部加阴影,使用CSS给图片添加阴影的方法
  9. component:(resolve) => require
  10. kali中怎么dos攻击