{
  "assessmentTests": {
    "javascript_test": {
      "name": "JavaScript 测试",
      "desc": "30 道情景题涵盖核心语法和类型强制转换、函数/作用域/闭包和 this、对象/数组和数据结构、事件循环/异步/原型成语——了解您的 JavaScript 知识是否符合招聘人员对 JavaScript 能力的期望。",
      "recommendation": "您的 JavaScript 技能档案",
      "results": {
        "beginner": {
          "name": "初级",
          "desc": "您可以编写可用的函数和对象，但您遗漏的问题围绕类型强制转换和引用语义而非语法——== 在比较前悄悄转换类型，数组赋值共享一个对象而非复制它，为什么 NaN 永远不等于自己。这都不是关于 JavaScript 不好的问题；这些是特定规则，它们让通过编写碰巧工作的代码来学习该语言的人而不是学习值和引用实际如何表现的人感到困惑。它们在工作中很重要，因为每一个都是代码运行没有错误但仍然做了错误的地方。",
          "recommendation": "按以下顺序从三件事开始：为什么 == 在比较前强制转换类型而 === 则不然；为什么将一个数组或对象赋值给新变量会复制引用而不是值；以及 var 如何被提升并初始化为 undefined，而 let 和 const 保持在暂时死区直到其声明运行。MDN 的 JavaScript 指南用可运行的示例涵盖所有三个。"
        },
        "intermediate": {
          "name": "中级",
          "desc": "您熟练地处理日常应用代码——函数、数组方法、直接的控制流——不会被常规功能工作拖慢。这里和高级之间的差距主要在 JavaScript 的作用域和时序规则相互作用时发生的事情：在循环的回调内捕获的 var 都读取相同的最后一个值，一个 this 在方法从其分离时悄悄停止指向对象，一个看起来深但只复制一层的展开副本。这些是通过快速阅读就能通过的错误类型，仅在特定运行时条件下显示。",
          "recommendation": "关注作用域和 this 在调用时而非定义时的实际解析方式：为什么基于 var 的 for 循环为每个回调提供相同的最后一个计数器值，而 let 则不然；this 如何由函数的调用方式而非编写位置决定；以及为什么展开或 Object.assign 只复制对象的顶级。然后是事件循环，因为这会让已经知道单个承诺的人困惑。"
        },
        "advanced": {
          "name": "高级",
          "desc": "这是大多数工作公告用\"强大的 JavaScript\"表示的级别。您阅读 filter/map/reduce 链并可以准确预测它返回什么而不改变原始数组，您知道为什么同步 console.log 总是击败微任务，而微任务总是击败 setTimeout，您使用 Promise.all 知道它在任何一个承诺拒绝时立即拒绝，而不是等待其余的。将此频带与顶部分离的是原型和对象模型工作的一侧：原型链如何解析继承方法与 hasOwnProperty，以及私有类字段实际隐藏了什么。",
          "recommendation": "推进保护其他代码也触及的状态的部分：JavaScript 的原型链如何解析不在实例本身上的方法查找；类的私有 #fields 实际上从外部访问隐藏了什么；以及 JSON.stringify 如何在纯对象内部与数组内部以不同方式处理 undefined 和函数。MDN 关于事件循环和原型链的资料对两者都是自然的下一步。"
        },
        "expert": {
          "name": "专家",
          "desc": "您在每个部分都获得了最高分——核心语法、类型和强制转换；函数、作用域、闭包和 this；对象、数组和数据结构；以及异步、事件循环、原型和成语。实际上，这意味着您可以接受陌生人的函数并解释它为什么返回它返回的内容，而不仅仅是语法说它应该做什么，这是更困难和更有价值的技能。在这个级别，语言本身很少成为限制因素；限制通常是异步控制流或其下的数据形状。",
          "recommendation": "现在返回在设计和诊断中：关于 Promise.all 和 Promise.race 之间的竞态条件进行推理而不是背诵其定义；选择闭包何时是私有状态的正确工具，何时是类的私有字段；以及在生产日志中读取微任务对比宏任务排序错误而非在测验中。如果您被筛选用于某个角色，请描述您发现的计时错误——陈旧的闭包变量或无序的异步回调——而非命名 JavaScript 功能；这表明了推理，而非仅仅是词汇。"
        }
      },
      "questions": [
        {"question": "console.log(x);\nvar x = 5;\n这会打印什么？", "options": [{"icon": "", "label": "5 — var 声明立即运行，在任何其他代码之前"}, {"icon": "", "label": "ReferenceError: x 未定义"}, {"icon": "", "label": "undefined — var 声明被提升到其作用域的顶部并初始化为 undefined，但赋值在原处执行，所以在其运行前变量存在但持有 undefined"}, {"icon": "", "label": "null"}]},
        {"question": "console.log(y);\nlet y = 5;\n会发生什么？", "options": [{"icon": "", "label": "它抛出 ReferenceError: 无法在初始化前访问 'y' — let 被提升到其作用域的顶部但保持在暂时死区直到声明行实际运行"}, {"icon": "", "label": "它打印 undefined，如 var"}, {"icon": "", "label": "它打印 5"}, {"icon": "", "label": "它在解析时抛出 SyntaxError"}]},
        {"question": "const arr = [1, 2, 3];\narr.push(4);\nconsole.log(arr.length);\n这会打印什么？", "options": [{"icon": "", "label": "4 — const 仅防止重新赋值 arr 绑定本身，而不是它指向的数组对象，因此 push 仍然有效"}, {"icon": "", "label": "它抛出 TypeError，因为 arr 是 const"}, {"icon": "", "label": "3 — push 在 const 数组上被阻止"}, {"icon": "", "label": "undefined"}]},
        {"question": "console.log('' == 0);\nconsole.log('' === 0);\n两行打印什么？", "options": [{"icon": "", "label": "true 然后 true"}, {"icon": "", "label": "false 然后 false"}, {"icon": "", "label": "true 然后 false — == 在比较前将空字符串强制转换为 0，而 === 无强制转换地比较类型和值，字符串永远不严格等于数字"}, {"icon": "", "label": "false 然后 true"}]},
        {"question": "console.log(NaN === NaN);\nconsole.log(Number.isNaN(NaN));\n两行打印什么？", "options": [{"icon": "", "label": "true 然后 true"}, {"icon": "", "label": "true 然后 false"}, {"icon": "", "label": "false 然后 false"}, {"icon": "", "label": "false 然后 true — NaN 是 JavaScript 中唯一在 === 下不等于自身的值，但 Number.isNaN() 仍然正确检测它"}]},
        {"question": "console.log(typeof null);\n这会打印什么？", "options": [{"icon": "", "label": "\"null\""}, {"icon": "", "label": "\"object\" — 语言中长期的缺陷：typeof null 返回 \"object\" 尽管 null 是原始值，自那以后就因向后兼容而被保留"}, {"icon": "", "label": "\"undefined\""}, {"icon": "", "label": "\"boolean\""}]},
        {"question": "以下哪个在 if 条件中使用时为真？", "options": [{"icon": "", "label": "0"}, {"icon": "", "label": "\"\"（空字符串）"}, {"icon": "", "label": "null"}, {"icon": "", "label": "\"0\" — 非空字符串在 JavaScript 中总是真，即使其内容本身是字符串 \"0\""}]},
        {"question": "foo();\nfunction foo() { console.log('a'); }\nbar();\nvar bar = function() { console.log('b'); };\n会发生什么？", "options": [{"icon": "", "label": "两个调用都打印，\"a\" 然后 \"b\""}, {"icon": "", "label": "foo() 打印 \"a\"；bar() 抛出 TypeError: bar 不是函数 — 函数声明随其主体一起完全提升，但 var 赋值的函数表达式仅提升 var 本身（作为 undefined），不提升赋值"}, {"icon": "", "label": "两个调用都抛出 ReferenceError"}, {"icon": "", "label": "foo() 抛出；bar() 打印 \"b\""}]},
        {"question": "function makeCounter() {\n  let count = 0;\n  return function() { count++; return count; };\n}\nconst counter = makeCounter();\nconsole.log(counter());\nconsole.log(counter());\n两行打印什么？", "options": [{"icon": "", "label": "1 然后 1"}, {"icon": "", "label": "0 然后 1"}, {"icon": "", "label": "undefined 然后 undefined"}, {"icon": "", "label": "1 然后 2 — 返回的函数闭合了来自 makeCounter 作用域的同一个 count 变量，该变量在调用之间持续而非重置"}]},
        {"question": "for (var i = 0; i < 3; i++) {\n  setTimeout(() => console.log(i), 0);\n}\n这会打印什么？", "options": [{"icon": "", "label": "0, 1, 2"}, {"icon": "", "label": "3, 3, 3 — var 具有函数作用域而非块作用域，所以所有三个回调都闭合同一个 i，它已完成循环并在它们实际运行时等于 3"}, {"icon": "", "label": "0, 0, 0"}, {"icon": "", "label": "undefined, undefined, undefined"}]},
        {"question": "for (let i = 0; i < 3; i++) {\n  setTimeout(() => console.log(i), 0);\n}\n这会打印什么？", "options": [{"icon": "", "label": "3, 3, 3"}, {"icon": "", "label": "0, 0, 0"}, {"icon": "", "label": "undefined, undefined, undefined"}, {"icon": "", "label": "0, 1, 2 — let 为每个循环迭代创建一个新的 i 绑定，所以每个回调闭合其自己的单独副本而非一个共享变量"}]},
        {"question": "function greet() { return `Hi, ${this.name}`; }\nconst person = { name: 'Ana' };\nconsole.log(greet.call(person));\n这会打印什么？", "options": [{"icon": "", "label": "\"Hi, Ana\" — Function.prototype.call 使用明确设置为其第一个参数的 this 调用函数，所以 this.name 针对 person 解析"}, {"icon": "", "label": "\"Hi, undefined\" — call() 仅在已是对象方法的函数上工作"}, {"icon": "", "label": "它抛出 TypeError，因为 greet 不是 person 的方法"}, {"icon": "", "label": "\"Hi, ${this.name}\" — call() 不计算模板文字"}]},
        {"question": "const timer = {\n  seconds: 0,\n  start: function() {\n    setInterval(() => { this.seconds++; }, 1000);\n  },\n};\n为什么人们在这里的 setInterval 内更喜欢箭头函数而非 function() { this.seconds++ }？", "options": [{"icon": "", "label": "箭头函数没有自己的 this — 它们从包含的 start 函数词汇捕获 this，即 timer（因为 start 被调用为 timer.start()），所以 this.seconds 在每个刻度上正确引用 timer.seconds"}, {"icon": "", "label": "箭头函数在运行时比普通函数执行得更快"}, {"icon": "", "label": "箭头函数自动将 this 绑定到全局对象，碰巧等于 timer"}, {"icon": "", "label": "箭头函数自动将 this 转换为全局变量"}]},
        {"question": "function multiply(a, b) { return a * b; }\nconst double = multiply.bind(null, 2);\nconsole.log(double(5));\n这会打印什么？", "options": [{"icon": "", "label": "它抛出错误，因为 bind 需要两个参数预先"}, {"icon": "", "label": "10 — bind 返回一个新函数，其 this 和任何前导参数预设；调用 double(5) 提供剩余参数 b=5，所以 multiply(2, 5) 运行"}, {"icon": "", "label": "NaN"}, {"icon": "", "label": "7，因为 bind 将其参数添加到调用中而不是替换它们"}]},
        {"question": "function outer() {\n  let x = 10;\n  function inner() {\n    let x = 20;\n    console.log(x);\n  }\n  inner();\n  console.log(x);\n}\nouter();\n两个 console.log 调用打印什么？", "options": [{"icon": "", "label": "20 然后 20"}, {"icon": "", "label": "20 然后 10 — inner 的自己的 let x 仅在 inner 的作用域内遮蔽 outer 的 x；outer 的自己的 x 从未被触及"}, {"icon": "", "label": "10 然后 10"}, {"icon": "", "label": "它抛出 ReferenceError，因为 x 被声明了两次"}]},
        {"question": "const a = [1, 2, 3];\nconst b = a;\nb.push(4);\nconsole.log(a.length);\n这会打印什么？", "options": [{"icon": "", "label": "3 — b 是 a 的单独副本"}, {"icon": "", "label": "undefined"}, {"icon": "", "label": "它抛出 TypeError，因为 a 是 const"}, {"icon": "", "label": "4 — 数组是对象，所以 const b = a 复制引用而非数组；通过 push 变异 b 也会变异 a 指向的精确数组"}]},
        {"question": "const original = { name: 'Ana', tags: ['x', 'y'] };\nconst copy = { ...original };\ncopy.name = 'Bea';\ncopy.tags.push('z');\nconsole.log(original.name, original.tags.length);\n这会打印什么？", "options": [{"icon": "", "label": "'Ana' 2 — spread 深度克隆每个嵌套对象和数组"}, {"icon": "", "label": "'Ana' 3 — spread 仅是浅复制：name 之类的原始属性按值复制，所以重新赋值 copy.name 永远不会触及 original，但 tags 之类的嵌套数组按引用复制，所以推送到 copy.tags 会变异 original.tags 指向的精确数组"}, {"icon": "", "label": "'Bea' 3 — spread 按引用复制所有内容，所以顶级和嵌套更改都会返回到 original"}, {"icon": "", "label": "'Bea' 2"}]},
        {"question": "const { a: x = 10, b: y = 20 } = { a: 5 };\nconsole.log(x, y);\n这会打印什么？", "options": [{"icon": "", "label": "undefined undefined"}, {"icon": "", "label": "5 undefined"}, {"icon": "", "label": "10 20"}, {"icon": "", "label": "5 20 — a 重命名为 x 且存在，所以它保留其值 5；b 重命名为 y 且缺失，所以它回退到其默认值 20"}]},
        {"question": "const [first, ...rest] = [1, 2, 3, 4];\nconsole.log(rest);\n这会打印什么？", "options": [{"icon": "", "label": "[1, 2, 3, 4]"}, {"icon": "", "label": "[2, 3, 4] — rest 将 first 之后的每个剩余元素收集到一个全新的数组中"}, {"icon": "", "label": "2"}, {"icon": "", "label": "[1]"}]},
        {"question": "const nums = [10, 1, 2];\nnums.sort();\nconsole.log(nums);\n这会打印什么？", "options": [{"icon": "", "label": "[1, 2, 10] — sort 总是默认按升序数值排序数字"}, {"icon": "", "label": "[10, 1, 2] — sort 不变异原始数组"}, {"icon": "", "label": "[1, 10, 2] — Array.prototype.sort 的默认比较器将元素转换为字符串并按字典顺序比较它们，所以 \"10\" 在 \"2\" 之前排序；sort 也在原地变异数组而非返回一个新的"}, {"icon": "", "label": "[2, 1, 10]"}]},
        {"question": "const user = { profile: { age: 0 } };\nconsole.log(user.profile?.age ?? 'unknown');\nconsole.log(user.address?.city ?? 'unknown');\n两行打印什么？", "options": [{"icon": "", "label": "'unknown' 然后 'unknown' — ?? 将 0 视为缺失，如 || 所做"}, {"icon": "", "label": "0 然后 'unknown' — ?? 仅在 null 或 undefined 上回退，不在 0 之类的其他虚假值上，所以 age (0) 被保留；user.address 是 undefined，所以 ?. 将整个链短路到 undefined，?? 然后提供 'unknown'"}, {"icon": "", "label": "0 然后 undefined"}, {"icon": "", "label": "'unknown' 然后 0"}]},
        {"question": "const obj = { a: undefined, b: function(){}, c: NaN, d: [undefined, function(){}, 1] };\nconsole.log(JSON.stringify(obj));\n这会打印什么？", "options": [{"icon": "", "label": "'{\"a\":null,\"b\":null,\"c\":null,\"d\":[null,null,1]}' — 每个不支持的值到处都变成 null，在对象和数组中相同"}, {"icon": "", "label": "'{\"a\":undefined,\"b\":undefined,\"c\":NaN,\"d\":[undefined,null,1]}'"}, {"icon": "", "label": "'{\"c\":null,\"d\":[null,null,1]}' — 值为 undefined 或函数的对象属性被完全省略，NaN 序列化为 null，但在数组内这些相同的不支持值变成 null 而非被删除，因为删除数组元素会移位每个后续索引"}, {"icon": "", "label": "它抛出 TypeError，因为 NaN 无法序列化为 JSON"}]},
        {"question": "console.log('1');\nsetTimeout(() => console.log('2'), 0);\nPromise.resolve().then(() => console.log('3'));\nconsole.log('4');\n这些按什么顺序打印？", "options": [{"icon": "", "label": "1, 2, 3, 4"}, {"icon": "", "label": "1, 4, 2, 3"}, {"icon": "", "label": "1, 4, 3, 2 — 同步代码首先运行（所以 1 和 4 立即打印），然后微任务队列在事件循环继续到下一个宏任务前完全排空，所以 3 在 setTimeout 回调的 2 前打印"}, {"icon": "", "label": "1, 3, 4, 2"}]},
        {"question": "async function foo() {\n  console.log('a');\n  await null;\n  console.log('b');\n}\nconsole.log('start');\nfoo();\nconsole.log('end');\n这些按什么顺序打印？", "options": [{"icon": "", "label": "start, a, end, b — foo() 同步运行至其第一个 await，所以当 foo() 被调用时 'a' 立即记录；await 然后暂停 foo 并返回控制到调用者，所以 'end' 在恢复的 'b' 前记录，后来作为微任务运行"}, {"icon": "", "label": "start, a, b, end"}, {"icon": "", "label": "start, end, a, b"}, {"icon": "", "label": "a, start, end, b"}]},
        {"question": "Promise.all([\n  Promise.resolve(1),\n  Promise.reject('err'),\n  Promise.resolve(3),\n]).then(r => console.log('ok', r)).catch(e => console.log('fail', e));\n这会打印什么？", "options": [{"icon": "", "label": "'fail err' — Promise.all 在任何一个承诺拒绝时立即拒绝，忽略他人的结果，所以 catch 处理程序以该拒绝原因运行"}, {"icon": "", "label": "'ok' [1, 3]"}, {"icon": "", "label": "'ok' [1, undefined, 3]"}, {"icon": "", "label": "它同步抛出且没有处理程序运行"}]},
        {"question": "const fast = new Promise(res => setTimeout(() => res('fast'), 10));\nconst slow = new Promise(res => setTimeout(() => res('slow'), 100));\nPromise.race([fast, slow]).then(v => console.log(v));\n这会打印什么？", "options": [{"icon": "", "label": "'slow'"}, {"icon": "", "label": "['fast', 'slow']"}, {"icon": "", "label": "'fast' — Promise.race 在其任何一个承诺解决时解决，10ms 计时器远在 100ms 计时器前解决"}, {"icon": "", "label": "它抛出错误，因为 race 需要恰好两个承诺"}]},
        {"question": "function Animal(name) { this.name = name; }\nAnimal.prototype.speak = function() { return `${this.name} makes a sound`; };\nconst dog = new Animal('Rex');\nconsole.log(dog.speak());\nconsole.log(dog.hasOwnProperty('speak'));\n两行打印什么？", "options": [{"icon": "", "label": "'Rex makes a sound' 然后 true"}, {"icon": "", "label": "undefined 然后 false"}, {"icon": "", "label": "它抛出 TypeError，因为 dog 没有其自己的 speak 方法"}, {"icon": "", "label": "'Rex makes a sound' 然后 false — speak 定义在 Animal.prototype 上，不在 dog 实例上；dog 在调用时通过原型链找到它，但 hasOwnProperty 仅检查实例自己的属性，不检查任何继承"}]},
        {"question": "class Counter {\n  #count = 0;\n  increment() { this.#count++; return this.#count; }\n}\nconst c = new Counter();\nconsole.log(c.increment());\nconsole.log(c.count);\n两行打印什么？", "options": [{"icon": "", "label": "1 然后 undefined — #count 是私有类字段，仅从类自己的方法内可读和可写；c.count（无 #）是一个完全不同的普通属性，从未设置，所以它是 undefined"}, {"icon": "", "label": "1 然后 0"}, {"icon": "", "label": "1 然后 1"}, {"icon": "", "label": "它在访问 c.count 时抛出 SyntaxError"}]},
        {"question": "const items = ['a', 'b'];\nconst label = `Items: ${items.length > 0 ? items.join(', ') : 'none'}`;\nconsole.log(label);\n这会打印什么？", "options": [{"icon": "", "label": "'Items: ${items.length > 0 ? items.join(\\', \\') : \\'none\\'}'"}, {"icon": "", "label": "'Items: none'"}, {"icon": "", "label": "'Items: a, b' — 模板文字计算放在 ${}... 内的任何表达式，包括完整的三元，并将其结果直接插入字符串"}, {"icon": "", "label": "它抛出 SyntaxError，因为三元在模板文字内不被允许"}]},
        {"question": "const nums = [1, 2, 3, 4, 5];\nconst result = nums.filter(n => n % 2 === 0).map(n => n * 10).reduce((sum, n) => sum + n, 0);\nconsole.log(result, nums);\n这会打印什么？", "options": [{"icon": "", "label": "60 [1, 2, 3, 4, 5] — filter、map 和 reduce 各自返回一个新数组或值而不变异原始数组，所以 nums 未改变，而 result 是最终减少和（2 和 4，加倍至 20 和 40，求和至 60）"}, {"icon": "", "label": "60 [2, 4, 20, 40]"}, {"icon": "", "label": "150 [1, 2, 3, 4, 5]"}, {"icon": "", "label": "60 [20, 40]"}]}
      ],
      "optionOrderVersion": "50fc0eba818f2d72"
    }
  }
}
