数据类型
js 一共有 8 种数据类型,其中 Object 是引用类型,其余 7 种(number、string、boolean、symbol、bigint、null、undefined)都是基本数据类型。基本类型按值访问:小整数(Smi,-2³¹ ~ 2³¹-1)会内联在指针里;其他基本类型一旦被装箱(例如 new Number(1) 或访问 'a'.length 触发的隐式装箱)就在堆上分配,String 同样始终在堆上。引用类型始终在堆上,栈只持有指向堆的引用地址——所以常说"对象是按引用传递",本质是"栈上的指针按值拷贝了一份"。
分类
围绕原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 然而,现有的原始包装器对象,如 new Boolean、new String 以及 new Number,因为遗留原因仍可被创建
Q&A
为什么
typeof null === 'object'?
不同的对象在底层都表示为二进制,js 早期实现版本中,如果二进制前三位为 000 的话会被判断为 'object',null 的二进制表示全为 0,自然前三位也是 0,所以执行 typeof 时会返回 'object'
Number(a)和new Number(a)的区别
Number(a)会将 a 转换为 number 数据new Number(a)会创建一个 Number 类型的包装对象。包装对象有Number、String、Boolean、Symbol、BigInt;null和undefined是仅有的两个无法被包装的基本类型
为什么部分原始类型能调用方法,比如
'test'.length
js 执行到'test'.length 时,会将 'test' 转化为 new String('test'),详见 http://es5.github.io/#x10.4.3
不过 2.toString() 会报错。原因藏在词法分析阶段:JS 规范里 NumericLiteral 允许以 . 结尾(2. 是值为 2 的合法数字字面量),解析器贪婪地吞掉第一个点号,于是 2.toString 整体被识别成一个非法数字。三种常见解法的本质都是"切断数字字面量":
2..toString():第二个.让数字字面量提前结束,被当作成员访问符2 .toString():空格强制词法分析断开(2).toString():括号把2提升为PrimaryExpression,第一个.失去被吞掉的机会
null和undefined的区别
- 语义:
undefined是 JS 引擎的"未初始化"状态(变量声明未赋值、函数没 return、访问不存在的属性);null是程序员主动设置的"空值" - 类型:
typeof undefined === 'undefined';typeof null === 'object'(历史 bug,参见上文) - 比较:
null == undefined为true(抽象相等互转);null === undefined为false - 算术:
Number(null) === 0;Number(undefined) === NaN,所以1 + null === 1、1 + undefined === NaN - JSON 序列化:
JSON.stringify({a: null, b: undefined})结果是'{"a":null}',undefined会被直接丢弃 - 使用习惯:变量/属性"暂时没值,未来会赋值"用
undefined(JS 默认行为);"明确表示为空"用null。比如querySelector找不到元素返回null、Map.get找不到键返回undefined,正是这两种语义的体现
类型检测
typeof。能识别 number、string、boolean、symbol、undefined、bigint、function;除null返回'object'(历史遗留)外,其余引用类型一律返回'object'instanceof。通过原型链判断构造函数的prototype是否出现在实例的原型链上。主要缺点:- 跨 realm 失效:不同
iframe/window的原型对象不共享 - 安全性差:任意对象只要原型链上能拼到
Array.prototype,就会被判定为数组 - 可被劫持:
Symbol.hasInstance一旦被重写,结果就不再可信
- 跨 realm 失效:不同
Object.prototype.toString.call。在 ES5 时代通过[[Class]]内部属性判断,是最严谨的类型判断方式;ES6 之后改读Symbol.toStringTag,自定义类只要实现该 Symbol 就能返回自定义标签。返回格式为[object Type](例如[object Array]、[object Null]),如果未定义Symbol.toStringTag,自定义构造函数实例只能返回[object Object]。注意这也是一次装箱操作,会产生临时对象。
一些特殊的情况如下:
typeof null; // "object"
typeof BigInt(1); // "bigint"
typeof undefined; // "undefined"
null === undefined; // false
null == undefined; // true
isNaN(1 + null); // false
isNaN(1 + undefined); // true
Q&A
怎么判断数组?
Array.isArray()Object.prototype.toString.callinstanceof(不建议)
怎么判断 plain object?
plain object 指的是通过字面量形式或者 new Object() 形式定义的对象,可以通过以下方式判断
function isPlainObject(obj) {
if (typeof obj !== "object" || obj === null) {
return false;
}
let temp = obj;
while (Object.getPrototypeOf(temp) !== null) {
temp = Object.getPrototypeOf(temp);
}
return Object.getPrototypeOf(obj) === temp;
}
instanceof 能否判断基本数据类型?
可以。instanceof 内部读取的是 Symbol.hasInstance,通过重写这个 Symbol 可以拦截判断逻辑,突破"只能判断引用类型"的限制:
class EvenNumber {
static [Symbol.hasInstance](obj) {
return typeof obj === "number" && obj % 2 === 0;
}
}
2 instanceof EvenNumber; // true
3 instanceof EvenNumber; // false
真值与假值
JS 中只有 8 个值在布尔上下文中为假(falsy),其余全部为真(truthy):
| falsy 值 | 类型 | 备注 |
|---|---|---|
false | boolean | — |
0、-0、0n | number / bigint | 注意 0n 是 BigInt 的零 |
"" | string | 空字符串;" "(空格)不是 |
null | null | — |
undefined | undefined | — |
NaN | number | 唯一不等于自身的 JS 值 |
document.all | object | 历史遗留的"假值对象",专门为了兼容旧代码 |
这就是
||、&&、!、三元表达式、if等条件判断的统一规则。![]为false是因为数组是对象(truthy),与[] == false是两套机制,不要混为一谈。
数据转换
显式转换
toNumber:
Number、parseInt、parseFloatNumber('1abc')→NaN,parseInt('1abc')→1(逐字符解析直到不能解析为止)Number(true)→1,Number(null)→0,Number(undefined)→NaN
toString:
String、toStringtoBoolean:
Boolean、!!、!、||、&&(判定规则见上文「真值与假值」)NaN 检测:
isNaN会先做Number()强转('abc'被认为"是 NaN"),Number.isNaN只在值本身就是 NaN 时返回 true。生产中几乎只用Number.isNaNisNaN("abc"); // true(先转为 NaN 再判断)
Number.isNaN("abc"); // false(不是 number 类型直接 false)
Number.isNaN(NaN); // true
隐式转换
==- 两两复杂数据类型对比,会比较地址是否一致。
- 复杂数据类型和基础类型对比,复杂数据类型要转换为 string,依次调用
valueOf()、toString(),如果对比的基础数据类型是 number 或 boolean,还需要进一步转换为 number 对比。 - 面试高频:
[] == ![]为什么是true?- 第一步:
![]会被转为布尔值false。此时表达式变成[] == false。 - 第二步:布尔值
false被转为数字0。此时表达式变成[] == 0。 - 第三步:数组
[]调用valueOf()依然是[],再调用toString()变成空字符串""。此时表达式变成"" == 0。 - 第四步:空字符串
""被转为数字0。此时0 == 0成立,返回true。
- 第一步:
- toNumber。
+[string] - toString。
''+[number]
数据比较
| 方式 | 类型转换 | NaN === NaN | +0 === -0 | 适用场景 |
|---|---|---|---|---|
== | 允许(隐式强转) | false | true | 几乎不推荐,面试八股用 |
=== | 禁止 | false | true | 日常默认选择 |
Object.is | 禁止 | true | false | 需要区分 NaN / ±0 的特化场景 |
NaN === NaN; // false
Object.is(NaN, NaN); // true
+0 === -0; // true
Object.is(+0, -0); // false
选型经验:默认
===;只有当你在写"同值查找表"(如 React Fiber 里的 effect tag diff)或"求和要保留零的符号"这类需求时,才换成Object.is。
BigInt 注意事项
BigInt不能与Number混用算术运算,1n + 1直接抛TypeErrorBigInt与Number用==/===比较永远为false(类型不同),必须显式转换typeof 1n === 'bigint',但Number.isInteger(1n) === true(BigInt 视为整数)1n === 1; // false
1n == 1; // false
1n + 1n === 2n; // true