本文摘自:人民邮电出版社异步图书《JavaScript框架设计(第2版)》
敲重点:
活动规则:试读样章,评论区留言说一下你对本书的一些感想,同时关注异步社区专栏,并留言你想要得到的图书。
活动时间:即日起-9月10日(活动奖项公告在9月11日)
赠书数量:1本 先到先得!
备注:可以选本书作为奖品也可以选择其他图书
更多好书可以来人邮社异步社区查看,申请下期活动:
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
显然以前,总是想着通过字符串生成标签,于是诞生了一些方法,如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;
}
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在各浏览器的得分是最高的。
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转义得到适合在页面中显示的内容,如将“<
”替换为“<
”`。此方法用于防止XSS攻击。
function escapeHTML(target) {
return target.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, """)
.replace(/'/g, "'");
}
unescapeHTML方法:将字符串中的html实体字符还原为对应字符。
function unescapeHTML(target) {
return String(target)
.replace(/'/g, '\'')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/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('&').split('<').join('<').split('>'). join('>');
} else {
value = value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
return value;
}
看情况是处理&
时出了分歧。但它们这么做其实也不能处理所有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环境中,并且性能也不好,于是人们发展出下面这些库。
https://github.com/mathiasbynens/he
https://github.com/mdevils/node-html-entities
escapeRegExp方法:将字符串安全格式化为正则表达式的源码。
function escapeRegExp(target) {
return target.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
}
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中加上wbr
:after { 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"
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 = 0
return '"' + ( 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方法。由于太常用,所以相应的实现也非常多。我们可以一起看看,顺便学习一下正则。
版本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, "" );
}
下面是一个比较晦涩的知识点——空白字符。根据屈屈的博文,浏览器会把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起废止 |
得益于Prototype.js的ruby式数组方法的侵略,让Jser()前端工程师大开眼界,原来对数组的操作也如此丰富多彩。原来JavaScript的数组方法就是基于栈与队列的那一套,像splice还是很晚加入的。让我们回顾一下它们的用法,如图2-4所示。
图2-4
substring、slice和substr是“三兄弟”,常用于转换类数组对象为真正的数组。
为了方便大家记忆,我们可以用图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
由于空位的处理规则非常不统一,所以建议避免出现空位。
数值没有什么好扩展的,而且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) { // 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;
};
}
追求完美的话,还存在这样一个版本,把里面的加、减、乘、除都重新实现了一遍。
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…这种循环小数。在有效数字的范围内进行舍入,就会产生误差。
ECMA262V5对函数唯一的扩展就是bind函数。众所周知,这是来自Prototype.js,此外,其他重要的函数都来自Prototype.js。
Prototype.js的函数扩展包括以下几种方法。
这些方法每一个都是别具匠心,影响深远。
我们先看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-curry或recurry。它强调的是递归调用自身来补全参数。
与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的应用场景在前端世界真心不多,前端讲究的是即时显示,许多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);
});
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 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();
}
http://imququ.com/post/bom-and-javascript-trim.html
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由Christopher Strachey以逻辑学家Haskell Curry命名的,尽管它是Moses Schnfinkel和Gottlob Frege发明的。patial,bind只是其一种变体。其用处有3:1.参数复用;2.提前返回;3.延迟计算/运行。
因篇幅问题不能全部显示,请点此查看更多更全内容