1. 聊聊为什么我还在研究 a == b 这种“基础题”
先说个稍微反常识的事:== 宽松比较在 JS 里已经存在了二十多年,但直到现在,我面试过的前端候选人里,能把 null == 0、[] == false 这种题目解释清楚的人,可能不到三成。大多数人要么直接甩一句“用 === 就好,别碰 ==”,要么就背了一堆结论,换个花样就翻车。
这其实挺可惜的。宽松比较恰恰是整个 JavaScript 类型系统里最浓缩的一块,它把 ToPrimitive、ToNumber、ToString 这些隐式转换规则全部串在一起。你只有把 == 彻底弄明白了,才算真的摸清了 JS 的变量在底层是怎么被对待的。另一个比较实际的原因是,很多老项目、第三方库、浏览器兼容代码里大量使用了 ==,你不懂它的规则,排查 bug 的时候就会各种抓瞎。
我写这篇东西,不会只给你列一张“结果对照表”,而是把整个比较流程拆开揉碎:JS 引擎在遇到 == 时到底做了哪几步?每个步骤分别调用了什么方法?null、undefined、布尔值、对象、日期、数组各自走哪条分支?全都会用实际例子跑一遍。
无论你是刚学 JS 的入门者,还是工作了两三年想补齐基础短板的前端,只要把这篇内容吃透,以后再看到任何宽松比较表达式,都能直接在心算里走到最终结果。我会尽量抛开教科书腔,用开发视角讲清楚每一处细节,包括容易踩的坑和团队规范怎么定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 宽松比较的底层机制:先搞清楚比较到底分了几个阶段
2.1 == 和 === 的根本差异在于“先转换再比较”
很多人在初学阶段就被告知“== 会做类型转换,=== 不会”,这句话大方向没问题,但它掩盖了一个关键事实:类型转换并不是无条件发生的。== 的背后有一套严格的分支判断逻辑,只有当左右两边的类型不一致时,引擎才会尝试把某一方转成另一方的类型,然后再做比较。如果两边类型本来就相同,== 和 === 的走向几乎是一致的。
我个人理解的方式比较简单:== 是一个“先看类型,再决定要不要转换,最后做值比较”的流程;=== 则是“类型不同直接 false,类型相同再比较”。这意味着,凡是你能用 === 判断的场景,你也能推导出 == 的结果,只是后续多走了一层转换逻辑。
举一个最直观的例子:
javascript复制"1" == 1; // true
"1" === 1; // false
"1" 和 1 类型不一致,== 会把字符串 "1" 转成数字 1,然后 1 == 1 成立。=== 发现类型不同,直接返回 false,不再看值。
从这里可以引出一个对理解整个章节都很重要的结论:要预测 == 的结果,首先要能判断左右两边的类型。类型相同走值比较;类型不同,才需要继续查看具体的转换规则。
2.2 核心方法 ToPrimitive:把对象转成原始值的必经之路
当 == 两边出现对象时,JavaScript 并不会直接拿对象去和原始值比较,它需要先把对象转成“原始值”(也就是 string、number、boolean、null、undefined、symbol 这几种),这个转换过程在规范里叫 ToPrimitive。
我们可以把 ToPrimitive 理解成一段内部逻辑:当需要把一个对象转成原始值时,引擎会尝试调用对象上的 Symbol.toPrimitive 方法。如果这个方法存在,就直接用它的返回值;如果不存在,再走 valueOf 和 toString 两条老路。在宽松比较的场景里,传入的 hint(转换倾向)通常是 default,也就是“默认模式”,它会在 Enum 的优先级上先取 valueOf,再取 toString。
具体到代码层面,基本就是这个流程:
javascript复制const obj = {
valueOf() {
return 42;
},
toString() {
return "42";
}
};
obj == 42; // true
String(obj); // "42"
上面这个例子里,obj == 42 触发了 ToPrimitive(obj, default),按规则先走 valueOf,拿到了数字 42,然后 42 == 42 成立。
这个规则的例外情况是 Date 对象:Date 内置了 Symbol.toPrimitive,并且当 hint 是 default 时会按 string 的优先级来处理,也就是先走 toString 再走 valueOf。这点很容易忽略,后面我会专门用一个章节讲 Date。
2.3 原始值之间的转换:ToNumber 和 ToString 的分工
当 == 两边的类型不一致,但又不是对象时,引擎会按下面这套规则选择转换方向:
- 字符串和数字比较:把字符串转成数字,再比较。
- 布尔值和其他类型比较:先把布尔值转成数字,
true转1,false转0,再继续前面的流程。 null和undefined:比较规则非常特殊,两者互等,但和0、空字符串、false 都不相等。- 对象和字符串/数字比较:先把对象转成原始值(走
ToPrimitive),再按上面的规则继续。 - BigInt 和 Number 比较:允许直接比较,BigInt 会转换成数字后再比较,但两边涉及的原始值不能是字符串混合布尔,否则会走其他规则。
再补充一个很多人都会混淆的点:NaN 不参与宽松比较的结果成立。NaN == NaN 永远是 false,因为规范里 NaN 不等于任何值,包括它自己。这意味着哪怕你写 NaN == 某个表达式,只要表达式的计算结果是 NaN,结果就是 false。
整个流程看下来,你会发现 == 并没有那么“随意”,它的每一步都有明确的规范支撑。真正让人迷惑的其实是“转换出来的结果是什么”这一步,而这一步恰恰是可以用 Number()、String() 这些显式函数来模拟的。所以我在面试别人时经常说一句话:“想理解 ==,先理解显式的类型转换,再看隐式转换就不慌了。”
3. 实战拆解一:null、undefined、布尔值与数字字符串之间的相爱相杀
3.1 null 和 undefined 的“独享”规则
先看最特殊的一对组合。规范里对 null == undefined 只留了一句话:两者互等。它们只等于对方,不等于其他任何值。
javascript复制null == undefined; // true
null == 0; // false
null == ""; // false
null == false; // false
undefined == 0; // false
undefined == null; // true
这个规则为什么特别?因为如果按照“把 null 转成 0,undefined 转成 NaN”的思路去推,结果完全是另一种样子。但规范直接规定它们是一个“相等集合”,不参与普通转换流程。
实际开发中这个规则最有用的场景是判空。很多工具函数会写:
javascript复制function isNil(value) {
return value == null; // 同时覆盖 null 和 undefined
}
这个写法非常经典,而且 eslint 里有一条规则 eqeqeq 开启 always 后,通常在 null 比较上会开 always 加 "null": "ignore" 或单独配置。我见过不少团队会在规范里写明:判断 value 是否为 null/undefined 时,允许使用 == null。它比 value === null || value === undefined 更简洁,并且能防止误把 0 或空字符串当成“空值”——这一点很多新手反而理解反了,总担心空字符串会被判定为空,但实际上 "" == null 是 false,不会误判。
顺带说一句,null 和 undefined 之间的唯一差别在于:undefined 是变量未被赋值时的默认值,而 null 通常表示“有意的空值”。但当我们用 == null 判断时,两者会同时命中,这也是这个写法能流行起来的原因。
3.2 布尔值参与比较时:所有结论都要先转成 0 或 1 再重新推导
布尔类型在 == 里其实是“隐藏炸弹”,因为引擎会把 true 转成 1,false 转成 0,然后所有比较都基于这个转换结果重新交锋。这意味着直接背 “false == "" 是 true” 是没用的,你得自己推一遍:false 转成 0,"" 转成数字也是 0,所以 0 == 0 成立。
但问题来了,下面的表达式结果是多少?
javascript复制"0" == false; // true
"" == false; // true
"false" == false; // false
第一行 "0" 转数字是 0,false 转数字是 0,所以 true。第二行空字符串转数字是 0,同样 true。第三行字符串 "false" 转数字是 NaN(因为字符串里不是合法的数字格式),NaN == 0 永远不成立,所以是 false。
很多人在这组例子里会犯迷糊,本质是因为把“字符串的布尔值含义”和“字符串转数字的规则”混在一起了。字符串 "false" 虽然在语义上代表 false,但它转成数字时结果是 NaN,不是 0,所以它在宽松比较里的表现完全不同于布尔值 false。
这里我要特别强调一个在实战中经常遇到的坑:
javascript复制const res = 0;
if (res == false) {
// 这里的代码会执行
}
如果你本意是想判断一个接口返回的“布尔值”是否为 false,却拿 0 去和 false 比较,就会误判。因为数字 0 在宽松比较中和 false 是相等的。这种问题在判断“是否选中”“是否开启”之类的业务字段时特别容易出 bug。后来我的习惯是:布尔判断里永远用 ===,或者是直接 if (res)、if (!res),绝对不用 == false 或 == true 这种写法。
3.3 字符串转数字的细节:不只是 parseInt 那么简单
把字符串转成数字时,Number() 的规则比 parseInt() 严格得多,它要求整个字符串是合法数字格式。一旦遇到非数值字符,返回 NaN;空字符串返回 0;前导空白会被忽略但不能有其他非法字符。
javascript复制" " == 0; // true,因为 Number(" ") 是 0
"1e3" == 1000; // true,指数形式会被解析
"0x10" == 16; // true,十六进制字面量会被解析
" " == 0; // true
"12px" == 12; // false,Number("12px") 是 NaN
"infinity" == Infinity; // false,大小写不匹配
这些结论在你写表单校验或配置解析时经常冒出来。比如用户输入一个 "12px",你想判断它是否等于某个数值,如果用了 == 就会拿到 false,因为 Number("12px") 是 NaN,而不是 12。这个场景下正确的做法是先解析出数字部分再比较。
字符串与数字比较还有一个很实用的工程结论:如果两个值都是字符串,== 会走字符串比较,不会发生转换;如果一个字符串一个数字,字符串会被 Number() 转换再比较。所以拿 "01" == 1 是 true,因为左边转成数字 1;而 "01" == "1" 是 false,因为两边类型相同,走的是纯字符串比较,逐字符比较导致不等。我之前在代码评审里见过有人把前端传参和后端返回做比较:前端传 "01",后端返回 1,写 "01" == 1 没问题,但写成 "01" === "1" 反而对不上,这里必须清楚数据结构再决定用哪种比较。
4. 实战拆解二:对象参与比较时,valueOf、toString 与方法优先级如何影响结果
4.1 对象和原始值比较:先走 when 分支还是直接走默认转换
对象参与 == 时,标准流程是:先把对象转成原始值,再进入原始值之间的比较。这里最需要注意的就是“转成什么”。
javascript复制const obj = {};
obj == "[object Object]"; // true
这是最简单的一个例子:obj 转原始值时,先走 valueOf,普通对象的 valueOf 返回对象自身(不是原始值,所以被放弃),再走 toString,得到 "[object Object]",然后字符串比较命中。
但 {}.toString() 能拿到 "[object Object]",换成数组就不一样了:
javascript复制const arr = [1, 2, 3];
arr == "1,2,3"; // true
数组的 toString 会把所有元素用逗号连接起来,所以 [1, 2, 3] 会转成字符串 "1,2,3"。
这个规则让我在最开始接触时特别容易混乱,因为对普通对象来说,valueOf 返回的是对象本身,不会产生原始值,于是只能走 toString;而数组、Date 这些内置对象又各自重写了转换逻辑。了解这一点后,遇到 {} == "[object Object]" 就不再神秘。
这里有个知识要点:所有非空对象调用默认 toString 时,基本都是 "[object XXX]" 结构,具体标签取决于内置的 Symbol.toStringTag 或内部类型。数组、函数、正则的默认 toString 又各不相同。做比较题时,先手动调用一次 String(obj) 通常就能得到正确答案,因为 String() 的转换逻辑和 ToPrimitive 最终走到的字符串结果一致(在非 Date 对象上基本一致)。
4.2 重写 valueOf 和 toString 之后的比较结果
业务代码里很少直接比较一个自定义对象,但面试题和底层库却很爱考这个点。当你自己定义了对象的转换行为,== 的结果就完全取决于你返回的原始值。
javascript复制const obj = {
valueOf() {
return 0;
},
toString() {
return "0";
}
};
obj == 0; // true
obj == false; // true,因为 0 == false
obj == ""; // true,因为 0 == ""
这里的关键是:== 默认 hint 是 default,对普通对象会优先 valueOf。所以 obj 返回了数字 0,后续比较全部基于 0 展开。
如果我把 valueOf 删掉,只留 toString:
javascript复制const obj2 = {
toString() {
return "0";
}
};
obj2 == 0; // true,"0" 转数字是 0
obj2 == "0"; // true,字符串和字符串比较
obj2 == false; // true,"0" 转数字是 0,和 false 转的 0 相等
从这个过程可以看出,转换方向取决于“转成原始值之后拿到的类型”。拿到字符串,就走字符串转换;拿到数字,就走数字比较。如果拿到的是布尔值,又会接着走布尔转换逻辑。这个链条只要理清了,{ valueOf: ... } 这类题的推导速度能提升不少。
4.3 Date 对象的特殊优先级:为什么它总是先走 toString
上一节提到 Date 在 ToPrimitive 时走的是 string 优先。落在宽松比较里,就是下面这个样子:
javascript复制const d = new Date(2024, 0, 1);
d == d.toString(); // true
而如果我们不主动和字符串比较,直接拿 Date 和数字比,情况更值得琢磨:
javascript复制const d = new Date(0); // 1970-01-01T00:00:00Z
d == 0; // true
为什么这里不是先走 toString 再比对字符串呢?因为 d 的 toString 结果是 "Thu Jan 01 1970 08:00:00 GMT+0800 (...)" 这种长字符串,把它转成数字会得到 NaN,NaN == 0 是 false。但实际结果是 true,说明引擎虽然对 Date 的 hint 是 string,但在比较数值时,还是通过 valueOf 拿到了时间戳数字。
这个事在规范里是这样落地的:Date 的 Symbol.toPrimitive 接收到 hint 为 "string" 时,先尝试 toString,再尝试 valueOf;接收到 "number" 或 "default" 时,却是先 valueOf 再 toString。而宽松比较传入的 hint 实际上会根据另一边的类型来决定:如果另一边是字符串,引擎更倾向于字符串转换;如果另一边是数字,更倾向于数值转换。这就导致 Date 和数字比较时能走 valueOf 拿到时间戳。
实操中,我判断两个时间是否相等是这么做的:
javascript复制const a = new Date("2024-01-01T00:00:00Z");
const b = new Date("2024-01-01T08:00:00+08:00");
a == b; // false,因为对象引用不同
a.getTime() == b.getTime(); // true,时间戳相同
所以我的习惯是:比较时间统一用 getTime() 或 +date,不要依赖 ==。因为对象之间的 == 是引用比较,就算两个 Date 表示同一时刻,只要不是一个实例,结果就是 false。
4.4 数组与原始值比较的经典场景
数组在宽松比较里的表现,几乎可以说是前端面试的“常青树”。
javascript复制[] == false; // true
[] == 0; // true
[] == ""; // true
[0] == false; // true
[null] == ""; // true
[undefined] == false; // true
推导过程:[] 转原始值时,valueOf 返回数组自身(不是原始值),于是走 toString,空数组得到 ""。空字符串转数字是 0,false 转数字也是 0,所以 [] == false 最终成立。
[null] 和 [undefined] 会转成字符串 "",因为数组的 toString 会把 null/undefined 元素当成空字符串处理。这种细节最容易被人忽略,一旦面试官换个姿势,把 [null] 抛出来,很多人就懵了。
我再给一个相对进阶的例子:
javascript复制[1, 2] == "1,2"; // true
[1, 2] == [1, 2]; // false
第一行是数组转成字符串和另一个字符串比较,因为类型都是字符串,走字符串相等,结果为 true。第二行是两个对象比较,对象之间永远是引用比较,只要是两个不同数组实例,哪怕内容完全一样,也是 false。这是 == 和 === 在“类型相同”时会表现出一致行为的一个例证——两个对象都不会发生隐式转换。
5. 工程实践:宽松比较到底该不该用,以及怎么用才不背锅
5.1 必须使用宽松比较的几种“安全姿势”
虽然社区主流规范倾向于使用 ===,但宽松比较并不是洪水猛兽。只要把它限制在少数几种语义清晰的场景里,代码会更简洁,也不容易出问题。
第一个是判断空值:
javascript复制if (value == null) {
// 同时处理 null 和 undefined
}
这个在前面已经讲过,是 == 最健康的使用场景。
第二个是从接口或输入框拿到的值,明确就是字符串或数字,但格式不完全统一:
javascript复制const status = String(response.data.status); // "0" 或 "1"
if (status == 1) {
// 这里允许宽松比较,避免后端返回 "1" 时还得每次转类型
}
这种写法需要你确定 status 不会是 "01"、true、0 这种容易混淆的值,否则也会踩坑。我的建议是:能用 Number() 包一层再比较就尽量包,除非你很确定两边的值语义。
第三种场景是与枚举值比较时,使用宽松比较可以避免频繁类型转换:
javascript复制const TYPE = { ACTIVE: 1, DISABLED: 0 };
if (row.type == TYPE.ACTIVE) { }
只要 row.type 是后端传的字符串 "1" 或者数字 1,结果都能成立。但这种写法对团队约定要求较高,必须保证枚举值不会出现 true、null 这类“危险分子”。
5.2 什么场景我绝对不用 ==
前面说了安全姿势,这里反过来列一下我在 code review 里一定会拦下来的写法。
不要用 == true 或 == false 判断布尔语义:
javascript复制if (res == true) { } // 不推荐
if (res === true) { } // 不推荐(除非 res 确实是布尔类型)
if (res) { } // 推荐
问题是,res == true 会把 1、"1" 都判定为 true,这在大多数业务场景里不是你想要的。而且一旦 res 是对象,任何非空对象 == true 都是 false——因为对象转原始值后还要再转数字,数字 0 时才和 false 相等,非空对象大概率会转成 NaN 或非零数字。这个推导过程非常绕,不建议让大家在业务代码里脑算这种负担。
不要用 == 判断“值是否等于空字符串或 0”:
javascript复制if (input == "") { } // 容易误判,比如 input 是 0 时也成立
if (input === "") { } // 明确
上面这个例子特别能说明问题:0 == "" 是 true。如果你本意是想判断用户是否没输入内容,却用了 == "",当用户输入数字 0 时会被错误地当成“空”。这里用 === 才符合直觉。
不要在接口响应值上无条件用 == 判断成功状态:
javascript复制if (response.code == 0) { } // 如果 code 是 "00" 或 "0.0",也可能成立
我曾经在一个项目里遇到过:后端某个接口返回的 code 字段在异常场景下是 "000",正常是 0。当时用了 ==,导致异常状态也被当成成功处理了。排查了很久才发现是宽松比较的问题。从那以后,成功状态判断一律先转成数字再比较,或者直接用 ===。
5.3 把规则落实到团队规范里:eslint 配置和代码评审要点
工程上,我强烈建议团队开启 eslint 的 eqeqeq 规则,并针对 null 比较做单独豁免。
javascript复制// .eslintrc.js
{
rules: {
eqeqeq: ["error", "always", { null: "ignore" }]
}
}
这个配置会强制所有非空比较都用 ===,同时允许 value == null 的写法来覆盖 null/undefined 判断。这两者结合是很常见的工程实践,能在不牺牲判空便利的情况下,把所有容易引起歧义的宽松比较拦在编译阶段。
代码评审时,我还会额外注意几个信号:看到 == 0、== ""、== true、== false,基本会建议改成更严格的写法,除非业务代码注明理由;看到 == null,则会确认它是否真的想同时匹配 undefined。
团队里如果有新人,我会建议他们先跑一遍“把所有 == 改成 ===,然后看哪些测试挂了”,再针对挂掉的用例分析是不是有必要用 ==。这个训练方法很笨,但特别能让人记住隐式转换的坑。
6. 常见问题与高频面试题实录
6.1 三道经典面试题的完整推演
题目1:[] == ![] 的值是多少?
这是流传很多年的老题,答案是 true。推导流程如下:
- 右侧
![]:数组是对象,Boolean([])是true(非空对象转布尔都是 true),取反得到false。 - 所以原式变成
[] == false。 false转数字得0。[]转原始值:valueOf返回自身,走toString得到""。""转数字得0。- 最终
0 == 0,结果为true。
这道题能串起取反运算、布尔转换、数组转字符串、字符串转数字四层规则,确实非常浓缩。
题目2:[0] == false 的结果?
这也是 true,但过程和 [] == false 有一点点差别。
false转数字得0。[0]转字符串得"0"。"0"转数字得0。0 == 0,true。
从这里可以看出,[0] 能成功转成数字 0,是因为它先变成字符串 "0",再转数字时不歧义。如果数组里是 [0, 1],"0,1" 转数字就成了 NaN,整个结果就是 false。
题目3:"0" == false 和 "" == false 结果分别是?
两个都是 true,但容易混的变形是 "0" == "",答案是 false。因为两边都是字符串,走纯字符串比较,"0" 和 "" 当然不相等。这也提醒我们,在推导时一定要先判断“两边转换完之后到底是什么类型”,如果已经是同类型,就不会再继续转换。
6.2 实际开发中的四个“事故现场”复盘
第一个现场:fs 文件读取结果判断。早期写 Node 时,我用 fs.readFile 的回调参数 err 做判断,写法是 if (err == null)。这是我第一次意识到 == null 有多好用——它同时覆盖了 err 为 null 和 undefined 的情况,避免了判断两遍。
第二个现场:统计数量等于 0 的判断。我有一次写了个列表页,需要判断“已选数量是否为 0”,最早写的是 if (selectedNumber == ""),结果当后端返回字符串 "0" 时,页面出了奇怪的 bug。排查后才发现 "0" == "" 是 false,但 0 == "" 是 true。最终改成 Number(selectedNumber) === 0,问题解决。
第三个现场:数组长度判断。if (arr.length == true) 这种写法也见过,会导致长度为 1 和大于 1 的数组都被当成 true,因为 true 转数字是 1,任何等于 1 的值都命中。这种问题的根源是把“布尔判断”误写成了“宽松数值判断”。
第四个现场:后端返回对象或字符串的混合场景。用户信息接口在数据异常时可能返回 {} 或字符串 "error",当时用 if (res == "[object Object]") 做兜底判断。结果在某些情况下会把非空对象也判定为出错,因为普通对象的 toString 都是 "[object Object]"。这个教训告诉我们:用 == 判断“是不是对象”本身就是反模式,正确做法是 typeof res === "object" && res !== null。
6.3 一张速查表:常见表达式的真值结果
为了方便你日常翻看,我把最常见的比较结果整理成一张表。这张表不能代替理解,但能作为心算时的快速校对工具。
| 表达式 | 结果 | 关键推导 |
|---|---|---|
null == undefined |
true | 规则直等 |
null == 0 |
false | null 不等于 0 |
undefined == 0 |
false | 同上 |
"0" == false |
true | "0" 转数字 0 |
"" == false |
true | 空字符串转数字 0 |
" " == 0 |
true | 空白字符串转数字 0 |
"0" == "" |
false | 同为字符串,纯字符串比较 |
[] == false |
true | [] 转 "",转数字 0 |
[0] == false |
true | [0] 转 "0",转数字 0 |
[[]] == "" |
true | 嵌套空数组转空字符串 |
[null] == "" |
true | null 元素转空字符串 |
{} == "[object Object]" |
true | 对象默认 toString |
new Date(0) == 0 |
true | Date 的 valueOf 返回时间戳 |
NaN == NaN |
false | NaN 不等于任何值 |
"1e3" == 1000 |
true | 指数字符串转数字 |
"12px" == 12 |
false | "12px" 转 NaN |
我建议你先别看这张表,自己把每行推导一遍,再对照查漏。能把这张表完整推出来,说明你对 ToPrimitive、ToNumber、布尔转换的链条已经过关了。
7. 我个人实际使用中的几点体会
做前端这些年,我对 == 的态度经历了好几个阶段:一开始是“统统一律 ===”的简单粗暴,后来是被各种 bug 教育后,慢慢理解了它背后的规范,再到现在,我会把 == 当成一种“在特定语义下可以合理简化的工具”,而不是非黑即白的禁区。
如果让我给一条具体的实战建议,那就是:团队规范写清楚,比个人偏好更重要。你可以规定“所有比较默认用 ===,只有判断 null/undefined 时用 == null”,也可以规定“所有后端枚举比较统一用 == 并附加注释”。关键是大家在读同一份代码时,不用再靠猜来判断作者意图。
另外,每当我遇到某个奇怪的比较结果,第一反应不是“这是 JS 的坑”,而是先手动模拟一遍转换流程。你会发现在绝大部分情况下,流程都是自洽的。JavaScript 这门语言确实有不少历史遗留问题,但 == 这套规则整体上逻辑严密,只要掌握了,它就不会再“坑”你。
下次如果再有人问 a == b 简不简单,你可以回一句:规则本身不复杂,复杂的是我们要不要在业务代码里给它留位置。弄懂它,是为了以后能更自信地选择不用它。
