FEATURED · 精选文章

JS变量与数据类型通关指南:从内存模型到类型转换

发布时间 / 2026/9/14 19:32:23
来源 / 创域科博编辑部
栏目 / 资讯中心
JS变量与数据类型通关指南:从内存模型到类型转换 1. 从一道“送命题”开始你真的了解变量吗能把“变量”这个词讲清楚的人ta 对 JavaScript 的理解肯定差不到哪里去。我面试过不少人问数组和对象的区别答得头头是道再问一句“那const obj {}为什么还能往 obj 里加属性”十个人里有八个会愣一下。这就是 JS 变量最迷惑人的地方表面上是“存数据”实际上背后藏着一整套内存模型、引用传递、类型转换的规则。很多新人写 bug最后排查下来十有八九不是语法写错了而是对变量和数据类型的基本功不扎实。这篇就是 JS 入门通关手册的第三篇核心就两件事变量怎么存类型怎么变。把这个“万能盒子”的原理摸透了后面再学函数、闭包、原型链你会觉得一路绿灯。如果你正处于刚学完 HTML 和 CSS、准备啃 JS 却到处碰壁的阶段这篇内容就是为你准备的。哪怕你已经工作一两年我也建议你把文中的数据装箱、类型判断、强制转换这几节重新捋一遍踩坑概率会小很多。2. 变量到底是什么内存里的“盒子”与“引用纸条”2.1 用“租房”理解变量赋值很多人一开始背概念“变量是用于存储数据的容器”。对但太抽象了。我自己上课的时候喜欢用租房来比喻学生反馈特别好。一个变量声明比如let age 18等于你租了一间房子门牌号叫 age屋子里放着值 18。往变量里重新赋值比如age 20就是把这间屋子里的旧家具扔了换一套新的。原始类型primitive type的赋值好比你把家具复制了一份送到新房子里。你改新房子的家具老房子一点不受影响。但对象、数组这类引用类型不一样——变量里存的不是“家具本身”而是一张写着“家具在仓库哪一排哪一格”的纸条。你把这个变量赋给另一个变量只是把纸条复印了一份两个人手拿纸条去仓库改的还是同一套家具。这就是我在开头说的那道送命题的答案。const obj {}限制的只是 obj 这个名字不能重新指向别的对象但 obj 指向的那个对象本身你想怎么改就怎么改因为它不是常量是“常量引用”。2.2 内存里的真实样子栈与堆的默契分工要再往深挖一层就得说内存了。JS 的引擎比如 V8在运行时会把内存主要分成栈stack和堆heap两块。原始类型的值小放在栈里存取速度极快按值访问。你复制变量就是在栈里又新压入了一个值。引用类型的对象本体比较大放在堆里。栈里的变量保存的只是一个指向堆内存的地址编号。为什么这么做因为栈的内存空间有限而且入栈出栈讲究顺序如果把一个超大对象直接塞进栈里性能要崩。画个简易示意栈Stack 堆Heap ----------- ----------- | age 18 | | | | obj 0x7 | -------------- | { name: 张三 } | ----------- -----------这里面有个很重要的推论当你做“浅拷贝”的时候比如直接const obj2 obj你只是把地址编号复制了一份obj 和 obj2 指向同一个对象改任意一个另一个都会跟着变。想要真正“独立”得做深拷贝把堆里的内容重新复制出一份来。2.3 词法环境与作用域链很多教程讲变量会跳过词法环境但我建议新人至少知道这个概念。JS 在执行一段代码时会先创建执行上下文每个上下文里都有一个词法环境Lexical Environment用来记录当前作用域内声明的变量。这是一个内部记录不是给你 get 用的。但你写的代码能不能访问某个变量访问到的是哪个值全看词法环境怎么记录。函数内部能访问外部变量是因为作用域链把内外层的词法环境串了起来。新人常见的误区是“我在函数里定义了一个变量外面怎么访问不到”这就正常了。函数内部的let变量只存在于该函数对应的词法环境函数执行完环境销毁变量跟着没了。3. 数据类型全家桶七个成员两种分类3.1 原始类型六块“乐高积木”JS 有七种内置数据类型六种原始类型一种引用类型Object。原始类型包括类型示例说明number423.14NaN整数和小数不分家还包括Infinitystringhihihi文本单双引号反引号都行booleantruefalse逻辑判断nullnull空值注意 typeof 有坑undefinedundefined声明了但没赋值symbolSymbol(id)独一无二的值对象属性名利器bigint9007199254740993n超大的整数这里我说句大实话symbol 和 bigint 这两个新成员新手日常写业务代码基本用不到但面试爱问。知道它们是原始类型、按值传递、不可变就够了。拿 bigint 举例JS 的 number 类型能安全表示的整数范围是-2^53 1到2^53 - 1超出这个范围精度就出问题了。比如9007199254740992 1得到的结果和9007199254740992一样因为后一位被取舍掉了。bigint 就是为解决这个痛点出现的末尾加一个n就是 bigint 类型。3.2 引用类型对象家族一脉相承再说对象。数组、函数、日期、正则甚至你自定义的 class它们底层都是 Object。区别只是“长相”和内置方法不同。数组有序列表用[]包裹适合放同一类数据对象键值对集合用{}包裹适合描述某个实体的属性函数可以执行的子程序但本质上也是对象所以你可以给函数挂属性这个“万物皆对象”的特性是 JS 灵活的一大源头。但也带来一个副作用引用传递导致的数据联动经常让新手心态崩溃。我见过最经典的一个场景把一个数组用const newArr arr赋值给另一个变量然后 sort 了一下 newArr结果原数组也被排序了。原因就是两者指向同一个数组对象。要复制数组得用[...arr]或者arr.slice()。3.3 字面量类型和“类型”不是一回事网络热词里有人搜“字面量类型和数据类型的区别”这个值得展开说说。字面量literal就是你直接写在代码里的那个具体值比如let num 5里的 5let str abc里的abc它们都是字面量。字面量是“字面意思”的值而类型是“这个值属于哪一类”的标签。5 是 number 类型abc是 string 类型。但在 TypeScript 里这个说法有特例。TS 中有字面量类型比如type Direction up | down这里的字面量类型指的是“值只能等于某个具体字面量”的一种类型约束。如果你搜到这个热词大概率是在学 TS这里顺手帮你区分一下。4. 变量声明的“三国演义”var、let、const4.1 三兄弟能力对比ES6 之后JS 有了三种变量声明方式很多新手傻傻分不清。我直接给一张对照表声明方式作用域变量提升能否重复声明能否重新赋值var函数作用域会提升并初始化为 undefined可以可以let块级作用域会提升但不初始化暂时性死区不可以可以const块级作用域会提升但不初始化不可以不可以4.2 为什么建议彻底告别 var我知道很多老教程还在教你用 var但现实是现代 JS 开发中var 基本可以躺进博物馆了。var 最大的问题有两个。第一是函数作用域会漏比如if (true) { var leak 我跑出来了; } console.log(leak); // 输出我跑出来了第二是变量提升带来的诡异表现console.log(a); // undefined不是报错 var a 1;因为你以为自己在用 a实际拿到的是 undefined这种 bug 一旦出现在业务里特别难排查。let 和 const 把这个问题修掉了在声明之前访问变量直接抛 ReferenceError帮你把隐患暴露在第一时间。4.3 const 不是“不可变”是“不可换名”这一点新手最容易搞混。我反复跟学员强调const 的意思是绑定关系不可变不是内容不可变。const person { name: 张三 }; person.name 李四; // 允许改的是对象内容 person { name: 王五 }; // TypeError: Assignment to constant variable那什么时候该选 const我的习惯是变量声明后不再重新赋值的一律用 const比如从接口拿到的配置对象、函数引用。需要重新赋值的才用 let。这样代码读起来很清晰看到 const 就知道这个变量从头到尾指向同一个东西看到 let 就知道它可能变。5. 类型判断你不知道的 typeof 与 instanceof5.1 typeof 的功能与局限判断数据类型最常用的工具是typeof操作符。它返回一个字符串。typeof 42; // number typeof hi; // string typeof true; // boolean typeof undefined; // undefined typeof Symbol(); // symbol typeof 10n; // bigint typeof function() {}; // function typeof []; // object typeof {}; // object typeof null; // object经典 bug看到没typeof null返回object这是个从 JavaScript 诞生起就存在的历史遗留 bug。你没法用 typeof 判断一个值是不是 null只能这样写const value null; if (value null) { console.log(这是 null); }另外还有个大坑typeof []返回的是object所以 typeof 也分不清数组和对象。要判断数组用Array.isArray()。5.2 instanceof 的正确打开方式如果 typeof 是判断“原始值”的利器那 instanceof 就是判断“对象到底是谁家的孩子”的放大镜。const arr []; arr instanceof Array; // true arr instanceof Object; // true数组也是对象 const date new Date(); date instanceof Date; // true date instanceof Object; // trueinstanceof 的底层机制是沿着原型链找右边构造函数的 prototype 对象在不在左边对象的原型链上。在就是 true不在就是 false。这也解释了为什么数组 instanceof Object 也是 true——数组的原型链上确实有 Object.prototype。但 instanceof 也有失灵的时候。最典型的是 iframe 场景一个 iframe 里创建的数组拿到外层用arr instanceof Array判断会得到 false。因为这个数组的原型链是 iframe 内部那个 Realm 的 Array.prototype和外层的 Array 不是一个东西。高版本的 JS 提供了Array.isArray()来处理这种跨领域判断这也侧面说明了为什么框架源码里到处都在用 isArray。5.3 Object.prototype.toString终极判断大法如果你想问“这些方法都不完美那最靠谱的判断方式是什么”答案是借用 Object.prototype.toString。Object.prototype.toString.call(42); // [object Number] Object.prototype.toString.call(hi); // [object String] 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(new Date()); // [object Date]直接返回一个统一的[object 类型]格式无差别识别。我在写通用工具函数时基本都用这个方式做类型判断稳得一批。6. 类型转换V8 引擎的“自动翻译”与“手动翻译”6.1 为什么需要强制转换数据类型的话题里最让人头大又最防不胜防的就是转换。前端从输入框拿到的值永远是字符串哪怕用户输入的是数字。你要做加法就得先把字符串变成数字不然就会得到字符串拼接的结果。const inputValue 5; const total inputValue 3; console.log(total); // 53不是 8这就是为什么“数据类型强制转换”会出现在热词里整个开发过程到处都要用。6.2 常用的显式转换套路三个全能的构造函数String()转字符串最省心String(123); // 123 String(true); // true String(null); // nullNumber()转数字细节很多Number(42); // 42 Number(3.14); // 3.14 Number(); // 0这个容易忘 Number( ); // 0纯空格也是 0 Number(12px); // NaN不是 12 Number(true); // 1 Number(false); // 0 Number(null); // 0 Number(undefined); // NaNBoolean()转布尔Boolean(0); // false Boolean(); // false Boolean(null); // false Boolean(undefined); // false Boolean(NaN); // false Boolean(0); // true字符串 0 是真值这里有个高频考查点字符串0会被转成 true。因为真值判断只看“这个值是不是那六个假值”六个假值是0、NaN、、null、undefined、false。字符串0不是空字符串所以它是真值。6.3 隐式转换才是真正的坑王比显式转换更折磨人的是隐式转换。JS 的“自动翻译”规则非常多而且前后版本还不一致我挑几个高频场景讲讲。加法运算符的特殊之处只要有一边是字符串结果就是字符串拼接1 2; // 12 1 2; // 12 1 2 3; // 33先算 123再拼上一个字符串 1 2 3; // 123从左到右全部变成字符串拼接 3 - 1; // 2减号没有拼接语义强制转数字 3 * 2; // 6两边都能转成数字就成功乘法相等运算符的模糊地带null undefined; // true null 0; // false诡异吧 0; // true字符串转数字后比较 [] false; // true数组转成空字符串再转成 0false 也转成 0看到这里你应该明白了为什么我在日常开发中不用一律用。设计初衷是方便实际带来的坑远远大于便利。6.4 解析 parseInt 与 Number 的本质差别处理“带单位的字符串”比如12px用 Number 会得到 NaN用 parseInt 却能拿到 12parseInt(12px); // 12 parseInt(px12); // NaN parseInt(12.7); // 12只取整数部分 parseFloat(12.7); // 12.7parseInt 的规则是从字符串头部开始解析直到遇到不能转换的字符就停。所以12px能解析出 12。而 Number 要求整个字符串都必须是合法的数字格式有一个字符不行就直接 NaN。关于 parseInt还有一个全网都讨论过的问题不传第二个参数怎么办parseInt(0x10)在旧引擎里会被当成十六进制解析得到 16。为了不模糊我建议永远传第二个参数parseInt(str, 10)明确告诉引擎按十进制解析。7. 深度实战写一个靠谱的工具函数7.1 需求实现一个真正的类型判断函数前面铺垫了那么多现在来个综合实战。很多人网上搜“js判断字符串是否包含”搜“js 数据类型判断”其实底层原理就是前面这些。咱们来写一个真正能打的 deepType 函数。function deepType(value) { return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); } deepType(42); // number deepType(hello); // string deepType([]); // array deepType({}); // object deepType(null); // null deepType(new Date()); // date deepType(/regex/); // regexp deepType(new Map()); // map deepType(function() {}); // function这个函数把[object Array]这种字符串里的Array部分切出来并转成小写返回一个干净的标识。跨 iframe、跨窗口也不会有问题因为它用的是最原始的 Object.prototype.toString而不是某个具体环境里的构造函数。7.2 实战安全的深拷贝有了类型判断能力再结合前面讲的栈堆原理咱们来实现一个经典工具——深拷贝。冷知识很多候选人能说出深拷贝的浅层实现但一遇到循环引用就当场翻车。function deepClone(value, map new WeakMap()) { if (value null || typeof value ! object) { return value; } if (map.has(value)) { return map.get(value); } if (value instanceof Date) { return new Date(value.getTime()); } if (value instanceof RegExp) { return new RegExp(value.source, value.flags); } const result Array.isArray(value) ? [] : {}; map.set(value, result); Object.keys(value).forEach((key) { result[key] deepClone(value[key], map); }); return result; }这里用 WeakMap 来记录已经克隆过的对象就是为了对付循环引用如果对象 A 里引用了对象 BB 又引用回 A没有 map 记录的话就会无限递归直到栈溢出。用 WeakMap 还有一个好处它的键是弱引用不会阻止垃圾回收所以不用担心内存泄漏。7.3 前端业务里的类型转换自动化再分享一个小场景封装接口请求的时候后端返回的数据经常要把“字符串的数字”转成真正的数字比如订单金额、数量、状态码。你可以写一个自动转换的递归函数function convertTypes(obj) { if (typeof obj string obj.trim() ! !isNaN(Number(obj))) { return Number(obj); } if (Array.isArray(obj)) { return obj.map(convertTypes); } if (obj ! null typeof obj object) { const result {}; for (const key of Object.keys(obj)) { result[key] convertTypes(obj[key]); } return result; } return obj; }这种工具在对接老接口时特别有用。但我不建议在正式项目中无差别使用因为有些字段可能需要保留字符串格式比如编号001转成数字就变成 1 了会导致数据失真。所以这类工具一定要配合白名单或黑名单字段使用。8. 常见问题与排坑手册8.1 变量作用域相关的疑难杂症现象在 for 循环里用 var 定义 i循环结束 i 值串了。for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出 3、3、3不是 0、1、2原因不复杂var 是函数作用域循环里每次迭代用的都是同一个 i等到定时器回调执行时i 早就已经变成 3 了。解决方式有三个换成 let块级作用域每次迭代生成新的绑定、用闭包造个立即执行函数或者用 setTimeout 的第三个参数传值。在我的代码规范里直接用 let 是最优先选项。现象用 const 定义基础类型之后想改值报错。这不是 bug是你的变量声明选错了。const 只适合“绑定关系不允许改变”的变量如果需要重新赋值这种场景从一开始就应该用 let。报错是 JS 保护你的方式不要拿const当“对象专用声明”。8.2 类型判断的边界场景现象判断一个值是否为整数用typeof value number然后判断value value不对。如果 value 是 NaNtypeof NaN number为 true但 NaN 不等于任何值连自己都不等。正确做法是用Number.isInteger(value)它会比typeof更严格还会排除掉小数。Number.isInteger(42); // true Number.isInteger(42.5); // false Number.isInteger(NaN); // false Number.isInteger(42); // false不会先转类型现象检查一个对象是否有某个属性用obj.prop判断得到 undefined 就以为没有。obj.prop等于 undefined可能真的是没有这个属性也可能是这个属性存在但值就是 undefined。想区分两种场景用prop in obj检查属性是否存在或者更严格地讲用Object.prototype.hasOwnProperty.call(obj, prop)检查是不是自身属性不包含原型链上的属性。8.3 内存泄漏变量不该成为“常驻嘉宾”前面提到词法环境和垃圾回收这里展开来说一个实战问题。全局变量是最容易造成内存泄漏的因为它一直在词法环境的根上挂着垃圾回收器不会动它。常见场景忘了用 let/const 声明直接someVar 1严格模式下会报错非严格模式下就默默变成了全局变量。事件监听器注册了但从不移除回调本身带着外层的引用整个作用域链都回收不了。定时器清除不干净setInterval 里引用的大对象一直留在内存里。我在项目里的做法是任何临时数据尽量在函数内用局部变量存函数执行完局部变量随执行上下文一起销毁window 上不挂临时数据使用完的定时器该 clearInterval 就 clearInterval。8.4 类型转换地狱的真实翻车案例有一次我在处理表格导出功能单元格数字是字符串我顺手用了value 转字符串。结果有个字段是 null拼出来的字符串变成了null导出后用户看到一列全是 null还以为是程序出了 bug。这就是典型的没考虑边界值的转换。const value null; const str String(value); // null后来我统一改成function safeToString(value) { return value null || value undefined ? : String(value); }遇到 null 和 undefined 直接返回空字符串符合大多数业务预期。这种细节写不到官方文档里但就是这些细节决定了你的工具函数好不好用。9. 写在最后的个人心得从最早的var时代走到let/const从手动判断类型到各种 framework 自带的处理逻辑JS 的变量和数据类型体系离“完美”还有距离但它已经足够撑起复杂的现代应用。我做前端这么多年一个深切的体会是凡是能把 null/undefined/NaN 这三个概念的边界梳理清楚、能在脑子里画出栈和堆结构的人写代码几乎不会有低级 bug。究其原因是这部分人对底层机制有敬畏心不会抱着“反正能跑就行”的心态去写。如果你正在学 JS我建议你今天就把代码编辑器打开把本文里那几张表挨个敲一遍重点体验3 - 1、3 1、[] false、null undefined这几个让人神经错乱的结果。别嫌这些题目简单很多工作了三四年的人遇到隐式转换照样栽跟头。还有一个小习惯我要强调写工具函数时先把输入的类型当成“不诚实的”来防。用户传了一个0哪个分支会出问题接口返回了null哪个字段会崩多问自己这两句话能省下来的排查时间远超你当初多写的那几行防御代码。JS 的变量和数据类型说到底是人与内存之间的“翻译层”。你把翻译规则吃透了无论未来用 React、Vue 还是直接写原生 JS底层逻辑都能一以贯之不会因为换个框架就重新开始踩坑。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻