当NaN遇上null:一文看懂JavaScript的类型迷宫

88 阅读4分钟

引言

JavaScript作为一种灵活的编程语言,其类型系统是开发者必须深入理解的基础知识。本文将详细剖析JavaScript的数据类型体系,特别关注简单类型与复杂类型的区别,以及类型判断的各种方法和陷阱。

JavaScript的类型体系

JavaScript共有七种数据类型,可分为两大类:

简单数据类型(Primitive Types)

简单数据类型包括:

  • string(字符串)
  • boolean(布尔值)
  • null(空值)
  • undefined(未定义)
  • symbol(符号,ES6新增)
  • numeric(数值类型,包括number和bigint)

其中numeric又可细分为:

  • number:常规数字
  • bigint:大整数(ES2020引入)

复杂数据类型(Reference Types)

复杂数据类型只有一种:

  • object(对象)

但object包含多种形式:普通对象、数组、函数、日期、正则表达式等。

内存模型:简单类型vs复杂类型

简单数据类型

// 简单数据类型存储在栈内存中
let a = 10;
let b = a;  // 值拷贝
b = 20;     // 修改b不影响a
console.log(a); // 10

简单数据类型具有以下特点:

  1. 存储在栈内存中
  2. 拷贝式赋值(值传递)
  3. 固定大小,访问迅速

复杂数据类型

// 复杂数据类型的引用存储在栈内存,数据存储在堆内存
let obj1 = {name: '张三'};
let obj2 = obj1;  // 引用传递
obj2.name = '李四'; // 修改obj2同时影响obj1
console.log(obj1.name); // 李四

复杂数据类型具有以下特点:

  1. 引用(地址)存储在栈内存中
  2. 实际数据存储在堆内存中
  3. 引用式赋值(地址传递)
  4. 大小可变

类型判断

typeof运算符

从提供的代码中,我们可以看到typeof是判断类型的基础工具:

function add(a,b){
    // 参数的校验
    if(typeof a !== 'number' || typeof b !== 'number' || isNaN(a) || isNaN(b)){
        throw new TypeError('参数必须是数字');
    }
    return a+b;
}

typeof运算符返回类型的字符串表示:

typeof返回值
'hello''string'
123'number'
true'boolean'
undefined'undefined'
Symbol()'symbol'
123n'bigint'
{}'object'
null'object'(这是一个历史遗留bug)
function(){}'function'

typeof的局限性

  1. 无法识别nulltypeof null返回'object'而非'null'
  2. 无法区分具体的对象类型:无法区分Array、Date等具体对象类型

增强的类型判断

// 对null的判断
function isNull(value) {
    return value === null;
}

// 使用instanceof判断对象类型
function isArray(value) {
    return value instanceof Array;
}

// 更可靠的数组判断
function isArrayReliable(value) {
    return Object.prototype.toString.call(value) === '[object Array]';
}

Numeric类型详解

JavaScript的numeric类型包括两种子类型:

Number类型

Number类型包括整数和浮点数,可表示的范围是±(2^53-1)。

let integer = 100;
let float = 3.14;
let scientific = 1e6;  // 科学记数法:1000000

特殊的Number值:

  • Infinity:无穷大
  • -Infinity:负无穷大
  • NaN:非数值(Not a Number)

BigInt类型

为了解决Number类型表示大整数的限制,ES2020引入了BigInt类型:

const bigNumber = 9007199254740991n;  // 末尾添加n表示BigInt
const result = bigNumber + 1n;  // BigInt只能与BigInt运算

BigInt特点:

  1. 可以表示任意精度的整数
  2. 不能与Number类型直接运算
  3. 不能使用Math对象方法

NaN的本质解析

从提供的代码中,我们可以看到NaN的多种产生方式:

console.log(0/0);  // NaN
console.log(Math.sqrt(-1));  // NaN
console.log(parseInt("123"), parseInt("a123"));  // 123, NaN
console.log(Number(undefined));  // NaN

NaN的特性

  1. NaN是Number类型typeof NaN返回'number'
  2. NaN不等于任何值,包括它自身NaN === NaN返回false
  3. 多种方式可产生NaN
    • 非法数学运算(如0/0)
    • 无法转换为数字的字符串转换
    • 对undefined进行数值转换
  4. 检测NaN:需要使用isNaN()函数或ES6的Number.isNaN()
console.log(NaN === NaN);  // false
console.log(isNaN(NaN), isNaN(0/0));  // true true

// ES6提供的更精确的方法
console.log(Number.isNaN(NaN));  // true
console.log(Number.isNaN('NaN'));  // false,不进行类型转换

isNaN与Number.isNaN的区别

isNaN()会先尝试将参数转换为数字,而Number.isNaN()不会进行转换:

console.log(isNaN('abc'));  // true,'abc'转换为数字失败,结果是NaN
console.log(Number.isNaN('abc'));  // false,'abc'不是NaN

实际应用:健壮的类型检查

基于以上知识,我们可以编写更健壮的类型检查函数:

function add(a, b) {
    // 参数的校验
    if(typeof a !== 'number' || typeof b !== 'number' || isNaN(a) || isNaN(b)) {
        throw new TypeError('参数必须是数字');
    }
    return a + b;
}

// 测试
try {
    console.log(add(1, 2));  // 3
    console.log(add(1, 'a'));  // 抛出TypeError
    console.log(add(1, NaN));  // 抛出TypeError
} catch(e) {
    console.error(e.message);
}

总结

JavaScript的类型系统虽然简单,但也有其复杂之处和需要注意的细节:

  1. 简单类型:存储在栈中,值传递,包括string、boolean、null、undefined、symbol和numeric(number和bigint)
  2. 复杂类型:引用存储在栈中,数据存储在堆中,引用传递
  3. 类型判断:typeof适合大多数简单类型判断,但对null和具体对象类型的判断需要其他方法
  4. NaN:虽然名为"非数字",但它是number类型的一个特殊值,具有独特的比较规则

深入理解JavaScript的类型系统,不仅能帮助我们编写更健壮的代码,还能避免许多常见的类型相关陷阱。