一、JS 8大数据类型与内存存储机制
1. 完整类型划分(ES标准8种)
| 分类 | 清单 | 包含细分 | 内存存放位置 |
|---|---|---|---|
| 7种原始(基本)类型 | Number / String / Boolean / Undefined / Null / Symbol / BigInt | 无细分,值本身不可修改 | 栈内存 Stack |
| 1种引用类型 | Object | 普通Object、Array、Function、Date、RegExp、Math等 | 堆内存 Heap |
2. 栈、堆内存核心区别
- 原始类型
栈直接存真实值;赋值、拷贝为完全复制一份值,新旧变量互不干扰;占用空间小、长度固定,栈读写速度快。 - 引用类型
栈只存堆内存地址指针,真实数据存在堆;多个变量共用同一个堆地址,一处修改全部同步;堆空间大、大小不固定,分配回收开销更高。 - JS运行三大内存区域
- 代码区:存放JS可执行代码;
- 栈区:执行上下文、原始值、引用地址;
- 堆区:对象、数组、闭包变量。
3. 闭包内存特殊规则
闭包变量不会随函数执行完毕销毁,JS引擎在堆创建内部closure(fn)对象存放闭包变量,因此闭包变量常驻堆。
经典例题1:引用共享
let a = { name: 'lee', age: 18 }
let b = a;
console.log(a.name); // lee
b.name = 'son';
console.log(a.name); // son
console.log(b.name); // son
原因:a和b栈中存同一个堆地址,修改堆内属性,所有指向该地址的变量全部变化。
经典例题2:函数传参引用细节(高频面试)
let a = { name: 'Julia', age: 20 }
function change(o) {
o.age = 24; // 通过地址修改堆内原对象,影响外部a
o = { name: 'Kath', age: 30 }; // 让局部o指向全新堆地址,和外部a彻底断开
return o;
}
let b = change(a);
console.log(b.age); // 30(新对象)
console.log(a.age); // 24(原对象被改)
关键:函数传参是地址拷贝。改属性影响原值;给形参整体重新赋值,仅改变局部变量指向,外部不受影响。
二、四种数据类型检测方案(优缺点+手写源码)
1. typeof(适合基础类型+函数)
执行结果一览
typeof 2 // number
typeof true // boolean
typeof 'str' // string
typeof undefined // undefined
typeof Symbol() // symbol
typeof 100n // bigint
typeof [] // object
typeof {} // object
typeof function(){} // function
typeof null // object(历史bug)
优缺点
✅ 优点:快速判断除null外的所有原始类型、精准区分function;
❌ 缺点:
null判定为object(早期二进制000标签导致);- 数组、正则、日期等所有引用类型(除function)全部返回
object,无法细分。
2. instanceof(判断引用类型原型链)
原理
顺着实例__proto__向上查找原型链,是否能匹配构造函数prototype;只能判断引用类型,原始类型全部false。
console.log(2 instanceof Number); // false 原始值无效
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
手写简易instanceof
function _instanceof(left, right) {
// 原始类型直接返回false
if (typeof left !== 'object' || left === null) return false;
let proto = right.prototype;
left = left.__proto__;
while (true) {
if (left === null) return false;
if (left === proto) return true;
left = left.__proto__;
}
}
❌ 缺陷:原型可被手动篡改;跨iframe失效;不支持基础类型。
3. constructor(通过构造器判断)
(2).constructor === Number // true
[].constructor === Array // true
致命坑:可随意修改原型,constructor会错乱:
function Fn(){}
Fn.prototype = new Array();
const f = new Fn();
console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true
不可靠,生产少用。
4. Object.prototype.toString.call()(最精准万能方案)
规则
调用Object原型原生toString,固定返回格式 [object 类型名],可精准区分所有内置类型。
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined)// "[object Undefined]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(/\d/) // "[object RegExp]"
Object.prototype.toString.call(new Date())// "[object Date]"
Object.prototype.toString.call(window) // "[object Window]"
Object.prototype.toString.call(document) // "[object HTMLDocument]"
封装通用精准类型判断函数
function getType(obj) {
// 基础类型直接用typeof返回
const type = typeof obj;
if (type !== "object") return type;
// object/null走toString解析
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}
// 测试
getType([]) // Array
getType(null) // Null
getType(window) // Window
getType(function(){}) // function
四种检测横向对比总结
| 方式 | 基础类型 | 引用细分 | 可篡改 | 推荐场景 |
|---|---|---|---|---|
| typeof | 绝大部分ok(null除外) | ❌ 除function都不行 | 不可改 | 快速判断原始、函数 |
| instanceof | ❌ | ✅ 原型链 | 可改 | 判断自定义类实例 |
| constructor | ✅ | ✅ | 极易篡改 | 几乎不推荐 |
| toString.call | ✅全覆盖 | ✅全覆盖 | 不可篡改 | 通用万能判断 |
三、JS三大类型转换:转布尔、转数字、转字符串
1. 强制转Boolean(条件判断核心)
6个假值:其余全为true
undefined、null、false、NaN、''空字符串、0、-0
Boolean(0) // false
Boolean('') // false
Boolean(null) // false
Boolean([]) // true 空数组是真值
Boolean({}) // true 空对象是真值
2. 对象转原始值通用规则(ToPrimitive)
所有对象参与运算、比较时,按固定优先级获取原始值:
- 优先调用
[Symbol.toPrimitive](hint)(优先级最高,hint:number/string/default) - 无上面方法:调用
valueOf(),返回原始值则终止 - 无上面:调用
toString(),返回原始值则终止 - 全不返回原始值:报错
示例重写优先级:
let obj = {
[Symbol.toPrimitive](){ return 200 },
valueOf(){ return 300 },
toString(){ return 'Hello' }
}
console.log(obj + 200); // 400,toPrimitive优先级最高
3. 强制转数字:Number()完整规则
- Boolean:true→1,false→0
- null → 0;undefined → NaN
- 字符串:纯数字/十六进制正常转;空串
''→0;含非数字字符→NaN - Symbol:直接报错
- 对象:先走ToPrimitive拿到原始值,再套用上面规则
Number('') // 0
Number(null) // 0
Number('1a') // NaN
Number(0X11) // 17
Number(-0X11) // -17
Number(true) // 1
补充:parseInt只解析开头数字,空字符串返回NaN,和Number不同。
4. 加号+隐式转换两条铁律
- 任意一边为字符串:两边全部转字符串,做拼接
1 + '1' // '11'
4 + [1,2,3] // '41,2,3' 数组toString为"1,2,3"
'a' + +'b' // aNaN +'b'先转NaN
- 两边无字符串:全部转为数字做加法
true + true // 1+1=2
null + 1 // 0+1=1
- 除
+外(* / > < -):一律把所有操作数转数字运算
4 * [] // 4*0=0
4 * [1,2] // 4*NaN=NaN
四、== 抽象相等完整隐式转换规则(高频面试)
完整执行步骤(按顺序匹配)
- 两边类型完全一致:直接用
===严格对比,不转换; - 一方
null、一方undefined→true;null/undefined和任何其他值对比全为false; - 一方Symbol → 直接false;
- 一边字符串、一边数字:字符串转Number再对比;
- 一边布尔值:布尔转数字(true=1,false=0)再对比;
- 一边对象、一边原始值:对象走ToPrimitive转原始,再回头重走规则。
经典必背案例逐行解析
'123' == 123 // true 字符串转数字
'' == null // false 规则2,null只认undefined
'' == 0 // true ''转数字0
[] == 0 // true []toString='' → ''==0
[] == '' // true 数组转空串
[] == ![] // true ![]是false → []==false → []转'',false转0 → 0==0
null == undefined // true
经典骚题:a==1 && a==2 && a==3
利用==每次对比都会调用valueOf:
let a = {
value: 0,
valueOf() {
this.value++;
return this.value;
}
}
console.log(a == 1 && a == 2 && a == 3); // true
每次比较触发一次valueOf自增,连续三次刚好命中。
五、null与undefined深度区别
- 含义不同
undefined:变量已声明、未赋值;函数无返回值;对象不存在属性;代表未定义。null:手动赋值空对象占位;预期要返回对象但无结果,主动赋值空;代表空值。
-
typeof历史bug
typeof null === 'object',源于早期32位系统二进制类型标签:对象以000开头,null全二进制为0,被误判;规范无法修改,保留至今。 -
相等判断
null == undefined → true;null === undefined → false。 -
安全获取undefined
undefined不是JS保留字,可以被变量覆盖;推荐用void 0获取绝对安全的undefined。
六、高频热身小题全套答案
'123' == 123 // true
'' == null // false
'' == 0 // true
[] == 0 // true
[] == '' // true
[] == ![] // true
null == undefined // true
Number(null) // 0
Number('') // 0
parseInt('') // NaN
{}+10 // "[object Object]10"
七、开发落地建议
- 判断类型:优先
Object.prototype.toString.call;简单场景用typeof;尽量不用constructor; - 相等判断:业务一律用
===严格相等,规避==隐式转换坑; - 对象传参:牢记传地址、改属性生效,重新赋值不影响外部;
- 空值:未知对象用null初始化;未赋值变量自然为undefined。