资讯动态

JS数据类型全解析:typeof陷阱、隐式转换与深浅拷贝

发布时间:2026/10/20 8:02:10 来源:尧图企业网站定制
typeof null object0.1 0.2不等于0.31 2得到12而1 - 2得到-1。如果你看到这几行代码没有觉得哪里不对劲那么这篇关于JS数据类型的笔记应该能在不少场景帮上忙。我自己整理JS数据类型笔记的时候最初的出发点其实特别简单前端面试几乎必问日常代码里因为类型问题踩的坑又实在太多。比如说后端返回一个字符串类型的数字前端直接拿来相加结果发现是字符串拼接又比如用判断两个值相等结果因为隐式转换得出完全相反的结论。这些问题的根源都出在“对JS类型系统没有一个清晰、完整的认识”上。这篇笔记会从8种数据类型的基本盘开始讲然后逐个拆解typeof的坑、隐式转换的规则、深浅拷贝的实现思路最后附上一份可以直接用的排查速查表。不管是刚入门前端的同学还是写了两三年业务代码想回头补基础的人读完都能有收获。如果你是奔着“临时查一下某个类型转换规则”来的直接翻到第6章的速查表即可。1. 先在JS的世界里认个门8种数据类型总览1.1 怎么理解这些类型原始类型与引用类型JS的数据类型在最新的ECMAScript标准里一共有8种undefined、null、boolean、number、string、symbol、bigint外加一种object。前7种都是原始类型最后一种是引用类型。原始类型和引用类型最大的区别在于存储方式。原始类型的数据直接存在栈内存里值是“复制”的引用类型的数据存的是地址值是“指向”的。这么说有点抽象看两行代码就明白let a 10; let b a; b 20; console.log(a); // 10a没变 let obj1 { name: 张三 }; let obj2 obj1; obj2.name 李四; console.log(obj1.name); // 李四obj1也跟着变了这就是为什么很多刚接触JS的同学会发现“两个对象明明内容一样为什么是false”因为比较引用类型时JS比的是内存地址不是内容。这个概念贯穿整个数据类型体系后面讲深浅拷贝时又会遇到它。1.2 面试必问的typeof坑一次说清楚typeof是最常用的类型判断运算符但它的行为不是那么“直觉”。比如typeof null; // object typeof undefined; // undefined typeof function() {}; // function typeof Symbol(); // symbol typeof 10n; // bigint typeof NaN; // numbertypeof null object是JS一个著名的历史遗留bug远古时代的实现里null的二进制标签和对象相同后来为了兼容不能修。明白这一点之后判断null就绝不能只用typeof要配合Object.prototype.toString.call()或 null来处理。还有一个日常很容易踩的点typeof一个未声明的变量不会报错返回undefinedtypeof notExist; // undefined利用这个特性可以在浏览器环境里安全地判断某个全局API是否存在比如检查window.xxx存不存在。理解了typeof的这几个行为才算过了第一关。2. 七种原始类型逐个拆搞清楚底层规则2.1undefined和null两个“空”有什么区别很多初学者会把undefined和null混为一谈但它们语义不同。undefined表示“变量声明了但还没赋值”或者“对象的属性不存在”null表示“变量存在但人为让它指向一个空值”。举个例子let x; console.log(x); // undefined let obj { name: 张三 }; console.log(obj.age); // undefined属性不存在 let y null; // 主动置空 console.log(y); // null console.log(null undefined); // true注意是 console.log(null undefined); // falsenull undefined为true这是隐式转换规则里专门为它们俩开的一个“后门”后面讲时再说细节。平时写代码我的建议是声明一个暂时不知道值的变量时用undefined或者干脆不初始化需要释放引用、重置状态的时候用null。团队协作时如果能把这两个的语义统一代码的可读性会有明显提升。2.2number的精度硬伤和BigInt的救场number类型在JS里只有一种不区分整数和浮点数底层都是IEEE 754双精度浮点数。这个设计带来的问题是著名的0.1 0.2 ! 0.3console.log(0.1 0.2); // 0.30000000000000004 console.log(0.1 0.2 0.3); // false原因在于二进制无法精确表示0.1和0.2存储时已经产生了舍入误差相加之后误差被放大了。解决方式不是去改底层而是避开浮点数比较的雷区。我的习惯是金额相关的计算尽量用“分”为单位存整数非要浮点运算时用toFixed做展示用“差值小于一个极小值”的思路来比较function isEqual(a, b) { return Math.abs(a - b) 1e-10; } console.log(isEqual(0.1 0.2, 0.3)); // true另一个和number相关的坑是安全整数范围。JS的Number.MAX_SAFE_INTEGER是9007199254740991超过这个范围再加减整数结果就可能不精确了。如果业务场景涉及超大数字比如雪花ID、订单号直接存number就会丢精度。这时候要用BigIntconst bigNum 9007199254740993n; console.log(bigNum 1n); // 9007199254740994n注意BigInt和普通number不能直接参与算术运算会抛异常必须显式转换。后端接口返回超大数字时很多公司直接用字符串传输也是为了避免精度丢失。2.3NaN到底是个什么存在NaN虽然名字叫 “Not a Number”但typeof NaN的结果是number挺反直觉的。它表示“一个不合法、不可表示的数值”一般出现在把非数字字符串转数字的失败场景里console.log(Number(abc)); // NaN console.log(parseInt(abc)); // NaN console.log(0 / 0); // NaNNaN有一个特别不友好的属性它不等于它自己。console.log(NaN NaN); // false console.log(NaN NaN); // false所以要判断一个值是不是NaN千万别直接和NaN比较。ES6之后有了Number.isNaN用法清晰console.log(Number.isNaN(NaN)); // true console.log(Number.isNaN(abc)); // false不会做类型转换 console.log(isNaN(abc)); // true全局isNaN会先转成number再判断Number.isNaN和全局isNaN的差别在于前者不会对参数做隐式类型转换后者会。这也是一个很隐蔽、容易在代码评审里被指出的点。3.string、boolean、symbol、bigint各有各的脾气3.1 字符串是不可变的拼接要讲究效率string类型存储的是Unicode字符序列而且它是不可变的。也就是说任何字符串方法toUpperCase、slice、replace都不会修改原字符串而是返回一个新字符串let str hello; let upper str.toUpperCase(); console.log(str); // hello console.log(upper); // HELLO这个特性在日常写业务时影响不大但在循环里用拼接大量字符串时性能问题会很明显。推荐用数组的join或者ES6的模板字符串来组织const parts [用户, 张三, 登录成功]; console.log(parts.join( )); // 用户 张三 登录成功另外很多同学容易忽略字符串的比较规则。JS字符串比较是按Unicode码点一位一位比的比如a B结果是true因为a的码点97大于B的码点66这一点在排序场景下需要注意。3.2boolean的隐式转换所有类型都有“真假”boolean类型只有两个值true和false但JS里所有类型的值都能转成布尔值。转换规则记起来并不难除了明确为“假”的那几个其他都是“真”。console.log(Boolean(0)); // false console.log(Boolean()); // false console.log(Boolean(null)); // false console.log(Boolean(undefined)); // false console.log(Boolean(NaN)); // false console.log(Boolean(false)); // false console.log(Boolean(0)); // true注意字符串0不是空 console.log(Boolean([])); // true空数组也是真 console.log(Boolean({})); // true空对象也是真这也就解释了为什么有些同学写if ([]) { ... }时代码永远会走进分支。空数组是真值不是说数组里有元素才是真。3.3symbolJS里的唯一标识symbol是ES6新增的类型它的核心特征是“唯一”。每次调用Symbol()得到的值都不一样哪怕是传了相同的描述const s1 Symbol(desc); const s2 Symbol(desc); console.log(s1 s2); // false它的主要应用场景有三个一是作为对象的私有属性名防止属性名冲突二是定义对象的迭代器方法Symbol.iterator三是实现一些元编程能力比如Symbol.toStringTag可以改变Object.prototype.toString返回的标签。Symbol还有一个特征它不能和字符串做隐式拼接symbol 会直接报错。如果要转成字符串得用String(symbol)或者symbol.toString()。3.4bigint超大整数的正确打开方式前文提到过BigInt解决大数精度问题这里再补充一下它的边界。BigInt的创建方式是数字后面加n或者调BigInt()const a 123n; const b BigInt(456); console.log(a b); // 579n它不能和number混用运算比如1n 1会直接抛TypeError。比较时1n 1是false但1n 1是true因为会做隐式转换。在涉及到对象属性键时BigInt和普通数字一样可以安全地作为键名不会出现类型混淆问题。4. 引用类型与对象复杂数据的内存与操作细节4.1 对象、数组、函数都逃不过“地址传递”引用类型里面我们接触最多的就是object、array和function。它们虽然使用场景不同但在“赋值传递的是引用”这件事上行为完全一致。数组也是一个对象所以const arr1 [1, 2, 3]; const arr2 arr1; arr2.push(4); console.log(arr1.length); // 4函数也同样把函数赋值给另一个变量两个变量指向同一个函数对象。理解了这一点就能解释为什么深拷贝这么麻烦如果只是简单地把对象赋值给新变量改的是同一份数据。4.2 判断类型不能只靠typeof一整套完整方案既然typeof有坑那么判断一个值具体是什么类型业界常用的方案是Object.prototype.toString.call()Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call({}); // [object Object] Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(undefined); // [object Undefined] Object.prototype.toString.call(Symbol()); // [object Symbol]这个方式基本覆盖所有内置类型而且不受typeof null那个历史bug影响。很多第三方库的类型判断工具底层就是封装了这一招。在数组判断上直接用Array.isArray()最靠谱。它比instanceof好在哪儿呢instanceof的检测依赖于原型链遇到跨iframe的场景数组实例的原型链可能连不到主窗口的Array.prototype结果就误判了。Array.isArray则不受这个影响。4.3 基本包装类型为什么字符串会有方法用abc.length能取到长度abc.toUpperCase()能转换大写但你有没有想过字符串明明是原始类型为什么能调用方法JS引擎在执行这个操作时会临时把abc包装成一个字符串对象调用方法后再销毁这个临时对象。这就是“基本包装类型”的概念类似的还有Number和Boolean。注意new String(abc)和abc不一样前者是对象后者是原始类型const primitive abc; const wrapped new String(abc); console.log(typeof primitive); // string console.log(typeof wrapped); // object实际操作中没必要特意用包装类型按原始类型写就行。除非在Object.prototype.toString判断场景里包装后的对象会被识别为对应标签其它业务场景基本用不上。5. 类型转换那些年我们踩过的隐式转换坑5.1 显式转换怎么写才稳妥显式转换主要记住三组函数Number()、String()、Boolean()。Number()的规则比较多样最容易出错的几个Number(); // 0注意空字符串转成0 Number( ); // 0空白字符串也是0 Number(null); // 0 Number(undefined); // NaN Number(12px); // NaN带单位的转不了和parseInt、parseFloat相比Number(12px)返回NaN而parseInt(12px)返回12。如果你需要解析“带有单位”的字符串比如12px、3.5rem应该用parseInt/parseFloat如果确认字符串是纯数字格式用Number更严格。String()的转换基本不会出问题String(123); // 123 String(true); // true String(null); // null String(undefined); // undefined String(Symbol()); // Symbol()Boolean()的规则前面已经说过了只要记住那7个假值即可。5.2和到底差在哪为什么推荐后者在做比较时如果两边类型不同会先做隐式转换转换规则相当绕则要求类型和值都相等不转换。举几个容易翻车的例子0 ; // true都是falsy 0 0; // true字符串转数字 0 false; // true布尔转数字 false; // true都转成0 null undefined; // true特殊规则 null 0; // falsenull不会转成0 [] false; // true空数组转成空字符串空字符串转成0看到没有的结果常常让人意外。我在代码评审里会建议团队默认全用只有明确要检查null或undefined时才用value null这种写法它会同时匹配null和undefined。这个写法很常见而且语义清晰。5.3 对象转原始值的规则valueOf和toString的博弈对象在做运算或比较时会先被转成原始值。规则是先找valueOf如果返回的不是原始类型再找toString。const obj { valueOf: () 42, toString: () forty-two }; console.log(obj 1); // 43valueOf被优先调用默认情况下普通对象的valueOf返回对象本身所以会继续调toString结果是[object Object]。这解释了为什么{} []的结果是[object Object]而[] {}也是[object Object]。数组也有自己的toString默认行为是joinconsole.log([1, 2, 3] 1); // 1,2,31数组先转成1,2,3然后再拼接理解了这个博弈过程再看隐式转换题就不会慌。6. 深浅拷贝和不可变数据处理6.1 浅拷贝有哪些现成手段上面提到引用类型赋值传的是地址所以希望“复制一份互不影响的新数据”时就要做拷贝。浅拷贝只复制第一层第二层及以下的引用关系仍然保留。常用手段有三个// 方式一展开运算符 const copy1 { ...original }; // 方式二Object.assign const copy2 Object.assign({}, original); // 方式三数组用slice或concat const arrCopy originalArr.slice();浅拷贝最大的坑在于如果原始对象有嵌套对象修改拷贝后的嵌套对象原始对象也会变。这在处理复杂表单数据时尤其容易踩。6.2 深拷贝的正确打开方式深拷贝要保证所有层级的对象都被递归复制。最简单的方案是JSON.parse(JSON.stringify(obj))但它有局限性const obj { name: 张三, age: undefined, sayHi: function() {}, birth: new Date(2020-01-01), reg: /abc/g, [Symbol(id)]: 1, }; const copy JSON.parse(JSON.stringify(obj)); // age、sayHi、Symbol都会丢失birth变成字符串reg变成空对象{}它还会在循环引用时直接抛异常const obj { name: 张三 }; obj.self obj; JSON.parse(JSON.stringify(obj)); // TypeError业务上如果数据结构简单、没有undefined/函数/循环引用用JSON方案省事如果数据复杂可以用浏览器的原生structuredCloneconst copy structuredClone(obj);它能处理循环引用、Date、Map、Set等内置类型是目前浏览器环境里最省心的深拷贝方案。Node.js 环境也可以直接用。手写深拷贝可以作为面试题去研究但在工程里优先用现成可靠的方案。7. 实战排查JS数据类型相关问题的定位思路7.1 高频疑问与解决方案速查下面这张表是我平时代码评审和排查问题时的“查错字典”遇到类型相关的bug先对照看一遍问题现象可能原因解决方案0.1 0.2结果不对浮点数存储误差将金额换算成整数计算比较时用差值容错超大数字尾数全是0超出安全整数范围改用BigInt或后端返回字符串typeof null返回object历史遗留bug用value null判断NaN ! NaN判断失效NaN不等于自身用Number.isNaN(value){}和[]比较/相加结果诡异隐式转换调用toString避免用明确转成字符串修改新对象导致原对象变化只做了浅拷贝根据数据结构选深拷贝方案后端数字字段变成字符串拼接类型未转换就做用Number()显式转换或先检查类型[1,2,3] 1得到1,2,31数组先转字符串再拼接利用join显式控制字符串格式某全局API在低版本浏览器不存在typeof判断误写用typeof window.xxx ! undefined保护7.2 几个新手的常见误区我也都踩过误区一认为Number()能解析带单位的字符串。实际上Number(12px)直接返回NaN想要提取数字就得用parseFloat。我曾经在处理样式相关的逻辑时没意识到这点结果一整段操作都失效了排查半天才发现是转换函数选错了。误区二用isNaN判断一个变量是否合法数字。isNaN会对参数隐式转数字导致isNaN(123)返回false看起来没问题但isNaN(abc)返回true你根本分不清是“变量本身是NaN”还是“变量转number之后是NaN”。需要严格判断时一定用Number.isNaN。误区三认为空数组和空对象是假值。它们是真值if ([])、if ({})都会进入分支。如果业务意图是“数组为空时不执行”必须显式判断arr.length 0不能依赖真值判断。误区四在console.log里查看对象时误以为打印的是那一刻的快照。控制台打印对象时展示的是展开那一刻的属性值。如果代码在打印后修改了对象属性再展开打印结果看到的往往是修改后的值。调试时可以先console.log(JSON.parse(JSON.stringify(obj)))转成字符串打印避免疑惑或者直接打断点看实时的属性状态。8. 整理笔记过程中最有价值的两个习惯8.1 给每个“反直觉”的API配一个“最稳用法”我在整理笔记时给每个坑都记了一行“实战建议”。比如判断nullvalue null判断NaNNumber.isNaN(value)判断数组Array.isArray(value)通用类型字符串Object.prototype.toString.call(value)这样做的价值在于笔记不是知识点的堆砌而是一份“决策手册”。遇到具体场景直接查最稳的写法不需要每次都在脑子里面过一遍转换规则。8.2 写一个简单的getType工具函数一劳永逸如果你经常要做类型判断可以把Object.prototype.toString封装一下做成一个通用的类型获取工具function getType(value) { const tag Object.prototype.toString.call(value); return tag.slice(8, -1).toLowerCase(); } console.log(getType([])); // array console.log(getType({})); // object console.log(getType(null)); // null console.log(getType(undefined)); // undefined console.log(getType(abc)); // string console.log(getType(10n)); // bigint console.log(getType(Symbol())); // symbol这个函数能规避typeof的所有坑尤其适合在公共工具库里作为底层基础设施。我自己维护的项目里一直保留着类似实现后续处理复杂类型时省了很多事。最后再分享一个小技巧整理任何语言的数据类型笔记都别只抄文档最好把“这个类型的坑在哪、实际项目里遇到什么场景要特别小心”给写进去。JS数据类型的知识点不算多但每个点都能引出至少一个线上事故。把这些经验和代码绑在一起笔记才真正有价值。以后遇到类型相关的问题翻翻自己的笔记往往比重新搜索来得更快更准。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑