JavaScript 新手的踩坑日記
引語
在1995年5月,Eich 大神在10天內就寫出了第一個腳本語言的版本,JavaScript 的第一個代號是 Mocha,Marc Andreesen 起的這個名字。由於商標問題以及很多產品已經使用了 Live 的前綴,網景市場部將它改名為 LiveScript。在1995年11月底,Navigator 2.0B3 發行,其中包含了該語言的原型,這個版本相比之前沒有什麼大的變化。在1995年12月初,Java 語言發展壯大,Sun 把 Java 的商標授權給了網景。這個語言被再次改名,變成了最終的名字——JavaScript。在之後的1997年1月,標準化以後,就成為現在的 ECMAScript。
近一兩年在客戶端上用到 JS 的地方也越來越多了,筆者最近接觸了一下 JS ,作為前端小白,記錄一下近期自己「踩坑」的成長經歷。
一. 原始值和對象
在 JavaScript 中,對值的區分就兩種:
1.原始值:BOOL,Number,String,null,undefined。
2.對象:每個對象都有唯一的標識且只嚴格的等於(===)自己。
null,undefined沒有屬性,連toString( )方法也沒有。
false,0,NaN,undefined,null," " ,都是false。
typeof 運算符能區分原始值和對象,並檢測出原始值的類型。
instanceof 運算符可以檢測出一個對象是否是特定構造函數的一個實例或者是否為它的一個子類。
null 返回的是一個 object,這個是一個不可修復的 bug,如果修改這個 bug,就會破壞現有代碼體系。但是這不能表示 null 是一個對象。
因為第一代 JavaScript 引擎中的 JavaScript 值表示為32位的字元。最低3位作為一種標識,表示值是對象,整數,浮點數或者布爾值。對象的標識是000,而為了表現 null ,引擎使用了機器語言 NULL 的指針,該字元的所有位都是0。而 typeof 就是檢測值的標誌位,這就是為什麼它會認為 null 是一個對象了。
所以判斷 一個 value 是不是一個對象應該按照如下條件判斷:
function isObject (value) { return ( value !== null && (typeof value === "object" || typeof value === "function")); }
null 是原型鏈最頂端的元素
Object.getPrototypeOf(Object.prototype)
判斷 undefined 和 null 可以用嚴格相等判斷:
if(x === null) { // 判斷是否為 null}if (x === undefined) { // 判斷是否為 undefined}if (x === void 0 ) { // 判斷是否為 undefined,void 0 === undefined}if (x != null ) { // 判斷x既不是undefined,也不是null // 這種寫法等價於 if (x !== undefined && x !== null )}
在原始值裡面有一個特例,NaN 雖然是原始值,但是它和它本身是不相等的。
NaN === NaN
原始值的構造函數 Boolean,Number,String 可以把原始值轉換成對象,也可以把對象轉換成原始值。
// 原始值轉換成對象var object = new String("abc")// 對象轉換成原始值String(123)
但是在對象轉換成原始值的時候,需要注意一點:如果用 valueOf() 函數進行轉換的時候,轉換一切正確。
new Boolean(true).valueOf()
但是使用構造函數將包裝對象轉換成原始值的時候,BOOL值是不能正確被轉換的。
Boolean(new Boolean(false))
構造函數只能正確的提取出包裝對象中的數字和字元串。
二. 寬鬆相等帶來的bug
在 JavaScript 中有兩種方式來判斷兩個值是否相等。
嚴格相等 ( === ) 和嚴格不等 ( !== ) 要求比較的值必須是相同的類型。
寬鬆相等 ( == ) 和寬鬆不等 ( != ) 會先嘗試將兩個不同類型的值進行轉換,然後再使用嚴格等進行比較。
寬鬆相等就會遇到一些bug:
undefined == null // undefined 和 null 是寬鬆相等的
關於嚴格相等( Strict equality ) 和 寬鬆相等( Loose equality ),GitHub上有一個人總結了一張圖,挺好的,貼出來分享一下,Github地址在 這裡
但是如果用 Boolean( ) 進行轉換的時候情況又有不同:
這裡為何對象總是為true ?
在 ECMAScript 1中,曾經規定不支持通過對象配置來轉換(比如 toBoolean() 方法)。原理是布爾運算符 || 和 && 會保持運算數的值。因此,如果鏈式使用這些運算符,會多次確認相同值的真假。這樣的檢查對於原始值類型成本不大,但是對於對象,如果能通過配置來轉換布爾值,成本很大。所以從 ECMAScript 1 開始,對象總是為 true 來避免了這些成本轉換。
三. Number
JavaScript 中所有的數字都只有一種類型,都被當做浮點數,JavaScript 內部會做優化,來區分浮點數組和整數。JavaScript 的數字是雙精度的(64位),基於 IEEE 754 標準。
由於所有數字都是浮點數,所以這裡就會有精度的問題。還記得前段時間網上流傳的機器人的漫畫么?
精度的問題就會引發一些奇妙的事情
變換一個位置,加一個括弧,都會影響精度。為了避免這個問題,建議還是轉換成整數。
( 8 + 7 + 6 + 5) / 4 / 10 ; // 0.65( 6 + 8 + 5 + 7) / 4 / 10 ; // 0.65
在數字裡面有4個特殊的數值:
2個錯誤值:NaN 和 Infinity
2個0,一個+0,一個-0。0是會帶正號和負號。因為正負號和數值是分開存儲的。
typeof NaN
(吐槽:NaN 是 「 not a number 」的縮寫,但是它卻是一個數字)
NaN 是 JS 中唯一一個不能自身嚴格相等的值:
NaN === NaN
[ NaN ].indexOf( NaN )
正確的姿勢有兩種:
第一種:
function realIsNaN( value ){ return typeof value === "number" && isNaN(value); }
上面這種之所以需要判斷類型,是因為字元串轉換會先轉換成數字,轉換失敗為 NaN。所以和 NaN 相等。
isNaN( "halfrost" )
第二種方法是利用 IEEE 754 標準裡面的定義,NaN 和任意值比較,包括和自身進行比較,都是無序的
function realIsNaN( value ){ return value !== value ; }
另外一個錯誤值 Infinity 是由表示無窮大,或者除以0導致的。
判斷它直接用 寬鬆相等 == ,或者嚴格相等 === 判斷即可。
但是 isFinite() 函數不是專門用來判斷Infinity的,是用來判斷一個值是否是錯誤值(這裡表示既不是 NaN,又不是 Infinity,排除掉這兩個錯誤值)。
在 ES6 中 引入了兩個函數專門判斷 Infinity 和 NaN的,Number.isFinite() 和 Number.isNaN() 以後都建議用這兩個函數進行判斷。
JS 中整型是有一個安全區間,在( -2^53 , 2^53)之間。所以如果數字超過了64位無符號的整型數字,就只能用字元串進行存儲了。
利用 parseInt() 進行轉換成數字的時候,會有出錯的時候,結果不可信:
parseInt(1000000000000000000000000000.99999999999999999,10)
parseInt( str , redix? ) 會先把第一個參數轉換成字元串:
String(1000000000000000000000000000.99999999999999999)
parseInt 不認為 e 是整數,所以在 e 之後的就停止解析了,所以最終輸出1。
JS 中的 % 求余操作符並不是我們平時認為的取模。
求余操作符會返回一個和第一個操作數相同符號的結果。取模運算是和第二個操作數符號相同。
所以比較坑的就是我們平時判斷一個數是否是奇偶數的問題就會出現錯誤:
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
字元串比較符,是無法比較變音符和重音符的。
"?"
五. Array
創建數組的時候不能用單個數字創建數組。
new Array(2) // 這裡的一個數字代表的是數組的長度
刪除元素會刪出空格,但是不會改變數組的長度。
var array = [1,2,3,4] array.length
所以這裡的刪除不是很符合我們之前的刪除,正確姿勢是用splice
var array = [1,2,3,4,56,7,8,9] array.splice(1,3) array
針對數組裡面的空缺,不同的遍歷方法行為不同
在 ES5 中:
在 ES6 中:規定,遍歷時不跳過空缺,空缺都轉化為undefined
六. Set 、Map、WeakSet、WeakMap
七. 循環
先說一個 for-in 的坑:
一般人看到這道題肯定就開始算了,累加,然後除以7 。那麼這題就錯了,如果把數組裡面的元素變的更加複雜:
var scores = [ 1242351,252352,32143,452354,51455,66125,74217 ];
遍歷對象的屬性,ES6 中有6種方法:
八. 隱式轉換 / 強制轉換 帶來的bug
var formData = { width : "100"};var w = formData.width;var outer = w + 20;console.log( outer === 120 ); // false;console.log( outer === "10020"); // true
九. 運算符重載
在 JavaScript 無法重載或者自定義運算符,包括等號。
十. 函數聲明和變數聲明的提升
先舉一個函數提升的例子。
function foo() { bar(); function bar() { …… } }
var 變數也具有提升的特性。但是把函數賦值給變數以後,提升的效果就會消失。
function foo() { bar(); // error! var bar = function () { …… } }
上述函數就沒有提升效果了。
函數聲明是做了完全提升,變數聲明只是做了部分提升。變數的聲明才有提升的作用,賦值的過程並不會提升。
JavaScript 支持詞法作用域( lexical scoping ),即除了極少的例外,對變數 foo 的引用會被綁定到聲明 foo 變數最近的作用域中。ES5中 不支持塊級作用域,即變數定義的作用域並不是離其最近的封閉語句或代碼塊,而包含它們的函數。所有的變數聲明都會被提升,聲明會被移動到函數的開始處,而賦值則仍然會在原來的位置進行。
function foo() { var x = -10; if ( x
這裡 tmp 就有變數提升的效果。
再舉個例子:
foo = 2;var foo; console.log( foo );
上面這個例子還是輸出2,不是輸出undefined。
這個經過編譯器編譯以後,其實會變成下面這個樣子:
var foo; foo = 2;console.log( foo );
變數聲明被提前了,賦值還在原地。 為了加深一下這句話的理解,再舉一個例子:
console.log( a ); var a = 2;
上述代碼會被編譯成下面的樣子:
var foo;console.log( foo ); foo = 2;
所以輸出的是undefined。
如果變數和函數都存在提升的情況, 那麼函數提升優先順序更高 。
foo(); // 1var foo;function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
上面經過編譯過會變成下面這樣子:
function foo() { console.log( 1 ); } foo(); // 1foo = function() { console.log( 2 ); };
最終結果輸出是1,不是2 。這就說明了函數提升是優先於變數提升的。
為了避免變數提升,ES6中引入了 let 和 const 關鍵字,使用這兩個關鍵字就不會有變數提升了。原理是,在代碼塊內,使用 let 命令聲明變數之前,該變數都是不可用的,這塊區域叫「暫時性死區」(temporal dead zone,TDZ)。TDZ 的做法是,只要一進入到這一區域,所要使用的變數就已經存在了,變數還是「提升」了,但是不能獲取,只有等到聲明變數的那一行出現,才可以獲取和使用該變數。
ES6 的這種做法也給 JS 帶來了塊級作用域,(在 ES5 中只有全局作用於和函數作用域),於是立即執行匿名函數(IIFE)就不在必要了。
十一. arguments 不是數組
arguments 不是數組,它只是類似於數組。它有length屬性,可以通過方括弧去訪問它的元素。不能移除它的元素,也不能對它調用數組的方法。
不要在函數體內使用 arguments 變數,使用 rest 運算符( ... )代替。因為 rest 運算符顯式表明了你想要獲取的參數,而且 arguments 僅僅只是一個類似的數組,而 rest 運算符提供的是一個真正的數組。
下面有一個把 arguments 當數組用的例子:
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);
上述代碼直接報錯:
Uncaught TypeError: Cannot read property "apply" of undefined at callMethod (:5:21) at:12:1
出錯的原因就在於 arguments 並不是函數參數的副本,所有命名參數都是 arguments 對象中對應索引的別名。因此通過 shift 方法移除 arguments 對象中的元素之後,obj 仍然是 arguments[0] 的別名,method 仍然是 arguments[1] 的別名。看上去是在調用 obj[add],實際上是在調用17[25]。
還有一個問題,使用 arguments 引用的時候。
function values() { var i = 0 , n = arguments.length; return { hasNext: function() { return 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
上述代碼是想構造一個迭代器來遍歷 arguments 對象的元素。這裡之所以會輸出 undefined,是因為有一個新的 arguments 變數被隱式的綁定到了每個函數體內,每個迭代器 next 方法含有自己的 arguments 變數,所以執行 it.next 的參數時,已經不是 values 函數中的參數了。
更改方式也簡單,只要聲明一個局部變數,next 的時候能引用到這個變數即可。
function values() { var i = 0 , n = arguments.length,a = arguments; return { hasNext: function() { return 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 缺少塊級作用域,但是到了 ES6 中,這個就可以不需要了。
十三. 函數中 this 的問題
在嵌套函數中不能訪問方法中的 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()
這時就會出現一個TypeError: Cannot read property "name" of undefined。
解決這個問題有兩種方法:
第一種:將 this 保存在變數中。
第二種:利用bind()函數
使用bind()給回調函數的this綁定固定值,即函數的this
第三種:利用 forEach 的第二個參數,把 this 指定一個值。
到了 ES6 裡面,建議能用箭頭函數的地方用箭頭函數。
簡單的,單行的,不會復用的函數,都建議用箭頭函數,如果函數體很複雜,行數很多,還應該用傳統寫法。
箭頭函數裡面的 this 對象就是定義時候的對象,而不是使用時候的對象,這裡存在「綁定關係」。
這裡的「綁定」機制並不是箭頭函數帶來的,而是因為箭頭函數根本就沒有自己的 this,導致內部的 this 就是外層代碼塊的 this,正因為這個特性,也導致了以下的情況都不能使用箭頭函數:
不能當做構造函數,不能使用 new 命令,因為沒有 this,否則會拋出一個錯誤。
不可以使用 argument 對象,該對象在函數體內不存在,非要使用就只能用 rest 參數代替。也不能使用 super,new.target 。
不可以使用 yield 命令,不能作為 Generator 函數。
不可以使用call(),apply(),bind()這些方法改變 this 的指向。
想要系統學習web前端和免費學習資料的 可以加裙六二三九六六八零六
※初學Web前端過程中,必須避開這5個大坑!
※想成為一個高效的Web前端嗎?看看大牛分享的經驗吧
※Java開發人員最常犯的10個錯誤
※Java程序員入門必須克服的5個障礙
※2017年6月TIOBE編程語言排行榜:Java穩居第一
TAG:IT技術java交流 |