写 JavaScript 也有年头了,每次和人聊到 JS 基础,我都会先问一个问题:基本数据类型和引用数据类型,到底差在哪?能当场说清楚的人真不多。不是大家不努力,而是很多人写了大量业务代码之后,始终没停下来想过一件事——变量里存的到底是“值本身”,还是“某个对象的地址”。
这个认知一旦模糊,后果比面试答不上来大多了。你会发现自己在不该修改原数组的地方把原数组改了;在需要拷贝对象的时候直接来了一次赋值;用两个“看起来完全一样”的对象做比较,得到 false,然后 Debug 到怀疑人生。如果你刚开始系统学前端,或者已经写了两年 JavaScript 但偶尔还会被类型问题坑到,这篇文章就是帮你把这块地基彻底填实的。
“基本类型/引用类型”不是一道死记硬背的面试八股,它直接决定了赋值、比较、传参、拷贝这一次高频操作的真实行为。把这个机制吃透了,很多看起来很玄学的毛病,根本不用去翻别人的踩坑总结,你自己就能推出来问题出在哪。
1. 先给整个类型体系照一张X光片
1.1 七种原始值:String、Number、Boolean、Undefined、Null、Symbol、BigInt
ECMAScript 语言规范把类型分成两大类:原始值(Primitive)和对象(Object)。目前规范里一共有 8 种内置类型:Undefined、Null、Boolean、String、Number、Symbol、BigInt 和 Object。注意,前 7 种都属于原始值,网上常说的“基本数据类型”,指的就是这一撮。
很多人写代码只见过 string、number、boolean、undefined、null,Symbol 和 BigInt 往往是被忽略的。Symbol 代表着“一个绝对不可能重复的标识”,通常用来做对象属性的 key,防止命名冲突;BigInt 是 ES2020 加进来的,用来处理超过 Number 安全整数范围的超大整数。这两种类型虽然业务中不像 string 那么常见,但它们仍然是正儿八经的原始值,遵守基本数据类型的一切规则,比如按值复制、按值比较。
所以在面试里如果只说“JS 有 5 种基本数据类型”,那其实还停留在 ES5 时代。现在正确的说法是 7 种原始值加上 1 种对象类型。有些老文章会把 null 剔除出去,这也是不严谨的,null 虽然在 typeof 里表现得像个历史遗留的 bug,但它的规范类型就是 Null,该算基本类型还是得算。
1.2 引用类型不是一种,而是一大类对象
引用数据类型本质上就是 Object,但 Object 下面又衍生出一大堆内置对象:Array、Function、Date、RegExp、Map、Set、WeakMap、WeakSet、Promise,包括你写的普通对象 {},全部属于对象类型。它们共享同一个根特征:变量里保存的是对某个内存结构的引用,而不是数据本体。
这个说法听起来有点抽象,但你只要记住一个现象就够了:任何对象类型的值,你用 typeof 去探测,绝大多数都会返回 "object",只有函数特殊,返回 "function"。数组也返回 "object",日期也是 "object",正则也是 "object"。这意味着在 JS 眼里,数组、字典、类实例本质上都是对象,只是内部挂载的“原型”不同,行为不同罢了。
1.3 可变性差异:字符串看着像对象,实际上是原始值
有个特别容易迷惑人的现象:字符串明明是基本类型,你却可以写 "hello".length 拿到长度,可以调用 toUpperCase()、slice() 这些方法。这不就和对象一样吗?
这里的关键在于 JS 的“自动装箱机制”。当你在一个字符串上访问属性或调用方法时,引擎会在那一瞬间创建一个临时的字符串包装对象,让这个临时对象去完成操作,操作结束立刻丢弃。对象感只是借来的,字符串本身仍然是原始值,一旦创建就不可变。
反过来看对象类型,它们的默认状态是可变的。你可以随时给对象加属性、删属性,往数组里 push 新元素,给已有 key 重新赋值。这种“能改”的属性,是引用类型和基本类型非常核心的一个差异点。你很难去“修改”一个字符串值本身,只能通过方法生成一个新的字符串再赋回去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 变量存的不是对象本体,而是一个“门牌号”
2.1 一个特别容易理解的类比
理解引用类型的捷径,是不要把对象想得太玄。你可以把“对象本体”想象成一套房子,把变量想象成一本通讯录。当你写 const p = { name: '张三' } 时,真正发生的事情是:内存里盖了一套房子,房子里存着 { name: '张三' } 这项数据,而变量 p 里记着的不是房子本身,只是房子的“门牌号”。
基本数据类型不一样。你写 let a = 18 时,变量 a 里就是干干净净地存着“18”这个数字本身,没有门牌号这一层中转。
这就带来了赋值行为上的本质差异。基本类型赋值,像是把值抄到对方的纸上,两边从此再无关系;引用类型赋值,等于把同一串门牌号抄给了对方,你和对方只要有人顺着门牌号走进去改东西,另一个人下次看到的就是改完的屋子。
2.2 亲眼看一下“改一个变量,另一个跟着变”
先看基本类型:
javascript复制let a = 20;
let b = a;
b = 30;
console.log(a); // 20
这个大家都能预料到,因为 a 和 b 都是独立内存里的数字,改 b 不影响 a。
再看引用类型:
javascript复制let user1 = { name: '张三' };
let user2 = user1; // 这里只复制了门牌号
user2.name = '李四';
console.log(user1.name); // '李四'
第二次操作里,user2 拿到的是同一个门牌号。你用 user2 进屋改掉了名字属性,其实就是在修改 user1 指向的那套房子的内容,所以 user1 看到的名字也成了“李四”。
很多人最开始不理解这个现象,会怀疑是不是框架或者 Vue/React 的响应式逻辑出了问题。其实没有,这就是 JavaScript 对象赋值最天然的行为:你复制的是引用,不是房子本身。
2.3 重新赋值和修改属性,是两码事
这里我要特别强调一个容易混淆的点:给变量重新赋值,和通过变量修改对象内容,是两种完全不同的操作。
javascript复制let a = { count: 1 };
let b = a;
b = { count: 999 }; // 让 b 指向一套新房子
console.log(a.count); // 1,a 那只门牌号没变,还是指向原来的房子
上面这个结果很多人会搞错。以为 b 之前指向 a 的房子,给 b 重新赋值后,a 也会跟着变。实际上不会,因为 b = { count: 999 } 是把 b 这本通讯录上的地址划掉,重新写了一个新门牌号,a 手里的门牌号完全没被碰过。
这个细节在函数传参时会引发更隐蔽的问题,后面第五章我会专门展开。
3. 比较操作与类型幻觉:为什么两个同样内容的对象不相等
3.1 基本类型比较的是值,引用类型比较的是门牌号
JS 用 === 做严格比较时,底层逻辑是:
- 基本类型之间比较,直接比较值是否相同。
1 === 1是 true,'abc' === 'abc'也是 true。 - 引用类型之间比较,比较的是两个引用是否指向同一个对象。就算两个对象长得一模一样,只要不是同一个门牌号,比较结果都是 false。
于是就有了下面这些让新手一脸懵的表达式:
javascript复制{} === {} // false
[] === [] // false
[1, 2, 3] === [1, 2, 3]; // false
你可能会想:这数组内容明明一模一样,凭什么不相等?因为这两个数组是在内存里分别创建的两套房子,虽然房间布局一样,但门牌号不同。=== 在比较对象时不会走进屋里逐个看家具,它就看门牌号一不一样。
只有一种情况,两个变量指向同一个对象时,严格比较才会返回 true:
javascript复制const arr1 = [1, 2, 3];
const arr2 = arr1;
console.log(arr1 === arr2); // true,因为 arr2 拿到的是同一个门牌号
3.2 想比较两个对象的“内容”,不能依赖运算符
业务里经常需要判断两个对象的内容是否一致。比如两个表单配置对象,字段和值都一样,就认为它们相等。最简单粗暴的方式是 JSON 序列化后再比较:
javascript复制JSON.stringify({ a: 1 }) === JSON.stringify({ a: 1 }); // true
但这种方法有一些明显局限:如果对象里的属性顺序不同,序列化得到的字符串就可能不同;如果值是 undefined、函数、Symbol,序列化时会直接丢失;如果有循环引用,直接抛异常。这里列一下典型的注意事项:
- JSON.stringify 会忽略值为 undefined 的属性和函数属性。
- Date 对象会被转成字符串,导致类型信息丢失。
- 对象的 key 顺序不同,序列化结果可能不同。
- 包含 Map、Set、RegExp 时,序列化结果不一定符合预期。
- 循环引用会直接报错。
如果只是简单场景,比如后端接口返回的纯数据对象,JSON 序列化方案能用。一旦涉及复杂对象结构,最好还是写一个递归比较函数,或者引入成熟的工具库,不要贪图方便埋雷。判断两个对象是否“结构相同”,本质上是在做深层次的字段遍历,这和深拷贝遇到的问题是一样的,后面第 6 章详细说。
3.3 NaN、+0 与 -0:等于运算里的边缘怪胎
还有几个特殊值需要注意。NaN 表示“不是一个数字”,它有一个反直觉的设定:NaN === NaN 的结果是 false。
javascript复制console.log(NaN === NaN); // false
Number.isNaN(NaN); // true,推荐用这个判断
0 也有正负之分,在多数业务里你可能感知不到,因为 +0 === -0 是 true。但在某些极限计算场景下,如果你确实需要区分正负零,ES6 提供的 Object.is() 可以帮忙:
javascript复制Object.is(NaN, NaN); // true
Object.is(0, -0); // false
Object.is(1, 1); // true
在项目里判断特殊数值时,优先使用 Number.isNaN 而不是全局 isNaN。因为全局 isNaN 做判断时会把参数隐式转成 Number,像 isNaN('abc') 会返回 true,这没问题,但 isNaN('123') 会返回 false,因为它先把字符串转成了数字 123,再判断“不是数字”这件事。你可能以为自己在判断非数字,实际在处理类型转换。Number.isNaN 不会做转换,它只对真正的 NaN 返回 true。
4. typeof、instanceof 与一堆判断类型的坑
4.1 typeof 的结果表
typeof 是 JS 里最常用的类型探测工具,语法简单,但它给出的结果并不严格等于“数据类型表”。先把完整结果摆出来:
| 表达式 | typeof 结果 |
|---|---|
| typeof undefined | "undefined" |
| typeof 'hello' | "string" |
| typeof 123 | "number" |
| typeof 10n | "bigint" |
| typeof true | "boolean" |
| typeof Symbol() | "symbol" |
| typeof null | "object" |
| typeof {} | "object" |
| typeof [] | "object" |
| typeof function(){} | "function" |
typeof null 返回 "object",是 JS 历史遗留问题:早期实现里 null 的二进制标识和对象一样,后来想改也改不了,会影响大量线上代码,于是只能将错就错。所以判断 null 只有一条靠谱的路:直接用全等比较,value === null。
typeof function 返回 "function" 这一点也常让人疑惑。函数本质上是一种可调用对象,但它同时又拥有对象的许多能力,能挂属性、能被继承。ECMAScript 为它单独设计了内部插槽 [[Callable]],typeof 会优先识别这个特性,于是返回了独立的 "function"。你只要记住,函数在类型家族里是对象,typeof 是一个带着历史包袱的探测工具就行了。
4.2 包装对象:你也可能亲手造出“假的字符串”
了解 typeof 之后,还要避开一个隐蔽的坑:包装对象。
你可以用 new 关键字创建原始类型对应的包装对象:
javascript复制const str1 = 'hello';
const str2 = new String('hello');
console.log(typeof str1); // "string"
console.log(typeof str2); // "object"
console.log(str1 === str2); // false
console.log(str1 == str2); // false,虽然宽松相等也不相等,因为类型不同
new String('hello') 返回的不是字符串,而是一个 String 对象。它是对象类型,所以 typeof 是 "object",和原始字符串严格比较时也不相等。Number、Boolean 都有同样的问题。
在实际编码里几乎不需要手工创建包装对象,但你要能认出这类问题。如果从某个 JSON 转换工具、老代码里突然冒出一个 typeof xxx === 'object' 的字符串,先看看它是不是被 new String 包了一层。
4.3 用 Object.prototype.toString 拿到可靠的类型标签
当 typeof 不够用的时候,业界公认最可靠的方法是借助内置的 Object.prototype.toString 方法。
javascript复制Object.prototype.toString.call('hello'); // "[object String]"
Object.prototype.toString.call(123); // "[object Number]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(function(){}); // "[object Function]"
Object.prototype.toString.call(new Date()); // "[object Date]"
它是怎么做到的?对象内部有一个 Symbol.toStringTag 属性,或者一个特殊的内置标签,Object.prototype.toString 会读取这个标签,然后拼出 "[object Xxx]" 这样的字符串。因为大部分内置对象都设置了对应的标签,所以这个方法在绝大多数场景下都能返回非常清晰的类型信息。
我们可以把它封装成一个函数:
javascript复制function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
getType([]); // "array"
getType(null); // "null"
getType(new Map()); // "map"
getType(new Set()); // "set"
getType(new Date()); // "date"
封装好之后,判断数组、日期、正则、普通对象就都能统一了。需要注意,如果对象上自定义了 Symbol.toStringTag,这个结果是可以被“伪造”的。不过业务代码里遇到这种覆盖的概率非常低,真遇到时说明你已经进入了比较边缘的框架开发场景。
判断数组还有一个更直接的 API:Array.isArray()。它比 instanceof Array 更可靠,因为 instanceof 是基于原型链查找的,如果数组来自另一个 iframe 或另一个 realm,也就是身处不同的全局环境,原型链可能对不上,而 Array.isArray 是可以跨环境工作的。
4.4 function、array、object 的区分要放在一起理解
有了上面的工具,区分 function、array、普通对象就会变得很轻松:
- typeof 直接区分函数和非函数。
- Array.isArray 区分数组和普通对象。
- 普通对象可以再靠 Object.prototype.toString 判断。
判断一个值是不是“纯对象”,也就是没有自定义原型、这类大家最常用的键值对容器,可以再给一个进阶示例:
javascript复制function isPlainObject(value) {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === null || proto === Object.prototype;
}
为什么要专门区分纯对象?因为在深拷贝、表单配置合并、数据比对这些场景里,你往往只希望处理最简单直接的键值对结构,而不希望把一个 Date、一个 Map、一个 class 实例当成普通对象来无脑遍历。加一道校验,能省掉后续大量的边界问题。
5. 函数传参不是玄学:参数到底是值还是引用?
5.1 两个实验直接说明结论
“JS 函数传参是按值传递还是按引用传递”,这个问题在网上吵了不知道多少年,很多面试回答也各执一词。与其背结论,不如亲手做两个对照实验。
实验一:函数内部给参数重新赋值,外部不受影响。
javascript复制function testReassign(p) {
p = { count: 999 }; // 把 p 指向一个新对象
}
const obj = { count: 1 };
testReassign(obj);
console.log(obj.count); // 1
实验二:函数内部修改参数对象的属性,外部受到影响。
javascript复制function testMutate(p) {
p.count = 999; // 通过 p 修改它指向的那个对象
}
const obj2 = { count: 1 };
testMutate(obj2);
console.log(obj2.count); // 999
看到这两个结果,你就能推出结论:JS 函数传参不是纯粹的引用传递。如果真是引用传递,实验一里 obj 就应该被换成新对象;它也不是传统意义上的值传递,因为如果只是单纯值传递,实验二里外部对象不应该被改动。
更准确的说法是:JS 传参时传递的是“引用的副本”。这句话拆开理解就是:外面的变量保存了一个门牌号,函数在入参时,把这个门牌号复制了一份递给函数内部的参数变量。此时外部变量和内部参数手里都拿着同一串门牌号,所以通过内部参数去修改屋里内容,外面的变量能看到;但如果你让内部参数换一个新门牌号,外面的变量完全不知情。
5.2 数组和对象方法里最常踩的原地修改问题
这个机制最常见的爆发点,不在普通对象上,而在数组方法上。
有些方法直接用“引用副本”找到了原数组,并在原数组上动手脚,比如 sort、splice、reverse、push、pop、shift、unshift、fill、copyWithin。这些方法调用完,原数组就已经变了。
javascript复制const arr = [3, 1, 2];
const sorted = arr.sort();
console.log(sorted === arr); // true,sort 返回的是原数组本身
console.log(arr); // [1, 2, 3],原数组已经变了
另一些方法不会动原数组,而是返回一个新数组,比如 concat、slice、filter、map。做函数式编程时经常用 map、filter,它们就是专门用来避免副作用的。
这里列一张常见方法速查表,至少业务开发时你能一眼看出代码是否会引发副作用:
| 方法 | 是否原地修改原数组 |
|---|---|
| push / pop / shift / unshift | 是 |
| splice | 是 |
| reverse / sort / fill / copyWithin | 是 |
| slice / concat / map / filter | 否 |
| forEach / reduce | 不修改原数组本身,但回调里改原数组无法被阻止 |
更隐蔽的问题是 sort 方法的默认行为。sort 不传比较函数时,会把所有元素先转成字符串,再按字符编码顺序排序。于是你可能得到这种看起来“不可理喻”的结果:
javascript复制const numbers = [10, 9, 2, 100];
numbers.sort();
console.log(numbers); // [10, 100, 2, 9]
10 排在 2 前面,100 排在 9 前面,因为排序时“100”比“9”小——字符串比较是按位比较字符编码的。所以给数字数组排序,永远要记得传比较函数:
javascript复制numbers.sort((a, b) => a - b);
console.log(numbers); // [2, 9, 10, 100]
5.3 对象的修改也会通过“引用副本”传染到外部
不只是数组,普通对象作为参数传入函数后,同样存在外部被修改的风险。比如在 Vue 或 React 项目里,你把 state 里的某个对象传给一个工具函数,工具函数内部顺手改了属性,页面状态就被污染了。
我在早期项目里踩过一个挺典型的坑:封装了一个“给默认配置补充用户配置”的函数,图省事直接在入参对象上赋值并返回。结果调用方发现自己原始的默认配置对象也变了,后续复用默认配置时拿到的是上一次用户改过的数据。修复方式并不复杂,函数开头先浅拷贝一份,再接用户配置。但如果不理解引用传参,你甚至都定位不到问题在哪,只会觉得“这个函数怎么有记忆功能”。
业务开发时的原则应该是:如果函数需要修改对象,尽量明确语义,比如命名成 updateXxx、setXxx;如果函数只是想要读取数据,就不要在内部修改传入对象。和数组一样,对象方法也存在修改型 API 与返回新对象型 API 的区别,从设计上倾向使用不可变数据流,能少掉一大部分由引用共享造成的 bug。
6. 拷贝对象前,先想清楚你要“浅”还是要“深”
6.1 拷贝为什么这么难
正是因为引用类型保存的是门牌号,当你需要得到一份“编辑后不影响原对象”的复制品时,就必须把房子里的内容重新盖一套出来,而不只是抄门牌号。
浅拷贝是第一层解决方案。它只新盖了一层“外壳”,如果属性值本身也是对象,那层对象还是老门牌号。
javascript复制const original = {
name: '张三',
address: {
city: '北京'
}
};
const shallowCopy = { ...original };
shallowCopy.name = '李四';
console.log(original.name); // '张三',第一层属性独立了
shallowCopy.address.city = '上海';
console.log(original.address.city); // '上海',嵌套对象的引用还在共享
从结果能清楚看到,扩展运算符和 Object.assign 都只能做到浅拷贝。如果原对象是数组,[...arr]、arr.slice()、arr.concat() 也全都只是浅拷贝,数组元素里如果还有对象,那些对象依然是共享状态。
6.2 深拷贝三件套:JSON、structuredClone 与手写递归
深拷贝要求把整个对象内部结构全部递归复制一遍,直到所有子属性都是基本类型为止。先看风险最低的方案。
第一个方案是 JSON 序列化,虽然最常用,但要非常小心:
javascript复制const data = {
name: '张三',
time: new Date(),
sayHi() { console.log('hi'); },
why: undefined,
reg: /abc/g
};
const copied = JSON.parse(JSON.stringify(data));
console.log(copied);
// {
// name: '张三',
// time: "2025-01-01T00:00:00.000Z",
// reg: {}
// }
// 函数和 undefined 字段消失,Date 变成了字符串,RegExp 变成了空对象
我在传输接口数据时就遇到过一次:对象里的时间字段经过 JSON 深拷贝后,从 Date 类型变成了字符串,后端要求传时间戳,一下子对不上;还有个对象里有字段值为 undefined,序列化后这个键直接消失,前端判断 if ('key' in obj) 时结果完全反了。所以 JSON 深拷贝只适合“纯 JSON 数据”,比如后端接口返回的那种。带上函数、Date、Map、循环引用的对象,不能指望用它。
第二个方案是浏览器的 structuredClone。这是现代运行时提供的原生深拷贝 API,支持循环引用,能完整保留 Date、Map、Set、RegExp、Blob 等类型,是目前普通业务场景里最推荐的选择。
javascript复制const original = {
name: '张三',
time: new Date(),
tags: ['a', 'b'],
map: new Map([['x', 1]])
};
const cloned = structuredClone(original);
cloned.tags.push('c');
console.log(original.tags); // ['a', 'b'],原对象不受影响
console.log(cloned.time instanceof Date); // true
但它有个硬限制:不能克隆函数。如果对象里有函数,调用会直接抛错。同时,结构化克隆的语义是“复制二进制数据结构”,不会保留对象原型链上的自定义属性和私有字段,class 实例克隆后会丢失原型,变成普通对象。换句话说,不要指望它能完美克隆任何 class 实例。
第三个方案是手写递归。如果你工作环境没有 structuredClone,或者你只需要覆盖最常用场景,写一个简单递归并不复杂:
javascript复制function deepClone(value) {
if (value === null || typeof value !== 'object') return value;
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (Array.isArray(value)) return value.map((item) => deepClone(item));
const result = {};
for (const key of Object.keys(value)) {
result[key] = deepClone(value[key]);
}
return result;
}
这个简版没有处理 Map、Set、循环引用,本质上只覆盖了普通对象和数组。真要处理完整场景,需要维护一个 WeakMap 记录已经遍历过的对象,防止循环引用导致死循环。手写深拷贝不是考试炫技,而是当你理解了基本类型和引用类型的区别后,自然就可以写出来的工具函数。
6.3 深拷贝不是万能的,很多业务场景其实不需要深拷贝
深拷贝能解决问题,但也会引入额外成本:数据量大的时候性能开销明显;class 实例会被扯掉原型;函数直接丢失;响应式框架里深拷贝后还会切断对象和代理之间的关联。遇到需要“复制一份配置”的场景,不要条件反射地总是上深拷贝。
一个更好的思路是:能手工重建就手工重建,能浅拷贝就浅拷贝,只有嵌套层次很深且确实需要完全独立的副本时才考虑深拷贝。Vue 3 的 reactive 对象如果直接深拷贝后交由响应式系统管理,很可能出现比连锁修改更复杂的性能问题。拷贝不是越多越好,而是越合适越好。
7. 隐式类型转换的几大雷区
7.1 加法运算符是最让人迷惑的
JavaScript 在处理运算符时需要用到哪个类型,就会尝试把操作数转换成那个类型。这里最出名的是二进制加法 + 号。规则说起来很简单:如果两个操作数中有一个是字符串,另一个会被转成字符串,整体变成字符串拼接。
javascript复制console.log(1 + '1'); // "11"
console.log(1 + 2 + '3'); // "33",先算 1 + 2 = 3,再拼字符串得到 "33"
console.log('1' + 2 + 3); // "123",第一步就变成了字符串拼接,后续全部是拼接
这个运算顺序极其容易踩坑。有人以为 '1' + 2 + 3 会先做数学运算再拼接,结果得到的是 "123"。如果真是想表达“1+2+3=6 然后拼个前缀”,要记得给数值运算加括号,比如 '结果:' + (1 + 2 + 3)。
减、乘、除运算符没有字符串拼接的语义,遇到非数字时,会尽量把操作数转成数字再去计算:
javascript复制console.log('5' - 1); // 4
console.log('5' * '2'); // 10
console.log('abc' - 1); // NaN
7.2 == 容易埋雷,建议只用 ===
宽松相等运算符 == 在做比较时,会对左右两边做隐式类型转换,它的规则多到正常人根本记不全。几个经典值:
javascript复制null == undefined; // true
1 == '1'; // true,因为 '1' 被转成了数字 1
0 == false; // true,false 转成 0
'' == false; // true
[] == ![]; // true,这道经典题把两边都转成字符串或数字后就撞上了
我不建议业务代码里用 ==,尤其是在做条件判断的时候。绝大多数情况下我写的都是 === 或 !==,这样至少能避免类型转换带来的意外。
如果确实要同时判断“值是 null 还是 undefined”,有个自然习惯可以使用:
javascript复制if (value == null) {
// 当且仅当 value 是 null 或 undefined 时进入
}
这里很多老手也会这么写,因为只有 null 和 undefined 在
