[
 {
  "cat": "Coercion & Equality",
  "code": [
   "[] == ![];"
  ],
  "ans": "true",
  "exp": "The right side coerces first: ![] is false because arrays are truthy, and false becomes 0. The left side, an empty array, is coerced straight to a number without ever becoming a boolean, and also lands on 0. 0 == 0.",
  "w": [
   "false",
   "0",
   "NaN"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "NaN === NaN;"
  ],
  "ans": "false",
  "exp": "Per IEEE 754, NaN never equals anything, including itself. It's the only JS value that isn't equal to itself under either == or ===",
  "w": [
   "true",
   "NaN",
   "undefined"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "typeof NaN;"
  ],
  "ans": "'number'",
  "exp": "NaN stands for 'Not a Number', but it's still a member of the Number type — it's the sentinel value for a failed numeric computation, not a separate type.",
  "w": [
   "'nan'",
   "'undefined'",
   "'float'"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "null == undefined;",
   "null === undefined;"
  ],
  "ans": "true\nfalse",
  "exp": "The spec special-cases null and undefined to loosely equal only each other and nothing else. Strict equality still treats them as different types entirely.",
  "w": [
   "false\ntrue",
   "true\ntrue",
   "false\nfalse"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "[] + [];"
  ],
  "ans": "''",
  "exp": "Both arrays convert to primitives via toString() first, which turns an empty array into an empty string. Concatenating two empty strings gives an empty string.",
  "w": [
   "0",
   "NaN",
   "'[]'"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "[] + {};"
  ],
  "ans": "'[object Object]'",
  "exp": "In expression position, [] converts to '' and {} converts to '[object Object]' via its default toString, then they concatenate.",
  "w": [
   "'[]{}'",
   "NaN",
   "'{}'"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "{}",
   "+ [];"
  ],
  "ans": "0",
  "exp": "At the start of a statement, a leading {} is parsed as an empty block, not an object literal. What's left is a standalone unary + on [], which coerces the empty array to the number 0.",
  "w": [
   "'[object Object]'",
   "NaN",
   "undefined"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "'b' + 'a' + +'a' + 'a';"
  ],
  "ans": "'baNaNa'",
  "exp": "The unary + before the third 'a' tries to convert it to a number, fails, and produces NaN. String concatenation then stitches everything together.",
  "w": [
   "'baaa'",
   "NaN",
   "'baNaN'"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "true + true;"
  ],
  "ans": "2",
  "exp": "Arithmetic operators coerce booleans to numbers first: true becomes 1, false becomes 0. Two trues add up like two 1s.",
  "w": [
   "1",
   "true",
   "'truetrue'"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "1 < 2 < 3;",
   "3 > 2 > 1;"
  ],
  "ans": "true\nfalse",
  "exp": "Both operators are left-associative. First: (1<2) is true, which coerces to 1, then 1<3 is true. Second: (3>2) is true → 1, then 1>1 is false.",
  "w": [
   "true\ntrue",
   "false\ntrue",
   "false\nfalse"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "null == 0;",
   "null >= 0;"
  ],
  "ans": "false\ntrue",
  "exp": "Loose equality never coerces null to a number — it only equals undefined. Relational operators like >= go through a different algorithm that does coerce null to 0, so null >= 0 ends up true.",
  "w": [
   "true\ntrue",
   "false\nfalse",
   "true\nfalse"
  ]
 },
 {
  "cat": "Coercion & Equality",
  "code": [
   "1 == '1';",
   "1 === '1';"
  ],
  "ans": "true\nfalse",
  "exp": "== coerces the string to a number before comparing, so 1 == 1. === refuses to coerce and treats a number and a string as inherently different types.",
  "w": [
   "true\ntrue",
   "false\nfalse",
   "false\ntrue"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "0.1 + 0.2;"
  ],
  "ans": "0.30000000000000004",
  "exp": "0.1 and 0.2 have no exact binary floating-point representation, so both are stored as close approximations. Their sum doesn't line up exactly with the closest approximation of 0.3.",
  "w": [
   "0.3",
   "0.30000000000000003",
   "0.29999999999999998"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "9999999999999999;"
  ],
  "ans": "10000000000000000",
  "exp": "This number exceeds Number.MAX_SAFE_INTEGER, so it gets rounded to the nearest value a double can actually represent — which happens to be a round 10 quadrillion.",
  "w": [
   "9999999999999999",
   "9999999999999998",
   "10000000000000001"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "Number.MIN_VALUE > 0;"
  ],
  "ans": "true",
  "exp": "MIN_VALUE isn't the most negative number — it's the smallest positive number representable in float precision (about 5e-324), i.e. the closest a double can get to zero without being zero.",
  "w": [
   "false",
   "0",
   "undefined"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "Math.min() > Math.max();"
  ],
  "ans": "true",
  "exp": "Called with zero arguments, Math.min() returns Infinity and Math.max() returns -Infinity — the identity elements for those operations. Infinity is indeed greater than -Infinity.",
  "w": [
   "false",
   "NaN",
   "SyntaxError"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "parseInt('42px');",
   "parseInt('px42');"
  ],
  "ans": "42\nNaN",
  "exp": "parseInt reads digits from the start of the string until it hits a character it can't parse, then stops. It never skips leading garbage to find digits later in the string.",
  "w": [
   "42\n42",
   "NaN\nNaN",
   "NaN\n42"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "parseInt(0.0000001);"
  ],
  "ans": "1",
  "exp": "The number is first converted to a string, and 0.0000001 stringifies to '1e-7'. parseInt reads the leading '1', hits 'e', and stops right there.",
  "w": [
   "0",
   "NaN",
   "1e-7"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "(0.1).toFixed(20);"
  ],
  "ans": "'0.10000000000000000555'",
  "exp": "toFixed at low precision rounds away the imprecision of floating point storage. Ask for 20 digits and you see what 0.1 actually looks like in memory — it was never exactly 0.1.",
  "w": [
   "'0.10000000000000000000'",
   "'0.1'",
   "'0.10000000000000000556'"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "10 - '4';",
   "10 + '4';"
  ],
  "ans": "6\n'104'",
  "exp": "Minus has no string meaning in JS, so both sides are forced to numbers. Plus prefers string concatenation the moment either operand is a string.",
  "w": [
   "6\n14",
   "'104'\n6",
   "14\n'104'"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "[10, 1, 3].sort();"
  ],
  "ans": "[1, 10, 3]",
  "exp": "Array.prototype.sort's default comparator converts elements to strings and compares them lexicographically. '10' sorts before '3' because '1' < '3' as characters.",
  "w": [
   "[1, 3, 10]",
   "[10, 3, 1]",
   "[10, 1, 3]"
  ]
 },
 {
  "cat": "Numbers & Precision",
  "code": [
   "0.1 + 0.2 === 0.3;",
   "Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON;"
  ],
  "ans": "false\ntrue",
  "exp": "You can't compare floats for exact equality after arithmetic — always compare within a small tolerance (epsilon) instead.",
  "w": [
   "true\ntrue",
   "false\nfalse",
   "true\nfalse"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "[1, 2, 3] + [4, 5, 6];"
  ],
  "ans": "'1,2,34,5,6'",
  "exp": "Both arrays convert to strings by joining their elements with commas, giving '1,2,3' and '4,5,6', which then just concatenate — no space or separator is inserted between them.",
  "w": [
   "[1, 2, 3, 4, 5, 6]",
   "'1,2,3,4,5,6'",
   "NaN"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "let a = [, , ,];",
   "a.length;"
  ],
  "ans": "3",
  "exp": "A trailing comma in an array literal doesn't create an extra element — it's just a stylistic allowance, same as trailing commas in function args. Three commas here describe three slots.",
  "w": [
   "4",
   "2",
   "undefined"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "typeof [];",
   "Array.isArray([]);"
  ],
  "ans": "'object'\ntrue",
  "exp": "typeof can't distinguish an array from any other object — arrays are objects under the hood. Array.isArray() is the reliable way to check.",
  "w": [
   "'array'\ntrue",
   "'object'\nfalse",
   "'array'\nfalse"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "const arr = [1, 2, 3];",
   "arr.length = 1;",
   "arr;"
  ],
  "ans": "[1]",
  "exp": "Array.length is writable. Setting it to a smaller value truncates the array in place immediately — it's not just a reported count, it's live.",
  "w": [
   "[1, 2, 3]",
   "[1, undefined, undefined]",
   "TypeError: cannot assign to read only length"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "const a = [1, 2, 3];",
   "delete a[1];",
   "a;"
  ],
  "ans": "[1, <1 empty item>, 3]  // length still 3",
  "exp": "delete removes the property at that index but leaves a hole — it doesn't shift later elements down or shrink the array. Use splice if you want that.",
  "w": [
   "[1, 3]  // length 2",
   "[1, undefined, 3]",
   "TypeError"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "new Array(3).map(x => 1);",
   "Array(3).fill(0).map(x => 1);"
  ],
  "ans": "[ <3 empty items> ]\n[1, 1, 1]",
  "exp": "new Array(3) creates 3 empty slots (holes), and map skips holes entirely — it only visits indices that actually have a value assigned. fill() assigns real values first, so map has something to visit.",
  "w": [
   "[1, 1, 1]\n[1, 1, 1]",
   "[ <3 empty items> ]\n[ <3 empty items> ]",
   "[1, 1, 1]\n[ <3 empty items> ]"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "[1, 2, NaN].indexOf(NaN);",
   "[1, 2, NaN].includes(NaN);"
  ],
  "ans": "-1\ntrue",
  "exp": "indexOf compares with strict equality, and NaN === NaN is false, so it can never find NaN. includes uses the SameValueZero algorithm instead, which treats NaN as equal to itself.",
  "w": [
   "-1\nfalse",
   "true\ntrue",
   "2\nfalse"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "[[1,[2,3]], [4,[5,[6]]]].flat(Infinity);"
  ],
  "ans": "[1, 2, 3, 4, 5, 6]",
  "exp": "flat() takes a depth argument for how many levels of nesting to flatten; passing Infinity flattens arbitrarily deep nesting in one call.",
  "w": [
   "[1, [2, 3], 4, [5, [6]]]",
   "[1, 2, 3, 4, 5, [6]]",
   "TypeError: depth must be finite"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "Array.from({ length: 3 }, (_, i) => i * 2);"
  ],
  "ans": "[0, 2, 4]",
  "exp": "Array.from's second argument is a map function applied while building the array, so you can go from an array-like (even one with no actual elements, just a length) straight to a mapped real array in one step.",
  "w": [
   "[0, 0, 0]",
   "[]",
   "[undefined, undefined, undefined]"
  ]
 },
 {
  "cat": "Arrays",
  "code": [
   "[1, 2, 3].join();",
   "[1, 2, 3].toString();"
  ],
  "ans": "'1,2,3'\n'1,2,3'",
  "exp": "join() with no separator defaults to a comma. Array.prototype.toString is literally implemented by calling join() internally, so the two are identical here.",
  "w": [
   "'[1,2,3]'\n'1,2,3'",
   "'1,2,3'\n'[1,2,3]'",
   "'123'\n'1,2,3'"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "typeof null;"
  ],
  "ans": "'object'",
  "exp": "This is a 20+ year old bug baked into the very first JS engine's type-tagging scheme (null was represented as a null pointer, and the pointer's type tag happened to be 'object'). It's permanently frozen in the spec for compatibility.",
  "w": [
   "'null'",
   "'undefined'",
   "'nil'"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "null instanceof Object;"
  ],
  "ans": "false",
  "exp": "instanceof walks an object's prototype chain looking for a match. null has no prototype chain to walk at all.",
  "w": [
   "true",
   "TypeError: null is not an object",
   "null"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "const obj = {};",
   "Object.freeze(obj);",
   "obj.a = {};",
   "obj.a.b = 1;",
   "obj.a.b;"
  ],
  "ans": "1",
  "exp": "Object.freeze only locks the object's own top-level properties. Anything nested inside — like a fresh object assigned to obj.a — is completely unaffected and stays mutable.",
  "w": [
   "TypeError: Cannot add property a",
   "undefined",
   "0"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "const parent = { greet(){ return 'hi' } };",
   "const child = Object.create(parent);",
   "for (const k in child) console.log(k);"
  ],
  "ans": "logs 'greet'",
  "exp": "for...in walks the entire prototype chain looking for enumerable properties, not just the object's own. Object.keys(child), by contrast, would return an empty array.",
  "w": [
   "logs nothing — for...in skips inherited props",
   "logs 'hi'",
   "TypeError: child is not iterable"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "{}.hasOwnProperty('toString');",
   "'toString' in {};"
  ],
  "ans": "false\ntrue",
  "exp": "toString is inherited from Object.prototype, not an own property of the literal {}. hasOwnProperty ignores the prototype chain; the in operator includes it.",
  "w": [
   "true\ntrue",
   "false\nfalse",
   "true\nfalse"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "(1).__proto__.__proto__.__proto__;"
  ],
  "ans": "null",
  "exp": "The primitive 1 gets auto-boxed into a Number wrapper to access __proto__. That wrapper's prototype is Number.prototype, whose prototype is Object.prototype, whose prototype is null — the top of every chain.",
  "w": [
   "Object.prototype",
   "Number.prototype",
   "undefined"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "const o = {};",
   "o.__proto__ = null;",
   "o.toString;"
  ],
  "ans": "undefined",
  "exp": "An object with a null prototype has no inherited members at all — not toString, not hasOwnProperty, nothing. It's a genuinely bare object, useful for things like a safe dictionary/map.",
  "w": [
   "'function'",
   "null",
   "TypeError: o.toString is not a function"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "const o = { 2:'b', 1:'a', x:'c', 0:'z' };",
   "Object.keys(o);"
  ],
  "ans": "['0', '1', '2', 'x']",
  "exp": "Integer-index-like keys are always enumerated first, in ascending numeric order, regardless of how they were written in the source — insertion order only governs the remaining string keys.",
  "w": [
   "['x', '0', '1', '2']",
   "['2', '1', 'x', '0']",
   "['0', 'x', '1', '2']"
  ]
 },
 {
  "cat": "Objects & Prototypes",
  "code": [
   "JSON.parse(JSON.stringify({",
   "  a: undefined,",
   "  b: function(){},",
   "  c: new Date(2024,0,1)",
   "}));"
  ],
  "ans": "{ c: '2024-01-01T00:00:00.000Z' }",
  "exp": "undefined and functions have no JSON representation and are silently dropped. Dates aren't native JSON types either — they serialize through their toJSON method into an ISO string, and never get reconstructed back into a Date on parse.",
  "w": [
   "{}",
   "{ a: undefined, b: undefined, c: '2024-01-01T00:00:00.000Z' }",
   "{ c: <a real Date object> }"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "'abc'[10];",
   "'abc'.charAt(10);"
  ],
  "ans": "undefined\n''",
  "exp": "Bracket access behaves like array indexing and returns undefined out of range. charAt is the older pre-ES5 API and was designed to always return a string, so it falls back to empty string instead.",
  "w": [
   "undefined\nundefined",
   "''\n''",
   "''\nundefined"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "''.split('');",
   "''.split(' ');"
  ],
  "ans": "[]\n['']",
  "exp": "Splitting on an empty separator that matches everywhere on an empty string yields no pieces at all. Splitting on a separator that's never found returns the whole original string as a single-element array.",
  "w": [
   "['']\n['']",
   "[]\n[]",
   "['']\n[]"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "String(null);",
   "String(undefined);"
  ],
  "ans": "'null'\n'undefined'",
  "exp": "The String() constructor called as a function has explicit special-case behavior for both, converting them to their literal name as text rather than throwing.",
  "w": [
   "null\nundefined",
   "'null'\n'null'",
   "''\n''"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "JSON.stringify('hi') === 'hi';"
  ],
  "ans": "false",
  "exp": "JSON.stringify wraps string values in literal quote characters as part of producing valid JSON text — the result is the four-character string \"hi\" with quotes included, not the original two characters.",
  "w": [
   "true",
   "SyntaxError: unexpected token",
   "undefined"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "'5' + 3;",
   "'5' - 3;"
  ],
  "ans": "'53'\n2",
  "exp": "+ prefers concatenation the instant either side is a string. - has no string semantics whatsoever, so both operands get forced to numbers first.",
  "w": [
   "8\n2",
   "'53'\n'53'",
   "8\n'53'"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "'Hi'.padStart(6, '0');"
  ],
  "ans": "'0000Hi'",
  "exp": "padStart repeats (and truncates) the pad string as many times as needed to reach the target total length, then prepends it.",
  "w": [
   "'Hi0000'",
   "'000000Hi'",
   "'00Hi'"
  ]
 },
 {
  "cat": "Strings",
  "code": [
   "[...'👍🏽'].length;",
   "'👍🏽'.length;"
  ],
  "ans": "2\n4",
  "exp": "String.length counts UTF-16 code units — an emoji plus skin-tone modifier is stored as two surrogate pairs, four units total. Spreading a string iterates by Unicode code point instead, correctly counting it as two visual/logical characters.",
  "w": [
   "4\n2",
   "2\n2",
   "4\n4"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "const obj = { name: 'a', greet: () => this.name };",
   "obj.greet();"
  ],
  "ans": "undefined",
  "exp": "Arrow functions don't get their own this — they capture it lexically from wherever they were defined, which here is the outer (module/global) scope, not obj. Only a regular function() would bind this to the caller.",
  "w": [
   "'a'",
   "TypeError: Cannot read properties of undefined",
   "the global object"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "const F = () => {};",
   "new F();"
  ],
  "ans": "TypeError: F is not a constructor",
  "exp": "Arrow functions have no internal [[Construct]] method and can never be used with new — there's no way to get an instance out of one.",
  "w": [
   "{} — an empty object",
   "undefined",
   "SyntaxError: arrow functions cannot be constructed"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "function f(){ return arguments.length }",
   "f(1, 2, 3);"
  ],
  "ans": "3",
  "exp": "Regular functions automatically get an array-like arguments object. Arrow functions don't — you'd need (...args) => args.length to get equivalent behavior.",
  "w": [
   "0",
   "1",
   "[1, 2, 3]"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "function f(a, b = a + 1, c){}",
   "f.length;"
  ],
  "ans": "1",
  "exp": "Function.length only counts the parameters that appear before the first one with a default value. Everything from that point on, defaulted or not, is excluded from the count.",
  "w": [
   "3",
   "2",
   "0"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "for (var i = 0; i < 3; i++) {",
   "  setTimeout(() => console.log(i), 0);",
   "}"
  ],
  "ans": "3\n3\n3   // (with let instead of var: 0, 1, 2)",
  "exp": "var is function-scoped, so there's only one i shared by all three closures — by the time the callbacks run, the loop has already finished and i is 3. let creates a fresh binding for every iteration, giving each closure its own snapshot.",
  "w": [
   "0\n1\n2",
   "0\n0\n0",
   "3\n2\n1"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "function f(){",
   "  return",
   "  { ok: true };",
   "}",
   "f();"
  ],
  "ans": "undefined",
  "exp": "Automatic Semicolon Insertion silently inserts a semicolon right after return when it's alone on a line, before the JS engine ever sees the object literal below. The braces are dead, unreachable code.",
  "w": [
   "{ ok: true }",
   "SyntaxError: unexpected token",
   "null"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "'use strict';",
   "function f(){ return this }",
   "f();"
  ],
  "ans": "undefined",
  "exp": "In strict mode, a plain function call leaves this as undefined instead of falling back to the global object — one of several intentional behavior changes strict mode introduces to catch bugs earlier.",
  "w": [
   "the global object",
   "null",
   "the function itself"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "function greet(){}",
   "const b1 = greet.bind(null);",
   "const b2 = greet.bind(null);",
   "b1 === b2;"
  ],
  "ans": "false",
  "exp": "Every call to .bind() allocates and returns a brand-new function object, even with identical arguments on the identical source function. bind is not memoized or cached.",
  "w": [
   "true",
   "undefined",
   "TypeError: bound functions cannot be compared"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "const fn = function counter(){",
   "  return typeof counter;",
   "};",
   "fn();"
  ],
  "ans": "'function'",
  "exp": "A named function expression's own name is only visible as a binding inside its own body — it's not added to any outer scope. Referencing counter outside fn would throw a ReferenceError.",
  "w": [
   "ReferenceError: counter is not defined",
   "'undefined'",
   "'number'"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "function outer(){",
   "  let x = 1;",
   "  return function inner(){ return ++x };",
   "}",
   "const c = outer();",
   "c(); c();"
  ],
  "ans": "3 (second call)",
  "exp": "outer() runs once and inner closes over that single x. Each subsequent call to the same inner reference mutates and reads that one shared variable, rather than starting fresh.",
  "w": [
   "2 (second call)",
   "1 (second call)",
   "4 (second call)"
  ]
 },
 {
  "cat": "Functions, this & Closures",
  "code": [
   "const obj = { count: 0, inc(){ this.count++ } };",
   "const { inc } = obj;",
   "inc();"
  ],
  "ans": "this is undefined → TypeError (strict) or the global object (sloppy)",
  "exp": "Destructuring a method pulls out the bare function, severing its connection to obj. When called standalone, this is determined by how it's called, not where it was defined — and a bare call has no receiver.",
  "w": [
   "obj.count becomes 1",
   "1",
   "NaN"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "console.log(x);",
   "var x = 5;"
  ],
  "ans": "undefined",
  "exp": "var declarations are hoisted to the top of their scope, but only the declaration — the assignment stays exactly where it was written. So x exists but is still undefined at the log.",
  "w": [
   "5",
   "ReferenceError: x is not defined",
   "null"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "console.log(y);",
   "let y = 5;"
  ],
  "ans": "ReferenceError: Cannot access 'y' before initialization",
  "exp": "let and const are hoisted too, but they land in a 'temporal dead zone' — accessible in name only, throwing if touched — until their declaration line actually executes.",
  "w": [
   "undefined",
   "5",
   "SyntaxError"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "console.log(typeof z);"
  ],
  "ans": "'undefined'",
  "exp": "typeof on a truly undeclared identifier is completely safe and doesn't throw — that safety only breaks down for a let/const variable that's declared later but still in its temporal dead zone.",
  "w": [
   "ReferenceError: z is not defined",
   "undefined",
   "null"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "function f(){",
   "  return g();",
   "  function g(){ return 'hoisted' }",
   "}",
   "f();"
  ],
  "ans": "'hoisted'",
  "exp": "Function declarations (unlike function expressions) are hoisted with their entire body attached, making them callable before their textual position in the code.",
  "w": [
   "ReferenceError: g is not defined",
   "undefined",
   "TypeError: g is not a function"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "var a; var a = 1; var a, a, a;"
  ],
  "ans": "no error — all merge into one binding",
  "exp": "var explicitly permits redeclaration; every var a in the same scope just refers to the same underlying variable.",
  "w": [
   "SyntaxError: Identifier 'a' has already been declared",
   "TypeError",
   "ReferenceError"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "let b;",
   "let b = 1;"
  ],
  "ans": "SyntaxError: Identifier 'b' has already been declared",
  "exp": "let and const forbid redeclaring the same name in the same scope, full stop — this is caught at parse time, before any code runs.",
  "w": [
   "no error — b is just reassigned",
   "ReferenceError",
   "undefined"
  ]
 },
 {
  "cat": "Scoping & Hoisting",
  "code": [
   "switch (1) {",
   "  case 0: let x = 'a'; break;",
   "  case 1: let x = 'b'; break;",
   "}"
  ],
  "ans": "SyntaxError: Identifier 'x' has already been declared",
  "exp": "All case clauses inside one switch share a single block scope — there's no implicit per-case scope — so the two let x declarations collide exactly like they would in any shared block.",
  "w": [
   "no error — each case has its own scope",
   "ReferenceError: x is not defined",
   "'b'"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "console.log('a');",
   "setTimeout(() => console.log('b'), 0);",
   "Promise.resolve().then(() => console.log('c'));",
   "console.log('d');"
  ],
  "ans": "a\nd\nc\nb",
  "exp": "Synchronous code always runs to completion first. Then the microtask queue (promise callbacks) drains completely before the event loop even looks at the macrotask queue (timers).",
  "w": [
   "a\nd\nb\nc",
   "a\nb\nc\nd",
   "d\na\nc\nb"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "async function f(){ return 1 }",
   "f();"
  ],
  "ans": "a Promise that resolves to 1, not the number 1",
  "exp": "Every async function implicitly wraps whatever it returns in a Promise — you always get a Promise back, even from a function that looks like it returns a plain value.",
  "w": [
   "1 — the number itself",
   "a Promise that resolves to undefined",
   "SyntaxError: async functions must return a value"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "async function f(){",
   "  const v = await 5;",
   "  return v;",
   "}",
   "f().then(console.log);"
  ],
  "ans": "logs 5",
  "exp": "Awaiting a non-Promise value just wraps it in a resolved Promise and immediately continues — it still yields to the microtask queue for exactly one tick, but the value passes through unchanged.",
  "w": [
   "logs undefined",
   "logs a Promise",
   "logs 5 synchronously, before the caller continues"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "const inner = Promise.resolve(1);",
   "const outer = Promise.resolve(inner);",
   "outer.then(v => console.log(v));"
  ],
  "ans": "logs 1",
  "exp": "Promise.resolve() detects that it was handed a thenable and assimilates it rather than nesting it — you never end up with a Promise resolved to another Promise.",
  "w": [
   "logs Promise { <resolved>: 1 }",
   "logs inner",
   "logs undefined"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "new Promise((resolve) => {",
   "  console.log('exec');",
   "  resolve();",
   "});",
   "console.log('after');"
  ],
  "ans": "'exec'\n'after'",
  "exp": "The Promise executor function runs synchronously the instant the Promise is constructed — it doesn't wait for a .then() or anything else. Only the callbacks attached afterward are deferred.",
  "w": [
   "'after'\n'exec'",
   "'exec' only",
   "'after' only"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "[1, 2, 3].forEach(async n => {",
   "  await Promise.resolve();",
   "  console.log(n);",
   "});",
   "console.log('done');"
  ],
  "ans": "'done' logs first, before any numbers",
  "exp": "forEach has no idea its callback returned a Promise — it doesn't await anything and just keeps looping synchronously. All three async callbacks get scheduled but 'done' wins the race.",
  "w": [
   "1\n2\n3, then 'done'",
   "'done' logs between the numbers",
   "3\n2\n1, then 'done'"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "try {",
   "  setTimeout(() => { throw new Error('boom') }, 0);",
   "} catch (e) {",
   "  console.log('caught');",
   "}"
  ],
  "ans": "the error is never caught — it's an uncaught exception",
  "exp": "The try block finishes and exits long before the timer callback ever runs on a later turn of the event loop. A try/catch can't reach across ticks.",
  "w": [
   "'caught' is logged",
   "'caught' is logged, then the error is swallowed",
   "the error is caught and logged"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "Promise.all([",
   "  Promise.resolve(1),",
   "  Promise.reject('x'),",
   "  new Promise(() => {})",
   "]).catch(e => console.log(e));"
  ],
  "ans": "logs 'x' quickly",
  "exp": "Promise.all rejects the instant any single input rejects, without waiting on the others — including that third Promise, which never settles at all.",
  "w": [
   "logs 'x' only after the third Promise settles",
   "logs 1 — the first resolution wins",
   "logs [1, 'x', undefined]"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "const p = Promise.resolve(1);",
   "p.then(v => console.log('A', v));",
   "p.then(v => console.log('B', v));"
  ],
  "ans": "A 1\nB 1",
  "exp": "Attaching multiple .then() handlers to the same settled Promise queues a separate microtask for each, and they run in the order they were attached.",
  "w": [
   "A 1 only",
   "B 1\nA 1",
   "A 1, then an error"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "queueMicrotask(() => console.log('micro'));",
   "setTimeout(() => console.log('macro'), 0);"
  ],
  "ans": "'micro'\n'macro'",
  "exp": "Same rule as promises vs timers: the microtask queue is always fully drained before the event loop proceeds to the next macrotask, regardless of which was scheduled first.",
  "w": [
   "'macro'\n'micro'",
   "'macro' only",
   "'micro' only"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "async function f(){",
   "  console.log(1);",
   "  await null;",
   "  console.log(2);",
   "}",
   "f();",
   "console.log(3);"
  ],
  "ans": "1\n3\n2",
  "exp": "Everything before the first await runs synchronously, immediately. The await then suspends the function and defers the rest to a microtask, letting the synchronous console.log(3) run first.",
  "w": [
   "1\n2\n3",
   "3\n1\n2",
   "2\n1\n3"
  ]
 },
 {
  "cat": "Async & Event Loop",
  "code": [
   "Promise.reject(new Error('x'))",
   "  .catch(() => {})",
   "  .then(() => console.log('handled'));"
  ],
  "ans": "logs 'handled', no unhandled-rejection warning",
  "exp": ".catch() returns a brand-new, already-fulfilled Promise — the rejection is considered handled and doesn't propagate any further down the chain.",
  "w": [
   "logs nothing — plus an unhandled-rejection warning",
   "logs 'handled' — plus an unhandled-rejection warning",
   "the rejection is still unhandled"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "class Foo { bar(){} }",
   "typeof Foo;"
  ],
  "ans": "'function'",
  "exp": "Classes are syntactic sugar over JS's original constructor-function pattern — under the hood, a class really is a (specially-flagged) function.",
  "w": [
   "'class'",
   "'object'",
   "undefined"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "console.log(new Foo());",
   "class Foo {}"
  ],
  "ans": "ReferenceError: Cannot access 'Foo' before initialization",
  "exp": "Unlike function declarations, class declarations are hoisted but land in the temporal dead zone, exactly like let/const — you can't use one before its definition runs.",
  "w": [
   "Foo {} — an empty instance",
   "undefined",
   "SyntaxError"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "class Foo { method(){} }",
   "Object.keys(Foo.prototype);"
  ],
  "ans": "[]",
  "exp": "Methods defined inside a class body are non-enumerable by default. The equivalent method written as an object literal property would show up in Object.keys — a class method deliberately doesn't.",
  "w": [
   "['method']",
   "['constructor']",
   "['method', 'constructor']"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "class Foo extends null {}",
   "new Foo();"
  ],
  "ans": "TypeError: Super constructor null of Foo is not a constructor",
  "exp": "Extending null removes any constructor to implicitly call via super() — without one, instantiation is impossible, even though 'extends null' is syntactically legal.",
  "w": [
   "works fine — an instance with no prototype",
   "null",
   "SyntaxError: cannot extend null"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "class Counter {",
   "  #count = 0;",
   "  inc(){ return ++this.#count }",
   "}",
   "const c = new Counter();",
   "c['#count'];"
  ],
  "ans": "undefined",
  "exp": "Private class fields aren't just a naming convention — the # is genuinely enforced by the engine. There's no bracket-notation or dot-notation string key that can reach a private field from outside the class.",
  "w": [
   "0",
   "TypeError: Cannot read private member #count from an object whose class did not declare it",
   "'#count'"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "class A {",
   "  name = 'A';",
   "  greet = () => `hi ${this.name}`;",
   "}",
   "const { greet } = new A();",
   "greet();"
  ],
  "ans": "'hi A'",
  "exp": "A class field initialized to an arrow function captures this from the constructor context at instance-creation time, binding it permanently to that instance — so it keeps working even after being destructured away from the object, unlike a normal prototype method.",
  "w": [
   "'hi undefined'",
   "TypeError: this is undefined",
   "undefined"
  ]
 },
 {
  "cat": "Classes",
  "code": [
   "class Foo {",
   "  static count = 0;",
   "  static inc(){ return ++Foo.count }",
   "}",
   "new Foo().inc;"
  ],
  "ans": "undefined",
  "exp": "static members live on the class itself (Foo.inc), never on instances (new Foo()). Instance methods and static methods occupy entirely separate namespaces.",
  "w": [
   "0",
   "1",
   "the inc function itself"
  ]
 },
 {
  "cat": "Regex",
  "code": [
   "'2024-01-15'.match(/(\\d+)-(\\d+)-(\\d+)/)[1];",
   "'2024-01-15'.match(/(\\d+)-(\\d+)-(\\d+)/g);"
  ],
  "ans": "'2024'\n['2024-01-15']",
  "exp": "Without the g flag, match() returns a rich result array including capture groups. With g, it switches to returning only an array of full match strings — capture groups are discarded entirely in that mode.",
  "w": [
   "['2024', '01', '15']\n['2024-01-15']",
   "'2024'\n['2024', '01', '15']",
   "['2024-01-15']\n['2024', '01', '15']"
  ]
 },
 {
  "cat": "Regex",
  "code": [
   "const re = /a/g;",
   "re.test('aaa'); re.test('aaa');",
   "re.test('aaa'); re.test('aaa');"
  ],
  "ans": "true, true, true, false (then cycles)",
  "exp": "A regex with the g flag is stateful — it remembers lastIndex between calls and resumes searching from there. Eventually it runs past the end of the string, returns false once, and then resets to search from the start again.",
  "w": [
   "true, true, true, true",
   "true, false, true, false",
   "false, true, true, true"
  ]
 },
 {
  "cat": "Regex",
  "code": [
   "'aaa'.replace(/a/, 'b');",
   "'aaa'.replace(/a/g, 'b');"
  ],
  "ans": "'baa'\n'bbb'",
  "exp": "Without g, replace only touches the very first match it finds. Add the g flag and it replaces every match in the string.",
  "w": [
   "'bbb'\n'baa'",
   "'baa'\n'baa'",
   "'ba'\n'bbb'"
  ]
 },
 {
  "cat": "Regex",
  "code": [
   "'a1b2'.matchAll(/\\d/);"
  ],
  "ans": "throws TypeError: matchAll called with a non-global RegExp",
  "exp": "Unlike match(), matchAll refuses to run at all unless the regex has the g flag — there's no non-global fallback behavior for it.",
  "w": [
   "[]",
   "null",
   "returns the matches, same as match()"
  ]
 },
 {
  "cat": "JSON",
  "code": [
   "JSON.stringify(undefined);",
   "JSON.stringify({ a: undefined });"
  ],
  "ans": "undefined  // the JS value, not the string\n'{}'",
  "exp": "undefined has no JSON representation. At the top level, stringify itself just returns the actual undefined value. Inside an object, any property whose value is undefined is silently dropped from the output.",
  "w": [
   "'undefined'\n'{}'",
   "undefined\nundefined",
   "'{}'\n'{}'"
  ]
 },
 {
  "cat": "JSON",
  "code": [
   "JSON.stringify(NaN);",
   "JSON.stringify(Infinity);"
  ],
  "ans": "'null'\n'null'",
  "exp": "Neither NaN nor Infinity has a JSON number representation, so both quietly become the JSON literal null instead of erroring.",
  "w": [
   "'NaN'\n'Infinity'",
   "NaN\nInfinity",
   "'null'\n'undefined'"
  ]
 },
 {
  "cat": "JSON",
  "code": [
   "JSON.stringify({ a: () => {}, b: 1 });"
  ],
  "ans": "'{\"b\":1}'",
  "exp": "Functions, like undefined, have no JSON equivalent and get dropped from the output entirely rather than causing an error.",
  "w": [
   "'{\"a\":null,\"b\":1}'",
   "'{\"a\":{},\"b\":1}'",
   "throws TypeError: functions cannot be stringified"
  ]
 },
 {
  "cat": "JSON",
  "code": [
   "const a = {};",
   "a.self = a;",
   "JSON.stringify(a);"
  ],
  "ans": "TypeError: Converting circular structure to JSON",
  "exp": "stringify walks the object graph and has no way to represent a reference back to something it's already serializing — it detects the cycle and throws rather than looping forever.",
  "w": [
   "'{\"self\":null}'",
   "'{\"self\":\"[Circular]\"}'",
   "'{}'"
  ]
 },
 {
  "cat": "JSON",
  "code": [
   "JSON.parse('{\"date\":\"2024-01-01\"}').date;"
  ],
  "ans": "the string '2024-01-01', not a Date object",
  "exp": "JSON has no native Date type. JSON.parse never automatically reconstructs Date instances from ISO strings on its own — you'd need a custom reviver function to do that conversion yourself.",
  "w": [
   "a Date object",
   "null",
   "throws TypeError"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "typeof document.all;",
   "document.all instanceof Object;",
   "document.all == null;",
   "document.all === null;"
  ],
  "ans": "'undefined'\ntrue\ntrue\nfalse",
  "exp": "document.all is a deliberate, spec-sanctioned exception — a 'willful violation' kept for legacy IE compatibility, made to act invisible to feature-detection code that checks typeof or === null/undefined, while still behaving like a real object otherwise.",
  "w": [
   "'object'\ntrue\ntrue\nfalse",
   "'undefined'\nfalse\nfalse\nfalse",
   "'object'\nfalse\nfalse\ntrue"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "document.getElementById('missing');",
   "document.querySelector('#missing');"
  ],
  "ans": "null\nnull",
  "exp": "Both consistently return null on no match — unlike, say, Array.prototype.find(), which returns undefined for a miss.",
  "w": [
   "undefined\nundefined",
   "null\nundefined",
   "undefined\nnull"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "el.innerHTML = el.innerHTML;"
  ],
  "ans": "destroys and rebuilds every child node",
  "exp": "Reassigning innerHTML — even to its own current value — reparses the markup from scratch and creates entirely new DOM nodes, silently detaching any event listeners that were attached to the old descendants.",
  "w": [
   "nothing happens — same markup, same nodes",
   "throws an error",
   "updates text only, keeps the nodes"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "const fn = () => console.log('click');",
   "btn.addEventListener('click', fn);",
   "btn.addEventListener('click', fn);"
  ],
  "ans": "the handler fires only once per click",
  "exp": "addEventListener silently deduplicates identical (function reference, event type, phase) registrations. Two separate function expressions with identical bodies would not be deduped and would both fire.",
  "w": [
   "the handler fires twice per click",
   "the handler fires twice per click in some browsers",
   "throws an error — duplicate listeners are rejected"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "// listener attached to a parent <ul>",
   "ul.addEventListener('click', e => {",
   "  console.log(e.target, e.currentTarget);",
   "});"
  ],
  "ans": "target = the actual <li> clicked, currentTarget = the <ul>",
  "exp": "target is whatever element originated the event; currentTarget is whichever element the listener is actually attached to. They only match when you click the element you listened on directly.",
  "w": [
   "target = the <ul>, currentTarget = the <li>",
   "target = the <li>, currentTarget = the <li>",
   "target = the <ul>, currentTarget = the <ul>"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "localStorage.setItem('user', { name: 'a' });",
   "localStorage.getItem('user');"
  ],
  "ans": "'[object Object]'",
  "exp": "localStorage only stores strings. Passing a non-string value silently calls .toString() on it — you need JSON.stringify/parse yourself to round-trip real objects.",
  "w": [
   "{ name: 'a' }",
   "'{\"name\":\"a\"}'",
   "null"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "window.name = 'x';",
   "// navigate to a completely different URL, same tab"
  ],
  "ans": "window.name is still 'x' after navigation",
  "exp": "window.name is one of the very few pieces of browser state that persists across a full page navigation in the same tab, without going through any storage API at all — a quirk occasionally (ab)used for cross-page data passing.",
  "w": [
   "window.name resets to ''",
   "window.name throws a SecurityError",
   "window.name persists only for same-origin pages"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "fetch('/does-not-exist').then(r => console.log(r.ok));"
  ],
  "ans": "logs false — the promise still resolves",
  "exp": "fetch() only rejects on a network-level failure (DNS, CORS, connection refused, etc). An HTTP error status like 404 or 500 is still a 'successful' fetch from the API's point of view — you have to check response.ok yourself.",
  "w": [
   "rejects with a TypeError",
   "logs true — the fetch succeeded",
   "throws a SyntaxError"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "history.pushState({}, '', '/new-url');"
  ],
  "ans": "URL changes, but no 'popstate' event fires",
  "exp": "pushState/replaceState update the address bar and history stack silently. popstate only fires from user or programmatic back/forward navigation, never from pushState itself.",
  "w": [
   "URL changes and 'popstate' fires immediately",
   "the page reloads with the new URL",
   "'hashchange' fires"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "new Date('2024-01-15');",
   "new Date('2024-01-15 00:00');"
  ],
  "ans": "parsed as UTC midnight\nparsed in the local timezone",
  "exp": "A bare date-only ISO string is specified to parse as UTC. The moment you add a time component in a non-ISO format, the same date string parses in the local timezone instead — the exact same calendar date can log a different clock hour.",
  "w": [
   "parsed in the local timezone\nparsed as UTC midnight",
   "both parsed as UTC midnight",
   "both parsed in the local timezone"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "const nodes = document.querySelectorAll('div');",
   "nodes.map(el => el.id);"
  ],
  "ans": "TypeError: nodes.map is not a function",
  "exp": "A NodeList is iterable and has forEach, but it isn't a real Array — no map, filter, or reduce. You need Array.from(nodes) or [...nodes] first.",
  "w": [
   "works fine — returns the element ids",
   "returns undefined",
   "returns []"
  ]
 },
 {
  "cat": "DOM & Browser",
  "code": [
   "el.style.width = '200px';",
   "console.log(getComputedStyle(el).width);"
  ],
  "ans": "forces a synchronous layout recalculation right there",
  "exp": "Reading a computed layout property immediately after writing a style change forces the browser to synchronously flush pending layout work to answer accurately — doing this repeatedly in a loop is the classic 'layout thrashing' performance trap.",
  "w": [
   "returns the last cached width with no extra work",
   "throws an error",
   "recalculates asynchronously, after the current task"
  ]
 },
 {
  "cat": "Timers",
  "code": [
   "setTimeout(() => console.log('now'), Infinity);"
  ],
  "ans": "logs almost immediately",
  "exp": "The delay is internally coerced into a 32-bit signed integer. Infinity can't fit and overflows, which effectively clamps the delay down to about 1ms instead of ever waiting.",
  "w": [
   "never fires",
   "fires after roughly 24 days",
   "throws a RangeError"
  ]
 },
 {
  "cat": "Timers",
  "code": [
   "clearTimeout(undefined);"
  ],
  "ans": "no-op, does not throw",
  "exp": "clearTimeout/clearInterval silently ignore invalid IDs or IDs for timers that already fired — they never throw, which makes them safe to call defensively.",
  "w": [
   "throws a TypeError",
   "clears every pending timer",
   "returns false"
  ]
 },
 {
  "cat": "Timers",
  "code": [
   "let i = 0;",
   "setInterval(() => { heavyWork(); console.log(i++); }, 100);"
  ],
  "ans": "ticks drift and can be skipped, not perfectly spaced",
  "exp": "If the callback (or anything else on the main thread) blocks past the interval, the browser doesn't queue up extra catch-up ticks — it just fires the next one as soon as it's free, so real elapsed time between logs can exceed 100ms.",
  "w": [
   "fires exactly every 100ms, no matter what",
   "queues the missed ticks and fires them back-to-back",
   "stops firing entirely"
  ]
 },
 {
  "cat": "Timers",
  "code": [
   "function tick(){ setTimeout(tick, 0); }",
   "tick();"
  ],
  "ans": "after several levels of nested 0ms timeouts, the browser clamps the delay to a minimum (~4ms)",
  "exp": "Per the HTML spec, deeply nested chains of zero-delay timeouts get throttled to a minimum delay, even though every call explicitly asked for 0 — it's a deliberate anti-starvation measure.",
  "w": [
   "fires at exactly 0ms, indefinitely",
   "the browser refuses to schedule any more",
   "the delay clamps to ~4ms from the very first level"
  ]
 },
 {
  "cat": "Destructuring & Spread",
  "code": [
   "let x;",
   "const { x: y = 1 } = { x };",
   "y;"
  ],
  "ans": "1",
  "exp": "x is undefined, so even though the property x genuinely exists on the source object, the default value still kicks in — defaults trigger on an undefined value, not on a missing key.",
  "w": [
   "undefined",
   "0",
   "TypeError"
  ]
 },
 {
  "cat": "Destructuring & Spread",
  "code": [
   "[...new Set([1, 2, 2, 3])];"
  ],
  "ans": "[1, 2, 3]",
  "exp": "Spread works on any iterable, not just arrays — Set's iteration protocol yields each value once, making this the standard one-liner for de-duplicating an array.",
  "w": [
   "[1, 2, 2, 3]",
   "[1, 2, 3, 3]",
   "TypeError: Set is not iterable"
  ]
 },
 {
  "cat": "Destructuring & Spread",
  "code": [
   "const obj = { get x(){ return Math.random() } };",
   "const copy = { ...obj };",
   "typeof Object.getOwnPropertyDescriptor(copy, 'x').get;"
  ],
  "ans": "'undefined' — copy.x is a plain value, the getter itself wasn't copied",
  "exp": "Object spread reads and copies the current value of each own enumerable property. It doesn't clone accessor (getter/setter) definitions — the copy just gets whatever the getter happened to return at spread time.",
  "w": [
   "'function' — the getter was copied",
   "a random number — the getter still runs on the copy",
   "TypeError"
  ]
 },
 {
  "cat": "Destructuring & Spread",
  "code": [
   "let a = 1, b = 2;",
   "[a, b] = [b, a];"
  ],
  "ans": "a = 2, b = 1",
  "exp": "Array destructuring assignment is the idiomatic JS swap — the right side's temporary array is fully built before any assignment happens, so no temp variable is needed.",
  "w": [
   "a = 1, b = 2",
   "a = 1, b = 1",
   "SyntaxError"
  ]
 },
 {
  "cat": "Destructuring & Spread",
  "code": [
   "function f(...rest, last){}"
  ],
  "ans": "SyntaxError: Rest parameter must be last formal parameter",
  "exp": "A rest parameter must always be the final parameter in the list — nothing, not even a single extra named parameter, is allowed to come after it.",
  "w": [
   "works fine — last is just undefined",
   "works fine — last gets the last argument",
   "TypeError at runtime"
  ]
 },
 {
  "cat": "Destructuring & Spread",
  "code": [
   "const [first, ...rest] = 'hello';"
  ],
  "ans": "first = 'h', rest = ['e', 'l', 'l', 'o']",
  "exp": "Strings are iterable by code point, so array destructuring works directly on a string without converting it first — each character comes out as its own array element.",
  "w": [
   "first = 'h', rest = 'ello'",
   "first = 'hello', rest = []",
   "TypeError: strings are not iterable"
  ]
 },
 {
  "cat": "Operators & ASI",
  "code": [
   "27.toString();",
   "27..toString();"
  ],
  "ans": "SyntaxError: Invalid or unexpected token\n'27'",
  "exp": "After a number literal, the parser reads a single . as the decimal point, not the start of property access — it expects more digits, not a method name. A second dot (or wrapping parens) disambiguates it as property access.",
  "w": [
   "'27'\n'27'",
   "SyntaxError\nSyntaxError",
   "'27'\nSyntaxError"
  ]
 },
 {
  "cat": "Operators & ASI",
  "code": [
   "const result = (1, 2, 3);"
  ],
  "ans": "3",
  "exp": "The comma operator evaluates every operand left to right for its side effects and yields only the value of the last one.",
  "w": [
   "1",
   "[1, 2, 3]",
   "undefined"
  ]
 },
 {
  "cat": "Operators & ASI",
  "code": [
   "const user = null;",
   "user?.profile?.name;"
  ],
  "ans": "undefined, no throw",
  "exp": "Optional chaining short-circuits the entire remainder of that one chain the instant it hits a nullish value — but each link needs its own ?. if you want every step protected, since a single ?. only guards the access immediately after it.",
  "w": [
   "TypeError: Cannot read properties of null",
   "null",
   "ReferenceError: user is not defined"
  ]
 },
 {
  "cat": "Operators & ASI",
  "code": [
   "0 || 'default';",
   "0 ?? 'default';"
  ],
  "ans": "'default'\n0",
  "exp": "|| falls through on any falsy value, including legitimate ones like 0. ?? only falls through on null or undefined specifically, so a real, intentional 0 survives untouched.",
  "w": [
   "0\n'default'",
   "'default'\n'default'",
   "0\n0"
  ]
 },
 {
  "cat": "Operators & ASI",
  "code": [
   "2 ** 3 ** 2;"
  ],
  "ans": "512",
  "exp": "The exponentiation operator is right-associative, unlike most other operators, so this evaluates as 2 ** (3 ** 2) = 2 ** 9, not (2 ** 3) ** 2 = 64.",
  "w": [
   "64",
   "4096",
   "256"
  ]
 },
 {
  "cat": "Operators & ASI",
  "code": [
   "(function(){",
   "  try { return 1 }",
   "  finally { return 2 }",
   "})();"
  ],
  "ans": "2",
  "exp": "A return (or throw) inside a finally block silently overrides whatever the try block was already returning or throwing — finally always gets the last word.",
  "w": [
   "1",
   "undefined",
   "SyntaxError"
  ]
 },
 {
  "cat": "Sorting & Comparison",
  "code": [
   "[10, 1, 3].sort((a, b) => a - b);"
  ],
  "ans": "[1, 3, 10]",
  "exp": "Supplying a numeric comparator fixes the default lexicographic-string sort — a - b sorts ascending numerically, which is almost always what you actually want for numbers.",
  "w": [
   "[1, 10, 3]",
   "[10, 3, 1]",
   "[3, 1, 10]"
  ]
 },
 {
  "cat": "Sorting & Comparison",
  "code": [
   "NaN < 1;",
   "NaN > 1;",
   "NaN <= 1;",
   "NaN === NaN;"
  ],
  "ans": "false\nfalse\nfalse\nfalse",
  "exp": "Every relational and equality comparison involving NaN evaluates to false, no exceptions — including comparing NaN to itself. There's no comparison operator that will ever return true when NaN is on either side.",
  "w": [
   "false\nfalse\nfalse\ntrue",
   "true\ntrue\ntrue\ntrue",
   "false\ntrue\nfalse\nfalse"
  ]
 },
 {
  "cat": "Sorting & Comparison",
  "code": [
   "const arr = [3, 1, 2];",
   "const sorted = arr.sort();",
   "arr === sorted;"
  ],
  "ans": "true",
  "exp": "Array.prototype.sort mutates the array in place and returns that same reference — it is not a copying operation like [...arr].sort() would be.",
  "w": [
   "false",
   "undefined",
   "TypeError"
  ]
 },
 {
  "cat": "Sorting & Comparison",
  "code": [
   "// two items with equal sort keys",
   "[{k:1,n:'a'},{k:1,n:'b'}].sort((x,y)=>x.k-y.k).map(o=>o.n);"
  ],
  "ans": "['a', 'b'] — guaranteed, since ES2019",
  "exp": "Before ES2019, sort's stability (whether equal elements keep their original relative order) was implementation-defined and inconsistent across browsers. It's now a spec guarantee, not just an implementation detail you got lucky with.",
  "w": [
   "['b', 'a'] — guaranteed, since ES2019",
   "['a', 'b'] — but not guaranteed in all browsers",
   "['b', 'a'] — but not guaranteed in all browsers"
  ]
 }
]