一、为什么数据类型是必过的一道坎
上一篇我们讲了原型链和面向对象,那是 JavaScript 的“组织方式”。这一篇要讲的是 JavaScript 的“基本粒子”——数据类型。
你写的每一行 JavaScript 代码,都在和数据打交道。变量存的是数据,函数处理的是数据,表达式计算的是数据。如果你不清楚数据在底层是如何存储、如何传递、如何比较的,你就会在某个深夜对着一个 typeof null === 'object' 的结果陷入沉思,或者被 0.1 + 0.2 !== 0.3 逼疯。
本篇的目标,就是把这些让人困惑的问题一个一个拆开,从底层原理到实战技巧,彻底讲清楚。
二、两种截然不同的存储方式:栈与堆
JavaScript 的数据类型虽多,但从存储方式上只分两类,这条分界线比任何官方分类都重要。
基本类型:存在栈里,按值访问
基本类型的数据直接存储在栈内存中。当你把一个基本类型的值赋给一个变量时,这个变量的内存空间里存的就是值本身。
let a = 10;
let b = a; // 把 a 的值复制一份给 b
a = 20;
console.log(b); // 10 —— b 不受 a 影响,因为 b 存的是独立的副本
基本类型有七种:string、number、boolean、null、undefined、symbol、bigint。
引用类型:存在堆里,按引用访问
对象(包括数组、函数)的数据存储在堆内存中。变量里存的不是对象本身,而是一个指向堆内存中该对象的地址(引用)。
let objA = { name: '张三' };
let objB = objA; // 把 objA 存的地址复制给 objB,两个变量指向同一个对象
objA.name = '李四';
console.log(objB.name); // '李四' —— objB 也变了!因为它俩指向同一个对象
这就是“引用类型”名字的由来——变量存储的是引用(地址),不是值本身。这解释了为什么修改 objA 会影响 objB,也解释了为什么两个内容相同的对象用 === 比较却返回 false(因为它们比较的是地址,不是内容)。
一张对比表
| 特性 | 基本类型 | 引用类型 |
|---|---|---|
| 存储位置 | 栈内存(值本身) | 堆内存(变量存地址) |
| 赋值行为 | 复制值,互不影响 | 复制地址,指向同一对象 |
| 比较行为 | 比较值 | 比较地址(引用) |
| 可变性 | 不可变(修改会创建新值) | 可变(可以直接修改属性) |
| 包含哪些 | string, number, boolean, null, undefined, symbol, bigint | Object, Array, Function, Date, RegExp 等 |
三、typeof:类型检测的第一把尺子
typeof 是检测数据类型最常用的操作符。它返回一个表示类型的字符串。但 typeof 有几个著名的坑,必须逐个说清楚。
基本用法
typeof 'hello'; // 'string'
typeof 42; // 'number'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Symbol(); // 'symbol'
typeof 123n; // 'bigint'
typeof {}; // 'object'
typeof []; // 'object'
typeof null; // 'object' ← 最著名的坑
typeof function(){}; // 'function'
坑一:typeof null === 'object'
这是 JavaScript 诞生之初就存在的 bug,而且因为历史原因无法修复。在 JavaScript 的底层实现中,值的类型信息存储在二进制的前几位。null 的二进制表示恰好全是 0,而当时判断对象的逻辑是“如果前三位是 0,就是对象”。于是 null 被错误地归类为 object。
如何正确判断 null:
let value = null;
// 不能用 typeof,要直接用 === 比较
if (value === null) {
console.log('这是 null');
}
坑二:typeof [] === 'object'
数组本质上也是对象,所以 typeof 无法区分数组和普通对象。要判断一个值是不是数组,用 Array.isArray():
Array.isArray([]); // true
Array.isArray({}); // false
坑三:typeof NaN === 'number'
NaN 的意思是“不是数字”(Not a Number),但它的类型却是 number。这句话听起来矛盾,但逻辑是通顺的:NaN 是数字类型中的一个特殊值,表示“这个数值无法表示”(比如 0 除以 0 的结果)。
判断一个值是不是 NaN 需要用 Number.isNaN():
Number.isNaN(NaN); // true
Number.isNaN('hello'); // false —— 全局的 isNaN() 会先把参数转成数字,不要用它
注意区分:全局函数 isNaN() 会先把参数转换成数字再判断,这导致 isNaN('hello') 返回 true(因为 'hello' 转数字是 NaN)。ES6 引入的 Number.isNaN() 不会做类型转换,更准确。
四、instanceof:沿着原型链向上查找
instanceof 用来判断一个对象是否是某个构造函数的实例。它的工作机制是:沿着对象的原型链向上查找,看是否能找到该构造函数的 prototype 属性。
[] instanceof Array; // true —— [] 的原型链上有 Array.prototype
[] instanceof Object; // true —— [] 的原型链上也有 Object.prototype
{} instanceof Object; // true
new Date() instanceof Date; // true
function Foo() {}
let f = new Foo();
f instanceof Foo; // true
f instanceof Object; // true
instanceof 的局限
- 不能判断基本类型:
42 instanceof Number返回false,因为42是基本类型,不是对象。只有new Number(42)这样的包装对象才能被instanceof识别。 - 跨 iframe 或跨窗口失效:不同窗口有各自独立的全局执行环境,它们的
Array.prototype是不同的对象。instanceof无法识别来自另一个窗口的数组。对于这种场景,用Array.isArray()。
更精确的类型检测:Object.prototype.toString.call()
这是一个“终极”类型检测方法,可以精确区分所有内置类型:
Object.prototype.toString.call('hello'); // '[object String]'
Object.prototype.toString.call(42); // '[object Number]'
Object.prototype.toString.call(true); // '[object Boolean]'
Object.prototype.toString.call(null); // '[object Null]'
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call({}); // '[object Object]'
Object.prototype.toString.call(function(){});// '[object Function]'
Object.prototype.toString.call(new Date()); // '[object Date]'
Object.prototype.toString.call(/regex/); // '[object RegExp]'
Object.prototype.toString.call(new Map()); // '[object Map]'
Object.prototype.toString.call(new Set()); // '[object Set]'
Object.prototype.toString.call(Symbol()); // '[object Symbol]'
这个方法内部调用了 [[Class]] 内部属性(规范中的叫法),返回一个格式为 [object Type] 的字符串。它比 typeof 精确,比 instanceof 适用范围广,是编写工具库时常用的类型判断手段。
五、类型转换:隐式转换的规则与陷阱
JavaScript 是弱类型语言——变量不绑定类型,不同类型之间可以自动转换。这种灵活性是双刃剑:写起来快,读起来容易踩坑。
转换为字符串
加号 + 只要有一侧是字符串,就会触发字符串拼接:
console.log('5' + 3); // '53' —— 数字 3 被转为字符串
console.log('5' + true); // '5true'
console.log('5' + null); // '5null'
console.log('5' + undefined); // '5undefined'
console.log('5' + {}); // '5[object Object]'
console.log('5' + [1,2]); // '51,2' —— 数组先转为字符串 '1,2'
转换为数字
减号 -、乘号 *、除号 / 会尝试把操作数转为数字:
console.log('5' - 3); // 2 —— '5' 转为数字 5
console.log('5' * '2'); // 10
console.log('5' - 'abc'); // NaN —— 'abc' 无法转为数字
console.log(true + 1); // 2 —— true 转为 1
console.log(false + 1); // 1 —— false 转为 0
console.log(null + 1); // 1 —— null 转为 0(这是一个常见陷阱!)
console.log(undefined + 1); // NaN —— undefined 转为 NaN
重点记忆:null 转数字是 0,undefined 转数字是 NaN。它们的转换结果不同,是很多 bug 的来源。
转换为布尔值
在条件判断(if、while、三元运算符)中,值会被转为布尔值。以下六个值转为 false,其余所有值都转为 true:
false0''或""(空字符串)nullundefinedNaN
这六个值统称为 falsy(假值)。其他所有值(包括空数组 []、空对象 {}、字符串 '0'、字符串 'false')都是 truthy(真值)。
if ([]) { console.log('空数组是真值'); } // 会执行
if ({}) { console.log('空对象是真值'); } // 会执行
if ('0') { console.log('字符串0是真值'); } // 会执行
if ('false') { console.log('字符串false是真值'); } // 会执行
== vs ===:松散相等与严格相等
===(严格相等)不会做类型转换,类型不同直接返回 false。
==(松散相等)会先做类型转换再比较。它的转换规则非常复杂,这里是几个最坑的例子:
console.log(0 == false); // true —— false 转数字为 0
console.log('' == false); // true —— 两边都转数字,'' 转 0,false 转 0
console.log(null == undefined);// true —— 规范明确规定它们相等
console.log(null == 0); // false —— null 只等于 undefined 和自己
console.log([] == false); // true —— [] 转字符串 '',再转数字 0
console.log([] == 0); // true —— [] 转字符串 '',再转数字 0
console.log([1,2] == '1,2'); // true —— 数组转字符串为 '1,2'
原则:始终使用 ===,除非你完全清楚 == 的转换规则并且有意利用它(比如判断 value == null 可以同时匹配 null 和 undefined)。
六、浮点数精度:0.1 + 0.2 !== 0.3
这是 JavaScript 中最经典的数值陷阱,但它不是 JavaScript 的 bug,而是所有使用 IEEE 754 标准的语言共有的特性。
原因
计算机用二进制存储数字。十进制的 0.1 和 0.2 在二进制中是无限循环小数,就像十进制的 1/3 是 0.33333… 无限循环一样。计算机只能用有限的位数来近似表示它们。当两个近似值相加时,误差累积,结果就不等于精确的 0.3。
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
解决方案
方案一:使用容差比较
不要求绝对相等,而是判断差值是否在一个极小范围内:
function isApproximatelyEqual(a, b, epsilon = 1e-10) {
return Math.abs(a - b) < epsilon;
}
console.log(isApproximatelyEqual(0.1 + 0.2, 0.3)); // true
ES6 引入了 Number.EPSILON,代表 JavaScript 能表示的最小精度差值:
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true
方案二:将小数转为整数计算
这在金额计算中常用——把元转成分(整数),计算完再转回元:
let result = (0.1 * 100 + 0.2 * 100) / 100; // 0.3
console.log(result); // 0.3
方案三:使用第三方库
对于高精度需求(如金融计算),使用 decimal.js 或 big.js 等库,它们用字符串或大整数来模拟精确的十进制运算。
七、Symbol 和 BigInt:ES6 后的两个新成员
Symbol:独一无二的值
Symbol 是 ES6 引入的基本类型。每个 Symbol() 调用返回的值都是独一无二的,即使传入相同的描述字符串:
let sym1 = Symbol('id');
let sym2 = Symbol('id');
console.log(sym1 === sym2); // false —— 每个 Symbol 都是唯一的
// Symbol 主要用作对象属性的键,避免属性名冲突
let obj = {
[sym1]: 'value1',
[sym2]: 'value2'
};
console.log(obj[sym1]); // 'value1'
Symbol.for() 可以创建全局共享的 Symbol:
let globalSym1 = Symbol.for('app.id');
let globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true —— 同一个全局 Symbol
BigInt:突破安全整数限制
JavaScript 的 number 类型使用 64 位浮点数存储,能精确表示的整数范围是 -(2^53 - 1) 到 2^53 - 1(即 Number.MIN_SAFE_INTEGER 到 Number.MAX_SAFE_INTEGER)。超过这个范围的整数计算会丢失精度。
BigInt(ES2020 引入)解决了这个问题,可以表示任意大小的整数:
let bigNum = 9007199254740993n; // 末尾加 n 表示 BigInt
let anotherBig = BigInt('9007199254740993'); // 也可以用 BigInt() 函数
console.log(bigNum + 1n); // 9007199254740994n —— 精确计算
console.log(bigNum + 1); // TypeError —— BigInt 不能和普通 number 混合运算
注意:BigInt 和普通 number 不能直接混合运算,必须显式转换。而且 Math 对象的方法不支持 BigInt。
八、综合演示:一个类型检测工具函数
下面这个函数综合运用了本篇学到的各种类型检测方法,可以作为你的工具箱储备:
/**
* 精确获取任意值的类型
* 返回规范中的 [[Class]] 类型名称
*/
function getType(value) {
// null 需要特殊处理,因为 typeof null === 'object'
if (value === null) return 'null';
// undefined 用 typeof 即可
if (value === undefined) return 'undefined';
// 基本类型用 typeof(但要注意 typeof 无法区分 null,null 已在上面处理)
let type = typeof value;
if (type !== 'object' && type !== 'function') {
return type;
}
// 对象类型用 Object.prototype.toString 获取精确类型
let classType = Object.prototype.toString.call(value);
// '[object Array]' → 'array'
return classType.slice(8, -1).toLowerCase();
}
// 测试
console.log(getType(42)); // 'number'
console.log(getType('hello')); // 'string'
console.log(getType(true)); // 'boolean'
console.log(getType(null)); // 'null'
console.log(getType(undefined)); // 'undefined'
console.log(getType(Symbol())); // 'symbol'
console.log(getType(123n)); // 'bigint'
console.log(getType([])); // 'array'
console.log(getType({})); // 'object'
console.log(getType(new Date())); // 'date'
console.log(getType(/regex/)); // 'regexp'
console.log(getType(new Map())); // 'map'
console.log(getType(new Set())); // 'set'
console.log(getType(function(){}));// 'function'
console.log(getType(NaN)); // 'number' —— NaN 类型确实是 number
九、本篇小结
这一篇我们拆解了 JavaScript 的数据类型系统:
- 栈与堆:基本类型存栈中(按值访问),引用类型存堆中(按引用访问)。这是理解赋值、传参、比较行为的基础。
typeof:最常用的类型检测,但有三个坑——null返回'object'、无法区分数组、NaN返回'number'。instanceof:沿着原型链检测,不能判断基本类型,跨窗口会失效。Object.prototype.toString.call():终极类型检测方案,可以精确区分所有内置类型。- 隐式类型转换:
+优先转字符串,-/*//转数字。记住六个 falsy 值。始终使用===。 - 浮点数精度:
0.1 + 0.2 !== 0.3是因为 IEEE 754 的二进制近似。用Number.EPSILON或整数运算解决。 Symbol和BigInt:ES6 后新增的两个基本类型,前者用于唯一标识,后者用于大整数精确计算。
数据类型是 JavaScript 的底层土壤。后面要学的执行上下文、作用域链、闭包、异步编程,都建立在你对数据如何存储和传递的理解之上。
下一篇预告
下一篇——《JavaScript 对象剖析》:我们将深入对象这一核心数据结构。对象属性的描述符(writable、enumerable、configurable)是什么?Object.defineProperty 能做什么?getter 和 setter 如何工作?如何用 Object.create() 实现精准的原型继承?这些问题,下一篇用大量代码实例逐一解答。
前端,每周更新。













暂无评论内容