《JavaScript框架設計(第2版)》之語言模塊

本文摘自:人民郵電出版社異步圖書《JavaScript框架設計(第2版)》
javascript


試讀本書: www.epubit.com.cn/book/detail…

敲重點:
活動規則:試讀樣章,評論區留言說一下你對本書的一些感想,同時關注異步社區專欄,並留言你想要獲得的圖書。
活動時間:即日起-9月10日(活動獎項公告在9月11日)
贈書數量:1本 先到先得!
備註:能夠選本書做爲獎品也能夠選擇其餘圖書
更多好書能夠來人郵社異步社區查看,申請下期活動:www.epubit.com.cn/css

第2章 語言模塊


1995年,Brendan Eich讀完了在程序語言設計中曾經出現過的全部錯誤,本身又發現了一些更多的錯誤,而後用它們創造出了LiveScript。以後,爲了緊跟Java語言的潮流,它被從新命名爲JavaScript。再而後,爲了追隨一種皮膚病的時髦名字,這個語言又命名爲ECMAScript。html


上面一段話出自博文《編程語言僞簡史》。可見,JavaScript受到了多麼辛辣的嘲諷,它在當時是多麼不受歡迎。拋開偏見,JavaScript的確有許多不足之處。因爲互聯網的傳播性及瀏覽器廠商大戰,JavaScript之父失去了對此門語言的掌控權。即使他想修復這些bug或推出某些新特性,也要全部瀏覽器廠商都點頭才行。IE6的市場獨佔性,打破了他的奢望。這個局面直到Chrome誕生,纔有所改善。前端


但在IE6時期,瀏覽器提供的原生API數量是極其貧乏的,所以各個框架都創造了許多方法來彌補這缺陷。視框架做者原來的語言背景不一樣,這些方法也是林林總總。其中最傑出的表明是王者Prototype.js,把ruby語言的那一套方式或範式搬過來,從底層促進了JavaScript的發展。ECMA262V6添加那一堆字符串、數組方法,差很少就是改個名字而已。java


即使是瀏覽器的API也不能盡信,尤爲是IE六、IE七、IE8處處是bug。早期出現的各類「JS庫」,例如遠古的prototype、中古的mootools,到近代的jQuery,再到大規模、緊封裝的YUI和Extjs,很大的一個目標就是爲了填「兼容性」這個「大坑」。node


在avalon2中,就提供了許多帶compact命名的模塊,它們就是專門用於修復古老瀏覽器的兼容性問題。此外,本章也介紹了一些很是底層的知識點,能讓讀者更熟悉這門語言。mysql


2.1 字符串的擴展與修復


筆者發現腳本語言都對字符串特別關注,有關它的方法特別多。筆者把這些方法分爲三大類,如圖2-1所示。git



圖2-1程序員


顯然之前,老是想着經過字符串生成標籤,因而誕生了一些方法,如anchor、big、blink、bold、fixed、fontcolor、italics、link、small、strike、sub及sup。github


剩下的就是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的字符串方法能夠說是十分貧乏的,所以後來的ES五、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、一、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)IE六、IE7不支持用數組中括號取它的每個字符,須要用charAt來取。


(2)IE六、IE七、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


因爲許多擴展也基於這些新的標準化方法,所以筆者先給出IE六、IE七、IE8的兼容方案,所有在數組原型上修復它們。


[1, 2, , 4].forEach(function(e){
console.log(e)
});
//依次打印出1,2,4,忽略第二、第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原生方法在各瀏覽器的差別,一個是IE六、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在一個參數的狀況下,IE六、IE七、IE8默認第二個參數爲零,其餘瀏覽器爲數組的長度,固然咱們要以標準瀏覽器爲準!


下面是最簡單的修復方法。


if ([1, 2, 3].splice(1).length == 0) {
//若是是IE六、IE七、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.延遲計算/運行。

相關文章
相關標籤/搜索