+4

Scope và closures trong Javascript

Trong JS, scope là những gì liên quan đến context code của bạn, Scope có thể được định nghĩa global hoặc là local Bạn sẽ hiểu được các biến hay function được phép truy cập ở phạm vi nào, liệu có thể thay đổi scope các context trong code của bạn. Việc viết code sẽ trở nên nhanh hơn, dễ maintain cũng như debug nhanh hơn.

I. Scope

I.1. Global scope

Là tất cả các biến được định nghĩa ở bên ngoài các function sẽ trở thành global, và chỉ có duy nhất một Global scope Các biến ở global scope có thể được truy cập và được thay đổi từ bất kỳ scope nào khác.

#test.js

   var gl = 2
  function print() {
      console.log(gl);
      gl = 4
  }
  console.log(gl); # trả về 2

khi gọi hàm kết quả sẽ là 2 vì nó nhận được biến gl từ bên ngoài, sau đó nó sẽ thay đổi giá trị của biến gl đó.

print(); #2

sau khi gọi hàm print tiếp tục gọi console để biết giá trị của biến gl sau khi được thay đổi qua hàm pint()

console.log(gl)#4

I.2. Local scope

Các biến được định nghĩa bên trong các function là nằm trong local scope. Và các các biến này là khác scope so với các biến ở trong các function khác. cũng như nó không thể được truy cập từ global scope. Điều đó có nghĩa là các biến cùng tên có thể được dùng trong các hàm khác nhau mà không sợ bị xung đột hay ghi đè.

// Global Scope
function someFunction() {
    // Local Scope #1
    function someOtherFunction() {
        // Local Scope #2
    }
}

// Global Scope
function anotherFunction() {
    // Local Scope #3
}
// Global Scope

Ví dụ dưới đây sẽ chứng minh việc local scope thì không thể truy cập ở bên ngoài global scope

function(){
  var lc = 2;
}
console.log(lc);

sẽ sinh ra lỗi

Uncaught SyntaxError: Unexpected token (

I.3. Function scope

Tất cả các scopes trong javascript chỉ được tạo ra với function scope, Các scopes sẽ không được tạo ra trong các block statement như for, while hay các câu lệnh if hay switch

if true {
  var test = "Inside if statemment";
};
console.log(test);

Khi run câu lệnh trên ở console develop tool nó sẽ in ra dòng chữ

Inside if statemment

Điều đó có nghĩa biến test kia là cùng scope với câu lệnh console.log(test)

I.4.Lexical Scope

Ta có thể hiểu như sau Lexical scope là khả năng truy cập của các biến trong inner function ra các scope cha của nó. (nhưng điều ngược lại thì không đúng, parent scope sẽ không access được children scope) Nó có các tên gọi khác nhau như Closure hay Static Scope. Xét ví dụ dưới đây để có thể hiểu rõ hơn.

//global scope
console.log(fatherName) // tại đây sẽ raise ra error: fatherName is not defined
function fatherFunction(){
  // scope cha
  var fatherName = "Peter";
  function childFunction() {
    // tại đây ta có thể access tới biến fatherName của scope cha
    console.log(fatherName); // Peter
  }
  childFunction();
}

Chạy câu lệnh trên ở console development tool bạn sẽ thấy child function có thể access được biến của parent scope.

II. Closures

Theo như "You don't know JS" định nghĩa:

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

Có thể tạm dịch ra: Closure là khi một hàm có khả năng nhớ và truy cập vào Lexical Scope của nó ngay cả khi hàm đó đang được excute bên ngoài Lexical scope của nó

Xét ví dụ dưới đây:

function foo() {
	var a = 2;

	function bar() {
		console.log( a );
	}

	return bar;
}

var baz = foo();

baz()

Bạn cũng có thể đoán được kết quả trả về của chuỗi câu lệnh này là 2, nhưng ẩn sâu bên trong nó là gì?

Bạn có thể thấy hàm foo() trả về một function chứ nó chưa thực sự thực thi hàm bar, câu lệnh cuối cùng khi ta gọi baz() thì lúc đó hàm bar() mới được thực thi, và việc thực thi này là bên ngoài của hàm foo() bên ngoài Lexical scope

Thông thường thì hàm sau khi thực thi thì toàn bộ scope bên trong hàm sẽ biến mất, nhưng đối với closure thì khác inner scope vẫn tồn tại và có thể được gọi sau đó. Như ta đã gọi baz() ở ví dụ trên Ta có thể gọi hàm baz() bất cứ khi nào, và nó luôn luôn tham chiếu tới Lexcial scope. Và đó là lý do tại sao khi ta gọi baz() bên ngoài hàm foo() mà nó vẫn lấy được giá trị của biến a trong scope của hàm foo()

II.1. Loop và Closure

for (var i=1; i<=5; i++) {
	setTimeout( function timer(){
		console.log( i );
	}, i*1000 );
}

Có thể bạn đang mong muốn nó sẽ in ra từ 1 -> 5 cho đoạn code trên, nhưng không nó chỉ in ra 5 lần số 6, Khi i = 6 thì hàm loop sẽ kết thúc và khi đó tất cả các hàm callback timeout mới thực sự chạy đó là lý do vì sao nó chỉ in ra 5 lần thay vì in ra từ 1 đến 5

Cách thức mà scope hoạt động ở đây là tất cả 5 function được định nghĩa cho mỗi vòng lặp và được bao bọc bởi cùng một global scope.

Để fix cho hàm này chạy như mong muốn ta cần: chạy callback ngay mỗi thời điểm tại vòng lặp và cần tạo ra riêng một variable cho chính bản thân của function, không dùng chung variable i của global scope.

Ta có thể viết theo 1 trong 2 cách sau:

for (var i=1; i<=5; i++) {
	(function(){
		var j = i;
		setTimeout( function timer(){
			console.log( j );
		}, j*1000 );
	})();
}
for (var i=1; i<=5; i++) {
	(function(j){
		setTimeout( function timer(){
			console.log( j );
		}, j*1000 );
	})( i );
}

Tham khảo

Bài viết được tham khảo từ: cuốn sách You dont know JS https://scotch.io/tutorials/understanding-scope-in-javascript https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí