Scope-and-Closure-in-javascript

Scope và Closure trong JavaScript

Scope là khái niệm quen thuộc trong hầu hết các ngôn ngữ lập trình, hiểu rõ về scope giúp người viết code tránh các side-effect, thiết kế code tốt hơn. Còn closure thì có ngôn ngữ có, có ngôn ngữ thì không (như C/C++ thì k có closure, Java hiện tại cũng chưa có) nên chưa chắc mọi người đều biết, nhưng trong JS thì closure là một thứ quan trọng và là cội nguồn của rất nhiều thứ hay ho sau này.

Scope

Scope quy định về visibility và life time của biến và các tham số, có thể hiểu scope là nơi chứa các biến do các đoạn mã JS tạo ra khi chạy. Trong C hay C++, các biến được khai báo trong dấu ngoặc (curly braces)- block scope, chúng nó sẽ chỉ được nhìn thấy và tồn tại trong cái block đó, khi ra khỏi dấu ngoặc thì sẽ được giải phóng (trừ khi nó được khai báo static).

C-scope

Nhưng buồn một cái là trong JS không có block scope => mọi sự sẽ vẫn diễn ra bình thường mà không có lỗi lầm nào cả :v

Vậy trong JS có gì? – JS có function scope. Scope trong JS không được tạo ra bởi các phát biểu như if, else,…. hay các vòng lặp như for, while, …

What does it mean? It mean: Các tham số và biến bạn tạo ra ở trong một fucntion nào đó thì nó chỉ được nhìn thấy trong function đó, ra ngoài thì không thể nhìn thấy nó nữa.

Đây là ví dụ về nested function và đống scope của chúng nó. Ta thấy inner function có thể sử dụng biến num được khai báo ở outer function. Nhưng outer function không thể access vào biến x của inner function được. Có thể nói inner function bao gồm scope của outer function.

Nhìn đoạn code này để hiểu thêm

Chiều ngược lại không có nhé

Như kia được gọi là Lexical scope, nghĩa là thằng ở trong sẽ có quyền truy cập đến các thuộc tính của thằng ở ngoài. Chiều ngược lại không đúng.

Quay trở lại đoạn code 3, khi scopeD muốn truy cập đến something, trình thông dịch sẽ tìm từ scopeD, không có thì ra scopeC tìm, sopeC không có thì nó ra scopeB, scopeB k có thì ra scopeA, không có nữa thì nó ra ngoài tìm tiếp, ở ngoài người ta gọi là globally scope, ở ngoài mà không có nữa thì nó quăng về lỗi ReferenceError. Chuỗi các scope như kia được gọi là scope chain.

Xét sự xung đột tên biến như đoạn code sau

Chạy đoạn code này bạn có thể thấy inner_func() lấy giá trị của something – là tham số của nó chứ không lấy something được khai báo ở ngoài outer_func(), nếu bạn không truyền một giá trị nào cho tham số của inner_func() thì something sẽ là undefined chứ nó cũng không lấy của thằng outer_func(). Điều này đúng theo scope chain, khi tìm thấy thứ nó muốn thì nó sẽ dừng lại.

Khi bạn học các ngôn ngữ khác như C, C++, … thì bạn sẽ được khuyên là nên khởi tạo biến muộn nhất có thể (lúc bắt đầu dùng) để tránh side effect, nhưng với JS, bạn nên khai báo biến cần dùng sớm nhất có thể để tránh lỗi sinh ra từ cái scope sida của JS. Tốt nhất là sau cái dấu ‘ { ‘.

Closure

Ta đến với 2 đoạn code sau đây

Điều đầu tiên chúng ta thấy là 2 đoạn code trên lúc thực thi xong đều in ra kết quả như nhau, đều là giá trị của biết name. 

Đoạn code 1 chỉ đơn thuần là hàm inner_func() nằm trong hàm outer_func(), có chức năng xuất ra giá trị của biến name được khai báo ở outer_func(), inner_func() hoàn toàn có thể truy xuất vào biến của outer_func(). Mình chỉ đơn thuần gán outer_func cho something rồi thực thi nó. Lý do nó in ra được kết quả là do sau khi định nghĩa xong inner_func() thì có outer_func() có lời gọi hàm inner_func() trên kia. Như vậy chỉ cần thực thi outer_func là đã xuất ra được kết quả.

Đoạn code 2 cũng gần giống đoạn code 1 nhưng chỉ khác ở chỗ mình cho outer_func() trả về hàm inner_func thay vì gọi nó như trên. Để ý chỗ khai báo something, ở đây thay vì gán outer_func cho something như ở trên thì mình gán thứ mà outer_func() trả về, đó chính là inner_func() (để ý dấu () ở dòng này). Như vậy, outer_func() đã thực thi xong, tiếp theo là gọi something để in ra giá trị của biến name như ở trên. Mọi thứ vẫn diễn ra rất bình thường. Nhưng khoan, outer_func() đã thực thi xong và trả về kết quả thì biến name ở đâu có để cho inner_func() nó thực thi ??? Có thể bạn sẽ thấy mâu thuẫn với những gì đã đọc được ở phần scope, nhưng thực tế thì không có gì mâu thuẫn cả, JS có một thứ khác được gọi là closure.

Ta có định nghĩa closure như sau:

A closure is the combination of a function and the lexical environment within which that function was declared.

Lexical environment – đó là môi trường chứa các biến cục bộ ở trong scope, tại thời điểm closure được khởi tạo. Như vậy closure gồm 2 thành phần là function và môi trường đã tạo ra nó. Vậy thì ở ví dụ trên, closure được tạo ra cùng với với hàm inner_func() ở đoạn code 2 đã giữ tham chiếu đến biến name lại, điều này giải thích tại sao inner_func() vẫn truy xuất đến biến name trong khi outer_func() đã thực thi xong. Tính chất quan trọng của closure là nó giữ tham chiếu đến những object nào nằm bên trong nó và closure liên hệ mật thiết với scope chain.

Đa số các trường hợp ta sử dụng closure thì cái environment kia chính là cái scope ở ngoài cái inner function ta đang xét.

Tiếp tục với ví dụ sau

Đoạn code trên làm gì?. Nó tính tổng 3 số.

Nó thực hiện ra sao?.

  • Với lời gọi add(1)(2)(3) thì hàm add sẽ nhận đối số là 1 (x = 1) và nó trả về hàm add1()
  • add1() nhận đối số là 2 (y = 2) và có closure chứa x = 1 (xem hình), nó trả về hàm add2().closure-debug-1
  • add2() nhận đối sối là 3 (z = 3) và có closure chứa x = 1 và y = 2 (xem hình), nó trả về tổng x + y + z.closure-debug-2

Ta thấy những gì ở trong closure đều được giữ lại sau khi outer function thực thi xong, vậy thì khi nào chúng không còn được giữ lại nữa? – Câu trả lời là khi không còn một tham chiếu nào đến bất cứ thứ gì trong closure nữa. Như ví dụ trên thì khi biến sum được gán giá trị bằng 6 thì không còn tham chiếu nào đến x, y, z hay là function nào trong đó nữa, lúc này x, y, z sẽ bị dọn dẹp bởi JS.

Vậy, closure được tạo ra khi nào? – Khi một function có tham chiếu đến một biến nằm ở trong một scope nào đó, mà trong scope đó function cũng được tạo ra.

Với những tính chất của closure, ta có thể viết được đoạn code sau đây:

Đoạn code trên có 1 cái Object constructor – function Student(name, id) {….}. Điều đặc biệt ở đây là thay vì sử dụng giá trị return mặc định như ở object thì mình cho nó return về 1 cái object literal với 3 phương thức là getName(), getID(), changeID(), vậy nó có cái property nào không? – Có, nhưng bị ẩn đi rồi, là cái _name, với _id đó. 3 thằng bên trong return có closure chứa _name và _id, vì thế chỉ có 3 thằng này mới có thể chạm tới _name và _id, ngoài chúng nó ra thì không có cách nào để đụng vào 2 giá trị này. Thể loại này giống với object mà có thuộc tính private trong C# hay java nè :p, đâu phải JS không làm như tụi nó được đâu :)).

Ở đây có một điều cần lưu ý, bạn nghĩ kết quả sẽ như thế nào nếu mình viết hàm changeID như thế này:

Thì khi bạn thực hiện lệnh: lamStudent.changeID(1455); Kết quả có còn giống như trên? – Câu trả lời là không. Tại sao ?? Vấn đề này liên quan tới this mà mình đã nói ở bài function

Khi bạn gọi changeID() như trên, thì changeID() sẽ nhận this là cái object lamStudent (mẫu method invocation ấy), nhưng thằng lamStudent nó không biết _id ở đâu cả, như vậy, câu lệnh this._id = newid; nó sẽ tạo một cái thuộc tính _id cho object lamStudent và gán nó bằng newid, việc này chẳng liên quan mẹ gì đến _id đang ở trong closure cả, kết quả là getID() sẽ vẫn quăng về giá trị của _id trong closure i chang như cũ :v. Cẩn thận chứ không debug sấp mặt ra đó.

this-object-lamstudent-debug

Sử dụng closure rất dễ gặp lỗi khi sử dụng nó với vòng lặp, một ví dụ kinh điển để minh hoạ cho trường hợp này là đoạn code dưới đây

Đoạn code này tạo ra 5 dòng link theo số thứ tự từ 1 đến 5, khi ta click vào một link nào đó thì nó sẽ thông báo số thứ tự của link đó. Trông đoạn code rất đỗi bình thường nhưng khi chạy thì ta gặp vấn đề như sau

the-infamous-problem-1

Click vào bất cứ link nào nó đều hiện 6 – chính là giá trị của i ở đoạn code trên sau khi chạy xong vòng lặp for().

Thế thì hiện tượng này là do đâu ? – Hãy nhìn lại đoạn code trên, chú ý chỗ dòng link.onclick = ….. ta thấy function được tạo ra và gán cho link.onclick có tham khảo đến i – là biến đếm ta tạo ở hàm helper. Rõ ràng với mỗi link thì function được gán cho link.onclick đều có closure chứa tham khảo tới i, như vậy khi vòng lặp for() chạy xong thì tất cả các closure này đều tham khảo tới i – lúc này có giá trị bằng 6 => in ra không đúng như ta mong đợi.

Cách giải quyết vấn đề này như sau:

Ta cho cái function được gán cho link.onclick kia trả về một function khác để in ra giá trị của index, còn index sẽ nhận giá trị từ i. Như vậy thì hàm bên trong kia sẽ có closure tham chiếu đến index thôi, mà index là tham số của hàm ở ngoài nên nó riêng biệt sau mỗi lần lặp của for(). Để ý chỗ ….}(i); ở đây ta thực thi ngay sau khi định nghĩa nên link.onclick sẽ nhận giá trị là hàm được return về => mọi thứ ổn thoả.

the-infamous-problem-2

Bạn nên cẩn thận với closure khi dùng với vòng lặp, ẩu ẩu cũng ngồi debug lòi họng đó.


Bài viết như vậy đã cover hầu hết những gì liên quan đến scope và closure của JS rồi đó, hiểu rõ về scope và closure giúp ta viết code tốt hơn và tránh được các lỗi mang tính chất debug thấy mẹ mà không ra như những ví dụ trên. Bài viết sau mình sẽ đến với Callback, một thứ cũng hay ho không kém gì closure của JS. Giờ thì mình đi ngủ, chúc các bạn code vui vẻ :v

Leave a Reply

Your email address will not be published. Required fields are marked *