{
  "assessmentTests": {
    "javascript_test": {
      "name": "การทดสอบ JavaScript",
      "desc": "คำถาม 30 ข้อแบบสถานการณ์เกี่ยวกับไวยากรณ์พื้นฐานและการแปลงชนิด ฟังก์ชัน/ขอบเขต/closures และ this อ็อบเจกต์/อาร์เรย์ และโครงสร้างข้อมูล และ event loop/async/idiom ต้นแบบ — เพื่อให้ทราบว่า JavaScript ของคุณตรงกับสิ่งที่นายจ้างหมายถึงโดย JavaScript ที่มีทักษะ",
      "recommendation": "โปรไฟล์ทักษะ JavaScript ของคุณ",
      "results": {
        "beginner": {
          "name": "ผู้เริ่มต้น",
          "desc": "คุณสามารถเขียนฟังก์ชันและอ็อบเจกต์ที่ใช้งานได้ แต่คำถามที่คุณพลาดจะเกี่ยวกับการแปลงชนิดและความหมายของการอ้างอิง ไม่ใช่ไวยากรณ์ — == จะแปลงชนิดโดยเงียบ ๆ ก่อนการเปรียบเทียบ การมอบหมายอาร์เรย์ใช้อ้างอิงร่วม ไม่ใช่คัดลอก ทำไม NaN จึงไม่เท่ากับตัวมันเอง ไม่ใช่ว่าคุณเก่งไม่พอที่จะเขียน JavaScript; นี่คือกฎเฉพาะที่ทำให้คนที่เรียนรู้ภาษาโดยการเขียนโค้ดที่ทำงานโดยบังเอิญ สับสน แทนที่จะเข้าใจว่าค่าและการอ้างอิงจริง ๆ ทำงานอย่างไร พวกเขาสำคัญในการทำงานเพราะแต่ละอันคือที่ที่โค้ดทำงานโดยไม่มีข้อผิดพลาด และยังคงทำสิ่งที่ผิด",
          "recommendation": "เริ่มต้นด้วยสามสิ่ง ตามลำดับนี้: ทำไม == จึงแปลงชนิดก่อนการเปรียบเทียบ ขณะที่ === ไม่ทำ; ทำไมการมอบหมายอาร์เรย์หรืออ็อบเจกต์ให้กับตัวแปรใหม่จึงคัดลอกการอ้างอิง ไม่ใช่ค่า; var ถูกยกขึ้นและเริ่มต้นเป็น undefined ได้อย่างไร ขณะที่ let และ const ยังคงอยู่ในเขตมรณะจนกว่าคำประกาศจะทำงาน คู่มือ MDN JavaScript ครอบคลุมทั้งสามด้วยตัวอย่างที่ใช้งานได้"
        },
        "intermediate": {
          "name": "ระดับกลาง",
          "desc": "คุณจัดการโค้ดแอปพลิเคชันประจำวัน — ฟังก์ชัน วิธีการอาร์เรย์ การไหลควบคุมง่าย ๆ — และจะไม่ชะลอตัวลงจากการพัฒนาฟีเจอร์ทั่ว ๆ ไป ช่องว่างระหว่างระดับกลางและระดับสูงส่วนใหญ่อยู่ในวิธีที่กฎหมายขอบเขตและการตั้งเวลาของ JavaScript โต้ตอบ: var ที่จับไว้ภายในคอลแบ็ก callbacks ของลูปที่อ่านค่าสุดท้ายเดียวกัน; this ที่หยุดชี้ไปที่อ็อบเจกต์เมื่อเมธอดถูกแยกออก; spread-copy ที่ดูลึก แต่คัดลอกเพียงระดับเดียว นี่คือบั๊ก ชนิดที่ผ่านการอ่านอย่างรวดเร็วและปรากฏเฉพาะในเงื่อนไขรันไทม์เฉพาะ",
          "recommendation": "เน้นไปที่วิธีที่ขอบเขตและ this แก้ไขที่เวลาเรียก ไม่ใช่เวลาคำนิยาม: ทำไมลูป for ที่มี var จึงส่งค่าตัวนับสุดท้ายเดียวกันให้กับแต่ละคอลแบ็ก ขณะที่ let ไม่ทำ; this ถูกกำหนดโดยวิธีการเรียกใช้ฟังก์ชัน ไม่ใช่ที่ที่เขียน; ทำไม spread หรือ Object.assign จึงคัดลอกเฉพาะระดับบนสุดของอ็อบเจกต์ จากนั้น event loop เนื่องจากเป็นสิ่งที่ทำให้คนที่เข้าใจ promises ทีละคนสับสนอยู่แล้ว"
        },
        "advanced": {
          "name": "ขั้นสูง",
          "desc": "นี่คือระดับที่ประกาศหลากหลายหมายถึง \"JavaScript ที่แข็งแกร่ง\" คุณอ่านสายโซ่ filter/map/reduce และสามารถทำนายได้อย่างแม่นยำว่าจะส่งคืนอะไร โดยไม่ต้องแก้ไขอาร์เรย์ดั้งเดิม คุณรู้ว่าทำไม console.log ซิงโครนัสจึงชนะ microtask ซึ่งชนะ setTimeout เสมอ; คุณเข้าสู่ Promise.all โดยทราบว่ามันปฏิเสธในทันทีเมื่อ promise ใด ๆ ปฏิเสธแทนที่จะรอส่วนที่เหลือ สิ่งที่แยกแถบนี้จากด้านบนคือด้านต้นแบบและโมเดลอ็อบเจกต์ของการทำงาน: วิธีที่สายโซ่ต้นแบบแก้ไขเมธอดที่สืบทอด เทียบกับ hasOwnProperty; ที่ฟิลด์ # ส่วนตัวของคลาส จริง ๆ แล้วซ่อน",
          "recommendation": "ผลักดันเข้าไปในส่วนที่ปกป้องสถานะที่โค้ดอื่น ๆ ยังสัมผัส: วิธีที่สายโซ่ต้นแบบ JavaScript แก้ไขการค้นหาเมธอดที่ไม่อยู่ในอินสแตนซ์เอง; ฟิลด์ # ส่วนตัวของคลาสที่ซ่อนจากการเข้าถึงภายนอก; JSON.stringify ปฏิบัติต่อ undefined และฟังก์ชันแตกต่างกันภายในอ็อบเจกต์ธรรมดา เทียบกับภายในอาร์เรย์ วัสดุ MDN เกี่ยวกับ event loop และสายโซ่ต้นแบบ คือขั้นตอนต่อไปที่เป็นธรรมชาติสำหรับทั้งคู่"
        },
        "expert": {
          "name": "ผู้เชี่ยวชาญ",
          "desc": "คุณได้คะแนนสูงสุดในทุกส่วน — ไวยากรณ์พื้นฐาน ชนิด และการแปลง; ฟังก์ชัน ขอบเขต closures และ this; อ็อบเจกต์ อาร์เรย์ และโครงสร้างข้อมูล; และ async event loop ต้นแบบ และ idioms ในทางปฏิบัติ หมายความว่า คุณสามารถนำฟังก์ชันของคนแปลกหน้ามา และอธิบายว่าทำไมมันจึงส่งคืนสิ่งที่ส่งคืน ไม่ใช่เพียงสิ่งที่ไวยากรณ์บอกว่าควรทำ — นั่นคือทักษะที่ยากขึ้นและมีค่ามากขึ้น ที่ระดับนี้ ภาษาเองนั้นหายากที่จะเป็นปัจจัยจำกัด; ขีด จำกัด มักจะอยู่ในการควบคุมการไหลแบบ async หรือรูปแบบของข้อมูลภายใต้นั้น",
          "recommendation": "ผลตอบแทนอยู่ในการออกแบบและการวินิจฉัย: การให้เหตุผลเกี่ยวกับเงื่อนไขการแข่งขันระหว่าง Promise.all และ Promise.race แทนที่จะคำหนึ่งคำนิยามของพวกเขา; เลือกเมื่อ closure เป็นเครื่องมือที่เหมาะสมสำหรับสถานะส่วนตัว เทียบกับ เมื่อฟิลด์ส่วนตัวของคลาส; อ่าน bug microtask-vs-macrotask การสั่งซื้อใน logs production แทนในแบบทดสอบ หากคุณถูกตรวจสอบสำหรับบทบาท อธิบาย bug เวลากำหนดที่คุณพบ — ตัวแปร closure เก่า หรือ callback async ที่สั่งซื้อผิด — แทนที่จะตั้งชื่อคุณสมบัติ JavaScript; มันสาธิตการให้เหตุผล ไม่ใช่แค่ศัพท์"
        }
      },
      "questions": [
        {
          "question": "console.log(x);\nvar x = 5;\nสิ่งนี้พิมพ์อะไร?",
          "options": [
            {
              "icon": "",
              "label": "5 — ประกาศตัวแปร var ทำงานทันที ก่อนโค้ดอื่น ๆ"
            },
            {
              "icon": "",
              "label": "ReferenceError: x is not defined"
            },
            {
              "icon": "",
              "label": "undefined — ประกาศตัวแปร var ถูกยกขึ้นไปยังด้านบนของขอบเขต และเริ่มต้นเป็น undefined แต่การกำหนดค่าจะทำงานในตำแหน่งเดิม ดังนั้นก่อนที่มันทำงาน ตัวแปรจะมีอยู่ แต่เก็บค่า undefined"
            },
            {
              "icon": "",
              "label": "null"
            }
          ]
        },
        {
          "question": "console.log(y);\nlet y = 5;\nจะเกิดอะไรขึ้น?",
          "options": [
            {
              "icon": "",
              "label": "มันจะโยน ReferenceError: Cannot access 'y' before initialization — 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": "ค่าใดต่อไปนี้เป็น truthy เมื่อใช้ในเงื่อนไข if?",
          "options": [
            {
              "icon": "",
              "label": "0"
            },
            {
              "icon": "",
              "label": "\"\" (สตริงว่าง)"
            },
            {
              "icon": "",
              "label": "null"
            },
            {
              "icon": "",
              "label": "\"0\" — สตริงที่ไม่ว่างนั้นเป็น truthy เสมอใน 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 is not a function — ประกาศฟังก์ชันถูกยกขึ้นอย่างเต็มที่พร้อมกับ body ของมัน แต่ฟังก์ชันนิพจน์ที่กำหนดค่าด้วย 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 — ฟังก์ชันที่ส่งกลับปิดตัวแปร count เดียวกันจากขอบเขต makeCounter และตัวแปรนั้นคงอยู่ระหว่างการเรียกแทนที่จะรีเซ็ต"
            }
          ]
        },
        {
          "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 สร้าง binding ใหม่ของ i สำหรับแต่ละครั้งของลูป ทำให้ callback แต่ละอัน closure ผ่านสำเนาของตัวเองแยกต่างหากแทนตัวแปรที่แชร์กัน"
            }
          ]
        },
        {
          "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 จึง resolve ผ่าน person"
            },
            {
              "icon": "",
              "label": "\"Hi, undefined\" — call() ใช้ได้เฉพาะกับฟังก์ชันที่เป็น method ของออบเจกต์"
            },
            {
              "icon": "",
              "label": "มันจะ throw TypeError เนื่องจาก greet ไม่ใช่ method ของ person"
            },
            {
              "icon": "",
              "label": "\"Hi, ${this.name}\" — call() ไม่ evaluate template literal"
            }
          ]
        },
        {
          "question": "const timer = {\n  seconds: 0,\n  start: function() {\n    setInterval(() => { this.seconds++; }, 1000);\n  },\n};\nทำไมผู้คนจึงชอบ arrow function มากกว่า function() { this.seconds++ } ภายใน setInterval ที่นี่?",
          "options": [
            {
              "icon": "",
              "label": "Arrow function ไม่มี this ของตัวเอง — มัน capture this จากบริบทของโค้ดนอก ของฟังก์ชัน start ที่ครอบมัน ซึ่งเป็น timer (เนื่องจาก start ถูกเรียกเป็น timer.start()) ดังนั้น this.seconds จึง refer ไปยัง timer.seconds ได้อย่างถูกต้องในทุกครั้ง"
            },
            {
              "icon": "",
              "label": "Arrow function ทำงานเร็วกว่า regular function ในเวลา runtime"
            },
            {
              "icon": "",
              "label": "Arrow function จะ bind this ไปยัง global object โดยอัตโนมัติ ซึ่งบังเอิญเท่ากับ timer"
            },
            {
              "icon": "",
              "label": "Arrow function convert this เป็น global variable โดยอัตโนมัติ"
            }
          ]
        },
        {
          "question": "function multiply(a, b) { return a * b; }\nconst double = multiply.bind(null, 2);\nconsole.log(double(5));\nผลลัพธ์เป็นอะไร?",
          "options": [
            {
              "icon": "",
              "label": "มันจะ throw error เนื่องจาก bind ต้องการอาร์กิวเมนต์ทั้งสองตัวล่วงหน้า"
            },
            {
              "icon": "",
              "label": "10 — bind return ฟังก์ชันใหม่โดยมี this และ leading argument set ล่วงหน้า; เรียก 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();\nconsole.log ทั้งสองครั้งพิมพ์อะไร?",
          "options": [
            {
              "icon": "",
              "label": "20 then 20"
            },
            {
              "icon": "",
              "label": "20 then 10 — let x ของ inner แช่ (shadow) let x ของ outer เฉพาะภายในขอบเขต (scope) ของ inner; let x ของ outer ไม่ถูกจัดการเลย"
            },
            {
              "icon": "",
              "label": "10 then 10"
            },
            {
              "icon": "",
              "label": "มันจะ throw ReferenceError เนื่องจาก x ประกาศสองครั้ง"
            }
          ]
        },
        {
          "question": "const a = [1, 2, 3];\nconst b = a;\nb.push(4);\nconsole.log(a.length);\nผลลัพธ์เป็นอะไร?",
          "options": [
            {
              "icon": "",
              "label": "3 — b เป็น copy ของ a แยกต่างหาก"
            },
            {
              "icon": "",
              "label": "undefined"
            },
            {
              "icon": "",
              "label": "มันจะ throw TypeError เนื่องจาก a เป็น const"
            },
            {
              "icon": "",
              "label": "4 — array คือ object ดังนั้น const b = a copy reference ไม่ใช่ array; mutate b ผ่าน push ก็จะ mutate array เดียวกันที่ 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 deep-clone ทุก object และ array ที่ nested"
            },
            {
              "icon": "",
              "label": "'Ana' 3 — spread เป็น shallow copy เท่านั้น: property primitif เช่น name ถูก copy ด้วย value ดังนั้นการ reassign copy.name ไม่ส่งผลกับ original แต่ nested array เช่น tags ถูก copy ด้วย reference ดังนั้นการ push เข้า copy.tags จึง mutate array เดียวกันที่ original.tags ชี้ไปด้วย"
            },
            {
              "icon": "",
              "label": "'Bea' 3 — spread copy ทุกอย่างด้วย reference ดังนั้นการเปลี่ยน top-level และ nested ทั้งคู่จึง carry กลับไปยัง 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 ถูก rename เป็น x และมี value ดังนั้นมันเก็บค่า 5; b ถูก rename เป็น y และไม่มี value ดังนั้นมันกลับไปใช้ default 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 รวบรวม element ทั้งหมดที่เหลือหลัง 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 จัดเรียงตัวเลขเป็นลำดับตัวเลขแบบ ascending ตามค่าเริ่มต้นเสมอ"
            },
            {
              "icon": "",
              "label": "[10, 1, 2] — sort ไม่ mutate array ต้นฉบับ"
            },
            {
              "icon": "",
              "label": "[1, 10, 2] — default comparator ของ Array.prototype.sort convert element เป็น string และเปรียบเทียบแบบ lexicographically ดังนั้น \"10\" จึง sort มาก่อน \"2\"; sort ยังจะ mutate array in place แทนที่จะ return array ใหม่"
            },
            {
              "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' then 'unknown' — ?? ถือว่า 0 เป็นค่าที่ขาดหายไป เช่นเดียวกับ || ที่ทำ"
            },
            {
              "icon": "",
              "label": "0 then 'unknown' — ?? จะใช้ค่าทดแทนเฉพาะเมื่อเป็น null หรือ undefined เท่านั้น ไม่ใช่ค่าเท็จอื่นๆ เช่น 0 ดังนั้น age (0) จึงเก็บไว้ตามเดิม user.address เป็น undefined ดังนั้น ?. ตัดวงจรห่วงโซ่ทั้งหมดเป็น undefined และ ?? จึงให้ 'unknown'"
            },
            {
              "icon": "",
              "label": "0 then undefined"
            },
            {
              "icon": "",
              "label": "'unknown' then 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 ไม่สามารถ serialize เป็น 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 จึงพิมพ์ทันที) จากนั้น microtask queue (Promise callbacks) จะหมดสิ้นโดยสิ้นเชิงก่อนที่ event loop จะไปยัง macrotask ถัดไป ดังนั้น 3 จึงพิมพ์ก่อน setTimeout callback ของ 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 แรกของมัน ดังนั้น 'a' จึงบันทึกทันทีเมื่อ foo() ถูกเรียก จากนั้น await จะหยุดชั่วคราว foo และส่งกลับการควบคุมให้ผู้เรียก ดังนั้น 'end' จึงพิมพ์ก่อน 'b' ที่ถูกเรียกใหม่ ซึ่งทำงานในภายหลังเป็น microtask"
            },
            {
              "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 handler จึงทำงานด้วยเหตุผลของการปฏิเสธนั้น"
            },
            {
              "icon": "",
              "label": "'ok' [1, 3]"
            },
            {
              "icon": "",
              "label": "'ok' [1, undefined, 3]"
            },
            {
              "icon": "",
              "label": "ส่งผ่านแบบซิงโครนัส และ handler ไม่มีการทำงานเลย"
            }
          ]
        },
        {
          "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' then true"
            },
            {
              "icon": "",
              "label": "undefined then false"
            },
            {
              "icon": "",
              "label": "ส่งผ่าน TypeError เพราะ dog ไม่มีวิธี speak ของตัวเองเลย"
            },
            {
              "icon": "",
              "label": "'Rex makes a sound' then false — speak ถูกกำหนดบน Animal.prototype ไม่ใช่บนอินสแตนซ์ dog ของตัวเอง dog จะค้นหามันผ่านโซ่ prototype เมื่อมันถูกเรียก แต่ 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 then undefined — #count เป็นฟิลด์คลาสส่วนตัว อ่านและเขียนได้เฉพาะจากภายในเมธอดของคลาสเท่านั้น c.count (โดยไม่มี #) เป็นคุณสมบัติธรรมดาที่ต่างออกไปโดยสิ้นเชิง ซึ่งไม่เคยตั้งไว้ ดังนั้นจึงเป็น undefined"
            },
            {
              "icon": "",
              "label": "1 then 0"
            },
            {
              "icon": "",
              "label": "1 then 1"
            },
            {
              "icon": "",
              "label": "ส่งผ่าน SyntaxError เมื่อเข้าถึง c.count"
            }
          ]
        },
        {
          "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' — template literals ประเมินนิพจน์ใดๆ ที่อยู่ใน $(...) รวมถึง ternary เต็ม และแทรกผลลัพธ์ของมันโดยตรงเข้าไปในสตริง"
            },
            {
              "icon": "",
              "label": "ส่งผ่าน SyntaxError เพราะไม่อนุญาต ternaries ภายใน template literals"
            }
          ]
        },
        {
          "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"
    }
  }
}
