DrillLab
第 03 / 25 节LESSON 03 / 25约 22 分钟~22 min

引擎与类型十问10 questions on the engine and types

引擎、REPL、原始值 vs 引用值、类型转换、== vs ===、短路、var/let/const、传值传引用、Set、Map。The engine, REPL, primitive vs reference values, type conversion, == vs ===, short-circuiting, var/let/const, passing values and references, Set, Map.

面试 · 第 2 部分Interview · Part 2
这一页有什么On this page11
学完这节你会After this lesson you can
  • 说清原始值和引用值在内存里的差别,并解释它怎么导致「改一个另一个也变」Explain how primitive values and reference values differ in memory, and why changing one variable can change another
  • 背出隐式转换的规则,并说明为什么 == 不该用State the rules for implicit conversion, and say why you should not use ==
  • 分清 var / let / const 在作用域、提升、重复声明三个维度上的差别Tell var / let / const apart on three points: scope, hoisting, and redeclaring
  • 在 Set vs Array、Map vs Object 之间给出选型理由Give a reason for choosing Set over Array, and Map over Object
这在考试里考什么What the exam does with this

类型和内存这一组是所有「诡异行为」的根源:为什么 [] == false 是 true、为什么函数里改了对象外面也变、为什么循环里的 var 拿到的都是最后一个值。答不清这些,后面闭包和异步的题也会答不稳。Types and memory are the source of every surprising result: why [] == false is true, why changing an object inside a function also changes it outside, and why a var in a loop ends up holding the last value. If you cannot answer these clearly, the closure and async questions later will not hold up either.

§01

什么是 JavaScript 引擎What is a JavaScript engine?

#276 What is the JavaScript engine

一句话:引擎是把 JS 源码变成机器能执行的东西的那个程序。Chrome / Node 用 V8,Firefox 用 SpiderMonkey, Safari 用 JavaScriptCore。

大致流程:源码 → 解析成 AST → 生成字节码 →解释器先跑起来, 同时监控哪段代码被反复执行(热点), 把热点交给 JIT 编译器编译成机器码。 这套「先解释、后即时编译」的做法让 JS 既能马上启动、 又能在热点上接近原生速度。

会追问:「引擎和运行时(runtime)什么区别?」 —— 这是真考点。引擎只管执行 JS 语言本身, 它不认识 setTimeoutfetchdocumentfs—— 这些都是宿主环境(浏览器 / Node)提供的 API。
所以「事件循环属于引擎吗?」答案是不属于, 它是运行时的一部分(见 #305)。这个区分答对了很加分。

还会追问内存:引擎管两块 ——调用栈(执行上下文、原始值)和(对象、数组、函数)。 这正好对应下一题。

In one line: the engine is the program that turns JS source into something the machine can run. Chrome and Node use V8, Firefox uses SpiderMonkey, Safari uses JavaScriptCore.

Roughly the pipeline: source → parse into an AST → emit bytecode → the interpreter starts running it while watching which parts run over and over (the hot paths), and hands those to the JIT compiler to be turned into machine code. Interpret first, compile later — that is what lets JS start instantly and still hit near-native speed where it counts.

Follow-up: “What is the difference between the engine and the runtime?” — this is the real question. The engine only executes the language itself. It knows nothing about setTimeout, fetch, document or fs — those are APIs the host environment (browser or Node) hands you.
So “is the event loop part of the engine?” — no, it belongs to the runtime (see #305). Getting this distinction right earns real credit.

Another follow-up, on memory: the engine manages two areas — the call stack (execution contexts, primitive values) and the heap (objects, arrays, functions). Which lines up exactly with the next question.

§02

什么是 REPLWhat is a REPL?

#277 What is REPL

一句话:Read-Eval-Print-Loop —— 读一行、求值、打印结果、再等下一行。 终端里敲 node 回车进去的那个交互环境, 以及浏览器 DevTools 的 Console,都是 REPL。

用来干什么:验证一小段语法、试一个 API 的返回值、 快速算个东西。不适合写多行逻辑—— 改一行要重敲一遍。

会追问:「在 REPL 里 let x = 1 会打印什么?」——undefined。 因为它打印的是表达式的值, 而变量声明语句的值就是 undefined。 这个小细节能看出你是不是真用过。

In one line: Read-Eval-Print-Loop — read a line, evaluate it, print the result, wait for the next one. Typing node in a terminal drops you into one, and the DevTools Console in a browser is one too.

What you use it for: checking a bit of syntax, seeing what an API returns, working out a quick number. Not for multi-line logic — changing one line means retyping the lot.

Follow-up: “What does let x = 1 print in a REPL?” — undefined. It prints the value of the expression, and the value of a variable declaration is undefined. A small detail, but it shows whether you have actually used one.

§03

原始值 vs 引用值Primitive values vs reference values

#278 Primitive data types vs Reference data types

一句话:原始值存在栈上、直接存值; 引用值存在堆上、栈上只存一个地址

七种原始类型(背下来,会让你数):stringnumberbooleanundefinednullsymbolbigint
其余全是引用类型: 对象、数组、函数、DateMapSet、正则…… (数组和函数本质都是对象)。

三个可观察的差别:

  • 不可变性—— 原始值本身改不了。s.toUpperCase()返回新字符串,原来那个没动。
  • 比较—— 原始值比, 引用值比地址。所以{} === {}false[1] === [1] 也是 false
  • 赋值—— 原始值复制一份; 引用值只复制地址,两个变量指向同一个对象。 这就是 #284。

会追问:typeof null 是什么?」——"object",这是个沿用了 30 年的 bug, 因为兼容性一直没修。判断 null 要用x === null
「怎么可靠地判断数组?」——Array.isArray(x), 别用 typeof(会得到 "object")。

In one line: a primitive sits on the stack and holds the value itself; a reference value sits on the heap, and the stack holds only an address.

Seven primitive types (learn the list — they will make you count them): string, number, boolean, undefined, null, symbol, bigint.
Everything else is a reference type: objects, arrays, functions, Date, Map, Set, regexes… (arrays and functions are objects underneath).

Three differences you can observe:

  • Immutability — a primitive value cannot be changed.s.toUpperCase() returns a new string; the original never moved.
  • Comparison — primitives compare by value, references compare by address. So {} === {} is false, and so is [1] === [1].
  • Assignment — a primitive gets copied; a reference copies only the address, so two variables point at the same object. That is #284.

Follow-up: “What is typeof null? ” — "object", a bug that has survived 30 years because fixing it would break too much. Test for null with x === null.
“How do you reliably detect an array?” — Array.isArray(x). Never typeof, which just says "object".

JavaScript值和地址Values and addresses示意Illustrative
1// 原始值:复制值
2let a = 1;
3let b = a;
4b = 2;
5console.log(a); // 1 —— a 没变
6
7// 引用值:复制地址
8let o1 = { n: 1 };
9let o2 = o1; // 同一个对象的第二个遥控器
10o2.n = 2;
11console.log(o1.n); // 2 ← 变了!
12
13// 比较
14console.log({} === {}); // false(两个不同的地址)
15console.log("a" === "a"); // true (比的是值)
16console.log(typeof null); // "object" ← 历史 bug
17console.log(Array.isArray([])); // true ← 判断数组要用这个
1// Primitive value: the value is copied
2let a = 1;
3let b = a;
4b = 2;
5console.log(a); // 1 —— a did not change
6
7// Reference value: the address is copied
8let o1 = { n: 1 };
9let o2 = o1; // a second remote control for the same object
10o2.n = 2;
11console.log(o1.n); // 2 ← it changed!
12
13// Comparing
14console.log({} === {}); // false (two different addresses)
15console.log("a" === "a"); // true (the values are compared)
16console.log(typeof null); // "object" ← a historical bug
17console.log(Array.isArray([])); // true ← use this to test for an array
§04

隐式转换 vs 显式转换Type coercion vs type conversion

#279 Type coercion vs Type conversion(题库里 #386 是同一题)#279 Type coercion vs Type conversion (#386 in the question bank is the same question)

一句话:转换(conversion / casting)是你主动写的强制转换(coercion)是引擎背着你干的

谁发起例子
显式(conversion)Number("42")String(42)Boolean(0)parseInt("42px")
隐式(coercion)引擎"5" * 21 + "1"if (arr.length)[] == false

隐式转换的两条核心规则(记住这两条, 大部分怪题就能推出来):

  • + 只要有一边是字符串,就变成拼接; 其他算术运算符(-*/)一律转成数字。 所以 1 + "1" === "11""3" - 1 === 2
  • 对象参与运算时先 valueOf()toString()。 数组的 toString() 是元素 join 逗号, 所以 [] + [] 得到空字符串,[] + {} 得到"[object Object]"

六个假值背下来(其余全是真):false0""nullundefinedNaN
注意 []{} 都是真值—— 所以判断数组空不空要看 arr.length

会追问:parseIntNumber 什么区别?」——parseInt("42px")42(从头读到读不动为止),Number("42px")NaN(整体不合法就失败)。 所以校验用户输入该用 NumberparseInt 会把脏数据悄悄放过去。

In one line: conversion (casting) is what you write on purpose; coercion is the engine doing it behind your back.

Who starts itExamples
Explicit (conversion)YouNumber("42"), String(42), Boolean(0), parseInt("42px")
Implicit (coercion)The engine"5" * 2, 1 + "1", if (arr.length), [] == false

Two rules cover almost all coercion — hold on to these and you can derive most of the trick questions:

  • + becomes concatenation the moment one side is a string; every other arithmetic operator (-, *, /) converts to number. Hence 1 + "1" === "11" but "3" - 1 === 2.
  • An object in an operation goes through valueOf() first, then toString(). An array’s toString() joins its elements with commas, so [] + [] gives an empty string and [] + {} gives "[object Object]".

Memorise the six falsy values (everything else is truthy): false, 0, "", null, undefined, NaN.
Watch out — [] and {} are both truthy, so check arr.length to tell whether an array is empty.

Follow-up: “What is the difference between parseInt and Number?” — parseInt("42px") gives 42 (it reads from the front until it cannot go on), while Number("42px") gives NaN (the whole string has to be valid). So validate user input with Number; parseInt waves dirty data straight through.

JavaScript隐式转换速查Coercion quick reference示意Illustrative
11 + "1" // "11" + 有字符串 -> 拼接
2"3" - 1 // 2 - 一律转数字
3"3" * "4" // 12
41 + true // 2 true -> 1
51 + null // 1 null -> 0
61 + undefined // NaN undefined -> NaN
7
8[] + [] // "" 两个空数组 toString 都是 ""
9[] + {} // "[object Object]"
10[1,2] + [3] // "1,23" join 逗号再拼
11
12Number("42px") // NaN 整体不合法
13parseInt("42px") // 42 读到读不动为止
14Number("") // 0 ← 注意,空字符串转数字是 0
15Number(" ") // 0 ← 空白也是 0,校验输入要小心
11 + "1" // "11" + with a string means concatenate
2"3" - 1 // 2 - always converts to number
3"3" * "4" // 12
41 + true // 2 true -> 1
51 + null // 1 null -> 0
61 + undefined // NaN undefined -> NaN
7
8[] + [] // "" toString of an empty array is ""
9[] + {} // "[object Object]"
10[1,2] + [3] // "1,23" join with commas, then concatenate
11
12Number("42px") // NaN the whole string has to be valid
13parseInt("42px") // 42 reads until it cannot read further
14Number("") // 0 ← note: an empty string converts to 0
15Number(" ") // 0 ← whitespace is 0 too, so validate input carefully
面试不会让你背全表,但会给两三个式子让你推。掌握「+ 看字符串、其他看数字」和「六个假值」就够推。An interview will not ask you to recite the whole table, but it will give you two or three expressions to work out. Remember that + looks for a string while every other operator converts to number, plus the six falsy values, and that is enough.
§05

== 和 === 的区别What is the difference between == and ===?

#280 What is the difference between == and ===

一句话:===类型不同直接 false==先把两边转成同一类型再比

== 的转换规则很绕,但实际只要记住四条:

  • null == undefinedtrue, 但它们和其他任何值都不 ==(包括 0"")。
  • 数字和字符串比 → 字符串转数字。
  • 布尔参与 → 布尔先转数字true→1,false→0)。 这就是 [] == falsetrue 的原因:[]""0false0
  • 对象和原始值比 → 对象先转原始值。

结论:一律用 ===唯一被普遍接受的 == 用法是x == null—— 一次同时判掉nullundefined

会追问:NaN === NaN?」——falseNaN和自己都不相等。 判断要用 Number.isNaN(x)(别用全局 isNaN,它会先做隐式转换,isNaN("abc")true)。
「有没有更严格的比较?」——Object.is(x, y)。它和 ===只有两处不同:Object.is(NaN, NaN)trueObject.is(0, -0)falseReact 判断 state 变没变用的就是它。

In one line: === returns false straight away when the types differ; == converts both sides to one type first, then compares.

The == conversion rules are convoluted, but four points cover practice:

  • null == undefined is true, and neither one is == to anything else (not 0, not "").
  • Number against string → the string becomes a number.
  • A boolean involved → the boolean becomes a number (true→1, false→0). That is why [] == false is true: []""0, and false0.
  • Object against primitive → the object becomes a primitive.

The conclusion: use === everywhere. The one == that everybody accepts is x == null — it rules out null and undefined in a single check.

Follow-up:NaN === NaN?” — false. NaN is not even equal to itself. Test it with Number.isNaN(x) — not the global isNaN, which coerces first, so isNaN("abc") is true.
“Is there anything stricter?” — Object.is(x, y). It differs from === in exactly two places: Object.is(NaN, NaN) is true, and Object.is(0, -0) is false. This is what React uses to decide whether state changed.

JavaScript为什么别用 ==Why you should not use ==示意Illustrative
10 == "0" // true 字符串转数字
20 == "" // true "" -> 0
30 == false // true false -> 0
4null == undefined // true 特例
5null == 0 // false null 只和 undefined 相等
6[] == false // true [] -> "" -> 0
7NaN == NaN // false 和自己都不相等
8
90 === "0" // false 类型不同,到此为止
10Object.is(NaN, NaN) // true ← React 用它判断 state 变没变
10 == "0" // true the string converts to a number
20 == "" // true "" -> 0
30 == false // true false -> 0
4null == undefined // true a special case
5null == 0 // false null only equals undefined
6[] == false // true [] -> "" -> 0
7NaN == NaN // false not even equal to itself
8
90 === "0" // false different types, so it stops there
10Object.is(NaN, NaN) // true ← React uses this to decide whether state changed
§06

什么是短路求值What is short-circuit evaluation?

#281 What is short-circuit evaluation

一句话:&&||结果一旦确定就不再算右边, 而且它们返回的是操作数本身,不是布尔值

  • a && b—— a 为假就返回 a,否则返回 b。「都真才真」, 所以遇到假就可以收工。
  • a || b—— a 为真就返回 a,否则返回 b。
  • a ?? b(空值合并)——只有 a 是 nullundefined才返回 b。

||??的区别是高频追问,而且是真实 bug 来源:count || 10count0 时会给出 10—— 因为 0 是假值。要默认值就用 ??

React 里最常见的用法是条件渲染:{loading && <Spinner />}坑在这儿:如果左边是list.length 而列表为空,0 && … 返回 0, 而 React 会把 0 渲染出来—— 页面上凭空多一个「0」。 所以要写成 list.length > 0 && …

In one line: && and || stop evaluating the moment the result is decided, and they hand back an operand, not a boolean.

  • a && b — returns a if a is falsy, otherwise b. “Both must be true”, so one falsy value ends the job.
  • a || b — returns a if a is truthy, otherwise b.
  • a ?? b (nullish coalescing) — returns b only when a is null or undefined.

The difference between || and ?? is a frequent follow-up and a real source of bugs: count || 10 gives you 10 when count is 0, because 0 is falsy. For default values, reach for ??.

The most common React use is conditional rendering: {loading && <Spinner />}. Here is the trap: if the left side is list.length and the list is empty, then 0 && … returns 0, and React renders that 0 — a stray “0” shows up on the page out of nowhere. Write list.length > 0 && … instead.

JSX短路的三个实际用法与两个坑Three real uses of short-circuiting, and two traps示意Illustrative
1// 短路返回的是操作数本身
2console.log(1 && 2); // 2
3console.log(0 && 2); // 0 ← 不是 false
4console.log("" || "默认"); // "默认"
5
6// || 和 ?? 的区别
7const count = 0;
8count || 10 // 10 ✗ 0 被当成「没传」
9count ?? 10 // 0 ✓
10
11// React 条件渲染的经典坑
12{list.length && <List />} // ✗ 空列表时页面上多一个 0
13{list.length > 0 && <List />} // ✓
14{list.length ? <List /> : null} // ✓ 也可以
1// Short-circuiting returns the operand itself
2console.log(1 && 2); // 2
3console.log(0 && 2); // 0 ← not false
4console.log("" || "default"); // "default"
5
6// The difference between || and ??
7const count = 0;
8count || 10 // 10 ✗ 0 is treated as "nothing was passed"
9count ?? 10 // 0 ✓
10
11// The classic React conditional-rendering trap
12{list.length && <List />} // ✗ an empty list prints a stray 0 on the page
13{list.length > 0 && <List />} // ✓
14{list.length ? <List /> : null} // ✓ this works too
§07

var、let、const 的区别What is the difference between var, let and const?

#282 What is the difference between var, let and const

一句话:var 是函数作用域、会提升成undefined、能重复声明;let / const 是块作用域、 有 TDZ、不能重复声明;const 还不能重新赋值。

varletconst
作用域函数{}
声明前访问undefinedReferenceErrorReferenceError
重复声明可以不行不行
重新赋值可以可以不行
挂到 window顶层会挂不挂不挂

TDZ(暂时性死区)let / const「从块开始到声明那一行」之间的区域。 变量确实被提升了,但被标记为「还不能用」, 所以访问会抛错而不是给 undefined。 这是刻意设计的 —— 让错误早暴露。

会追问:const 的对象能改属性吗?」——const 锁的是绑定(这个名字不能再指向别的东西), 不是。 想冻结内容用 Object.freeze()

还会追问循环那道经典题——for (var i…)setTimeout 会打出三个 3,let 会打出 0 1 2。 因为 let每次迭代都创建一个新的绑定, 而 var 全程只有一个 i。 这题和闭包(#298)连着考。

In one line: var is function-scoped, hoists to undefined and can be redeclared; let / const are block-scoped, have a TDZ and cannot be redeclared; const on top of that cannot be reassigned.

varletconst
ScopeFunctionBlock {}Block
Read before the declarationundefinedthrows ReferenceErrorthrows ReferenceError
RedeclareAllowedNoNo
ReassignAllowedAllowedNo
Lands on windowYes at top levelNoNo

The TDZ (temporal dead zone) is the stretch between the start of the block and the let / const line itself. The variable really is hoisted, but flagged as “not usable yet”, so reading it throws instead of handing you undefined. That is deliberate — it makes mistakes surface early.

Follow-up: “Can you change the properties of a const object?” — yes. const locks the binding (the name cannot point at anything else), not the value. To freeze the contents, use Object.freeze().

Another follow-up — the classic loop question: for (var i…) with setTimeout prints three 3s; let prints 0 1 2. Because let creates a fresh binding on every iteration, while var has one single i the whole way through. This gets asked together with closures (#298).

JavaScript三个必背的例子Three examples worth memorising示意Illustrative
1// 经典循环题
2for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
3// 3 3 3 —— 只有一个 i,回调跑的时候它已经是 3
4
5for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));
6// 0 1 2 —— 每次迭代一个新的 j
7
8// TDZ
9console.log(a); // undefined var 提升成 undefined
10var a = 1;
11
12console.log(b); // ReferenceError: Cannot access 'b' before initialization
13let b = 1;
14
15// const 锁绑定,不锁内容
16const o = { n: 1 };
17o.n = 2; // ✓ 可以
18o = { n: 3 }; // ✗ TypeError: Assignment to constant variable
1// The classic loop question
2for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
3// 3 3 3 —— there is only one i, and by the time the callbacks run it is 3
4
5for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));
6// 0 1 2 —— every iteration gets a new j
7
8// TDZ
9console.log(a); // undefined var is hoisted as undefined
10var a = 1;
11
12console.log(b); // ReferenceError: Cannot access 'b' before initialization
13let b = 1;
14
15// const locks the binding, not the contents
16const o = { n: 1 };
17o.n = 2; // ✓ allowed
18o = { n: 3 }; // ✗ TypeError: Assignment to constant variable
§08

传值 vs 传引用Pass by value vs pass by reference

#284 Pass by Value vs Pass by Reference

一句话(这句要说准):JavaScript 永远是按值传递—— 只是当值是对象时,传的那个「值」是一个地址。 严格说叫 pass by sharing

为什么这个说法重要?因为它一句话解释了两个看似矛盾的现象:

  • 函数里 obj.n = 2外面会变—— 因为两个变量指着同一个对象。
  • 函数里 obj = { n: 2 }外面不变—— 因为你只是让函数内部那个参数变量指向了新对象, 外面的变量还指着旧的。

如果真是「传引用」,第二种情况外面也会变。所以是传值。

会追问:「怎么避免改到外面?」—— 先复制。{ ...obj } /arr.slice()浅拷贝(只复制一层,嵌套对象还是共享); 深拷贝用 structuredClone(obj)(现代浏览器和 Node 17+ 原生支持, 比 JSON.parse(JSON.stringify()) 强 —— 后者会丢掉 undefined、函数、Date 会变字符串、还处理不了循环引用)。

这题和 React 直接相关:React 判断 state 变没变是比引用, 所以「改了对象属性但界面不动」就是这个原理 —— 必须造新对象。

In one line — get this sentence exactly right: JavaScript is always pass by value. It is just that when the value happens to be an object, the “value” being passed is an address. The precise name for this is pass by sharing.

Why does the wording matter? Because it explains two things that look like a contradiction:

  • obj.n = 2 inside the function does show up outside — both variables point at the same object.
  • obj = { n: 2 } inside the function does not show up outside — all you did was point the parameter variable inside the function at a new object; the outer variable still points at the old one.

If this really were pass by reference, the second case would change the outside too. So it is pass by value.

Follow-up: “How do you avoid mutating the caller’s data?” — copy it first. { ...obj } and arr.slice() are shallow copies (one level only; nested objects are still shared). For a deep copy use structuredClone(obj) — native in modern browsers and Node 17+, and better than JSON.parse(JSON.stringify()), which drops undefined and functions, turns Date into a string, and cannot handle circular references at all.

This one ties straight into React: React compares references to decide whether state changed, which is exactly why “I changed a property and the UI did not move” happens — you have to build a new object.

JavaScript改属性 vs 换指向Changing a property vs pointing somewhere else示意Illustrative
1function mutate(o) { o.n = 2; } // 改属性
2function reassign(o) { o = { n: 3 }; } // 换指向
3
4const obj = { n: 1 };
5mutate(obj); console.log(obj.n); // 2 ← 外面变了
6reassign(obj); console.log(obj.n); // 2 ← 外面没变(不是 3)
7
8// 浅拷贝只管一层
9const a = { x: 1, inner: { y: 2 } };
10const b = { ...a };
11b.x = 9; console.log(a.x); // 1 ✓ 独立
12b.inner.y = 9; console.log(a.inner.y); // 9 ✗ 还是共享的
13
14const c = structuredClone(a); // 深拷贝,嵌套也独立
1function mutate(o) { o.n = 2; } // change a property
2function reassign(o) { o = { n: 3 }; } // point at something else
3
4const obj = { n: 1 };
5mutate(obj); console.log(obj.n); // 2 ← the outside changed
6reassign(obj); console.log(obj.n); // 2 ← the outside did not change (not 3)
7
8// A shallow copy only covers one level
9const a = { x: 1, inner: { y: 2 } };
10const b = { ...a };
11b.x = 9; console.log(a.x); // 1 ✓ independent
12b.inner.y = 9; console.log(a.inner.y); // 9 ✗ still shared
13
14const c = structuredClone(a); // a deep copy, so nested objects are separate too
§09

Set vs Array

#286 Set vs Array

一句话:Set元素唯一、查找是 O(1)、没有下标; 数组允许重复、有顺序和下标、includes 是 O(n)。

ArraySet
重复元素允许自动去重
查「在不在」includes O(n)has O(1)
下标访问arr[0]没有
取长度lengthsize
map / filter没有,要先转数组

什么时候用 Set:去重、以及在循环里反复判断「见过没有」—— 后者是性能差别最大的场景, 数组的 includes 会让复杂度从 O(n) 变 O(n²)。

会追问:「Set 去重能去掉重复的对象吗?」——不能。Set 用的是SameValueZero(≈===), 两个内容一样的对象是不同的引用。 要按内容去重得自己用Map 按某个 key 存。
NaN 呢?」—— Set 里NaN 只会存一个, 这是 SameValueZero=== 唯一的差别。

In one line: a Set holds unique elements, checks membership in O(1), and has no indices; an array allows duplicates, has order and indices, and its includes is O(n).

ArraySet
DuplicatesAllowedDropped automatically
“Is it in there?”includes O(n)has O(1)
Index accessarr[0]None
Lengthlengthsize
map / filterYesNo — convert to an array first

When to use a Set: deduping, and answering “have I seen this before?” inside a loop — the second is where the performance gap is biggest, because an array includes turns O(n) into O(n²).

Follow-up: “Can a Set dedupe identical objects?” — no. A Set uses SameValueZero (≈===), and two objects with the same contents are still two different references. To dedupe by content, key them yourself in a Map.
“What about NaN?” — a Set stores NaN only once. That is the only difference between SameValueZero and ===.

JavaScriptSet 的两个真实用途Two real uses for Set示意Illustrative
1// 去重一行
2const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
3
4// 循环里判重:O(n²) -> O(n)
5const seen = new Set();
6for (const x of list) {
7 if (seen.has(x)) continue; // O(1),换成 arr.includes 就是 O(n)
8 seen.add(x);
9}
10
11// 去不掉对象
12new Set([{ id: 1 }, { id: 1 }]).size; // 2 ← 引用不同
13
14// 按内容去重要用 Map
15const byId = new Map(items.map((i) => [i.id, i]));
16const deduped = [...byId.values()];
1// Dedupe in one line
2const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
3
4// Checking for duplicates in a loop: O(n²) -> O(n)
5const seen = new Set();
6for (const x of list) {
7 if (seen.has(x)) continue; // O(1); arr.includes here would be O(n)
8 seen.add(x);
9}
10
11// It cannot dedupe objects
12new Set([{ id: 1 }, { id: 1 }]).size; // 2 ← different references
13
14// To dedupe by content, use a Map
15const byId = new Map(items.map((i) => [i.id, i]));
16const deduped = [...byId.values()];
§10

Map vs Object

#287 Map vs Object

一句话:Map键可以是任何类型保证插入顺序、有 size、 能直接遍历;对象的键只能是字符串或 symbol, 而且带着一条原型链。

ObjectMap
键的类型字符串 / symbol(数字会被转成字符串任何值,包括对象和函数
顺序整数键会被排序,其余按插入严格按插入顺序
大小Object.keys(o).lengthmap.size
遍历要先 Object.entries本身可迭代,直接 for…of
原型污染有风险 —— o["toString"]本来就有值没有,Map 是干净的
JSON 序列化直接可以不行,要先转数组

怎么选:

  • 用 Object:结构固定的记录 (一个用户、一份配置),要 JSON 序列化, 字段名写死在代码里。
  • 用 Map:键是动态的、会频繁增删、 数量大、键不是字符串。

会追问:「为什么说对象有原型污染风险?」—— 因为空对象也「有」toStringconstructor 这些继承来的键。 拿对象当字典时,if (dict[key]) 遇到用户输入"constructor" 会误判成存在。Map 没这个问题, 非要用对象就 Object.create(null)
WeakMap 呢?」—— 键必须是对象, 而且不阻止垃圾回收。 适合给对象挂额外数据又不想造成内存泄漏。

In one line: a Map takes keys of any type, guarantees insertion order, has size, and is iterable on its own; an object’s keys can only be strings or symbols, and it drags a prototype chain along with it.

ObjectMap
Key typesString / symbol (numbers become strings)Any value, objects and functions included
OrderInteger keys get sorted, the rest are insertion orderStrictly insertion order
SizeObject.keys(o).lengthmap.size
IterationNeeds Object.entries firstIterable itself — for…of just works
Prototype pollutionA risk — o["toString"] already has a valueNone; a Map is clean
JSON serialisationWorks directlyNo — convert to an array first

How to choose:

  • Use an Object for records with a fixed shape (one user, one config), for anything you serialise to JSON, and when the field names are written into the code.
  • Use a Map when the keys are dynamic, entries come and go often, there are a lot of them, or the keys are not strings.

Follow-up: “Why is an object a prototype pollution risk?” — because even an empty object “has” inherited keys like toString and constructor. Use an object as a dictionary and if (dict[key]) reports a hit when the user types "constructor". A Map does not have this problem; if you must use an object, build it with Object.create(null).
“And WeakMap?” — its keys must be objects, and it does not hold off garbage collection. Good for hanging extra data on an object without leaking memory.

JavaScript两个真实会踩的差别Two differences you will actually hit示意Illustrative
1// 对象的键会被转成字符串
2const o = {};
3o[1] = "a";
4o["1"] = "b";
5console.log(o); // { "1": "b" } ← 只有一个键!
6
7const m = new Map();
8m.set(1, "a").set("1", "b");
9console.log(m.size); // 2 ← 数字 1 和字符串 "1" 是不同的键
10
11// 原型污染
12const dict = {};
13console.log(dict["toString"]); // ƒ toString() ← 凭空有值
14console.log(new Map().get("toString")); // undefined ✓ 干净
15
16// Map 不能直接 JSON
17JSON.stringify(m); // "{}" ← 全丢了
18JSON.stringify([...m]); // '[[1,"a"],["1","b"]]' ✓
1// Object keys are converted to strings
2const o = {};
3o[1] = "a";
4o["1"] = "b";
5console.log(o); // { "1": "b" } ← only one key!
6
7const m = new Map();
8m.set(1, "a").set("1", "b");
9console.log(m.size); // 2 ← the number 1 and the string "1" are different keys
10
11// Prototype pollution
12const dict = {};
13console.log(dict["toString"]); // ƒ toString() ← a value appears from nowhere
14console.log(new Map().get("toString")); // undefined ✓ clean
15
16// A Map does not turn into JSON directly
17JSON.stringify(m); // "{}" ← everything is lost
18JSON.stringify([...m]); // '[[1,"a"],["1","b"]]' ✓
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

「改了对象外面也变」Changing an object inside a function also changes it outside
引用值只复制地址;先浅拷贝或 structuredCloneA reference value copies only the address; make a shallow copy first, or use structuredClone
「界面不更新但 log 是对的」The screen does not update, but the log shows the right value
React 比引用,必须造新对象React compares references, so you have to build a new object
看到 [] == false 这类怪题A puzzle question such as [] == false
+ 看字符串、其他看数字、六个假值With +, one string makes it concatenation; every other operator converts to number; six values are falsy
「count 是 0 却拿到了默认值」count is 0 but you still get the default value
把 || 换成 ??Replace || with ??
循环里 setTimeout 拿到最后一个值A setTimeout inside a loop sees the last value
var 只有一个绑定,换 letvar has only one binding; use let
循环里反复 includesincludes is called again and again inside a loop
换 Set.has,O(n²) 变 O(n)Use Set.has: O(n²) becomes O(n)
拿对象当字典且键来自用户输入An object used as a dictionary, with keys that come from user input
用 Map,避免原型污染Use Map; it avoids prototype pollution
这节的要点What to take away
  1. 七种原始类型存值,其余存地址;typeof null 是 "object"(历史 bug),判数组用 Array.isArray。The seven primitive types hold the value itself, everything else holds an address; typeof null is "object", which is an old bug, so test for an array with Array.isArray.
  2. 隐式转换记两条:+ 有字符串就拼接,其他转数字;六个假值 false/0/""/null/undefined/NaN。Two rules for implicit conversion: with + one string makes it concatenation, every other operator converts to number; the six falsy values are false/0/""/null/undefined/NaN.
  3. 一律用 ===,唯一例外是 x == null;NaN 和自己不相等,React 用 Object.is。Always use ===, with x == null as the only exception; NaN is not equal to itself, and React compares with Object.is.
  4. &&、|| 返回操作数本身;要默认值用 ??,React 条件渲染写 length > 0 &&。&& and || return one of the operands, not a boolean; for a default value use ??, and for conditional rendering in React write length > 0 &&.
  5. var 函数作用域会提升成 undefined,let/const 块作用域有 TDZ;const 锁绑定不锁内容。var is function scoped and is hoisted as undefined; let and const are block scoped and have a temporal dead zone; const locks the binding, not the contents.
  6. JS 永远传值,对象传的是地址值 —— 所以改属性外面变、换指向外面不变。JavaScript always passes a value, and for an object that value is an address — so changing a property is visible outside, but reassigning the parameter is not.
  7. Set 查找 O(1) 但去不掉重复对象;Map 键可任意类型、保序、无原型污染,但不能直接 JSON。A Set looks up in O(1) but will not remove duplicate objects; a Map takes any type as a key, keeps insertion order and has no prototype pollution, but cannot go straight into JSON.

接下来What next

  1. 接着看下一节Continue to the next lesson函数与作用域十二问12 questions on functions and scope
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 10 道八股10 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: CSS 八问8 questions on CSS