Babel Coder

7 เรื่องพื้นฐานชวนสับสนใน JavaScript สำหรับผู้เริ่มต้น

beginner

JavaScript ช่างเป็นภาษาที่น่าปวดหัวสำหรับผู้เริ่มต้นยิ่งนักจนมีคนเขียนบทความ The World’s Most Misunderstood Programming Language แต่นั่นละฮะท่านผู้ชม เมื่อเราต้องการใช้ภาษานี้เราก็ต้องเรียนรู้สิ่งที่ภาษานี้เป็น และนี่คือ7สิ่งมหัศจรรย์ของโลก JavaScript ที่อาจทำให้ผู้เริ่มต้นเอ๋อไปตามๆกัน

สารบัญ

1. Closure

ก่อนที่ผมจะอธิบายว่าสิ่งนี้คืออะไร อยากให้เพื่อนๆลองพิจารณาตัวอย่างนี้กันก่อนครับ

function print() {
  const name = 'Nuttavut Thongjor'
  
  // return function ออกไปเมื่อเรียกฟังก์ชัน print
  return function() {
    console.log(name)
  }
}

const printName = print()
printName() // Nuttavut Thongjor

เพื่อนๆสังเกตุเห็นอะไรจากตัวอย่างนี้บ้างครับ? ในบรรทัดที่10เราเรียกฟังก์ชัน print ที่คืนค่ากลับมาเป็นฟังก์ชันอีกตัวหนึ่ง ภายหลังจากเรียกฟังก์ชันแล้วตัวแปร name ที่อยู่ข้างในควรถูกทำลายใช่ไหมครับ เพราะเป็น local variable เมื่อจบการเรียก print() ควรถูกทำลาย แต่น่าแปลกทำไมเมื่อเราเรียก printNameในบรรทัดที่11กลับพบว่าฟังก์ชันนี้ยังเข้าถึงตัวแปร name ได้อยู่? (ดูบรรทัดที่6ประกอบ)

closure เป็นอ็อบเจ็กต์สุดพิเศษที่รวมฟังก์ชันและสภาพแวดล้อมที่ห่อหุ้มมันอยู่ในตอนที่ closure เองโดนสร้างขึ้นมา ดังนั้นเจ้า printName ของเราจึงเป็น closure ครับ เพราะมันเป็นฟังก์ชันและโดนโอบล้อมด้วยฟังก์ชันอีกตัว เช่นนี้แล้วมันจึงเข้าถึงสภาพแวดล้อมที่ห่อหุ้มมันอยู่ได้ หนึ่งในนั้นคือตัวแปร name นั่นเอง

แม้ฟังก์ชัน print จะสิ้นชีพไปแล้วหลังการเรียกใช้ แต่ตัวแปร name นั้นยังคงอยู่เพราะมีฟังก์ชันภายในที่อ้างไปถึงมัน สิ่งที่ต้องเข้าใจเป็นพิเศษคือ closure ไม่ใช่การก็อบปี้ตัวแปรนะครับ ตัวแปร name ไม่ได้โดนก็อบปี้เข้ามาเก็บไว้ในฟังก์ชันภายในนะ เพราะถ้าเป็นเช่นนั้นจริง เรามีฟังก์ชันซ้อนกันอยู่ซักร้อยตัว อ้างถึง name ทุกฟังก์ชัน การก็อบปี้ name มาใส่แต่ละฟังก์ชันจะเปลืองหน่วยความจำซึ่งไม่สมเหตุสมผล

เราได้ประโยชน์อะไรจาก closure บ้าง? อย่างแรกเลยคือ เราสามารถสร้างฟังก์ชันที่อาศัยตัวแปรจากฟังก์ชันที่ห่อหุ้มมันอยู่ได้ เช่น

function print(lastName) {
  return function(firstName) {
    // ภายใต้ฟังก์ชันนี้ตัวแปร lastName ไม่หายไปไหน
    console.log(`${firstName} ${lastName}`)
  }
}

// ภายหลังเรียก print ตัวแปร lastName ควรถูกทำลาย
// แต่ด้วยความสามารถของ closure ทำให้ตัวแปรนี้ยังคงอยู่
// และเข้าถึงได้จากฟังก์ชันภายใน
const printSmithFamily = print('Smith')

printSmithFamily('John') // John Smith
printSmithFamily('Adam') // Adam Smith

นอกจากนี้ closure ยังนำไปใช้กับ module pattern ได้อีกด้วย

let sum = 0

function add(number) {
  sum += number
}

add(10)
console.log(sum)
sum = 0
add(10)
console.log(sum)

จากตัวอย่างข้างบน เราประกาศตัวแปรชื่อ sum ไว้นอกสุดแบบนี้ทำให้โค๊ดส่วนอื่นของเราอาจไปเปลี่ยนแปลงแก้ไขมันได้ เช่นในบรรทัดที่9 ทั้งๆที่เราเรียก add(10) สองครั้งควรได้ผลลัพธ์เป็น 20 แต่เราดันเผลอไปแก้ไข sum ซะก่อนโดยไม่ได้ตั้งใจ เพื่อเป็นการป้องกันเหตุบังเอิญเช่นนี้ เราจึงควรให้ sum มีผลเฉพาะที่ด้วยการทำ Immediately-Invoked Function Expression (IIFE) ดังนี้

let sum = 0

const utils = (
  function() {
    // sum ตัวนี้ใช้ใน scope นี้ไม่เกี่ยวกับ sum ตัวนอก
    // การแก้ไข sum ตัวนอกจะไม่กระทบกับ sum ตัวนี้
    let sum = 0
    
    return {
      add(number) {
        // ด้วยผลของ closure 
        // sum ตัวนี้จึงหมายถึง sum ในบรรทัดที่7
        sum += number
      },
      getSum() {
        return sum
      }
    }
  }
)() 
// เนื่องจากของข้างในเป็นฟังก์ชัน เราต้องการเรียกใช้ฟังก์ชันทันทีจึงใส่ () เข้าไป
// ผลลัพธ์ที่ได้จากการเรียกฟังก์ชันนี้จะคืนค่าเป็นอ็อบเจ็กต์ที่ประกอบด้วย add และ getSum
// เรียกเทคนิคนี้ว่า IIFE

utils.add(10)
console.log(sum) // 0
sum = 0
utils.add(10)
console.log(sum) // 0
console.log(utils.getSum()) // 20

สำหรับเพื่อนๆที่ใช้งาน ES2015 ผมแนะนำให้แยกโมดูลออกเป็นไฟล์แล้วใช้คำสั่ง import/export ในการจัดการโมดูลแทน อ่านเพิ่มเติมที่พื้นฐาน ES2015 สำหรับการเขียน JavaScript สมัยใหม่

เอาหละสิ่งสุดท้ายสำหรับ closure ที่อยากจะพูดถึงครับนั่นคือความเข้าใจผิดในการใช้งาน closure กับ loop

for(var i = 0; i < 3; i++) {
  setTimeout(
    // callback function ที่จะเรียกทำงานหลังผ่านไป 1 วินาที
    function() { console.log(i) },
    1000
  )
}

// 3
// 3
// 3

เราวนลูปค่าตั้งแต่ 0 ถึง 2 เพื่อให้พิมพ์ค่า i ออกหน้าจอทุก 1 วินาที แต่ทำไมคำตอบที่ได้กลับเป็น3ล้วน?

การทำงานของ setTimeout เป็นแบบ asynchronous คือจะทำงานเมื่อเวลาผ่านไป 1 วินาที JavaScript จะอ่านโค๊ดจากบนลงล่าง พร้อมทั้งทำงานโค๊ดที่เป็น synchronous เช่นการวนลูปทั้งหมด แต่ถ้ามันเจอการทำงานแบบ asynchronous มันจะยังไม่ทำงานเพียงแต่เก็บ callback function ไว้เพื่อปลุกมาทำงานภายหลังอีกที เนื่องจาก callback เป็นฟังก์ชันที่โดนห่อหุ้มภายใต้ตัวแปร i มันจึงเป็น closure ด้วย ถึงตอนนี้ JavaScript จะวนลูปจนเสร็จก่อนเพราะเป็นโค๊ดแบบ synchronous ทำงานทันที เมื่อวนลูปเสร็จจึงไปเรียก callback เมื่อถึงเวลา ในตอนนี้ค่าของตัวแปร i จึงเป็น 3 รายละเอียดเพิ่มเติมศึกษาได้จาก รู้ลึกการทำงานแบบ Asynchronous กับ Event Loop

เพื่อเป็นการแก้ไขปัญหานี้เราสามารถใช้ let ในการประกาศตัวแปรแทนได้ รายละเอียดเพิ่มเติมศึกษาได้จาก พื้นฐาน ES2015 สำหรับการเขียน JavaScript สมัยใหม่

for(let i = 0; i < 3; i++) {
  setTimeout(
    function() { console.log(i) },
    1000
  )
}

2. JavaScript Hoisting

ก่อนที่จะอธิบายถึงเรื่องนี้ ลองดูตัวอย่างกันก่อนดีกว่าครับ

console.log(x) // undefined
console.log(print()) // print
console.log(y) // y is not defined

var x = 3

function print() {
  console.log('print')
}

แปลกใจไหมครับ เราประกาศตัวแปร x ไว้ที่บรรทัด5 นั่นคือประกาศตัวแปรหลังการเรียกใช้ แต่ทำไมบรรทัดแรกสุดถึงไม่บ่น error ว่าไม่ได้ประกาศตัวแปร? เหตุผลก็คือเพราะ JavaScript จะกระดึ๊บๆส่วนประกาศตัวแปรหรือฟังก์ชันไปไว้บนสุดนั่นเอง ย้ำนะครับว่าแค่ส่วนประกาศ แต่มันไม่เอาค่าตั้งต้นไปไว้บนสุดด้วย เพราะถ้าเอาค่าตั้งต้นไปไว้บนสุด เราคงเห็นแล้วว่ามันพิมพ์เลข3ออกมา เราเรียกการกระดึ๊บส่วนประกาศเช่นนี้ว่า Hoisting

คราวนี้ลองเปลี่ยนใหม่ด้วยการประกาศตัวแปรผ่าน let ดังนี้

console.log(x) // ReferenceError: x is not defined
console.log(print()) // print
console.log(y) // y is not defined

let x = 3

function print() {
  console.log('print')
}

let และ const ยังคง hoisting อยู่ครับ แต่มีสองสิ่งที่แตกต่างไปจากการประกาศตัวแปรด้วย var คือ

  • var จะ hoist ตัวแปรไปไว้บนสุดของ function scope หรือกระดึ๊บตัวแปรไปไว้ใกล้ๆจุดประกาศฟังก์ชัน แต่ let และ const นั้นต่างออกไป มันจะ hoist ตัวแปรไปไว้บนสุดของ block scope หรือง่ายๆก็คือจุดที่ปีกกา {} ครอบมันอยู่
  • ในบรรทัดแรก เราเข้าถึงตัวแปร x แต่ยังไม่เจอการประกาศตัวแปรนี้ สำหรับ let และ const จะโยน ReferenceError ออกมา ทั้งนี้เป็นเพราะ ES2015 ไม่อนุญาตให้เข้าถึงตัวแปรประเภท let และ const ก่อนถึงจุดประกาศตัวแปร เราเรียกจุดบอดก่อนถึงคำสั่งประกาศตัวแปรนี้ว่า Temporal dead zone

สรุปเรื่อง hoisting อย่างสั้นๆได้ตารางนี้ครับ

ประเภท Hoising
var function scope
let/const block scope + Temporal dead zone
class ไม่ทำ
function hoist อย่างสมบูรณ์
ประโยค import hoist อย่างสมบูรณ์

เพื่อเป็นการป้องกันผลลัพธ์ที่ไม่คาดฝัน จึงแนะนำให้ประกาศตัวแปรไว้บนสุดเลย (ขอบคุณคุณ saknarak ที่แจ้งข้อผิดพลาดบทความในหัวข้อนี้ครับ)

3. Equality Operators

น่าจะทราบกันดีครับว่า JavaScript มีเครื่องหมายตรวจสอบการเท่ากันอยู่สองแบบคือ === และ == โดยเครื่องหมาย == ใช้ตรวจสอบเพียงว่ามีค่าเท่ากันหรือไม่ แต่ === นอกจากตรวจสอบว่ามีค่าเท่ากันหรือไม่แล้วยังตรวจสอบว่าข้อมูลเป็นชนิดเดียวกันหรือไม่ด้วย

console.log(1 == '1') // true

// ข้างซ้ายเป็นตัวเลข ข้างขวาเป็น string จึงเป็นคนละชนิดข้อมูล
console.log(1 === '1') // false

แต่สำหรับการเปรียบเทียบอ็อบเจ็กต์นั้นหลายคนมักเข้าใจผิดว่าแค่ใช้ === ก็เพียงพอแล้วสำหรับการตรวจสอบค่าที่เท่ากัน ซึ่งความเป็นจริงนั้นไม่ใช่เพราะสิ่งที่ตัวแปรเก็บนั้นคือ memory address ของอ็อบเจ็กต์ ทุกครั้่งที่สร้างอ็อบเจ็กต์ใหม่แม้จะมีไส้ในเหมือนกัน แต่ memory address ที่ได้จะต่างกัน

const obj1 = { a: 1 }
const obj2 = { a: 1 }

console.log(obj1 === obj2) // false

4. null และ undefined

ใน JavaScript นั้น null และ undefined ไม่ใช่ค่าเดียวกัน การที่เราประกาศตัวแปรโดยไม่กำหนดค่าให้จะได้ว่าตัวแปรนั้นเป็น undefined กรณีของ null จะเกิดขึ้นเมื่อเราตั้งค่าให้ตัวแปรนั้่นเป็น null เพื่อเป็นการบอกว่าตัวแปรนี้ไม่ได้ชี้ไปที่อ็อบเจ็กต์ไหนเลย

let a
let a
console.log(typeof a) // undefined
console.log(typeof undefined) // undefined
console.log(typeof null) // object

จากตัวอย่างข้างบนพบว่าตัวของ null เองนั้นเป็นอ็อบเจ็กต์ ฉะนั้นแล้วเราจึงกำหนดค่าให้ตัวแปรใดๆเป็น null ในกรณีที่เราต้องการบอกว่าตัวแปรนั้นไม่ได้ชี้ไปที่อ็อบเจ็กต์ใดเลยในขณะนั้น

let obj = {
  text: 'object'
}

// obj ไม่ได้ชี้ไปที่อ็อบเจ็กต์ไหนแล้ว
obj = null

เมื่อใช้เครื่องหมาย == และ === เพื่อเปรียบเทียบค่าระหว่าง null และ undefined จะพบผลลัพธ์ดังนี้

// null และ undefined ต่างใช้สื่อความหมายถึงการไม่มีค่าทั้งคู่ จึงมีค่าเท่ากัน
console.log(null == undefined) // true

// แต่ null มีชนิดข้อมูลเป็นอ็อบเจ็กต์จึงต่างจาก undefined
console.log(null === undefined) // false

ด้วยเหตุผลทั้งปวงตามที่กล่าวมาแล้วจึงควรระวังเมื่อใช้ null หรือ undefined ในประโยคเงื่อนไขต่างๆ

let a

if(a) console.log('exists')
else console.log('not exists')

if(a === undefined) console.log('undefined')
else console.log('defined')

5. คีย์เวิร์ด this

เรื่องนี้เป็นเรื่องชวนปวดหัวมากเมื่อเราอ้างถึง this ใน JavaScript แต่ละครั้งอาจได้ผลลัพธ์ไม่เหมือนกัน ดังนี้

const obj = {
  text: 'object',
  print() {
    console.log(this.text)
  },
  waitOneSecBeforePrinting() {
    setTimeout(
      function() { console.log(this.text) },
      1000
    )
  }
}

obj.print() // object
obj.waitOneSecBeforePrinting() // ไม่มีอะไรพิมพ์ออกมา

this นั้นขึ้นอยู่กับการเรียก ในบรรทัดที่14เราเรียก print ผ่าน obj ทำให้ this หมายถึงตัว obj เอง บรรทัดที่4จึงสามารถเรียก text ซึ่งเป็น property ของ obj ผ่าน this ได้

ในกรณีของ waitOneSecBeforePrinting คนที่เรียกฟังก์ชันในบรรทัดที่8คือตัว setTimeout ที่จะเรียกเมื่อเวลาผ่านไปหนึ่งวินาที นั่นละครับตัวsetTimeoutเองไม่มี text เป็นของตัวเอง เราจึงไม่สามารถเรียก this.text แล้วได้ผลลัพธ์ออกมาได้

เพื่อเป็นการป้องกันความสับสนในการใช้ this เราสามารถใช้ arrow function ใน ES2015 เพื่อกำหนด lexical scope ได้ดังนี้

const obj = {
  text: 'object',
  print() {
    console.log(this.text)
  },
  waitOneSecBeforePrinting() {
    setTimeout(
       // เปลี่ยนเป้น arrow function ทำให้ this ชี้ไปที่ obj หรือสโคปที่ครอบมันอยู่
      () => { console.log(this.text) },
      1000
    )
  }
}

obj.print() // object
obj.waitOneSecBeforePrinting() // object

นอกจากนี้เรายังใช้ bind เพื่อเปลี่ยนแปลงค่า this ได้ รายละเอียดสำหรับการใช้ bind และ arrow function สามารถอ่านเพิ่มเติมได้จาก ข้อแตกต่างของ bind, apply และ call ใน JavaScript กับการใช้งาน และ พื้นฐาน ES2015 สำหรับการเขียน JavaScript สมัยใหม่

6. Asynchronous Programming

JavaScript นั้นอุดมไปด้วยโค๊ดแบบ Asynchronous หรือการทำงานแบบไม่ได้เรียงลำดับ เช่น

const result = $.getJSON('http://api.example.com/v1/books/1')
console.log(result.title)

โค๊ดข้างบนนี้ใช้ฟังก์ชัน getJSON ของ jQuery เพื่อดึงข้อมูลหนังสือที่มี ID เป็น 1 จาก URL ที่กำหนด เมื่อโค๊ดนี้ทำงาน JavaScript จะทำงานตามลำดับจากบนลงล่าง เริ่มจากเจอการร้องขอข้อมูล JSON ในบรรทัดแรกสุด เนื่องจากการขอข้อมูลไปที่เซิร์ฟเวอร์นั้นเป็น asynchronous คือ JavaScript จะไม่หยุดการทำงานเพื่อรอผลลัพธ์จากเซิร์ฟเวอร์ เพราะมันเสียเวลา เราไม่มีทางทราบว่าเมื่อไหร่เซิร์ฟเวอร์จะตอบกลับมา เมื่อ JavaScript ไม่อาจรอคอยได้ จึงทำโค๊ดบรรทัดถัดไปเลยคือพิมพ์ result.title ออกมา นั่นละฮะเราไม่ได้ผลลัพธ์อะไรเลยเพราะในเวลาที่โค๊ดนี้ทำงานยังไม่ได้คำตอบจากเซิร์ฟเวอร์กลับมานั่นเอง

เพื่อให้การทำงานแบบ asynchronous สมบูรณ์ เราต้องมี callback function ส่งเข้าไปเพื่อบอก JavaScript ให้เรียกฟังก์ชันนี้เมื่อผลลัพธ์จากเซิร์ฟเวอร์ส่งกลับมา

$.getJSON(
  'http://api.example.com/v1/books/1', 
  (data) => { console.log(data.title) }
)

รายละเอียดเชิงลึกของการเขียนโปรแกรมแบบ asynchronous ใน JavaScript ศึกษาเพิ่มเติมที่ รู้ลึกการทำงานแบบ Asynchronous กับ Event Loop ครับ

7. Prototype-based programming

และแล้วเราก็มาถึงเรื่องน่าปวดหัวลำดับเจ็ด นั่นคือเรื่องของ prototype ภาษา JavaScript นั้นไม่ได้เป็น OOP แบบที่เราใช้งานกันอยู่ ทุกส่วนของสิ่งที่เราพยายามจะจินตนาการให้เป็น OOP นั้นทำผ่านสิ่งที่เรียกว่า prototype

ในการนิยาม class ของ JavaScript นั้นเราอาศัยการสร้างผ่านฟังก์ชันที่เรียกว่า constructor function ดังนี้

const Person = function(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

const person1 = new Person('Nuttavut', 'Thongjor')

เราสามารถสร้างเมธอดผ่าน constructor ได้เช่นกันดังนี้

const Person = function(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  this.getFullName = function() {
    return `${this.firstName} ${this.lastName}`
  }
}

const person1 = new Person('Nuttavut', 'Thongjor')
console.log(person1.getFullName()) // Nuttavut Thongjor

const person2 = new Person('John', 'Smith')
console.log(person2.getFullName()) // John Smith

เนื่องจากวิธีนี้ทุกครั้งที่สร้างอ็อบเจ็กต์ของ Person ผ่าน constructor มันก็จะสร้างเมธอด getFullName ขึ้นมาใหม่ทุกครั้งเพราะเราดันนิยามเมธอดไว้ภายใต้ constructor ที่จะโดนเรียกทุกครั้งเมื่อเราออกคำสั่ง new นี่ไม่ใช่วิธีที่ดีแน่ เพราะถ้าเรา new อ็อบเจ็กต์ซักร้อยครั้งก็จะมี getFullName เกิดขึ้นร้อยครั้งเช่นกัน เพื่อเป็นการป้องกันเราจึงนิยามเมธอดผ่าน prototype ดังนี้

const Person = function(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

Person.prototype.getFullName = function() {
  return `${this.firstName} ${this.lastName}`
}

const person1 = new Person('Nuttavut', 'Thongjor')
console.log(person1.getFullName()) // Nuttavut Thongjor

const person2 = new Person('John', 'Smith')
console.log(person2.getFullName()) // John Smith

ในภาษา JavaScript นั้นอ็อบเจ็กต์จะมีการเชื่อมโยงไปหา prototype โดยถ้าเราเรียกเมธอดหรืออื่นใดผ่านอ็อบเจ็กต์ ถ้ามันหาสิ่งนั้นในอ็อบเจ็กต์ไม่เจอ มันจะไปขุดคุ้ยจาก prototype อีกที กรณีข้างต้นเราไม่ได้นิยาม this.getFullName ไว้นั่นหมายความว่าอ็อบเจ็กต์ของเราจะไม่มี getFullName เมื่อเราเรียก getFullName มันจึงต้องวิ่งขึ้นไปหา getFullName จาก prototype อีกที สุดท้ายก็เจอ!

prototype ใน JavaScript นั้นมีความสามารถสูงมาก เราสามารถใช้ prototype เพื่อสร้าง subclass หรือทำ inheritance หรือทำ encapsulation เหมือนการมีคีย์เวิร์ด private และ protected ในภาษาอื่นๆ เป็นต้น

แต่ด้วยความยุ่งยากที่ไม่คุ้นเคยของผู้คนที่มาจาก OOP ภาษาอื่นๆ ใน ES2015 เรามีคีย์เวิร์ด class เพื่อใช้สร้างคลาส และคีย์เวิร์ด extends เพื่อทำการสืบทอดคลาสแล้ว รายละเอียดสามารถอ่านเพิ่มเติมได้ที่พื้นฐาน ES2015 สำหรับการเขียน JavaScript สมัยใหม่ครับ

ตรงนี้ขอทำความเข้าใจก่อนว่าถึงแม้ ES2015 จะมีคีย์เวิร์ดคลาสให้เรียกใช้แล้ว แต่ความสมบูรณ์นั้นเทียบชั้นไม่ติดกับการใช้ prototype เลย คลาสใน ES2015 นั้นไม่สามารถห่อหุ้มข้อมูลด้วยคีย์เวิร์ด private/protected ได้ และอื่นๆ ด้วยเหตุนี้เราจึงอาจต้องพิจารณาใช้ prototype ในกรณีที่ต้องการความยืนหยุ่นในการใช้งานด้าน OOP หรือไม่เช่นนั้นก็โยกย้ายส่ายสะโพกไปใช้ Typescript ครับ

JavaScript นั้นถือว่าเป็นภาษาปราบเซียนภาษานึงเมื่อต้องลงลึกในการใช้งาน ถ้าเราจับต้องแบบผิวเผินมันก็ไม่ได้ต่างอะไรจากภาษาทั่วไปนัก แต่ก็นั่นละฮะเมื่อเราต้องการใช้สิ่งใด เราก็ต้องศึกษาในความเป็นไปของสิ่งนั้นอย่างถ่องแท้ บทความนี้จึงตั้งความหวังไว้ว่าจะช่วยเบิกทางในส่วนที่เข้าใจยากของ JavaScript ให้เพื่อนๆได้เข้าใจมากขึ้นเพื่อเปิดทางในการศึกษา JavaScript ที่สูงขึ้นในลำดับถัดไป

เอกสารอ้างอิง

MDN. let. Retrieved June, 7, 2016, from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

Fabrício S. Matté (2015). TEMPORAL DEAD ZONE (TDZ) DEMYSTIFIED. Retrieved June, 7, 2016, from http://jsrocks.org/2015/01/temporal-dead-zone-tdz-demystified/

Dr. Axel Rauschmayer. Variables and scoping. Retrieved June, 7, 2016, from http://exploringjs.com/es6/ch_variables.html


แสดงความคิดเห็นของคุณ


Nuttavut Thongjor2 ปีที่แล้ว

@saknarak

ที่ let/const ยังต้อง hoist เพราะว่าถ้าไม่ทำแล้วมีโอกาสที่จะเอาผลลัพธ์จากตัวแปรนอกสโคปมาใช้ครับ

let a = 1

function test() {
  console.log(a) // ถ้า a ไม่ hoist ตรงนี้จะได้ 1 ซึ่งมันผิดจากคอนเซ็ปต์ของ let ที่ควรจะโยน error
  let a = 2
}

test()

Nuttavut Thongjor2 ปีที่แล้ว

มีปุ่มแล้วฮะ @saknarak ทดสอบด้วยคน

const message = 'hello world'
console.log(message)

saknarak2 ปีที่แล้ว

ขอบคุณครับสำหรับเอกสารอ้างอิง แต่สงสัยสเปคจะ hoist ขึ้นไปทำไม ในเมื่อก็ใช้ไม่ได้อยู่ดี กลายเป็น ReferenceError

ปล. comment ใส่โค้ดได้แล้วเหรอครับ ทดสอบหน่อย

let obj1 = {x:1,y:2,z:3,a:4,b:5,c:6};
let {x,y,z,...obj2} = obj1;
console.log(obj2);

Nuttavut Thongjor2 ปีที่แล้ว

@saknarak ขอบคุณอีกครั้งครับ

เป็นความผิดพลาดของผมเองแหละ ที่ไม่ได้ดูผลลัพธ์ให้ดีก่อน ผมทดสอบโค๊ดกับ Babel REPL เมื่อใส่ ES2015 ดังนี้

console.log(x)
console.log(print()) // print
console.log(y) // y is not defined

const x = 3

function print() {
  console.log('print')
}

มันจะแปลงเป็น ES5 คือ

'use strict';

console.log(x);
console.log(print()); // print
console.log(y); // y is not defined

var x = 3;

function print() {
  console.log('print');
}

แค่เปลี่ยนเป็น var เฉยๆ เลยไม่มี ReferenceError ไม่แน่ใจว่าเป็นข้อผิดพลาดของ Babel รึเปล่า เท่าที่ดูใน Github ยังไม่เห็นใคร report เรื่องนี้นะครับ (อาจยังหาไม่ละเอียด)

ส่วนเรื่อง hoisting ผมอ้างอิงจากเอกสารของ MDN ซึ่งเขียนไว้ว่า

In ECMAScript 2015, let will hoist the variable to the top of the block. However, referencing the variable in the block before the variable declaration results in a ReferenceError. The variable is in a “temporal dead zone” from the start of the block until the declaration is processed.

และ Exploring ES6: Upgrade to the next version of JavaScript ที่แสดงไว้ว่า let/const ทำ hoisting แต่ติด temporal dead zone

ใน test262 ของ TC39 ผู้ดูแลมาตรฐาน ECMAScript ก็ได้พูดถึง hoisting ของ let เหมือนกันครับใน var-env-lower-lex-non-strict.js รวมถึงเอกสาร ecma262 ก็เช่นกัน

ทั้งหมดนี้ผมจึงอนุมานด้วยตนเองว่า let และ const ทำ hoisting + temporal dead zone

ขอบคุณสำหรับ ReferenceError นะครับ ผมอัพเดทบทความเรียบร้อยละฮะ 😃


saknarak2 ปีที่แล้ว

ในหัวข้อ 2. JavaScript Hoisting โค้ดชุดแรกเลย เอาไป run แล้ว error เพราะความเป็นจริง ทั้ง let/const ไม่ hoist ครับ มีเฉพาะ var กับ function เท่านั้นที่ hoist ไปบนสุด สามารถทดสอบเองได้เลยนะครับ


Nuttavut Thongjor2 ปีที่แล้ว

@saknarak ขอบคุณสำหรับการแจ้งข้อผิดพลาดครับ ช่วยได้เยอะเลย ^^ แก้ไข getFullName แล้วนะครับ ส่วน const นั้นในความเข้าใจของผมคือทั้ง let/const ต่าง hoist ตัวแปรไปไว้บนสุดของ block scope ผิดกับ var ที่จะ hoist ตัวแปรไปไว้บนสุดของ function scope // ต้องไปทำให้ช่องสนทนาดีขึ้นกว่านี้ซะละ พิมพ์ยากจังครับ T__T


saknarak2 ปีที่แล้ว

ทุกครั้งที่สร้างอ็อบเจ็กต์ของ Person ผ่าน constructor มันก็จะสร้างเมธอด fullName แก้เป็น getFullName


saknarak2 ปีที่แล้ว
  1. JavaScript Hoisting บรรทัดที่ 5 ต้องประกาศ var หรือเปล่าครับ เพราะ const มันไม่ hoisting นะครับ

Nuttavut Thongjor2 ปีที่แล้ว

ขอบคุณฮะ


Tan Tanangular2 ปีที่แล้ว

นั่นละฮ่ะ รวบรวมออกหนังสือเถิดครับ ท่านผู้ชม


Tan Tanangular2 ปีที่แล้ว

ถือเป็นบทความไขความลับจักรวาลของ javascript version ภาษาไทยเลยครับ ไม่เคยมีใครอธิบายเรื่องนี้ได้ดีเข้าใจเป็นภาษาไทยได้ดีเยี่ยมเลยครับ สุดยอดมากๆ


ไม่ระบุตัวตน2 ปีที่แล้ว

ดีงามพระรามแปด ^^