阅读 4288

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

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


试读本书:www.epubit.com.cn/book/detail…

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


/**
  www.alloyteam.com/2013/12/js-…
计算字符串所占的内存字节数,默认使用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="zh.wikipedia.org/wiki/UTF-8"…
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="zh.wikipedia.org/wiki/UTF-16…
@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, "&#39;");
}复制代码

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


function unescapeHTML(target) {
return String(target)
.replace(/&#39;/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 = a
return div.innerHTML
}

var unescapeHTML = function (a) {
div.innerHTML = a
return 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环境中,并且性能也不好,于是人们发展出下面这些库。


github.com/mathiasbyne…
github.com/mdevils/nod…复制代码

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


github.com/es-shims/es…复制代码

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
//github.com/bestiejs/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 = 0
return '"' + ( reEscape.test(value)? String(value).replace(reEscape, escapeChar) : value ) + '"'
}

avalon.quote = typeof JSON !== 'undefined' ? JSON.stringify : quote复制代码

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


//github.com/ecomfe/etpl…
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中偷来的。


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


// es5.github.com/#x9.4
// jsperf.com/to-integer
var toInteger = function(n) {
n = +n;
if (n !== n) { // isNaN
n = 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;
};
}复制代码

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


github.com/es-shims/es…


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) {//setter
if (cur !== neo) {
cur = neo;
}
} else {//getter
return 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.Object
if (!Obj || Obj === Object)
return
var 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”,后面的时分秒可省略。这个所有浏览器都支持。此构造器的兼容列表可见下文。


dygraphs.com/date-format…复制代码

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


github.com/kriskowal/e…复制代码

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 efficiency
var 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] imququ.com/post/bom-an…

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

文章分类
前端
文章标签