JavaScript 新手的踩坑日記

ELFTeresa 7年前發布 | 32K 次閱讀 iOS開發 JavaScript開發 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)

< 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


  

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

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


// 對象轉換成原始值
String(123)
<'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 標準。

由于所有數字都是浮點數,所以這里就會有精度的問題。還記得前段時間網上流傳的機器人的漫畫么?

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

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

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

  • 2個錯誤值:NaN 和 Infinity

  • 2個0,一個+0,一個-0。0是會帶正號和負號。因為正負號和數值是分開存儲的。

typeof NaN
<"number"

(吐槽:NaN 是 “ not a number ”的縮寫,但是它卻是一個數字)

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

NaN === NaN


  

所以不能通過 Array.prototype.indexOf 方法去查找 NaN (因為數組的 indexOf 方法會進行嚴格等的判斷)。

[ NaN ].indexOf( NaN )
<-1

正確的姿勢有兩種:

  • 第一種:

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)<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'





 < 'b'






  

五. 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 中:

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

六. Set 、Map、WeakSet、WeakMap

七. 循環

先說一個 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。

遍歷對象的屬性,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 < 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 (: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;
      },
      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,正因為這個特性,也導致了以下的情況都不能使用箭頭函數:

  • 不能當做構造函數,不能使用 new 命令,因為沒有 this,否則會拋出一個錯誤。

  • 不可以使用 argument 對象,該對象在函數體內不存在,非要使用就只能用 rest 參數代替。也不能使用 super,new.target 。

  • 不可以使用 yield 命令,不能作為 Generator 函數。

  • 不可以使用call(),apply(),bind()這些方法改變 this 的指向。

十四. 異步

異步編程有以下幾種:

  • 回調函數callback

  • 事件監聽

  • 發布 / 訂閱

  • Promise對象

  • Async / Await

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

 

來自:http://www.cocoachina.com/ios/20170619/19573.html

 

 本文由用戶 ELFTeresa 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!