Scope và Closure trong JavaScript?

Đã bao giờ bạn gặp bug undefined khi code JS mà không biết lý do tại sao? Hay thắc mắc các biến được lưu và xóa khỏi bộ nhớ như thế nào? Có thể những gì bạn cần là tìm hiểu xem nguyên lý của Scope và Closure trong JS. Cùng với hoisting, Scope và Closure không phải là những kiến thức cao siêu gì, mà hoàn toàn là fundamental của JS. Tuy nhiên với nhiều người khi mới học ngôn ngữ được coi là lỏng lẻo này lại không mấy khi để ý tới những concept kể trên. Vì vậy, bài viết này sẽ cho bạn góc nhìn cơ bản về Scope và Closure.

Scope

Khái niệm

  • Là phạm vi thực thi mà trong đó biến và biểu thức có thể gọi đến được.
  • Khi có nhiều scope lồng nhau, phạm vi con có thể access đến phạm vi cha
function log(message) {
	const type = 'log';
	console[type](message);
}

log("hello");
console.log(type);

Đoạn code sau sẽ trả ra lỗi sau khi thực thi

VM64:7 Uncaught ReferenceError: type is not defined
    at <anonymous>:7:13

Đơn giản bởi biến type chỉ được định nghĩa trong phạm vi hàm log() . Bạn không thể gọi đến nó ở ngoài.

Các loại phạm vi

  • Global scope: Scope cho toàn bộ code chạy trong chế độ thực thi
  • Module scope: Scope cho code chạy trong chế độ module
  • Function scope: Được tạo ra bởi 1 function
  • Block scope: Được tạo ra bởi 1 cặp đóng mở ngoặc {}

Global scope

let global_IP = '192.168.11.34';
console.log(global_IP);

Biến global_IP được định nghĩa ở ngoài function, cặp ngoặc nhọn hay module. Từ đó nó có thể dùng trong toàn chương trình.

Module scope

export { countries, states, print };

Các biến, hàm, class được export từ 1 module, có thể sử dụng được ở module khác bằng cách import.

Function scope

function sayHi(){
    let word = 'Hi there!';
    console.log(word);
}

sayHi();
console.log(word); // -> throw error

Biến word được khai báo trong hàm sayHi() , vì vậy bạn chỉ có thể dùng nó khi gọi đến hàm này. Mọi truy cập ngoài phạm vi của hàm đều báo lỗi.

Khi một hàm được gọi đến, nó tạo ra môt scope mới.

Block scope

let x = 10;

{
    let x = 20;
    console.log(x); // -> 20
}

console.log(x); // -> 10

Ở ví dụ trên, biến x được định nghĩa ở trong cặp ngoặc nhọn chỉ có ý nghĩa trong phạm vi đó, không gây ảnh hưởng đến biến x định nghĩa ở ngoài.

Chuỗi scope

let y = 200;
{
	let y = 100;
	console.log(y); // -> 100
}

Các lệnh thực thi luôn tìm đến biến được định nghĩa ở phạm vi gần nhất.

let y = 200;
{
	console.log(y); // -> Uncaught ReferenceError: Cannot access 'y' before initialization"
	let y = 100;
}

Tuy rằng biến y được định nghĩa ở phạm vi global, nhưng nó luôn ưu tiên chọn trong phạm vi gần nhất (ở ví dụ này là chính trong phạm vi của nó). Vì vậy trả ra lỗi access trước khi khởi tạo.

Khi nào một biến bị xóa khỏi bộ nhớ?

  • Biến global: Khi chương trình dừng. Nên tránh dùng loại biến này để tối ưu bộ nhớ.
  • Biến trong function scope: Khi hàm thực thi xong.
  • Biến trong block scope: Khi block thực thi xong.

Closure

Khái niệm

Closure là một combo các hàm bao bọc nhau và ở đó, hàm bên trong có thể truy cập đến biến được định nghĩa ở hàm bên ngoài.

Closure có khả năng:

  • truy cập biến được định nghĩa trong phạm vi của nó
  • truy cập biến được định nghĩa trong phạm vi của hàm bên ngoài nó.
  • truy cập biến global.
function createCounter() {
	let counter = 1;

	return function() {
		console.log(counter++);
	}
}

const counter1 = createCounter();

counter1();
counter1();
counter1();

Output

// output: 1
// output: 2
// output: 3

Như đã đề cập ở bài Scope, khi gọi đến hàm createCounter() , nó tạo ra 1 phạm vi rồi xóa ngay khi hàm thực thi xong. Tuy nhiên trong ví dụ trên, hàm này lại trả ra 1 hàm con khác, hàm con này lại được trả ra ngay tại global scope, vì vậy phạm vi đó được giữ lại trong bộ nhớ. Mỗi khi hàm counter1() được gọi đến, nó lại truy cập đến scope đó.

Tác dụng

Tạo private property (như tính đóng của OOP)

function Product () {
	let quantity = 1000;
	
	return {
		getQuantity: function() {
			return quantity;
		},
    setQuantity: function(number) {
    	quantity = number;
      return quantity;
    }
	}
}

const product = Product();
console.log(product.getQuantity());
product.setQuantity(2000);
console.log(product.getQuantity());

Tạo hàm theo ngữ cảnh

Giả sử có 1 bài toán tạo common logger

const createLogger = (level) => {
	return (message) => {
		logger({
			level: level,
			message: message,
			// ... other common options
		});
	}
}

const infoLogger = createLogger('info');
const warningLogger = createLogger('warning');
const errorLogger = createLogger('error');

infoLogger('Hello world!') // [INFO] Hello world!
warningLogger('Warning') // [WARN] Warning
errorLogger('Something went wrong') // [ERROR] Something went wrong

Tổng kết

Yeah, vậy trên đây là những ví dụ cơ bản nhất về 2 core concept của JS là Scope và Closure. Hy vọng sau khi đọc bài viết này, bạn sẽ không còn phải vắt não hàng giờ để bới lông tìm vết, truy lùng thủ phạm làm đoạn code của bạn không chạy.