本文深入解析JavaScript中类型检查的常见陷阱。重点讨论typeof对null和数组的误判、instanceof的原型链机制,以及利用虚值(undefined、false等)进行属性检查时的逻辑错误。通过对比传统方法与现代特性(如??操作符、in运算符),提供准确的类型验证方案。
JavaScript动态类型概述
JavaScript的动态类型机制具有两面性:其优势在于变量无需声明具体类型,但劣势在于开发者难以直接确定变量的实际类型。
typeof运算符可用于识别JavaScript中的6种基本类型:
typeof运算符基础
typeof 10; // => 'number'
typeof 'Hello'; // => 'string'
typeof false; // => 'boolean'
typeof { a: 1 }; // => 'object'
typeof undefined; // => 'undefined'
typeof Symbol(); // => 'symbol'
与此同时,instanceof运算符用于检测构造函数的prototype属性是否存在于实例对象的原型链中。
instanceof运算符原理
class Cat { }
const myCat = new Cat();
myCat instanceof Cat; // => true
然而,typeof和instanceof的部分行为可能引发混淆,因此有必要提前了解一些边缘情况以避免错误。
1. typeof null
表达式typeof myObject === 'object'可用于判断myObject是否为对象类型。示例如下:
typeof null的陷阱
const person = { name: '前端小智' };
typeof person; // => 'object'
由于person是一个普通的JavaScript对象,typeof person返回'object'。
在某些场景下,变量值需要明确指定为null,常见情况包括:
可以使用null来跳过配置对象中的某些指示;使用null初始化稍后保存对象的变量;当函数因故无法构造对象时返回null。例如,若正则表达式无匹配项,str.match(regExp)方法将返回null:
const message = 'Hello';
message.match(/Hi/); // => null
由此引出一个问题:能否使用typeof区分有值的对象和值为null的对象?
let myObject = null;
typeof myObject; // => 'object'
myObject = { prop: 'Value' };
typeof myObject; // => 'object'
如上所示,无论对象有值还是值为null,typeof返回的结果均为'object'。
可采用如下方法检测变量是否为非null的对象:
function isObject(value) {
return typeof value === 'object' && value !== null;
}
isObject({}); // => true
isObject(null); // => false
除了检查value是否为object(typeof value === 'object')外,还需明确验证其不为null(value !== null)。
2. typeof array
若试图检测变量是否包含数组,常见的错误是直接使用typeof操作符:
数组类型检测
const colors = ['white', 'blue', 'red'];
typeof colors; // => 'object'
检测数组的正确方法是使用Array.isArray():
const colors = ['white', 'blue', 'red'];
const hero = { name: 'Batman' };
Array.isArray(colors); // => true
Array.isArray(hero); // => false
Array.isArray(colors)返回布尔值true,表明colors是一个数组。
3.虚值类型检查
JavaScript中的undefined是一个特殊值,表示变量未被初始化。
若访问未初始化的变量或不存在的对象属性,获取到的值为undefined:
虚值与属性检查
let city;
let hero = { name: '前端小智', villain: false };
city; // => undefined
hero.age; // => undefined
访问未初始化的变量city和不存在的属性hero.age,结果均为undefined。
要检查属性是否存在,可在条件中使用object[propName],但当值为虚值或undefined时,这种做法不可靠:
function getProp(object, propName, def) {
// 错误方式
if (!object[propName]) {
return def;
}
return object[propName];
}
const hero = { name: '前端小智', villain: false };
getProp(hero, 'villain', true); // => true
hero.villain; // => false
若对象中不存在propName,object[propName]的值为undefined。使用if (!object[propName]) { return def }来保护缺失属性的做法存在缺陷。
hero.villain属性存在且值为false,但该函数在访问villain值时错误地返回true:getProp(hero, 'villain', true)。
undefined属于虚值,此外false、0、''和null也均为虚值。
不应使用虚值进行类型检查,而应明确验证属性是否存在于对象中:
typeof object[propName] === 'undefined'
propName in object
object.hasOwnProperty(propName)
接下来,改进getProp()函数:
function getProp(object, propName, def) {
// Better
if (!(propName in object)) {
return def;
}
return object[propName];
}
const hero = { name: '前端小智', villain: false };
getProp(hero, 'villain', true); // => false
hero.villain; // => false
使用if (!(propName in object)) { ... }条件可正确判断属性是否存在。
4. 逻辑运算符
建议避免使用逻辑运算符||作为默认值处理,因为这容易打断阅读流程:
逻辑运算符与默认值处理
const hero = { name: '前端小智', villain: false };
const name = hero.name || 'Unknown';
name; // => '前端小智'
hero.name; // => '前端小智'
// 不好方式
const villain = hero.villain || true;
villain; // => true
hero.villain; // => false
hero对象存在属性villain,值为false,但表达式hero.villain || true的结果为true。
当逻辑操作符||用于访问属性默认值时,若属性存在且具有虚值,该操作符无法正确工作。
若要在属性不存在时设置默认值,更好的选择是使用新的双问号(??)操作符:
const hero = { name: '前端小智', villan: false };
// 好的方式
const villain = hero.villain ?? true;
villain; // => false
hero.villain; // => false
或使用解构赋值:
const hero = { name: '前端小智', villain: false };
// Good
const { villain = true } = hero;
villain; // => false
hero.villain; // => false
5. typeof NaN
整数、浮点数及特殊数字(如Infinity、NaN)的类型均为number。
typeof NaN的处理
typeof 10; // => 'number'
typeof 1.5; // => 'number'
typeof NaN; // => 'number'
typeof Infinity; // => 'number'
NaN是在无法创建数字时生成的特殊数值,代表not a number。
在以下情况下无法创建有效数字:
Number('oops'); // => NaN
5 * undefined; // => NaN
Math.sqrt(-1); // => NaN
NaN + 10; // => NaN
由于NaN意味着数字操作失败,因此对数字有效性的检查需要额外步骤。
以下isValidNumber()函数也可防止由NaN导致的错误:
function isValidNumber(value) {
// Good
return typeof value === 'number' && !isNaN(value);
}
isValidNumber(Number('Z99')); // => false
isValidNumber(5 * undefined); // => false
isValidNumber(undefined); // => false
isValidNumber(Number('99')); // => true
isValidNumber(5 + 10); // => true
除了检查typeof value === 'number'外,还需验证!isNaN(value)以确保结果准确。
6. instanceof和原型链
JavaScript中的每个对象都引用一个特殊函数:对象的构造函数。
object instanceof Constructor是用于检查对象构造函数的运算符:
instanceof与原型链
const object = {};
object instanceof Object; // => true
const array = [1, 2];
array instanceof Array; // => true
const promise = new Promise(resolve => resolve('OK'));
promise instanceof Promise; // => true
现在,定义一个父类Pet及其子类Cat:
class Pet {
constructor(name) {
this.name;
}
}
class Cat extends Pet {
sound = 'Meow';
}
const myCat = new Cat('Scratchy');
接下来,尝试确定myCat的实例类型。
myCat instanceof Cat; // => true
myCat instanceof Pet; // => true
myCat instanceof Object; // => true
instanceof运算符显示myCat是Cat、Pet甚至Object的实例。
instanceof操作符通过整个原型链搜索对象的构造函数。要准确检测创建对象的构造函数,需检测constructor属性。
myCat.constructor === Cat; // => true
myCat.constructor === Pet; // => false
myCat.constructor === Object; // => false
仅当myCat.constructor === Cat计算结果为true时,表示Cat是myCat实例的构造函数。
7. 总结
运算符typeof和instanceof用于类型检查,尽管易于使用,但需注意一些特殊情况。
需注意:typeof null等于'object'。要确定变量包含非null对象,需显式指明null:
typeof myObject === 'object' && myObject !== null。检查变量是否包含数组的最佳方法是使用Array.isArray(variable)内置函数。
由于undefined是虚值,我们常在条件句中直接使用它,但这种做法易出错。更好的选择是使用prop in object来验证属性是否存在。
使用双问号操作符object.prop ?? def 或 { prop = def } = object 来访问可能丢失的属性。
NaN是类型为number的特殊值,由无效数字操作创建。为确保变量具有正确数字,建议使用更详细的验证:!isNaN(number) && typeof number === 'number'。
最后,请记住instanceof通过prototype链搜索实例的构造函数。若不了解此机制,使用父类验证子类实例时可能得到错误结果。
代码部署后可能存在的BUG无法实时知晓,事后为解决这些BUG需花费大量时间进行log调试。此处推荐一个有用的BUG监控工具Fundebug。