引語 在1995年5月,Eich 大神在10天內(nèi)就寫出了第一個(gè)腳本語言的版本,JavaScript 的第一個(gè)代號(hào)是 Mocha,Marc Andreesen 起的這個(gè)名字。由于商標(biāo)問題以及很多產(chǎn)品已經(jīng)使用了 Live 的前綴,網(wǎng)景市場部將它改名為 LiveScript。在1995年11月底,Navigator 2.0B3 發(fā)行,其中包含了該語言的原型,這個(gè)版本相比之前沒有什么大的變化。在1995年12月初,Java 語言發(fā)展壯大,Sun 把 Java 的商標(biāo)授權(quán)給了網(wǎng)景。這個(gè)語言被再次改名,變成了最終的名字——JavaScript。在之后的1997年1月,標(biāo)準(zhǔn)化以后,就成為現(xiàn)在的 ECMAScript。 近一兩年在客戶端上用到 JS 的地方也越來越多了,筆者最近接觸了一下 JS ,作為前端小白,記錄一下近期自己“踩坑”的成長經(jīng)歷。 一. 原始值和對象 在 JavaScript 中,對值的區(qū)分就兩種:
null,undefined沒有屬性,連toString( )方法也沒有。 false,0,NaN,undefined,null,' ' ,都是false。 typeof 運(yùn)算符能區(qū)分原始值和對象,并檢測出原始值的類型。 instanceof 運(yùn)算符可以檢測出一個(gè)對象是否是特定構(gòu)造函數(shù)的一個(gè)實(shí)例或者是否為它的一個(gè)子類。
null 返回的是一個(gè) object,這個(gè)是一個(gè)不可修復(fù)的 bug,如果修改這個(gè) bug,就會(huì)破壞現(xiàn)有代碼體系。但是這不能表示 null 是一個(gè)對象。 因?yàn)榈谝淮?JavaScript 引擎中的 JavaScript 值表示為32位的字符。最低3位作為一種標(biāo)識(shí),表示值是對象,整數(shù),浮點(diǎn)數(shù)或者布爾值。對象的標(biāo)識(shí)是000,而為了表現(xiàn) null ,引擎使用了機(jī)器語言 NULL 的指針,該字符的所有位都是0。而 typeof 就是檢測值的標(biāo)志位,這就是為什么它會(huì)認(rèn)為 null 是一個(gè)對象了。 所以判斷 一個(gè) value 是不是一個(gè)對象應(yīng)該按照如下條件判斷: function isObject (value) { return ( value !== null && (typeof value === 'object' || typeof value === 'function'));} null 是原型鏈最頂端的元素 Object.getPrototypeOf(Object.prototype)<>null 判斷 undefined 和 null 可以用嚴(yán)格相等判斷: if(x === null) { // 判斷是否為 null}if (x === undefined) { // 判斷是否為 undefined}if (x === void 0 ) { // 判斷是否為 undefined,void 0 === undefined}if (x != null ) { // 判斷x既不是undefined,也不是null // 這種寫法等價(jià)于 if (x !== undefined && x !== null )} 在原始值里面有一個(gè)特例,NaN 雖然是原始值,但是它和它本身是不相等的。 NaN === NaN 原始值的構(gòu)造函數(shù) Boolean,Number,String 可以把原始值轉(zhuǎn)換成對象,也可以把對象轉(zhuǎn)換成原始值。 // 原始值轉(zhuǎn)換成對象var object = new String('abc')// 對象轉(zhuǎn)換成原始值String(123)'123' 但是在對象轉(zhuǎn)換成原始值的時(shí)候,需要注意一點(diǎn):如果用 valueOf() 函數(shù)進(jìn)行轉(zhuǎn)換的時(shí)候,轉(zhuǎn)換一切正確。 new Boolean(true).valueOf() 但是使用構(gòu)造函數(shù)將包裝對象轉(zhuǎn)換成原始值的時(shí)候,BOOL值是不能正確被轉(zhuǎn)換的。 Boolean(new Boolean(false)) 構(gòu)造函數(shù)只能正確的提取出包裝對象中的數(shù)字和字符串。 二. 寬松相等帶來的bug 在 JavaScript 中有兩種方式來判斷兩個(gè)值是否相等。
寬松相等就會(huì)遇到一些bug: undefined == null // undefined 和 null 是寬松相等的 關(guān)于嚴(yán)格相等( Strict equality ) 和 寬松相等( Loose equality ),GitHub上有一個(gè)人總結(jié)了一張圖,挺好的,貼出來分享一下,Github地址在 這里
但是如果用 Boolean( ) 進(jìn)行轉(zhuǎn)換的時(shí)候情況又有不同:
這里為何對象總是為true ? 在 ECMAScript 1中,曾經(jīng)規(guī)定不支持通過對象配置來轉(zhuǎn)換(比如 toBoolean() 方法)。原理是布爾運(yùn)算符 || 和 && 會(huì)保持運(yùn)算數(shù)的值。因此,如果鏈?zhǔn)绞褂眠@些運(yùn)算符,會(huì)多次確認(rèn)相同值的真假。這樣的檢查對于原始值類型成本不大,但是對于對象,如果能通過配置來轉(zhuǎn)換布爾值,成本很大。所以從 ECMAScript 1 開始,對象總是為 true 來避免了這些成本轉(zhuǎn)換。 三. Number JavaScript 中所有的數(shù)字都只有一種類型,都被當(dāng)做浮點(diǎn)數(shù),JavaScript 內(nèi)部會(huì)做優(yōu)化,來區(qū)分浮點(diǎn)數(shù)組和整數(shù)。JavaScript 的數(shù)字是雙精度的(64位),基于 IEEE 754 標(biāo)準(zhǔn)。 由于所有數(shù)字都是浮點(diǎn)數(shù),所以這里就會(huì)有精度的問題。還記得前段時(shí)間網(wǎng)上流傳的機(jī)器人的漫畫么?
精度的問題就會(huì)引發(fā)一些奇妙的事情 0.1 + 0.2 ; // 0.300000000000004( 0.1 + 0.2 ) + 0.3; // 0.60000000000010.1 + ( 0.2 + 0.3 ); // 0.6(0.8+0.7+0.6+0.5) / 4 // 0.65(0.6+0.7+0.8+0.5) / 4 // 0.6499999999999999 變換一個(gè)位置,加一個(gè)括號(hào),都會(huì)影響精度。為了避免這個(gè)問題,建議還是轉(zhuǎn)換成整數(shù)。 ( 8 + 7 + 6 + 5) / 4 / 10 ; // 0.65( 6 + 8 + 5 + 7) / 4 / 10 ; // 0.65
在數(shù)字里面有4個(gè)特殊的數(shù)值:
typeof NaN'number' (吐槽:NaN 是 “ not a number ”的縮寫,但是它卻是一個(gè)數(shù)字) NaN 是 JS 中唯一一個(gè)不能自身嚴(yán)格相等的值: NaN === NaN 所以不能通過 Array.prototype.indexOf 方法去查找 NaN (因?yàn)閿?shù)組的 indexOf 方法會(huì)進(jìn)行嚴(yán)格等的判斷)。 [ NaN ].indexOf( NaN )<>1 正確的姿勢有兩種:
function realIsNaN( value ){ return typeof value === 'number' && isNaN(value);} 上面這種之所以需要判斷類型,是因?yàn)樽址D(zhuǎn)換會(huì)先轉(zhuǎn)換成數(shù)字,轉(zhuǎn)換失敗為 NaN。所以和 NaN 相等。 isNaN( 'halfrost' )
function realIsNaN( value ){ return value !== value ;} 另外一個(gè)錯(cuò)誤值 Infinity 是由表示無窮大,或者除以0導(dǎo)致的。 判斷它直接用 寬松相等 == ,或者嚴(yán)格相等 === 判斷即可。 但是 isFinite() 函數(shù)不是專門用來判斷Infinity的,是用來判斷一個(gè)值是否是錯(cuò)誤值(這里表示既不是 NaN,又不是 Infinity,排除掉這兩個(gè)錯(cuò)誤值)。 在 ES6 中 引入了兩個(gè)函數(shù)專門判斷 Infinity 和 NaN的,Number.isFinite() 和 Number.isNaN() 以后都建議用這兩個(gè)函數(shù)進(jìn)行判斷。 JS 中整型是有一個(gè)安全區(qū)間,在( -2^53 , 2^53)之間。所以如果數(shù)字超過了64位無符號(hào)的整型數(shù)字,就只能用字符串進(jìn)行存儲(chǔ)了。 利用 parseInt() 進(jìn)行轉(zhuǎn)換成數(shù)字的時(shí)候,會(huì)有出錯(cuò)的時(shí)候,結(jié)果不可信: parseInt(1000000000000000000000000000.99999999999999999,10)<> parseInt( str , redix? ) 會(huì)先把第一個(gè)參數(shù)轉(zhuǎn)換成字符串: String(1000000000000000000000000000.99999999999999999)'1e+27' parseInt 不認(rèn)為 e 是整數(shù),所以在 e 之后的就停止解析了,所以最終輸出1。 JS 中的 % 求余操作符并不是我們平時(shí)認(rèn)為的取模。 -9%7<>2 求余操作符會(huì)返回一個(gè)和第一個(gè)操作數(shù)相同符號(hào)的結(jié)果。取模運(yùn)算是和第二個(gè)操作數(shù)符號(hào)相同。 所以比較坑的就是我們平時(shí)判斷一個(gè)數(shù)是否是奇偶數(shù)的問題就會(huì)出現(xiàn)錯(cuò)誤: function isOdd( value ){ return value % 2 === 1;}console.log(-3); // falseconsole.log(-2); // false 正確姿勢是: function isOdd( value ){ return Math.abs( value % 2 ) === 1;}console.log(-3); // trueconsole.log(-2); // false 四. String 字符串比較符,是無法比較變音符和重音符的。 '?' <>'b' 五. Array 創(chuàng)建數(shù)組的時(shí)候不能用單個(gè)數(shù)字創(chuàng)建數(shù)組。 new Array(2) // 這里的一個(gè)數(shù)字代表的是數(shù)組的長度<[ ,="" ,="">[>new Array(2,3,4)<>2,3,4] 刪除元素會(huì)刪出空格,但是不會(huì)改變數(shù)組的長度。 var array = [1,2,3,4]array.length4delete array[1]array<>1, ,3,4]array.length4 所以這里的刪除不是很符合我們之前的刪除,正確姿勢是用splice var array = [1,2,3,4,56,7,8,9]array.splice(1,3)array<>1, 56, 7, 8, 9]array.length5 針對數(shù)組里面的空缺,不同的遍歷方法行為不同 在 ES5 中:
在 ES6 中:規(guī)定,遍歷時(shí)不跳過空缺,空缺都轉(zhuǎn)化為undefined
六. Set 、Map、WeakSet、WeakMap
七. 循環(huán) 先說一個(gè) for-in 的坑: var scores = [ 11,22,33,44,55,66,77 ];var total = 0;for (var score in scores) { total += score;}var mean = total / scores.length;mean; 一般人看到這道題肯定就開始算了,累加,然后除以7 。那么這題就錯(cuò)了,如果把數(shù)組里面的元素變的更加復(fù)雜: var scores = [ 1242351,252352,32143,452354,51455,66125,74217 ]; 其實(shí)這里答案和數(shù)組里面元素是多少無關(guān)。只要數(shù)組元素個(gè)數(shù)是7,最終答案都是17636.571428571428。 原因是 for-in 循環(huán)的是數(shù)組下標(biāo),所以 total = ‘00123456’ ,然后這個(gè)字符串再除以7。
遍歷對象的屬性,ES6 中有6種方法:
八. 隱式轉(zhuǎn)換 / 強(qiáng)制轉(zhuǎn)換 帶來的bug var formData = { width : '100'};var w = formData.width;var outer = w + 20;console.log( outer === 120 ); // false;console.log( outer === '10020'); // true 九. 運(yùn)算符重載 在 JavaScript 無法重載或者自定義運(yùn)算符,包括等號(hào)。 十. 函數(shù)聲明和變量聲明的提升 先舉一個(gè)函數(shù)提升的例子。 function foo() { bar(); function bar() { …… }} var 變量也具有提升的特性。但是把函數(shù)賦值給變量以后,提升的效果就會(huì)消失。 function foo() { bar(); // error! var bar = function () { …… }} 上述函數(shù)就沒有提升效果了。 函數(shù)聲明是做了完全提升,變量聲明只是做了部分提升。變量的聲明才有提升的作用,賦值的過程并不會(huì)提升。 JavaScript 支持詞法作用域( lexical scoping ),即除了極少的例外,對變量 foo 的引用會(huì)被綁定到聲明 foo 變量最近的作用域中。ES5中 不支持塊級(jí)作用域,即變量定義的作用域并不是離其最近的封閉語句或代碼塊,而包含它們的函數(shù)。所有的變量聲明都會(huì)被提升,聲明會(huì)被移動(dòng)到函數(shù)的開始處,而賦值則仍然會(huì)在原來的位置進(jìn)行。 function foo() { var x = -10; if ( x <>0) { var tmp = -x; …… } console.log(tmp); // 10} 這里 tmp 就有變量提升的效果。 再舉個(gè)例子: foo = 2;var foo; console.log( foo ); 上面這個(gè)例子還是輸出2,不是輸出undefined。 這個(gè)經(jīng)過編譯器編譯以后,其實(shí)會(huì)變成下面這個(gè)樣子: var foo; foo = 2;console.log( foo ); 變量聲明被提前了,賦值還在原地。 為了加深一下這句話的理解,再舉一個(gè)例子: console.log( a ); var a = 2; 上述代碼會(huì)被編譯成下面的樣子: var foo;console.log( foo ); foo = 2; 所以輸出的是undefined。 如果變量和函數(shù)都存在提升的情況, 那么函數(shù)提升優(yōu)先級(jí)更高 。 foo(); // 1var foo;function foo() { console.log( 1 );}foo = function() { console.log( 2 );}; 上面經(jīng)過編譯過會(huì)變成下面這樣子: function foo() { console.log( 1 );}foo(); // 1foo = function() { console.log( 2 );}; 最終結(jié)果輸出是1,不是2 。這就說明了函數(shù)提升是優(yōu)先于變量提升的。 為了避免變量提升,ES6中引入了 let 和 const 關(guān)鍵字,使用這兩個(gè)關(guān)鍵字就不會(huì)有變量提升了。原理是,在代碼塊內(nèi),使用 let 命令聲明變量之前,該變量都是不可用的,這塊區(qū)域叫“暫時(shí)性死區(qū)”(temporal dead zone,TDZ)。TDZ 的做法是,只要一進(jìn)入到這一區(qū)域,所要使用的變量就已經(jīng)存在了,變量還是“提升”了,但是不能獲取,只有等到聲明變量的那一行出現(xiàn),才可以獲取和使用該變量。 ES6 的這種做法也給 JS 帶來了塊級(jí)作用域,(在 ES5 中只有全局作用于和函數(shù)作用域),于是立即執(zhí)行匿名函數(shù)(IIFE)就不在必要了。 十一. arguments 不是數(shù)組arguments 不是數(shù)組,它只是類似于數(shù)組。它有l(wèi)ength屬性,可以通過方括號(hào)去訪問它的元素。不能移除它的元素,也不能對它調(diào)用數(shù)組的方法。 不要在函數(shù)體內(nèi)使用 arguments 變量,使用 rest 運(yùn)算符( ... )代替。因?yàn)?rest 運(yùn)算符顯式表明了你想要獲取的參數(shù),而且 arguments 僅僅只是一個(gè)類似的數(shù)組,而 rest 運(yùn)算符提供的是一個(gè)真正的數(shù)組。 下面有一個(gè)把 arguments 當(dāng)數(shù)組用的例子: function callMethod(obj,method) { var shift = [].shift; shift.call(arguments); shift.call(arguments); return obj[method].apply(obj,arguments);}var obj = { add:function(x,y) { return x + y ;}};callMethod(obj,'add',18,38); 上述代碼直接報(bào)錯(cuò): Uncaught TypeError: Cannot read property 'apply' of undefined at callMethod (:5:21) at:12:1 出錯(cuò)的原因就在于 arguments 并不是函數(shù)參數(shù)的副本,所有命名參數(shù)都是 arguments 對象中對應(yīng)索引的別名。因此通過 shift 方法移除 arguments 對象中的元素之后,obj 仍然是 arguments[0] 的別名,method 仍然是 arguments[1] 的別名。看上去是在調(diào)用 obj[add],實(shí)際上是在調(diào)用17[25]。 還有一個(gè)問題,使用 arguments 引用的時(shí)候。 function values() { var i = 0 , n = arguments.length; return { hasNext: function() { return i < n;="" },="">next: function() { if (i >= n) { throw new Error('end of iteration'); } return arguments[i++]; } }}var it = values(1,24,53,253,26,326,);it.next(); // undefinedit.next(); // undefinedit.next(); // undefined 上述代碼是想構(gòu)造一個(gè)迭代器來遍歷 arguments 對象的元素。這里之所以會(huì)輸出 undefined,是因?yàn)橛幸粋€(gè)新的 arguments 變量被隱式的綁定到了每個(gè)函數(shù)體內(nèi),每個(gè)迭代器 next 方法含有自己的 arguments 變量,所以執(zhí)行 it.next 的參數(shù)時(shí),已經(jīng)不是 values 函數(shù)中的參數(shù)了。 更改方式也簡單,只要聲明一個(gè)局部變量,next 的時(shí)候能引用到這個(gè)變量即可。 function values() { var i = 0 , n = arguments.length,a = arguments; return { hasNext: function() { return i < n;="" },="">next: function() { if (i >= n) { throw new Error('end of iteration'); } return a[i++]; } }}var it = values(1,24,53,253,26,326,);it.next(); // 1it.next(); // 24it.next(); // 53 十二. IIFE 引入新的作用域 在 ES5 中 IIFE 是為了解決 JS 缺少塊級(jí)作用域,但是到了 ES6 中,這個(gè)就可以不需要了。 十三. 函數(shù)中 this 的問題 在嵌套函數(shù)中不能訪問方法中的 this 變量。 var halfrost = { name:'halfrost', friends: [ 'haha' , 'hehe' ], sayHiToFriends: function() { 'use strict'; this.friends.forEach(function (friend) { // 'this' is undefined here console.log(this.name + 'say hi to' + friend); }); }}halfrost.sayHiToFriends() 這時(shí)就會(huì)出現(xiàn)一個(gè)TypeError: Cannot read property 'name' of undefined。 解決這個(gè)問題有兩種方法:
sayHiToFriends: function() { 'use strict'; var that = this; this.friends.forEach(function (friend) { console.log(that.name + 'say hi to' + friend); });}
使用bind()給回調(diào)函數(shù)的this綁定固定值,即函數(shù)的this sayHiToFriends: function() { 'use strict'; this.friends.forEach(function (friend) { console.log(this.name + 'say hi to' + friend); }.bind(this));}
sayHiToFriends: function() { 'use strict'; this.friends.forEach(function (friend) { console.log(this.name + 'say hi to' + friend); }, this);} 到了 ES6 里面,建議能用箭頭函數(shù)的地方用箭頭函數(shù)。 簡單的,單行的,不會(huì)復(fù)用的函數(shù),都建議用箭頭函數(shù),如果函數(shù)體很復(fù)雜,行數(shù)很多,還應(yīng)該用傳統(tǒng)寫法。 箭頭函數(shù)里面的 this 對象就是定義時(shí)候的對象,而不是使用時(shí)候的對象,這里存在“綁定關(guān)系”。 這里的“綁定”機(jī)制并不是箭頭函數(shù)帶來的,而是因?yàn)榧^函數(shù)根本就沒有自己的 this,導(dǎo)致內(nèi)部的 this 就是外層代碼塊的 this,正因?yàn)檫@個(gè)特性,也導(dǎo)致了以下的情況都不能使用箭頭函數(shù):
十四. 異步 異步編程有以下幾種:
(這個(gè)日記可能一直未完待續(xù)......) |
|