ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JS数据类型全解析:typeof、类型转换与避坑指南

JS数据类型全解析:typeof、类型转换与避坑指南 先问一个问题typeof null输出什么如果你能秒答object并且知道这不是因为 null 是对象而是一个从 JS 诞生那天就存在的历史遗留 bug那你算过了第一关。JS 数据类型就是这么个东西——天天在用但真到笔试、面试、排查线上问题时总有几个瞬间让人怀疑自己是不是没学过 JS。这篇文章我想把这些年跟 JS 数据类型“相爱相杀”的经验一次性梳理清楚内置类型到底怎么分类、每个类型有什么隐藏坑、判断类型最好用哪招、类型转换的规则到底是什么。不管你是刚入门的前端小白还是写了几年业务代码想系统性补课的老手这篇都值得花十分钟看完。重点不是背概念而是搞清楚“为什么”以后遇到奇怪现象心里能有个底。1. 先搞清楚JS 数据类型到底有几种1.1 八种内建类型七种原始类型加一种引用类型很多人能报出 string、number、boolean、null、undefined但说到 Symbol 和 BigInt 就开始含糊再问“数组是什么类型”就更容易答偏。实际上ECMAScript 标准里内建类型一共是八种其中七种是原始类型primitive type一种是大类上的引用类型类型示例typeof 输出说明stringhellostring字符串不可变number42、3.14number整数和浮点统一用一种booleantrue、falseboolean布尔值undefinedundefinedundefined声明但未赋值nullnullobject空值typeof 是个 bugsymbolSymbol(id)symbolES6 新增唯一值bigint10nbigintES2020 新增大整数object{}、[]、function(){}object或function引用类型统称注意function虽然是 object 的一种但typeof function(){}输出的是function这也是 typeof 的一个特例。数组、日期、正则、Map、Set 这些统统属于 object。我个人的记忆方法很简单原始类型存的是“值本身”引用类型存的是“指向值的地址”。这个差异是所有类型坑的根源后面讲深拷贝、传参、比较时都会回到这里。1.2 数组、函数、日期为什么都被归为 object先说结论它们本质都是对象只是内部有各自特殊的“行为标签”。数组有length属性、有push、pop、map这些方法但它的底层仍然是对象只是引擎给数组加了一套“索引到值”的映射规则并且让length自动跟着元素数量变化。函数也一样函数在 JS 里是一等公民可以赋值给变量、可以当参数传递这本质上是因为函数本身就是一个可调用的对象。怎么验证用Object.prototype.toString.call()一眼就能看出真实类型Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call(function(){}); // [object Function] Object.prototype.toString.call(new Date()); // [object Date] Object.prototype.toString.call(/abc/); // [object RegExp]这个 API 才是 JS 判断类型的“终极大招”后面我会专门讲。理解数组、函数都是对象还有一个实际意义当你做浅拷贝、深拷贝、比较引用是否相等时它们的表现跟原始类型完全不同。const a []; const b a;之后a b为 true但const c [...]的另一个数组即使内容和 a 完全一样a c也是 false因为比较的是地址。1.3 栈与堆类型不同存储方式也不同JS 在运行时把数据分为栈stack和堆heap两块区域理解这个模型比背概念有用得多。原始类型的数据大小固定直接存在栈里。所以每次给变量赋一个基本类型值拷贝的就是那个值本身let a 10; let b a; b 20; console.log(a); // 10a 不受影响引用类型的对象本体存在堆里栈里只存一个指向堆内存的地址。当你做let obj2 obj1时拷贝的是地址不是对象内容const obj1 { name: 张三 }; const obj2 obj1; obj2.name 李四; console.log(obj1.name); // 李四因为 obj2 和 obj1 指向同一个对象这也是为什么业务里经常要“深拷贝”对象。你用展开运算符{...obj1}拷贝一层之后如果对象里还有嵌套对象里面的子对象仍然是引用改子对象还是会互相影响。透彻理解这个存储模型遇到“我明明复制了一份为什么原数据也变了”的问题5 秒就能定位到原因。2. 逐类拆解每种数据类型的表现与高频坑2.1 number0.1 0.2 为什么不是 0.3这是 JS 最著名的“翻车现场”之一。直接打开控制台console.log(0.1 0.2); // 0.30000000000000004原因不是 JS 故意搞事而是计算机用二进制存浮点数十进制小数转二进制时很多是无限循环只能按精度截断导致误差累积。IEEE 754 双精度标准下这不是 JS 独有的问题Python、Java、C 里同样存在只不过 JS 只有一种 number 类型没有 BigDecimal所以问题特别明显。处理方案按场景选金额计算不要用浮点数存分直接以“分”为单位的整数或者用Math.round((0.1 0.2) * 100) / 100。展示需要用toFixed(2)转换成字符串再显示注意它返回的是字符串。复杂计算引入 decimal.js、big.js 这类库更省心。另外两个 number 相关的坑也一并说了NaNNot a Number是 number 类型的一个特殊值表示“这不是一个合法数字”。它最大的特点是NaN NaN为 false判断只能用Number.isNaN()或者Object.is(NaN, NaN)。Infinity和-Infinity表示无穷大一般在除以 0 时出现别指望它帮你做业务判断几乎全是 bug。2.2 string不可变性与字符串包含判断字符串是原始类型意味着它不可变。你做的所有字符串操作本质都是“创建了一个新字符串再赋回去”原字符串从未变过let s hello; s.toUpperCase(); // 返回 HELLO但 s 还是 hello s s.toUpperCase(); // 这样 s 才变成 HELLO这个特性在循环里拼接大字符串时要小心性能比如 for 循环里str item会频繁创建新字符串数据量大时可以用数组push再join或者直接用模板字符串。字符串包含判断是日常开发里最高频的操作之一。最常用的写法是includes传入一个子串返回布尔值const url https://example.com/page?id1; console.log(url.includes(id1)); // true console.log(url.startsWith(https://)); // true console.log(url.endsWith(/page)); // false如果是老项目需要兼容更老的浏览器或者需要拿子串位置就用indexOf找不到返回-1。需要正则匹配时test返回布尔值match返回匹配结果数组。后面第 5 部分我还会展开讲这套 API 的选型。2.3 boolean 与真假值判断布尔类型只有true和false但在 JS 的条件判断里几乎所有值都会被“隐式转换”成布尔值。记住一个准则JS 里只有 6 个值在条件判断时为 false其余全是 true。具体的 falsy 值是false0和-00nBigInt 的零空字符串nullundefinedNaN在实际开发中最常见的坑是if () { /* 不执行 */ } if (0) { /* 不执行 */ } if ([]) { /* 执行空数组是 true */ } if ({}) { /* 执行空对象是 true */ } if (false) { /* 执行非空字符串是 true */ }很多人以为[]是“空”所以是 false实际上任何对象转布尔都是 true。我在 code review 里见过不少if (arr.length)写错成if (arr)导致空数组也进入逻辑分支的问题。判断数组有没有元素一定要看length不要直接用数组本身当条件。2.4 null 和 undefined 的边界这两个都代表“空”但语义不同undefined声明了变量但没有赋值或者函数没有 return 时返回的值。JS 引擎默认给的“未定义”。null开发者主动把变量置为空表示“这里有意不放任何对象”。实际开发里我有一套约定我主动“清空”某个变量时用null比如this.timer null;。而undefined通常是 JS 运行时自然产生的比如对象里没有某个属性时取值得到 undefined。函数参数不传也是 undefined。判空时要注意别乱用console.log(null undefined); // true双等号下二者相等 console.log(null undefined); // false严格相等下不同如果想判断“既不是 null 也不是 undefined”最保险的写法if (val ! null) { // 注意这里是双等号会同时排除 null 和 undefined // val 一定是有值的 }单独判断 null 用val null判断 undefined 用val undefined更严谨。typeof val undefined也可以尤其是在变量还没声明的场景下不会直接抛错。2.5 symbol 和 bigintES6 之后的两个“新面孔”Symbol 是 ES6 引入的第七种原始类型每次调用Symbol()都会生成一个绝对唯一的值即使描述文字一样也不相等const s1 Symbol(id); const s2 Symbol(id); console.log(s1 s2); // false它最典型的用途是当作对象属性名防止属性名冲突尤其是多人协作或引入第三方库时防止别人覆盖你的属性。另一个用途是定义“隐藏”属性但严格说并不隐藏reflect.ownKeys 能枚举到只是普通遍历 for...in 不会出现。BigInt 是 ES2020 新增的用来表示任意大的整数。number 类型最大安全整数是2^53 - 1约 9000 万亿超过这个范围就会丢失精度。比如后端返回一个雪花 ID 或订单号经常超过这个范围直接 JSON.parse 就会出错。解决办法是把大数字转成字符串传输或者用 BigIntconst big 9007199254740993n; // 末尾加 n console.log(big 1n); // 9007199254740994n注意 BigInt 不能直接和 number 混用运算1n 1会直接抛 TypeError必须显式转换。这个限制看着烦其实是防止你精度丢了都不知道。3. 类型判断typeof、instanceof、toString 三件套怎么选3.1 typeof 的规则和三个意料之外的输出typeof是运算符不是函数用来判断一个值的类型但它有非常明显的局限。正常情况typeof abc; // string typeof 42; // number typeof true; // boolean typeof undefined; // undefined typeof Symbol(); // symbol typeof 10n; // bigint typeof {}; // object真正坑人的三个现象第一typeof null object前面已经说过历史 bug不再解释。第二typeof function(){} function。函数确实是 object但 typeof 单独给了一个结果这个不算坑反而方便判断一个变量是不是函数时直接typeof fn function就够了。第三typeof一个未声明的变量不会报错返回undefined。在浏览器里判断全局 API 存不存在时非常实用if (typeof jQuery ! undefined) { // 只有在定义了 jQuery 时才执行 }如果你的业务需要在老代码里做“特性检测”这招很常用。但要注意typeof对数组、日期、正则、Map 这些全部返回object完全无法区分所以它只能做基础分类。3.2 instanceof 与原型链的判定逻辑instanceof用来判断一个对象是否是某个构造函数或类的实例原理是沿着原型链找右边的原型对象是否出现在左边对象的原型链上。[] instanceof Array; // true [] instanceof Object; // true因为 Array 的原型链上也有 Object.prototype new Date() instanceof Date; // true问题也不少原始类型不是对象abc instanceof String为 false。跨 iframe 或跨 window 时Array 构造函数不是同一个引用[] instanceof window.parent.Array可能为 false。对象原型链上一旦有被别人改过结果也会变。因此我一般只在判断“某个对象是不是某个自定义类的实例”时用instanceof比如表单校验场景里判断错误类型。3.3 Object.prototype.toString.call()判断类型终极大招最靠谱、最通用的判断方法就这一句Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call({}); // [object Object] Object.prototype.toString.call(new Date()); // [object Date] Object.prototype.toString.call(/x/); // [object RegExp] Object.prototype.toString.call(new Map()); // [object Map] Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(undefined); // [object Undefined]它利用的是Object.prototype.toString内部会读取this的Symbol.toStringTag属性或内置的 type tag返回一个固定的格式。因此我们可以封装一个通用函数function getType(value) { return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); } getType([]); // array getType({}); // object getType(abc); // string getType(null); // null getType(undefined); // undefined getType(new Map()); // map这套封装我用了很多年基本不用再担心判断错类型。判断纯对象时可以用getType(value) object要注意它会把 null、数组排除掉非常精确。4. 类型转换显式、隐式与强制转换的铁律4.1 显式转换的常用姿势显式转换就是你自己明确调用转换函数常见三类字符串转数字Number(42); // 42 Number(42px); // NaN不是 42 parseInt(42px, 10); // 42 parseFloat(3.14em); // 3.14用Number()转换是严格解析整个字符串必须是有效数字才行parseInt/parseFloat则从左到右解析到不能解析为止。反过来也有坑Number(); // 0空字符串会转成 0 Number( ); // 0全是空格也是 0 Number(null); // 0 Number(undefined); // NaN数字转字符串String(42); // 42 (42).toString(); // 42 String(null); // null转布尔值Boolean(0); // false Boolean(); // false Boolean([]); // true只有 6 个 falsy 值才转成 false4.2 隐式转换最容易翻车的地方隐式转换是 JS 被人诟病最多的部分本质是引擎在你不注意的时候帮你调用了类型转换逻辑。第一个高发区是加号。有两个语义数字相加、字符串拼接。规则是有字符串就拼字符串否则就尝试转数字1 2; // 12 1 2 3; // 33先算 123 再拼 3 1 2 3; // 123从头到尾都是拼接 5 - 2; // 3减号没有拼接语义字符串转数字 5 * 2; // 10第二个高发区是和。永远记住比较时如果类型不同直接返回 false不会转换会先转换再比较。业务代码里强制自己只用能消除一大批隐蔽 bug但有一个例外if (val ! null) { // 这个写法等价于 val ! null val ! undefined是安全且简练的 }第三个高发区是数组和对象参与运算。对象转原始值时JS 会先调用valueOf()如果结果不是原始值再调用toString()。数组默认的toString()相当于join(,)[] []; // 空数组各自转成空字符串 [] {}; // [object Object] [1, 2] [3]; // 1,23不对是 1,23不对…… 实际上是 1,23对[1, 2] [3]结果是1,23因为两个数组都转成了字符串再拼接。这种代码正常人不会主动写但如果是接口返回数据处理不及时或者模板字符串里不小心嵌套了对象就会看到[object Object]出现在页面上。4.3 字面量类型和数据类型的区别很多人在搜索“字面量类型和数据类型的区别”这里专门说说。字面量literal指的是代码里直接写出来的那个“值”而不是变量。比如const name 张三; // 张三 是一个字符串字面量 const age 18; // 18 是一个数字字面量 const obj { a: 1 }; // { a: 1 } 是一个对象字面量 const arr [1, 2]; // [1, 2] 是一个数组字面量所以“字面量”和“数据类型”并不是同一维度的概念。数据类型回答“这个值属于哪一种类型”字面量只是表示“这个值是直接写在代码里的”。在没有变量名的情况下它的类型依然可以根据字面量的写法判断出来。在 TypeScript 里还有“字面量类型”这个专门术语比如type A a | b指的是把字符串、数字、布尔值的具体取值作为一种类型来约束变量。这个概念是从 JS 的字面量衍生出去的理解时别把两层含义混在一起。5. 实战场景字符串判断、JSON 转换以及与其他语言数据类型的横向对比5.1 字符串包含判断includes、indexOf、match 怎么选工作里最常碰到的一个需求判断一个 URL 是否包含某个参数、判断文件名是不是图片、判断用户输入是否包含敏感词。API 简单列一下const s hello world; s.includes(world); // true推荐布尔值直观 s.indexOf(world) -1; // true老项目兼容方案 s.search(/world/); // 6正则返回索引 s.match(/world/); // [world, index: 6, ...]返回数组或 null /world/.test(s); // true正则的布尔判断选型建议只是判断存在与否用includes语义最清楚。需要拿到子串位置用indexOf或search。需要提取匹配内容比如从邮箱里提取用户名用match。只需要判断是否符合某个正则模式用RegExp.test。实际项目里我经常这样组合用const fileName avatar_20240101.png; const isImage /\.(png|jpe?g|gif|webp)$/i.test(fileName); if (isImage) { // 走图片上传逻辑 }5.2 JSON 和数组转换类型转换的另一处闭环后端返回的字符串数据要转 JSON前端提交时要转字符串这基本是每天都会碰到的操作const jsonStr {name:张三,age:18}; const obj JSON.parse(jsonStr); // 字符串 - 对象 const backToStr JSON.stringify(obj); // 对象 - 字符串几个高频踩坑点JSON.parse(undefined)、JSON.parse()会直接抛错解析接口返回之前先判断数据是否存在。JSON.stringify会丢掉值为undefined、函数、Symbol 的属性也会把NaN、Infinity转成null日期对象会转成 ISO 字符串Map、Set默认转成{}。深层 JSON 数据如果太大递归序列化会卡死页面这时要考虑分批处理或用流式方案。数组和类数组转换也常和类型判断挂钩const arrayLike document.querySelectorAll(div); // NodeList不是数组 Array.from(arrayLike); // 类数组/可迭代对象转数组 [...arrayLike]; // 扩展运算符效果相同 Array.isArray([]); // true判断数组的推荐方式Array.from还能传一个 map 函数类似Array.from(arrayLike, item item.id)在批量处理表单数据时很顺手。5.3 横向对比JS 和其他语言的数据类型设计理解 JS 的类型体系最好和别的语言放一起看因为很多困惑来自“为什么别的语言不是这样”。语言/环境主要数据类型特点JavaScriptstring、number、boolean、null、undefined、symbol、bigint、object动态类型运行时确定类型隐式转换多Pythonint、float、str、bool、None、list、tuple、dict、set动态类型内置容器丰富类型通常靠鸭子类型Javaint、long、float、double、char、boolean、String、数组、对象静态类型编译期确定基本类型和包装类型共存C/Cint、char、float、double、指针、数组、结构体、类静态类型贴近硬件需要手动管理内存RedisString、Hash、List、Set、ZSetSorted Set所有键都存字符串值类型由命令决定横向对比最大的启发有两点第一JS 只有“统一数字类型”number没有单独的 int、float好处是简单坏处是精度问题难以避免。BigInt 的出现就是为了补上大整数的短板。第二JS 的动态类型和黄式转换是一把双刃剑。Java、C 这类静态语言在赋值、传参时类型不对直接编译报错JS 不行只能在运行时用 typeof、instanceof、Object.prototype.toString 自己检查。这也解释了为什么前端项目越来越依赖 TypeScript——本质上是在工程化阶段把“动态”变成“静态”减少运行时才知道类型错了的尴尬。6. 高频报错与避坑速查表6.1 开发中遇到最多的三个类型关联报错Cannot read properties of undefined或reading xxx of undefined这个报错应该排在所有前端报错第一名。核心原因就是某处对象是 undefined 你还去取它的属性// 接口返回可能没有 list 字段 const list res.data.list; // 如果 res.data 是 undefined这里直接炸排查思路先确认接口返回结构再做一层保护const list res?.data?.list ?? [];可选链和空值合并运算符能省掉绝大多数判空样板代码但它要求环境支持 ES2020老浏览器需要编译或转译。xxx is not a function常见场景误把 undefined 当函数调用、对象里没有这个方法、或者隐式转换后类型不对const method obj.callback; // callback 不存在method 是 undefined method(); // TypeError: method is not a function排查时先打印变量确认它到底是什么类型。这里我用得最多的是console.log(typeof method)比猜快得多。数据运算结果突然变成 NaN我见过不少案例是后端返回金额字段100.5带引号前端直接做加减法结果字符串拼接或者 NaNconst price 100.5; const count 2; console.log(price * count); // 201乘法会隐式转数字这个没问题 console.log(price count); // 100.52加号变成字符串拼接处理方式金额类字段前端拿到后第一件事就统一转成分单位整数或者封装toNumber函数Number(value)并判断Number.isNaN(result)。6.2 避坑速查表场景常见坑建议做法判断值类型typeof null object用 Object.prototype.toString.call() 封装 getType判断数组[] instanceof Array 跨环境可能失效用 Array.isArray数组判空if(arr) 对空数组也是 true判断 arr.length 0对象判空if(obj) 对空对象也是 true用 Object.keys(obj).length 0字符串数字相加1 2 得到 12先 Number() 或 parseInt() 再运算浮点计算0.1 0.2 ! 0.3转整数计算或用专门库大整数丢失精度9007199254740993 变成 9007199254740992后端转字符串或前端用 BigInt解析 JSON 报错JSON.parse() 抛异常先 try/catch 再判断数据非空可选链兼容性?. 在旧浏览器报 SyntaxError用 babel 转译或用 判断判断空值value null 是双等号有的歧义明确用 null 或 undefined 不可时允许 !!不同项目可能还有各自的奇葩约定但这个表基本覆盖了 JS 数据类型相关的 80% 问题。最后再分享一个我个人的习惯写公共方法时入口处一定会做类型校验避免脏数据一路传到最深处才炸。类型问题不像语法错误那么好发现它往往是在某次交互、某个接口异常时突然冒出来的而且复现难度高。多加一行类型防御未来可能少熬夜排查一个多小时。
返回列表