Scope & Closure in JavaScript

จากเมื่อวานนี้ที่เขียน Blog เรื่องเกี่ยวกับ Event Loop เลยอยากกลับมา review ความรู้ตัวเองด้าน Frontend สักนิด ไหนๆก็เป็น Frontend Engineer ละ ถ้าไม่มี blog เกี่ยวกับ Frontend เลยสงสัยจะโดนนายไล่ออก

จริงๆแล้วเกิดจากตอนที่ไปสอนเช่นกัน ตอนที่ถามน้องๆว่า เข้าใจ Scope ของ JavaScript กันดีแล้วใช่ไหม เพราะมันไม่เหมือน C++ หรือ Java ที่น้องๆเรียนกันนะ หลายคนทำตาใสแจ๋วบ้องแบ๊วบอกว่า เข้าใจครับพี่ แต่ผมยังห่วงอยู่ลึกๆว่าน่าจะยังไม่เก็ทกัน เลยขอแอบมาสรุปให้อีกที

Global Scopes & Function Scope

ในเรื่อง Scope นี้เราจะยังไม่รวมเอา Block Scope ใน ES6 มารวมนะครับ โดย Global Scope หรือ Window Scope นั้นคือ Scope ที่อยู่ในระดับสูงสุดของ Browser ซึ่งสามารถ access ได้จาก code ที่ส่วนไหนก็ได้นั่นเอง ซึ่งคงเทียบกันได้กับ Super Global ในภาษาโปรแกรมมิ่งต่างๆนั่นเอง ส่วน Function Scope หรือบางทีเราก็เรียกกันติดปากว่า Local Scope นั้นก็คือ Scope ที่สามารถ Access ได้ภายใน function นั้นๆ เท่านั้นนั่นเอง

// Global Scope
var bear = "Brother Bear";

function greetTheBear() {
  return alert(bear);
}

// Function Scope (Local Scope)
function greetTheBigBear() {
  var greet = "BearHunter";
  return alert(greet);
}
alert(greet); //Error here !!!!

// Function Scope (Nested)
function hailTheHusky(huskyName) {
  function caplitalize() {
    return huskyName.toUpperCase();
  }
  var capitalized = caplitalize();
  return capitalized;
}
alert(hailTheHusky("HuskyBear"));

JavaScript Scopes : Global & Function Scope
ซึ่งเมื่อเราวาดภาพออกมาเราจะเห็น Scope ที่ว่าหน้าตาประมาณแบบนี้

1_dX5fnosmiuZsd0qCU-uH6w

Source: https://www.codementor.io/javascript/tutorial/es6-template-strings-destructuring-function-arguments
แต่ว่าถ้าเราสังเกตุดูดีๆในส่วนของ Function Scope อันที่ 2 (nested) เราจะเห็นว่าใน Function Scope นั้นเราสามารถ access “ของ” ที่อยู่ภายใน function ได้ประหนึ่ง global ซึ่งนั่นเป็นที่มาของอีก Scope นึง ซึ่งมัน “เสมือนว่ามีอยู่” ใน JavaScript นั่นคือ Closure

function adder(x) {
  return function (y) {
    x = x+y;
    return x;
  }
}

var add = adder(10);
add(1); // 11
add(1); // 12
add(3); // 15

เราจะสังเกตุว่า closure นั้นถูกสร้างขึ้นหลังจากที่ javascript runtime ทำการ compile js แล้ว เมื่อมีการสร้างตัวแปร add จาก adder function จะทำให้เกิด closure จาก inner function scope ของ adder ทำให้ ตัวแปร x ที่ถูกกำหนดค่า และ function ที่ส่งกลับไปอยู่ใน function scope เดียวกัน ทำให้สามารถ access ถึงกันและกันได้ โดย x ที่เก็บเอาไว้นั้นจะเป็น reference ไปถึงตัวแปรเดิมนั่นเอง

Function & Closure Scope with Variable Reference

เนื่องจากในการ reference ค่าใน scope นั้น มีความสับสนในการใช้งานตัวแปร เช่นจากตัวอย่างนี้เราต้องการ log ค่าของ i ลงไปใน console ซึ่งเรา expect ค่า “0,1,2,3,4” แต่ปรากฎว่าในการทำงานจริงกลับได้ “5,5,5,5,5” เพราะว่าในการ reference นั้นเราจะได้ค่า i เดียวกันในทุกๆ callback เพราะมันอยู่ใน scope เดียวกันนั่นเอง !!!!

for(var i = 0; i < 5; i++) {
  setTimeout(
    function(num) { console.log(num) } (i),
    1000
  )
}

ซึ่งกรณีเช่นนี้ เราสามารถใช้ closure ในการแก้ปัญหานี้ได้ตามโค้ดด้านล่าง ซึ่งใช้ closure มาช่วยในการ reference ค่าที่ถูกต้องได้ในแต่ละ scope ของ function ที่เราเรียกใช้งานนั่นเอง

for(var i = 0; i < 5; i++) {
  setTimeout(
    function(num) { console.log(num) } (i),
    1000
  )
}

เราจะเห็นได้ว่าถ้าเราไม่สามารถทำความเข้าใจกับ Scope และ reference ใน JavaScript ได้แล้ว จะทำให้เรามีโอกาสสร้างความผิดพลาดในการพัฒนาในฝั่งของ JavaScript ได้มากทีเดียว และที่สำคัญ Scope และ Closure ยังเป็น key สำคัญที่จะทำให้เราเข้าใจ JavaScript Framework/Library ต่างๆมากขึ้นอีกด้วย

References: