JavaScript 需要檢查變數型別嗎

2dunn發表於2017-04-14

2018.3.1更:

有贊·微商城部門招前端啦,最近的前端hc有十多個,跪求大佬扔簡歷,我直接進行內推實時反饋進度,有興趣的郵件 lvdada#youzan.com,或直接微信勾搭我 wsldd225 瞭解跟多

有贊開源元件庫·zanUI


javascript作為一門動態型別語言,具有很高的動態靈活性,當定義函式時,傳入的引數可以是任意型別。但我們在實際編寫函式邏輯時預設是對引數有一定要求的。這也容易導致預期引數與實際引數不符的情況,從而導致bug的出現。本文在這個層面探討javascript檢查引數的必要性。

為什麼要進行型別檢查?

從兩點常見的場景來看這個問題:

  • 程式中期望得到的值與實際得到的值型別不相符,在對值進行操作的時候程式報錯,導致程式中斷。

舉個我們最常見的呼叫服務端ajax請求取到返回值進行操作的例子:

ajax('/getContent', function (json) {
	
	// json的返回資料形式
	// {data: 18}
	var strPrice = (data.data).toFixed(2);
})
複製程式碼

如果服務端返回的資料形式以及返回的data一定是number型別,我們這樣操作肯定沒有問題。

但是如果服務端返回的資料發生了變化,返回給我們的形式變成了:

{
	data: '18.00'
}
複製程式碼

而我們在js中並沒有對變數做檢測,就會導致程式報錯。

'18.00'.toFixed(2) // Uncaught TypeError: "18.00".toFixed is not a function
複製程式碼
  • 跟第一點相似也是期望得到的值與實際得到的值型別不相符,但是對值操作不會報錯,js利用隱式型別轉換得到了我們不希望得到的值,這種情況會加大我們對bug的追蹤難度。

舉一個也是比較常見的例子:

/**
* input1 [number]
* input2 [number]
* return [number]
**/
function sumInput (input1, input2) {
	return input1 + input2;
}
複製程式碼

sumInput方法的兩個入參值可能來自外界使用者輸入,我們無法保證這是一個正確的number型別值。

sumInput(1, ''); // return '1'
複製程式碼

sumInput方法本來期望得到number型別的值,但是現在卻得到了string型別的'1' 。雖然值看起來沒有變化,但是如果該值需要被其他函式呼叫,就會造成未知的問題。

再舉一個罕見的例子:

parseInt()方法要求第一個引數是string型別,若不是,則會隱式轉換成string型別。

parseInt(0.0000008) // 8
複製程式碼

匪夷所思吧?我們預計這個方法的結果應該是0,但結果卻是8。在程式中我們無法捕獲這個錯誤,只能隱沒在流程中,最終的計算結果我們也無法確保正確。

原因是parseInt(0.0000008)會變成parseInt("8e-7"),結果輸出8

型別檢查原則

由於js語言的動態性,以及本身就沒有對型別做判斷的機制,我們是否需要對所有變數值進行型別判斷?這樣做無疑增加了編碼的冗餘度,且無需對變數型別做檢查也正是動態語言的一個優勢。

那為了避免一些由此問題帶來的bugs,我們需要在一些關鍵點進行檢查,而關鍵點更多的是由業務決定的,並沒有一個統一的原則指導我們哪裡必須進行型別判斷。

但大體趨勢上可以參考以下我總結的幾點意見。

一、「返回值」呼叫外部方法獲取的值需要對型別做判斷,因為我們對方法返回的值是有期望值型別,但是卻不能保證這個介面返回的值一直是同一個型別。

換個意思講就是我們對我們不能保證的,來源於外部的值都要保持一顆敬畏之心。這個值可能來自第三方工具函式的返回值,或者來自服務端介面的返回值,也可能是另一位同事寫的抽離公共方法。

二、「入參」在書寫一個函式並給外部使用的時候,需要對入參做較嚴格的型別判斷。

這裡強調的也是給外部使用的場景,我們在函式內部會對入參做很多邏輯上的處理,如果不對入參做判斷,我們無法確保外部使用者傳入的到底是什麼型別的引數。

三、「自產自銷」除了以上兩類與外部互動的場景,更多需要考慮的是我們在編寫業務程式碼時,“自產自銷”的變數該如何處理。

解釋一下“自產自銷”的意思,在編寫業務程式碼時,我們會根據業務場景定義很多函式,以及會呼叫函式取返回值。在這個過程中會有入參的情況,而這些引數完全是自己編寫自己使用,在這種對程式碼相對了解的前提下無條件的進行變數型別判斷無疑會增加編碼的複雜度。

在實際編碼中我們更多的會使用強制型別轉換[Number String Boolean]對引數進行操作,轉換成我們期望的型別值。具體的方式會在下一章節闡述。

如何處理和反饋變數型別與期望不符的情況

首先談談如何判斷變數型別,我們可以使用原生js或者es6的語法對型別進行準確判斷,但更多的可以使用工具庫,類似於lodash。包含了常用的isXXX方法。

  • isNumber
  • isNull
  • isNaN
  • ...

對變數進行型別判斷後,我們該如何進行處理及反饋?

  • 「靜默處理」只對符合型別預期的值進行處理,不符合預期的分支不做拋錯處理。這樣做可以防止程式報錯,不阻塞其他與之無關的業務邏輯。
if (isNumber(arg)) {
	xxx
} else {
	console.log('xxx 步驟 得到的引數不是number型別');
}
複製程式碼
  • 「拋錯誤」不符合預期的分支做拋錯處理,阻止程式執行。
if (isNumber(arg)) {
	xxx
} else {
	throw new TypeError(arg + '不是number型別');
}
複製程式碼
  • 「強制轉換」將不符合預期的值強制轉換成期望的型別。
if (isNumber(arg)) {
    (arg).toFixed(2);
} else {
    toNumber(arg).toFixed(2);
}

//但是強制轉換更多的在我們對變數型別教有掌控力的前提下使用,所以我們不會進行判斷,直接在邏輯中進行強制轉換。
toNumber(arg).toFixed(2);

複製程式碼

以上三種途徑是我們在對變數進行型別判斷後積極採取反饋的通用做法。那麼結合上一章提到的3大型別檢查原則,我們分別是採用哪種做法?

「返回值」呼叫外部函式、介面得到的引數該如何處理反饋?

對於由外部介面得到的值,我們沒法確保這個型別是永恆的。所以進行型別判斷很有必要,但是究竟是採用「靜默處理」、「拋錯誤中斷」還是「強制轉換型別」呢?這裡還是需要根據具體場景具體業務採用不同的方式,沒有一個恆定的解決方案。

看個例子:

// 業務程式碼入口
function main () {
	
	// 監控程式碼 與業務無關
	(function () {
		var shopList = getShopNameList(); // return undefined
		Countly.push(shopList.join()); // Uncaught TypeError: Cannot read property 'join' of undefined
	})()

	// 業務程式碼
	todo....
}
複製程式碼

上述例子中的我們呼叫了一個外部函式getShopNameList, 在對其返回值進行操作時與主要業務邏輯無關的程式碼塊出錯,會直接導致程式中斷。而對shopList進行判斷後靜默處理,也不會影響到主要業務的執行,所以這種情況是適合「靜默處理」的。靜默處理的最大優勢在於可以防止程式報錯,但是使用的前提是這步操作不會影響其他相關聯的業務邏輯。

如果被靜默處理的值與其他業務邏輯還有關聯,那麼整條邏輯的最終值都會受到影響,但是我們又靜默掉了錯誤資訊,反而會增加了尋找bug的難度。

// 業務程式碼入口
function main () {
	
	// 監控程式碼 與業務無關
	(function () {
		var shopList = getShopNameList(); // return undefined
		if (isArray(shopList)) {
			Countly.push(shopList.join());
		}
	})()

	// 業務程式碼
	todo....
}
複製程式碼

當然除了「靜默處理」外我們還可以選擇「強制轉換」,將返回值轉換成我們需要的值型別,完成邏輯的延續。

// 業務程式碼入口
function main () {
	
	// 監控程式碼 與業務無關
	(function () {
		var shopList = getShopNameList(); // return undefined
		Countly.push(isArray(shopList) ? shopList.join() : '');
	})()

	// 業務程式碼
	todo....
}
複製程式碼

「入參」在書寫一個函式並給外部使用的時候,對入參該如何處理反饋?

當我們寫一個函式方法提供給除自己之外的人使用,或者是在編寫前端底層框架、UI元件,提供給外部人員使用,我們對入參(外部使用者輸入)應該要儘可能的檢查詳細。因為是給外部使用,我們無法知道業務場景,所以使用「靜默處理」是不合適的,我們無法知道靜默處理的內容與其他業務邏輯有否有耦合,既然靜默了最終還是會導致bugs出現,還不如直接「拋錯誤」提醒使用者。

在第三方框架中,都會自定義一個類似於warn的方法用於丟擲變數檢查的不合法結果。而且為了防止檢查程式碼的增加而導致的線上程式碼量的增加,通常檢查過程都會區分本地開發環境和線上生產環境。


// 程式碼取自vue原始碼
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    warn(
      'Invalid default value for prop "' + key + '": ' +
      'Props with type Object/Array must use a factory function ' +
      'to return the default value.',
      vm
    )
  }
複製程式碼

這段判斷指令碼結合webpack構建生產環境的程式碼時就會被刪除,不會增加生產環境的程式碼量。

vue框架的元件系統中對元件傳參的行為vue在框架層面上就支援了檢查機制。如果傳入的資料不符合規格,vue會發出警告。

Vue.component('example', {
  props: {
    // 基礎型別檢測 (`null` 意思是任何型別都可以)
    propA: Number,
    // 多種型別
    propB: [String, Number],
    // 必傳且是字串
    propC: {
      type: String,
      required: true
    },
    // 數字,有預設值
    propD: {
      type: Number,
      default: 100
    },
    // 陣列/物件的預設值應當由一個工廠函式返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定義驗證函式
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})
複製程式碼

因為我們編寫vue元件也會提供給他人是使用,也屬於與外部互動的場景。Vue在框架層面整合了檢查功能,也方便了我們開發者再手動檢查引數變數了。

「自產自銷」除了以上兩類與外部互動的場景,更多需要考慮的是我們在編寫業務程式碼時,“自產自銷”的變數該如何處理?

外部互動的場景,我們對入參以及返回值具有不可控性,但對於開發者開發業務時的場景,傳參時,或者是函式返回值,都是我們自己定義的,相對具有很強的可控性。

規定引數型別是string字串時,我們大概率不會傳入一個陣列,而且變數的值也不會由外部環境的變化而變化(ajax返回的引數,外部介面返回的引數,型別可能會變)。

那麼剩下的情況大部分會集中在js標量基礎型別值。

  • 規定傳入number 13,我們傳入了string '13'
  • 規定傳入boolean true,我們傳入了真值 '123'
  • ...

針對這種情況,我們對入參的值具有一定的可預期性,預期型別可能不同,為了程式的健壯性,可讀性更高,更容易使協作同學理解,我們一般採用「強制轉換」將值轉換成我們期望的型別。即使「強制轉換」的過程中程式發生了報錯從而中斷,這也是在除錯過程中產生程式中斷問題,也能更好的提前暴露這個問題,避免線上上環境發生bugs。

function add(num1, num2) {
	return (toNumber(num1) + toNumber(num2))
}
add('123', '234');
複製程式碼
  • toInteger
  • toNumber
  • toString
  • toSafeInteger
  • !!(toBoolean)

隱式強制型別轉換會踩到哪些坑?

因為js會默默的進行隱式型別轉換,所以多數坑都是發生在對值的操作過程中發生了隱式型別轉換。

另外型別轉換越清晰,可讀性越高,更容易理解。

  • string型數字呼叫toFixed()方法報錯
'123'.toFixed(2) // Uncaught TypeError: "123".toFixed is not a function
複製程式碼
  • + 法中有字串出現則操作變成字串拼接
function add(num1, num2) {
	return num1 + num2
}
add(123, ''); //  return string '123'
複製程式碼
  • 當我們使用==進行值相等判斷的時候兩邊的值會進行隱式強制型別轉換,而轉換的結果往往不盡人意。

function test(a) {
	if (a == true) { // 不推薦
		console.log('true')
	} else {
		console.log('false')		
	}
}
test('22')  // 'false'

// 原因
'22' == true

兩邊都會發生隱式強制轉換,'22' --> 22 , true --> 1, 
因此 22 == 1  // false
複製程式碼
function test(a) {
	if (a == '') {
		console.log('true')
	} else {
		console.log('false')		
	}
}
test(0)  // 'true'

// 原因
0 == ''

字串會發生隱式型別轉轉 '' --> 0
因此 0 == 0 // true

相同的場景還有

[] == 0 // true
[] == '' // true
複製程式碼

所以當我們進行相等判斷時涉及到[], 0, '', boolean,不應該使用==,而應該採用===,杜絕發生隱式強制型別轉換的操作。

全域性環境如何做到變數的型別檢查?

依靠開發者進行引數變數的型別檢查,非常考驗js開發者的js基礎功,尤其在團隊協作下很難做到完美的型別檢查。vue2的原始碼開發使用了flow協助進行型別檢查。

Flow 是一個facebook出品靜態型別檢測工具;在現有專案中加上型別標註後,可以在程式碼階段就檢測出對變數的不恰當使用。Flow 彌補了 JavaScript 天生的型別系統缺陷。利用 Flow 進行型別檢查,可以使你的專案程式碼更加健壯,確保專案的其他參與者也可以寫出規範的程式碼;而 Flow 的使用更是方便漸進式的給專案加上嚴格的型別檢測。

// @flow
function getStrLength(str: string): number{ 
    return str.length; 
}
getStrLength('Hello World'); 
複製程式碼

另外還有微軟出品的TypeScript,採用這門js超集程式語言也能開發具有靜態型別的js應用。

  • TypeScript 增加了程式碼的可讀性和可維護性,可以在編譯階段就發現大部分錯誤,這總比在執行時候出錯好。
  • TypeScript 是 JavaScript 的超集,.js 檔案可以直接重新命名為 .ts 即可
  • 有一定的學習成本,需要理解介面(Interfaces)、泛型(Generics)、類(Classes)、列舉型別

總結

本文從3個型別檢查原則「返回值」「入參」「自產自銷」為出發點,分別闡述了這三種情況下的處理方法「靜默處理」「拋錯誤」「強制轉換」。本文闡述的是一種思路,這三種處理方法其實在各個原則中都會使用,最重要的還是取決於業務的需求和理解。但是儘量的對變數型別做檢查是沒有錯的!

本文來自二口南洋,有什麼需要討論的歡迎找我。

相關文章