JS Basic: EP2. Scope

ต่อจากบล็อกที่แล้ว JS Basic: EP1. พื้นฐานแบบเน้นๆ เราจะมาต่อกันที่เรื่องของ Scope นะครับ ซึ่งเป็นเรื่องพื้นฐานบน Javascripts มากๆเพราะเป็นเรื่องของการทำงานของโปรแกรม แต่ก่อนหน้านั้น เราต้องมาทำความเข้าใจเรื่องการวิธีการทำงานของตัว Javascript Engine กันก่อนนะครับ ว่าตอนที่เรารันโปรแกรมนั้น มันทำอะไรบ้าง

Javascripts Engine & Friends

เมื่อเราทำการรันตัวไฟล์ Javascripts บน Runtime จะมี 3 ส่วนหลักๆที่ทำหน้าที่รันโค้ดด้วยกัน คือ

LHS & RHS

Left Hand Side คือค่าที่อยู่ฝั่งซ้ายมือของ = เป็นแค่การหา References ของตัวแปรนั้นๆ
Right Hand Side คือค่าที่อยู่ฝั่งขวามือของ = ทำการหา Value และรัน Operator ต่างๆ

ยกตัวอย่างเช่น

var a = 10;
var b = a + 2;

LHS คือ a และ b คือการหาตัวแปรที่เราจะเข้าไปเปลี่ยนแปลงค่า
RHS คือ 10 และ a + 2 จะไปทำการดึงค่าของตัวแปรและรันคำสั่ง เพื่อทำการจัดเก็บไปที่ LHS

ลองมาดูกันว่า ในการรันโปรแกรมครั้งนึงนั้น แต่ละส่วนจะคุยกันอย่างไรบ้างเพื่อให้โปรแกรมทำงานได้

var a = 10;
var b = a + 2;

Compiler: 	มีหน้าที่คอมไพล์อย่างเดียว คอมไพล์ไป
Engine: 	ประกาศตัวแปร a
Engine:		assign ค่าให้ a  // a = 10
Engine: 	ประกาศตัวแปร b
Engine:		มีการเรียกใช้งานตัวแปร a ต้องไปถาม scope "เฮ้ scope ขอที่อยู่ตัวแปร a หน่อย"
Scope: 		หาตำแหน่งของ a และส่งกลับไปให้ engine
Engine: 	ได้ตำแหน่งมาแล้วก็ไปดึงค่า a แล้วทำงานต่อ b = 10 + 2;

หลักการคร่าวๆก็จะประมานนี้เพื่อให้ได้เห็นภาพกันนะครับ คงพอจะได้เห็นได้ว่า Compiler และ Engine นั้นมีหน้าที่คอมไพล์และรันโปรแกรม แต่ Scope นั้นทำหน้าที่หาตำแหน่งที่อยู่ของตัวแปรในโปรแกรมให้กับ Engine มาดูกันว่า Scope บน Javascript นั้นจะเกิดขึ้นเมื่อไหร่ และมีการทำงานยังไง

Lexical Scope

Scope เป็นพื้นที่ในการรันคำสั่งและเรียกใช้ตัวแปร ซึ่งใน Javascripts เราจะแบ่ง scope ด้วยเครื่องหมายปีกกา {} โดยพื้นที่นอกสุด เป็น Global Scope

var a = 10;		 // Global Scope

if(a == 10) {		 // Block Scope
	var c = 20;
}

function foo() {	 // Function Scope
	var b = 10;
}

Block Scope

สามารถพบเห็นได้ทั่วไปทั้งการใช้คำสั่ง if..else for while และอื่นๆ โดย Block Scope เป็นพื้นที่สำหรับการรันคำสั่งๆในแต่ละ Block แต่จะไม่ครอบคลุมถึงการ declare ตัวแปรของ var ซึ่งจะถูก declare นอก Block Scope เช่น

if(true) {
	var a = 10;
} else {
	var b = 10;
}

a 	// 10
b 	// undefined

เราจะสังเกตุได้ว่า b ไม่ใช่ ReferenceError ทั้งๆที่โปรแกรมไม่ได้เข้าไปทำงานใน else เนื่องจากเวลารันจริงตัว declare จะถูกประกาศด้านนอก block scope แบบนี้

var a, b;
if(true) {
	a = 10
} else {
	b = 10;
}

เช่นเดียวกันกับ for loop

for(var i = 0; i < 10; i++) {
	i 	// 0..9	
}

i // 10

เราจะเห็นได้ว่า var ที่ประกาศนั้นเป็นการประกาศนอก Block Scope ดังนั้นหากเราใช้ asynchronous ฟังก์ชันใน loop เช่น setTimeout และเรียกใช้ตัวแปรนั้น ค่าของตัวแปรจะไม่ถูกต้อง

for(var i = 0; i < 10; i++) {
	i 		// 0..9
	setTimeout(function() {
		i 		// 10
	}, 1000)
}
i // 10

เนื่องมาจากฟังก์ชันนั้นไม่ได้ในรันในทันที เมื่อฟังก์ชันนั้นถูกรันภายหลัง ก็จะมองหาตัวแปร i ซึ่งตัวแปร i ใน scope นั้น ซึ่งกลายเป็นค่าที่ถูกบวกไปเรื่อยๆตาม loop ไปแล้ว จึงได้ค่าเป็น 10 นั่นเอง (ไม่ได้มองเห็นค่า i ตาม loop เนื่องจาก loop ได้ทำงานเสร็จไปแล้ว)

Loop Asynchronous

Function Scope

มาดู Scope ที่เกิดขึ้นจากฟังก์ชันกันบ้าง

var a = 10;
function foo(x) {
	var b = 10;
	b 		// 10
	x		// 4
}

foo(4);
b 		// ReferenceError ( no "b" in this scope )
x		// ReferenceError ( no "x" in this scope )

ข้อแตกต่างที่แตกต่างจาก Block Scope นั้นคือ ตัว Function Scope นั้นจะ Declare Variable ในฟังก์ชัน ดังนั้น เราไม่สามารถเรียกใช้ตัวแปรที่ไม่ได้อยู่ใน scope และของ Parent scope นั้นได้

Nested Function Scope

เมื่อเราทำการประกาศฟังก์ชันแยกย่อยลงไป Scope ก็จะแยกย่อยลงไปด้วย

var a = 1;

function foo(x) {
	var b = 2;
	function bar(y) {
		var c = 3;
	}
}

หากจะมอง scope เป็นภาพง่ายๆก็จะเป็นประมานนี้

Scope

Variable Lookup

ทีนี้มาดูกันก่อนว่าในแต่ละ Scope เมื่อเราทำการรันโปรแกรมจะมีวิธีการหาค่าของตัวแปรกันอย่างไร

การหาค่าของตัวแปรนั้น จะเป็นการหาค่าแบบ Down>>Top คือ หาใน Scope ตัวเองเสร็จ หาไม่เจอ ก็จะค่อยๆหาขึ้นไปใน Parent Scope ขึ้นไปเรื่อยๆจนถึง Global Scope เมื่อเจอตัวแปรที่ Scope ที่ใกล้ที่สุดก็ใช้ตัวแปรนั้นในการรันครั้งนั้น

var a = 1;
var b = 5;

function foo() {
	var b = 2;
	function bar() {
		var c = a + b;
		c 		// 1 + 2 = 3
	}
	bar();
}
foo();

เมื่อการเรียกใช้ตัวแปรในฝั่ง RHS ในที่นี้คือ a และ b ตัว Engine ต้องการจะรันคำสั่ง ก็จะคุยกับ Scope เพื่อหาค่าของตัวแปร

Engine: 	เฮ้ อยากได้ค่า a อะ มันอยู่ที่ไหน
Scope: 		แปปนะ หาก่อน เอ... ไม่เจอในชั้น `bar` แฮะ เดี๋ยวขอขึ้นไปดูชั้นบนก่อนนะ
Scope: 		ชั้น `foo` ก็ไม่มีแฮะ เดี๋ยวขึ้นไปดูชั้นบนสุด
Scope: 		อะ เจอแล้ว อยู่ชั้นบนสุด ไปดึงค่ามาจากที่นี่นะ
Engine: 	Thxกิ้ว

Lookup Example

เรามาลองดูกันว่าเข้าใจวิธีการหาตัวแปรของ Scope กันรึเปล่านะครับ

var a = 1;

function bar() {
	a 			// 1
}
function foo() {
	var a = 2;
	a 			// 2
	bar();
}
foo();

ใน scope ของ foo นั้นมีตัวแปร a อยู่ ดังนั้นเมื่อมีการเรียกตัวแปร a ในฟังก์ชันนี้ Scope ก็จะส่งค่า a ใน scope นี้ไปให้ จึงได้ค่าเป็น 2 ในขณะที่ scope bar นั้นไม่มีตัวแปร a ตัว Scope เองจึงต้องขึ้นไปดูที่ Global Scope เมื่อเจอค่า a ก็ส่งกลับไปให้ ค่า a ใน bar จึงเป็น 1

ทีนี้พอเข้าใจการทำงานของ Scope แล้ว มาลองแก้ปัญหา setTimeout ใน loop ตามหัวข้อด้านบนกันดูนะครับ หากจะแก้ปัญหา เราจำเป็นต้องนำ function มาใช้เพื่อให้ได้ Scope ใหม่ภายใต้ loop นั้น เช่น การใช้ IIFE Function หรือเรียก setTimeout ในฟังก์ชันอื่น

Loop Asynchronous Fix

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

หรือ

var delay = function(j) {
	setTimeout(function() {
		console.log(j)
	}, 1000);
}

for(var i = 0; i < 10; i++) {
	delay(i)
}

จะเห็นได้ว่า เราประกาศตัวแปรใหม่คือ j และ assign ค่าจาก i เข้าไป โดยคำสั่งนี้จะถูกรันในระหว่าง loop ค่า j จึงเป็นการนับตามค่า i และเนื่องจากเป็นการประกาศ IIFE ฟังก์ชันใหม่ค่า j ของเราในแต่ละ loop นั้น จะเป็นคนละ scope กัน ค่า j จึงเป็น 0..9 ของแต่ละ loop ทำให้ผลรันจึงออกมาถูกต้องนั่นเอง

ES6

ใน ES6 นั้น เราใช้ let ในการประกาศตัวแปรแทน var ซึ่ง let มันมีรูปแบบของ scope ที่แตกต่างกันกับ var มาดูว่าแตกต่างกันยังไงบ้างนะครับ

Let Scope

Scope ของ let นั้นแตกต่างจาก var ในส่วนของ Block Scope โดยหากเราย้อนกลับไปดูหัวข้อ Block Scope นั้น การประกาศตัวแปรโดยใช้ var จะสามารถเรียกใช้ได้นอก Block Scope เนื่องจากการ declare variable นั้นอยู่นอก scope แต่ใน letนั้น จะไม่สามารถใช้ได้ เนื่องจากมันจะถูก declare ใน Block Scope นั้นๆและใช้ได้แค่เพียงใน Scope นั้นเท่านั้น เช่น

if(true) {
	var a = 2;
} else {
	var b = 3;
}
a 	// 2
b 	// undefined

if(true) {
	let c = 2;
}
c 	// ReferenceError

และแน่นอนว่าเมื่อ let นั้นใช้สร้าง scope ใหม่ภายใน block scope ด้วย ดังนั้นปัญหากับ loop แบบ var จึงไม่เกิดขึ้น

for(let i = 0; i < 10; i++) {
	setTimeout(function() {
		i 		// 0..9
	}, 1000)
}

i 		// ReferenceError

และแน่นอนว่า scope ของ const ก็เหมือนกับ let เลยครับ

References

Author

Anonymous

Web Developer

Nextzy Technology Co,. LTD.