引擎与类型十问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.
这一页有什么On this page11
- 01 什么是 JavaScript 引擎What is a JavaScript engine?
- 02 什么是 REPLWhat is a REPL?
- 03 原始值 vs 引用值Primitive values vs reference values
- 04 隐式转换 vs 显式转换Type coercion vs type conversion
- 05 == 和 === 的区别What is the difference between == and ===?
- 06 什么是短路求值What is short-circuit evaluation?
- 07 var、let、const 的区别What is the difference between var, let and const?
- 08 传值 vs 传引用Pass by value vs pass by reference
- 09 Set vs Array
- 10 Map vs Object
- 迁移模式Transfer
- 说清原始值和引用值在内存里的差别,并解释它怎么导致「改一个另一个也变」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
类型和内存这一组是所有「诡异行为」的根源:为什么 [] == 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.
什么是 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 语言本身, 它不认识 setTimeout、fetch、document、fs—— 这些都是宿主环境(浏览器 / 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.
什么是 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.
原始值 vs 引用值Primitive values vs reference values
#278 Primitive data types vs Reference data types
一句话:原始值存在栈上、直接存值; 引用值存在堆上、栈上只存一个地址。
七种原始类型(背下来,会让你数):string、number、boolean、undefined、null、symbol、bigint。
其余全是引用类型: 对象、数组、函数、Date、Map、Set、正则…… (数组和函数本质都是对象)。
三个可观察的差别:
- 不可变性—— 原始值本身改不了。
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
{} === {}isfalse, 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".
隐式转换 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" * 2、1 + "1"、if (arr.length)、[] == false |
隐式转换的两条核心规则(记住这两条, 大部分怪题就能推出来):
+只要有一边是字符串,就变成拼接; 其他算术运算符(-、*、/)一律转成数字。 所以1 + "1" === "11"而"3" - 1 === 2。- 对象参与运算时先
valueOf()再toString()。 数组的toString()是元素 join 逗号, 所以[] + []得到空字符串,[] + {}得到"[object Object]"。
六个假值背下来(其余全是真):false、0、""、null、undefined、NaN。
注意 [] 和 {} 都是真值—— 所以判断数组空不空要看 arr.length。
会追问:「parseInt 和 Number 什么区别?」——parseInt("42px") 得 42(从头读到读不动为止),Number("42px") 得 NaN(整体不合法就失败)。 所以校验用户输入该用 Number,parseInt 会把脏数据悄悄放过去。
In one line: conversion (casting) is what you write on purpose; coercion is the engine doing it behind your back.
| Who starts it | Examples | |
|---|---|---|
| Explicit (conversion) | You | Number("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. Hence1 + "1" === "11"but"3" - 1 === 2.- An object in an operation goes through
valueOf()first, thentoString(). An array’stoString()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.
== 和 === 的区别What is the difference between == and ===?
#280 What is the difference between == and ===
一句话:===类型不同直接 false;== 会先把两边转成同一类型再比。
== 的转换规则很绕,但实际只要记住四条:
null == undefined是true, 但它们和其他任何值都不==(包括0和"")。- 数字和字符串比 → 字符串转数字。
- 布尔参与 → 布尔先转数字(
true→1,false→0)。 这就是[] == false为true的原因:[]→""→0,false→0。 - 对象和原始值比 → 对象先转原始值。
结论:一律用 ===。唯一被普遍接受的 == 用法是x == null—— 一次同时判掉null 和 undefined。
会追问:「NaN === NaN?」——false,NaN和自己都不相等。 判断要用 Number.isNaN(x)(别用全局 isNaN,它会先做隐式转换,isNaN("abc") 是 true)。
「有没有更严格的比较?」——Object.is(x, y)。它和 ===只有两处不同:Object.is(NaN, NaN) 是 true,Object.is(0, -0) 是 false。React 判断 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 == undefinedistrue, and neither one is==to anything else (not0, not"").- Number against string → the string becomes a number.
- A boolean involved → the boolean becomes a number (
true→1,false→0). That is why[] == falseistrue:[]→""→0, andfalse→0. - 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.
什么是短路求值What is short-circuit evaluation?
#281 What is short-circuit evaluation
一句话:&& 和 ||结果一旦确定就不再算右边, 而且它们返回的是操作数本身,不是布尔值。
a && b—— a 为假就返回 a,否则返回 b。「都真才真」, 所以遇到假就可以收工。a || b—— a 为真就返回 a,否则返回 b。a ?? b(空值合并)——只有 a 是null或undefined时才返回 b。
|| 和 ??的区别是高频追问,而且是真实 bug 来源:count || 10 在 count 为0 时会给出 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 isnullorundefined.
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.
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 还不能重新赋值。
var | let | const | |
|---|---|---|---|
| 作用域 | 函数 | 块 {} | 块 |
| 声明前访问 | undefined | 报 ReferenceError | 报 ReferenceError |
| 重复声明 | 可以 | 不行 | 不行 |
| 重新赋值 | 可以 | 可以 | 不行 |
挂到 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.
var | let | const | |
|---|---|---|---|
| Scope | Function | Block {} | Block |
| Read before the declaration | undefined | throws ReferenceError | throws ReferenceError |
| Redeclare | Allowed | No | No |
| Reassign | Allowed | Allowed | No |
Lands on window | Yes at top level | No | No |
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).
传值 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 = 2inside 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.
Set vs Array
#286 Set vs Array
一句话:Set元素唯一、查找是 O(1)、没有下标; 数组允许重复、有顺序和下标、includes 是 O(n)。
Array | Set | |
|---|---|---|
| 重复元素 | 允许 | 自动去重 |
| 查「在不在」 | includes O(n) | has O(1) |
| 下标访问 | arr[0] | 没有 |
| 取长度 | length | size |
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).
Array | Set | |
|---|---|---|
| Duplicates | Allowed | Dropped automatically |
| “Is it in there?” | includes O(n) | has O(1) |
| Index access | arr[0] | None |
| Length | length | size |
map / filter | Yes | No — 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 ===.
Map vs Object
#287 Map vs Object
一句话:Map的键可以是任何类型、保证插入顺序、有 size、 能直接遍历;对象的键只能是字符串或 symbol, 而且带着一条原型链。
Object | Map | |
|---|---|---|
| 键的类型 | 字符串 / symbol(数字会被转成字符串) | 任何值,包括对象和函数 |
| 顺序 | 整数键会被排序,其余按插入 | 严格按插入顺序 |
| 大小 | Object.keys(o).length | map.size |
| 遍历 | 要先 Object.entries | 本身可迭代,直接 for…of |
| 原型污染 | 有风险 —— o["toString"]本来就有值 | 没有,Map 是干净的 |
| JSON 序列化 | 直接可以 | 不行,要先转数组 |
怎么选:
- 用 Object:结构固定的记录 (一个用户、一份配置),要 JSON 序列化, 字段名写死在代码里。
- 用 Map:键是动态的、会频繁增删、 数量大、键不是字符串。
会追问:「为什么说对象有原型污染风险?」—— 因为空对象也「有」toString、constructor 这些继承来的键。 拿对象当字典时,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.
Object | Map | |
|---|---|---|
| Key types | String / symbol (numbers become strings) | Any value, objects and functions included |
| Order | Integer keys get sorted, the rest are insertion order | Strictly insertion order |
| Size | Object.keys(o).length | map.size |
| Iteration | Needs Object.entries first | Iterable itself — for…of just works |
| Prototype pollution | A risk — o["toString"] already has a value | None; a Map is clean |
| JSON serialisation | Works directly | No — 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.
换一道题也能用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.
- 七种原始类型存值,其余存地址;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.
- 隐式转换记两条:+ 有字符串就拼接,其他转数字;六个假值 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.
- 一律用 ===,唯一例外是 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.
- &&、|| 返回操作数本身;要默认值用 ??,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 &&.
- 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.
- 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.
- 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.