跳到主要内容

JavaScript 数据类型与类型转换

JavaScript 的类型属于值:一个变量可以先保存数字,再保存字符串。判断一段代码的结果,要先确定值的类型,再看当前操作是否会转换类型、是否在修改同一个对象。

这些规则直接影响表单输入、接口数据、状态更新和现场编码。比起记住几组特殊输出,更有用的是能解释为什么对象赋值后会互相影响、为什么 typeof null 不能用来识别空值,以及字符串进入计算前应该怎样检查。

1. 面试核心​

先记住这几条,再读正文
  • JavaScript 有 七种原始类型和一种对象类型。数组、普通函数和日期都是对象,不是与 Object 并列的语言类型。卡 · 值的类型
  • 原始值不可变;对象可以被多个变量共享。修改对象和重新给变量赋值是两种操作,传参时同样要区分。卡 · 对象共享与传参
  • typeof 适合做基本分类,但 null 需要单独判断,数组优先用 Array.isArray。instanceof 检查原型关系,不能代替所有类型检查。卡 · typeof 与专用判断
  • 转换前先确认输入约定。Number 转换整个值,parseInt 读取整数前缀;转换成功也不等于满足业务范围。卡 · 数字、字符串与布尔值
  • === 不转换类型;== 按规则转换;Object.is 区分正负零并认为 NaN 与自身相同。对象比较的是身份,不是内容。卡 · 相等判断
  • Number 有精度边界,BigInt 用于整数运算;Symbol 可作为独立属性键。选择它们时,要考虑运算、序列化和调用方约定。卡 · Symbol 属性键

2. 值的类型​

原始值是不能通过修改内部内容来改变的值。JavaScript 的七种原始类型如下。

类型例子需要理解的边界
Undefinedundefined变量声明后尚未赋值、缺失属性等情况会得到它
Nullnull一个独立的空值,常用于明确表示“没有对象或结果”
Booleantrue、false条件判断会把其他值转换成布尔值
Number42、0.5、NaN使用双精度浮点数表示,也包括无穷和正负零
BigInt42n表示任意精度整数;不是小数类型
String"hello"字符串值不可变;长度按 UTF-16 码元计算
SymbolSymbol("id")创建独立标识,可作为对象属性键

第八种语言类型是 Object。对象有身份,可以持有属性;数组、函数、Date、Map 等分别在对象语义上增加了不同能力。普通函数能够调用,但它仍然是对象。

“不可变”说的是值,不是变量:

let title = "code";
title = title.toUpperCase();
console.log(title); // CODE

toUpperCase() 返回新的字符串,随后 title 改为保存新值。原来的字符串没有被原地改写。与此相对,数组的 push() 会修改数组对象。

const 则限制变量绑定:初始化以后不能给这个变量重新赋值。它不会自动冻结对象,因此 const items = [] 后执行 items.push(1) 合法。

3. 对象共享与传参​

对象身份用来区分是不是同一个对象。两个对象即使属性相同,也可以有不同身份;两个变量也可以指向同一个对象。

const original = { count: 1 };
let alias = original;
alias.count = 2;
console.log(original.count); // 2

alias = { count: 3 };
console.log(original.count, alias.count); // 2 3

alias = original 没有复制对象内容,两个变量最初共享一个对象。修改 alias.count 就是在修改共享对象。后来给 alias 赋新对象,只改变 alias,不会把 original 一起换掉。

函数调用也遵循值传递。对于对象,可以用“把对象引用这个值复制给形参”理解共享关系,但不能把它说成函数拿到了调用方变量本身。

function update(record) {
record.count += 1;
record = { count: 100 };
}

const item = { count: 1 };
update(item);
console.log(item.count); // 2

形参 record 起初与 item 指向同一个对象,所以加一可见。record = ... 只替换函数内部的绑定,item 仍然指向原对象。如果希望调用方换成新对象,应返回新值,再由调用方赋值。

“原始值放栈上、对象放堆上”不足以解释这些结果。实际存放位置会受到引擎表示和优化影响;语言层面保证的是值、对象身份和操作语义。面试推导先使用这些规则,不把特定内存布局当作 JavaScript 的定义。

4. 类型识别​

类型检查应当服务于下一步操作:要遍历数组就检查是不是数组,要做有限数计算就检查是不是有限的 number,不必先构造一个包揽所有对象的“万能类型名”。

4.1 typeof 与专用判断​

typeof 返回字符串,能区分大部分原始类型,并将普通可调用函数识别为 "function"。

console.log(typeof undefined); // undefined(字符串)
console.log(typeof null); // object(字符串)
console.log(typeof []); // object(字符串)
console.log(typeof (() => 1)); // function(字符串)
console.log(typeof NaN); // number(字符串)

typeof null === "object" 是保留下来的历史行为,不表示 null 属于对象类型。检查 null 用 value === null;数组用 Array.isArray(value)。Number.isNaN 只判断参数本身是不是数值 NaN,不会先把字符串转成数字。

console.log(Array.isArray([])); // true
console.log(Array.isArray({ 0: "a" })); // false
console.log(Number.isNaN("oops")); // false
console.log(Number.isNaN(Number("oops"))); // true

对于接口中的对象,还要检查业务结构,例如 id 是否为字符串、items 是否为数组。typeof value === "object" 只能提供很粗的分类,不能验证对象的字段。

4.2 instanceof 与对象标签​

instanceof 的普通判断过程检查右侧构造函数的 prototype 是否出现在左侧对象的原型链上。因此,它回答的是原型关系,而不是“这个值一定具有哪些内部能力”。它还可以被 Symbol.hasInstance 定制。

下面的对象具有数组原型链,但不是真正的数组:

const pretender = Object.create(Array.prototype);
console.log(pretender instanceof Array); // true
console.log(Array.isArray(pretender)); // false

不同页面执行环境有各自的 Array 构造函数。从同源 iframe 得到的数组,不一定通过当前页面的 instanceof Array;Array.isArray 可以识别这样的数组。这也是判断数组优先用专用 API 的原因。

Object.prototype.toString.call(value) 可以得到 "[object Array]" 等标签,适合检查部分内置对象。不过对象能通过 Symbol.toStringTag 定制标签,不能把它当成对不可信输入的安全证明。

5. 类型转换​

类型转换是按照某个操作的规则,把一个类型的值转换为另一个类型的值。显式调用 Number、String、Boolean 能表达意图;运算符也可能触发隐式转换。

5.1 数字、字符串与布尔值​

HTML 输入框读到的 value 是字符串。进入计算前,要确定允许的是完整数字、整数前缀,还是必须符合固定格式的业务标识。

表达式结果原因
Number("12.5")12.5整个字符串能转换为数字
Number("12px")NaN后缀不符合数字转换规则
parseInt("12px", 10)12读取十进制整数前缀后停止
parseInt("12.5", 10)12小数点不属于整数前缀
Number("")0空字符串转换为零
Number(null)0null 的数值转换结果为零
Number(undefined)NaNundefined 不能转换成有效数值

parseInt 先把输入转为字符串,再按进制读取整数前缀,省略进制时也不是简单的“永远按十进制”:0x 前缀会影响进制判断。处理约定为十进制的输入时,显式传 10,并检查是否应当接受前缀解析。

如果业务要求非负整数,Number(text) 后还需要检查空输入、是否有限、是否为整数和是否在允许范围内。手机号或订单号通常是标识,不能仅因全由数字组成就转为 Number,否则可能丢失前导零或整数精度。

布尔转换中,undefined、null、false、正负零、0n、NaN 和空字符串是假值;普通对象、空数组和字符串 "0"、"false" 都是真值。浏览器遗留的 document.all 是特殊例外,不用它设计业务判断。

5.2 对象参与转换​

对象参与某些运算时,需要先转换成原始值。转换过程会优先使用对象的 Symbol.toPrimitive;没有该方法时,再按提示尝试 valueOf()、toString()。普通对象在数值提示下先尝试 valueOf(),字符串提示下先尝试 toString();具体内置对象可能另有约定。

加号会先获取两边的原始值:只要有一边是字符串,就做字符串拼接;否则尝试数值相加。

console.log(1 + "2"); // "12"
console.log("2" - 1); // 1
console.log([] + 1); // "1"

第二行的减号需要数值,把 "2" 转成 2。第三行中,普通空数组转换得到空字符串,随后执行 "" + 1,结果是字符串 "1"。同一个值放进不同操作中,会走不同的转换规则,不能只背“字符串优先”这一条。

对象自定义的转换方法可以执行代码或抛异常。处理不可信数据时,先按接口契约验证结构,再做必要转换,避免把隐式转换当作输入校验。

6. 相等判断​

比较前先确定要判断的是类型和值、缺失状态,还是同一个对象。JavaScript 提供了不同的比较规则。

比较是否转换类型NaN 与自身+0 与 -0
===否不相等相等
==按规则转换不相等相等
Object.is否相等不相等
SameValueZero否相等相等

SameValueZero 是 Set、Map 的键以及 Array.prototype.includes 等使用的比较规则,不是一个名为 SameValueZero 的全局函数。

console.log(0 === false); // false
console.log(0 == false); // true
console.log(null == undefined); // true
console.log(null == 0); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false
console.log([NaN].includes(NaN)); // true

== 不等于“把两边都变成数字”。例如 null 与 undefined 按专门规则相等,与 0 比较则不会直接套用 Number(null) 的结果。对于通常的业务值,使用 === 可以减少隐式转换;如果项目明确用 value == null 同时处理 null 和 undefined,也应知道这是有意使用规则。

所有这些比较都不会递归比较两个不同对象的内容:

const first = { id: 1 };
const second = { id: 1 };
const alias = first;
console.log(first === second); // false
console.log(first === alias); // true

如果业务要比较记录,应明确比较字段、标识或版本。用 JSON.stringify 比较所有对象并不可靠:属性顺序、未序列化的值、循环结构和对象类型都可能影响结果。

7. Number、BigInt 与 Symbol​

7.1 数值精度​

Number 使用 IEEE 754 双精度浮点数。不是每个十进制小数都能用有限的二进制位精确表示,因此 0.1 + 0.2 === 0.3 为 false。

连续整数能安全表示的范围为 -(2 ** 53 - 1) 到 2 ** 53 - 1。超出安全整数范围后,不同整数可能落到同一个可表示值上。Number.isSafeInteger 可以检查一个数是不是安全整数,但不能恢复此前已经丢掉的精度。

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(Number.MAX_SAFE_INTEGER + 1 ===
Number.MAX_SAFE_INTEGER + 2); // true
console.log(BigInt("9007199254740993") + 1n); // 9007199254740994n

BigInt 适合需要精确整数的计算。应从准确的字符串或 BigInt 字面量构造它,不要先把超大整数转成已经失真的 Number。它不能与 Number 直接混合算术运算,整数除法也会截去小数部分;JSON 默认不能直接序列化 BigInt,需要双方约定字符串等表示。

对于金额,可以在明确货币最小单位和数值范围后使用整数,或使用满足需求的十进制运算方案。浮点误差判断要考虑量级和允许误差,不能把 Number.EPSILON 当作所有业务通用的比较阈值。

7.2 Symbol 属性键​

Symbol() 每次创建不同的原始值,即使描述文字相同。用它作为属性键,可以避免与同名字符串属性或另一个新 Symbol 冲突。

const internalId = Symbol("id");
const otherId = Symbol("id");
const record = { id: "public", [internalId]: 101 };

console.log(internalId === otherId); // false
console.log(record[internalId]); // 101
console.log(Object.keys(record)); // ["id"]
console.log(Object.getOwnPropertySymbols(record).length); // 1

Object.keys 只返回可枚举的自有字符串键;要检查自有 Symbol 键,需要 Object.getOwnPropertySymbols,或用 Reflect.ownKeys 同时拿到两类自有键。Symbol 不是安全隔离机制,其他代码仍然能枚举到这些键。

Symbol.for("id") 则会查找全局 Symbol 注册表,重复使用相同键会得到同一个 Symbol。它与每次独立创建的 Symbol("id") 用途不同。

8 · 面试题卡片​

本页题目与参考回答

8.1 JavaScript 有哪些数据类型,数组和函数属于哪一类?​

考察重点:区分语言类型与对象的具体种类,解释原始值和对象的差异。

相关内容:值的类型。

参考回答

JavaScript 有 Undefined、Null、Boolean、Number、BigInt、String、Symbol 七种原始类型,以及 Object 对象类型。 数组和普通函数都属于对象;函数增加了可调用能力,typeof 因而对普通函数返回 "function",但这不代表它是第九种语言类型。

原始值不可变,对象可以持有可变属性并被多个变量共享。解释赋值和传参时,应依据值与对象身份,不依赖“必定在栈或堆上”的简化描述。

8.2 对象赋值给另一个变量后,修改属性与重新赋值有什么区别?​

考察重点:共享对象、变量绑定和函数参数之间的关系。

相关内容:对象共享与传参。

参考回答

直接赋值不会克隆对象,两个变量起初指向同一个对象。 通过任一变量修改这个对象的属性,另一方能看到;给其中一个变量赋新对象,只改变该变量的绑定。

传参时,形参也没有获得调用方变量本身。函数可以修改双方共享的对象,却不能通过给形参重新赋值替换调用方变量。const 只限制重新赋值,也不阻止对象属性修改。

8.3 typeof、instanceof 和 Array.isArray 分别适合检查什么?​

考察重点:根据目的选择检查方式,并解释 null、数组、跨执行环境和原型伪装的边界。

相关内容:typeof 与专用判断、instanceof 与对象标签。

参考回答

typeof 做基本分类,instanceof 检查原型关系,Array.isArray 检查数组身份。 typeof null 和 typeof [] 都返回 "object",因此 null 单独用全等判断,数组优先用 Array.isArray。

instanceof 会受原型链、不同执行环境的构造函数以及 Symbol.hasInstance 定制影响。对象标签也可能被 Symbol.toStringTag 改写。检查接口数据最终还要检查业务字段,不能只看一个类型名。

8.4 字符串转数字时,Number 和 parseInt 有什么区别?​

考察重点:完整转换与前缀解析的区别,以及空值、进制和范围校验。

相关内容:数字、字符串与布尔值、数值精度。

参考回答

Number 按整个输入转换,parseInt 按指定进制读取整数前缀。 所以 Number("12px") 是 NaN,parseInt("12px", 10) 是 12。parseInt 不能用来证明整个字符串都是整数,也不适合直接当作所有数字的截断工具。

还要处理 Number("") === 0、非有限值、整数要求和安全范围。对订单号等标识保留字符串,避免前导零和精度丢失。十进制前缀解析显式传 10。

8.5 ===、== 与 Object.is 有哪些区别?​

考察重点:类型转换、对象身份及特殊数值比较。

相关内容:相等判断。

参考回答

=== 不转换类型,== 按抽象相等规则转换,Object.is 采用 SameValue 比较。 Object.is(NaN, NaN) 为 true,Object.is(0, -0) 为 false;=== 对这两组值的结果相反。

== 不是统一转数字,null == undefined 为 true 而 null == 0 为 false。三种方式都不做对象内容的深比较。业务比较应明确身份、字段和缺失值约定。

8.6 Symbol 属性键为什么不能只靠 Object.keys 获取?​

考察重点:字符串键、Symbol 键及属性枚举的范围。

相关内容:Symbol 属性键。

参考回答

Object.keys 只返回可枚举的自有字符串键,不包含 Symbol 键。 Object.getOwnPropertySymbols 返回自有 Symbol 键,Reflect.ownKeys 返回两类自有键,包括不可枚举的键。

如果复制或检查对象时只遍历 Object.keys,可能遗漏 Symbol 属性。Symbol 可以减少命名冲突,但能被枚举,不是秘密字段。

8.7 设计一个按指定类型筛选数组元素的函数,怎样处理 null 和数组?​

题型:综合题

考察重点:类型检查边界、输入约定与可扩展的判断规则。

相关内容:值的类型、类型识别。

参考回答

先定义支持的类型名称,再用对应谓词检查;null 和数组单独处理。 下例支持指定的几类值,object 明确表示“非 null、非数组、typeof 为 object 的值”,并不声称只接受普通对象。

const typeChecks = {
null: value => value === null,
array: Array.isArray,
object: value => value !== null &&
typeof value === "object" && !Array.isArray(value),
number: value => typeof value === "number",
string: value => typeof value === "string",
boolean: value => typeof value === "boolean",
undefined: value => value === undefined,
bigint: value => typeof value === "bigint",
symbol: value => typeof value === "symbol",
function: value => typeof value === "function",
};

function filterByType(values, type) {
if (!Array.isArray(values) || !Object.hasOwn(typeChecks, type)) {
throw new TypeError("不支持的输入或类型");
}
return values.filter(typeChecks[type]);
}

console.log(filterByType([null, [], {}, 1], "array")); // [[]]

增加日期等业务分类时,新增对应判断函数即可,不必修改筛选过程。当前 number 包含 NaN 和无穷;如果要求有限数,应单独定义 Number.isFinite 规则。验证空数组、不支持的类型名、null、数组、普通对象与特殊数值,不能把类型名和业务有效性混为一谈。

9. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明