Skip to main content

数据类型

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、BigIntnullundefined 是仅有的两个无法被包装的基本类型

为什么部分原始类型能调用方法,比如 '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,第一个 . 失去被吞掉的机会

nullundefined 的区别

  • 语义undefined 是 JS 引擎的"未初始化"状态(变量声明未赋值、函数没 return、访问不存在的属性);null 是程序员主动设置的"空值"
  • 类型typeof undefined === 'undefined'typeof null === 'object'(历史 bug,参见上文)
  • 比较null == undefinedtrue(抽象相等互转);null === undefinedfalse
  • 算术Number(null) === 0Number(undefined) === NaN,所以 1 + null === 11 + undefined === NaN
  • JSON 序列化JSON.stringify({a: null, b: undefined}) 结果是 '{"a":null}'undefined 会被直接丢弃
  • 使用习惯:变量/属性"暂时没值,未来会赋值"用 undefined(JS 默认行为);"明确表示为空"用 null。比如 querySelector 找不到元素返回 nullMap.get 找不到键返回 undefined,正是这两种语义的体现

类型检测

  • typeof。能识别 number、string、boolean、symbol、undefined、bigint、function;除 null 返回 'object'(历史遗留)外,其余引用类型一律返回 'object'
  • instanceof。通过原型链判断构造函数的 prototype 是否出现在实例的原型链上。主要缺点:
    • 跨 realm 失效:不同 iframe / window 的原型对象不共享
    • 安全性差:任意对象只要原型链上能拼到 Array.prototype,就会被判定为数组
    • 可被劫持:Symbol.hasInstance 一旦被重写,结果就不再可信
  • 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.call
  • instanceof(不建议)

怎么判断 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 值类型备注
falseboolean
0-00nnumber / bigint注意 0n 是 BigInt 的零
""string空字符串;" "(空格)不是
nullnull
undefinedundefined
NaNnumber唯一不等于自身的 JS 值
document.allobject历史遗留的"假值对象",专门为了兼容旧代码

这就是 ||&&!、三元表达式、if 等条件判断的统一规则。![]false 是因为数组是对象(truthy),与 [] == false 是两套机制,不要混为一谈。

数据转换

显式转换

  • toNumber:NumberparseIntparseFloat

    • Number('1abc')NaNparseInt('1abc')1(逐字符解析直到不能解析为止)
    • Number(true)1Number(null)0Number(undefined)NaN
  • toString:StringtoString

  • toBoolean:Boolean!!!||&&(判定规则见上文「真值与假值」)

  • NaN 检测:isNaN 会先做 Number() 强转('abc' 被认为"是 NaN"),Number.isNaN 只在值本身就是 NaN 时返回 true。生产中几乎只用 Number.isNaN

    isNaN("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适用场景
==允许(隐式强转)falsetrue几乎不推荐,面试八股用
===禁止falsetrue日常默认选择
Object.is禁止truefalse需要区分 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 直接抛 TypeError

  • BigIntNumber== / === 比较永远为 false(类型不同),必须显式转换

  • typeof 1n === 'bigint',但 Number.isInteger(1n) === true(BigInt 视为整数)

    1n === 1; // false
    1n == 1; // false
    1n + 1n === 2n; // true