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。javascript

近一兩年在客戶端上用到 JS 的地方也愈來愈多了,筆者最近接觸了一下 JS ,做爲前端小白,記錄一下近期本身「踩坑」的成長經歷。前端

一. 原始值和對象

在 JavaScript 中,對值的區分就兩種:java

1.原始值:BOOL,Number,String,null,undefined。
2.對象:每一個對象都有惟一的標識且只嚴格的等於(===)本身。git

null,undefined沒有屬性,連toString( )方法也沒有。github

false,0,NaN,undefined,null,' ' ,都是false。編程

typeof 運算符能區分原始值和對象,並檢測出原始值的類型。
instanceof 運算符能夠檢測出一個對象是不是特定構造函數的一個實例或者是否爲它的一個子類。數組

操做數 typeof
undefined 'undefined'
null object
布爾值 boolean
數字 number
字符串 string
函數 function
其餘的常規值 object
引擎建立的值 可能返回任意的字符串

null 返回的是一個 object,這個是一個不可修復的 bug,若是修改這個 bug,就會破壞現有代碼體系。可是這不能表示 null 是一個對象。安全

由於第一代 JavaScript 引擎中的 JavaScript 值表示爲32位的字符。最低3位做爲一種標識,表示值是對象,整數,浮點數或者布爾值。對象的標識是000,而爲了表現 null ,引擎使用了機器語言 NULL 的指針,該字符的全部位都是0。而 typeof 就是檢測值的標誌位,這就是爲何它會認爲 null 是一個對象了。數據結構

因此判斷 一個 value 是否是一個對象應該按照以下條件判斷:app

function isObject (value) {
  return ( value !== null 
    && (typeof value === 'object' 
    || typeof value === 'function'));
}複製代碼

null 是原型鏈最頂端的元素

Object.getPrototypeOf(Object.prototype)

< null複製代碼

判斷 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
<false複製代碼

原始值的構造函數 Boolean,Number,String 能夠把原始值轉換成對象,也能夠把對象轉換成原始值。

// 原始值轉換成對象
var object = new String('abc')


// 對象轉換成原始值
String(123)
<'123'複製代碼

可是在對象轉換成原始值的時候,須要注意一點:若是用 valueOf() 函數進行轉換的時候,轉換一切正確。

new Boolean(true).valueOf()
<true複製代碼

可是使用構造函數將包裝對象轉換成原始值的時候,BOOL值是不能正確被轉換的。

Boolean(new Boolean(false))
<true複製代碼

構造函數只能正確的提取出包裝對象中的數字和字符串。

二. 寬鬆相等帶來的bug

在 JavaScript 中有兩種方式來判斷兩個值是否相等。

  1. 嚴格相等 ( === ) 和嚴格不等 ( !== ) 要求比較的值必須是相同的類型。
  2. 寬鬆相等 ( == ) 和寬鬆不等 ( != ) 會先嚐試將兩個不一樣類型的值進行轉換,而後再使用嚴格等進行比較。

寬鬆相等就會遇到一些bug:

undefined == null // undefined 和 null 是寬鬆相等的
<true

2 == true  // 不要誤認爲這裏是true
<false

1 == true 
<true

0 == false
<true 

' ' == false // 空字符串等於false,可是不是全部的非空字符串都等於true
<true

'1' == true
<true

'2' == true
<false

'abc' == true // NaN === 1
<false複製代碼

關於嚴格相等( Strict equality ) 和 寬鬆相等( Loose equality ),GitHub上有一我的總結了一張圖,挺好的,貼出來分享一下,Github地址在這裏

可是若是用 Boolean( ) 進行轉換的時候狀況又有不一樣:

轉換成BOOL值
undefined false
null false
BOOL 與輸入值相同
數字 0,NaN 轉換成false,其餘的都爲 true
字符串 ' '轉換成false,其餘字符串都轉換成true
對象 全爲true

這裏爲什麼對象老是爲true ?
在 ECMAScript 1中,曾經規定不支持經過對象配置來轉換(好比 toBoolean() 方法)。原理是布爾運算符 || 和 && 會保持運算數的值。所以,若是鏈式使用這些運算符,會屢次確認相同值的真假。這樣的檢查對於原始值類型成本不大,可是對於對象,若是能經過配置來轉換布爾值,成本很大。因此從 ECMAScript 1 開始,對象老是爲 true 來避免了這些成本轉換。

三. Number

JavaScript 中全部的數字都只有一種類型,都被當作浮點數,JavaScript 內部會作優化,來區分浮點數組和整數。JavaScript 的數字是雙精度的(64位),基於 IEEE 754 標準。

因爲全部數字都是浮點數,因此這裏就會有精度的問題。還記得前段時間網上流傳的機器人的漫畫麼?

精度的問題就會引起一些奇妙的事情

0.1 + 0.2 ;  // 0.300000000000004

( 0.1 + 0.2 ) + 0.3;    // 0.6000000000001
0.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複製代碼

變換一個位置,加一個括號,都會影響精度。爲了不這個問題,建議仍是轉換成整數。

( 8 + 7 + 6 + 5) / 4 / 10 ;  // 0.65
( 6 + 8 + 5 + 7) / 4 / 10 ;  // 0.65複製代碼
轉換成Number值
undefined NaN
null 0
BOOL false = 0,true = 1
數字 與原值相同
字符串 解析字符串中的數字(忽略開頭和結尾的空格);空字符轉換成0。
對象 調用 ToPrimitive( value,number) 並轉換成原始類型

在數字裏面有4個特殊的數值:

  1. 2個錯誤值:NaN 和 Infinity
  2. 2個0,一個+0,一個-0。0是會帶正號和負號。由於正負號和數值是分開存儲的。
typeof NaN
<"number"複製代碼

(吐槽:NaN 是 「 not a number 」的縮寫,可是它倒是一個數字)

NaN 是 JS 中惟一一個不能自身嚴格相等的值:

NaN === NaN
<false複製代碼

因此不能經過 Array.prototype.indexOf 方法去查找 NaN (由於數組的 indexOf 方法會進行嚴格等的判斷)。

[ NaN ].indexOf( NaN )
<-1複製代碼

正確的姿式有兩種:

第一種:

function realIsNaN( value ){
  return typeof value === 'number' && isNaN(value);
}複製代碼

上面這種之因此須要判斷類型,是由於字符串轉換會先轉換成數字,轉換失敗爲 NaN。因此和 NaN 相等。

isNaN( 'halfrost' )
<true複製代碼

第二種方法是利用 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)
<1複製代碼

parseInt( str , redix? ) 會先把第一個參數轉換成字符串:

String(1000000000000000000000000000.99999999999999999)
<"1e+27"複製代碼

parseInt 不認爲 e 是整數,因此在 e 以後的就中止解析了,因此最終輸出1。

JS 中的 % 求餘操做符並非咱們平時認爲的取模。

-9%7
<-2複製代碼

求餘操做符會返回一個和第一個操做數相同符號的結果。取模運算是和第二個操做數符號相同。

因此比較坑的就是咱們平時判斷一個數是不是奇偶數的問題就會出現錯誤:

function isOdd( value ){
  return value % 2 === 1;
}

console.log(-3);  // false
console.log(-2);  // false複製代碼

正確姿式是:

function isOdd( value ){
  return Math.abs( value % 2 ) === 1;
}

console.log(-3);  // true
console.log(-2);  // false複製代碼

四. String

字符串比較符,是沒法比較變音符和重音符的。

'ä' < 'b'
<false

'á' < 'b'
<false複製代碼

五. Array

建立數組的時候不能用單個數字建立數組。

new Array(2)  // 這裏的一個數字表明的是數組的長度
<[ , , ]

new Array(2,3,4)
<[2,3,4]複製代碼

刪除元素會刪出空格,可是不會改變數組的長度。

var array = [1,2,3,4]
array.length
<4
delete array[1]

array
<[1, ,3,4]
array.length
<4複製代碼

因此這裏的刪除不是很符合咱們以前的刪除,正確姿式是用splice

var array = [1,2,3,4,56,7,8,9]
array.splice(1,3)
array
<[1, 56, 7, 8, 9]
array.length
<5複製代碼

針對數組裏面的空缺,不一樣的遍歷方法行爲不一樣

在 ES5 中:

方法 針對空缺
forEach() 遍歷時跳過空缺
every() 遍歷時跳過空缺
some() 遍歷時跳過空缺
map() 遍歷時跳過空缺,可是最終結果會保留空缺
filter() 去除空缺
join() 把空缺,undefined,null轉化爲空字符串
toString() 把空缺,undefined,null轉化爲空字符串
sort() 排序時保留空缺
apply() 把每一個空缺轉化爲undefined

在 ES6 中:規定,遍歷時不跳過空缺,空缺都轉化爲undefined

方法 針對空缺
Array.from() 空缺都轉化爲undefined
...(擴展運算符有) 空缺都轉化爲undefined
copyWithin() 連空缺一塊兒複製
fill() 遍歷時不跳過空缺,視空缺爲正常的元素
for...of 遍歷時不跳過空缺
entries() 空缺都轉化爲undefined
keys() 空缺都轉化爲undefined
values() 空缺都轉化爲undefined
find() 空缺都轉化爲undefined
findIndex() 空缺都轉化爲undefined p0p0

六. Set 、Map、WeakSet、WeakMap

數據結構 特色
Set 相似於數組,可是成員值惟一,注意(這裏是一個例外),這裏 NaN 等於自身
WeakSet 成員只能是對象,而不能是其餘類型的值。對象的引用都是弱引用,因此不能引用 WeakSet 的成員,不可遍歷它(由於遍歷的過程當中隨時均可以消失)
Map 相似於對象,鍵值對的集合,鍵的範圍不限於字符串,各類類型均可以,是「值—值」的映射,這一點區別於對象的「字符串—值」的映射
WeakMap 於 Map 相似,區別在於它只接受對象做爲鍵名( null 除外),鍵名指向的對象也不計入垃圾回收機制中,它也沒法遍歷,也沒法清空clear

七. 循環

先說一個 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 。那麼這題就錯了,若是把數組裏面的元素變的更加複雜:

var scores = [ 1242351,252352,32143,452354,51455,66125,74217 ];複製代碼

其實這裏答案和數組裏面元素是多少無關。只要數組元素個數是7,最終答案都是17636.571428571428。

緣由是 for-in 循環的是數組下標,因此 total = ‘00123456’ ,而後這個字符串再除以7。

循環方式 遍歷對象 反作用
for 寫法比較麻煩
for-in 索引值(鍵名),而非數組元素 遍歷全部(非索引)屬性,以及繼承過來的屬性(能夠用hasOwnProperty()方法排除繼承屬性),主要是爲遍歷對象而設計的,不適用於遍歷數組
forEach 不方便break,continue,return
for...of 內部經過調用 Symbol.iterator 方法,實現遍歷得到鍵值 不可遍歷普通的對象,由於沒有 Iterator 接口

遍歷對象的屬性,ES6 中有6種方法:

循環方式 遍歷對象
for...in 循環遍歷對象自身的和繼承的可枚舉屬性(不包含Symbol屬性))
Object.key(obj) 返回一個數組,包括對象自身的(不含繼承的)全部可枚舉屬性(不含Symbol屬性)
Object.getOwnPropertyNames(obj) 返回一個數組,包含對象自身的全部屬性(不含 Symbol 屬性,可是包含不可枚舉的屬性)
Object.getOwnPropertySymbols(obj) 返回一個數組,包含對象自身的全部 Symbol 屬性
Reflect.ownKeys(obj) 返回一個數組,包含對象自身的全部屬性,無論屬性名是 Symbol 或者字符串或者是否可枚舉
Reflect.enumerate(obj) 返回一個 Iterator對象,遍歷對象自身的和繼承的全部可枚舉屬性(不包含 Symbol 屬性),與 for...in循環相同

八. 隱式轉換 / 強制轉換 帶來的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 < 0) {
    var tmp = -x;
    ……
 }
 console.log(tmp);  // 10
}複製代碼

這裏 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(); // 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 );
};複製代碼

上面通過編譯過會變成下面這樣子:

function foo() { 
   console.log( 1 );
}
foo(); // 1
foo = 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 (<anonymous>:5:21)
    at <anonymous>: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;
      },
      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();   // undefined
it.next();   // undefined
it.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;
      },
      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();   // 1
it.next();   // 24
it.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 保存在變量中。

sayHiToFriends: function() {
 'use strict';
  var that = this;
  this.friends.forEach(function (friend) {
      console.log(that.name + 'say hi to' + friend);
  });
}複製代碼

第二種:利用bind()函數

使用bind()給回調函數的this綁定固定值,即函數的this

sayHiToFriends: function() {
 'use strict';
  this.friends.forEach(function (friend) {
      console.log(this.name + 'say hi to' + friend);
  }.bind(this));
}複製代碼

第三種:利用 forEach 的第二個參數,把 this 指定一個值。

sayHiToFriends: function() {
 'use strict';
  this.friends.forEach(function (friend) {
      console.log(this.name + 'say hi to' + friend);
  }, this);
}複製代碼

到了 ES6 裏面,建議能用箭頭函數的地方用箭頭函數。

簡單的,單行的,不會複用的函數,都建議用箭頭函數,若是函數體很複雜,行數不少,還應該用傳統寫法。

箭頭函數裏面的 this 對象就是定義時候的對象,而不是使用時候的對象,這裏存在「綁定關係」。

這裏的「綁定」機制並非箭頭函數帶來的,而是由於箭頭函數根本就沒有本身的 this,致使內部的 this 就是外層代碼塊的 this,正由於這個特性,也致使瞭如下的狀況都不能使用箭頭函數:

  1. 不能當作構造函數,不能使用 new 命令,由於沒有 this,不然會拋出一個錯誤。
  2. 不可使用 argument 對象,該對象在函數體內不存在,非要使用就只能用 rest 參數代替。也不能使用 super,new.target 。
  3. 不可使用 yield 命令,不能做爲 Generator 函數。
  4. 不可使用call(),apply(),bind()這些方法改變 this 的指向。

十四. 異步

異步編程有如下幾種:

  1. 回調函數callback
  2. 事件監聽
  3. 發佈 / 訂閱
  4. Promise對象
  5. Async / Await

(這個日記可能一直未完待續......)

相關文章
相關標籤/搜索