JavaScript 需要检查变量类型吗

2,459 阅读13分钟

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个类型检查原则「返回值」「入参」「自产自销」为出发点,分别阐述了这三种情况下的处理方法「静默处理」「抛错误」「强制转换」。本文阐述的是一种思路,这三种处理方法其实在各个原则中都会使用,最重要的还是取决于业务的需求和理解。但是尽量的对变量类型做检查是没有错的!

本文来自二口南洋,有什么需要讨论的欢迎找我。