Scope và closure trong javascript
Học hỏi chính là kinh nghiệm. Những thứ khác chỉ là thông tin. -- Albert Einstein Link gốc bài viết tại đây. Đối với những bạn lập trình web nói chung và lập trình js nói riêng thì những kiến thức về scope, closure là cần phải nắm rõ. Js là một ngôn ngữ lập trình khá khó, nếu ...
Học hỏi chính là kinh nghiệm. Những thứ khác chỉ là thông tin.
-- Albert Einstein
Link gốc bài viết tại đây.
Đối với những bạn lập trình web nói chung và lập trình js nói riêng thì những kiến thức về scope, closure là cần phải nắm rõ. Js là một ngôn ngữ lập trình khá khó, nếu không nắm rõ cách hoạt động thì sẽ gặp phải những vấn đề khá nan giải.
Giới thiệu về scope.
Scope là một block memory để lưu trữ các biến cụ thể nào đó.
Nếu ai đó đã từng lập trình C sẽ biết rằng trong C, scope được tạo khi sử dụng toán tử {}, gọi là block scope. Mỗi khi dấu ngoặc nhọn được khai báo thì trình biên dịch (compiler) sẽ tạo ra một scope.
Javascript cũng sử dụng toán tử {} nhưng lại không sử dụng scope mỗi khi có toán tử đó khai báo giống trong C.
Ví dụ một đoạn code cơ bản trong C như sau:
for (i=0; i < 4; i++) { //Outer loop for (i=0; i < 2; i++) { //Inner loop document.write("Hello World"); } }
Đoạn code trên thông thường sẽ in ra 8 lần từ "Hello World" trong C hoặc 1 số ngôn ngữ lập trình khác, nhưng trong JS nếu bạn viết đoạn code trên thì vòng lặp sẽ chạy mãi không dừng.
Bạn có biết vì sao không?
Vì đơn giản, JS không tạo scope lưu biến i khi có toán tử {} như trong C nên biến i trong Outer loop và trong Inner loop là một. Do đó vòng lặp Inner loop luôn luôn reset lại biến i dẫn tới vòng lặp Outer loop có biến i không thể đạt tới gía trị = 4 để dừng.
Những người phát triển JS đã nhận ra sự thiếu sót đó dẫn tới nhiều lập trình khá lúng túng khi tiếp cận nên trong những phiên bản JS sau này, họ đã cung cấp thêm từ khóa let để tạo block scope:
for (let i=0; i < 4; i++) { //Outer loop for (let i=0; i < 2; i++) { //Inner loop document.write("Hello World"); } }
JS không phải không sử dụng scope mà chúng chỉ tạo scope khi nó là một hàm, hay còn gọi là function scope.
Function Scope
Function scope là scope được tạo ra chỉ cho function đó sử dụng. Nó chính là mọi thứ nằm trong dấu {} của hàm.
var foo = "Goodbye"; var message = function() { var foo = "Hello"; document.write(foo); } message(); //Hello document.write(foo); //Goodbye
Đoạn code trên có 1 function scope được sử dụng cho hàm message. Biến foo trong hàm message và biến foo ngoài hàm đó là hoàn toàn khác nhau.
Nếu bạn sử dụng một biến trong function scope ở ngoài function đó thì sẽ báo lỗi ngay:
var message = function() { var foo = "Hello"; document.write(foo); } message(); //Hello console.log(foo); //error here
Nhưng nếu bạn khai báo biến trong scope mà không có từ var thì js sẽ hiểu biến đó chính là global nên bạn vẫn có thể truy cập được ngoài scope.
var message = function() { foo = "Hello"; document.write(foo); } message(); //Hello document.write(foo); //Hello
Nếu bạn đã từng đọc best practice trong jQuery thì các chuyên gia khuyên bạn nên khai báo jQuery cách như sau:
(function($) { //Do things here - they are scoped }(JQuery))
hoặc:
(function($) { //Do things here - they are scoped })(JQuery)
Bạn có từng thắc mắc vì sao họ lại khuyên nên code như thế bao giờ chưa?
Hai đoạn code trên là như nhau và nó được gọi là Immediately-Invoked Function Expression (IIFE) tức là nó được gọi thực thi ngay sau khi hàm được khai báo. Bản chất của 2 đoạn code trên là:
var rootFunction = function($) {// $ là tham số truyền vào function //Do things here - they are scoped } rootFunction(JQuery) // lời gọi function ở đây, JQuery là đối số tryền vào
Quay lại 2 đoạn code IIFE phía trên, phía cuối cùng họ sử dụng dấu () để gọi thực thi hàm, với đối số truyền vào là JQuery.
Trong JS, dấu () để gọi thực thi hàm.
Có dấu () khiến cho hàm khai báo ngay trước sẽ được thực thi ngay. Trong JQuery, kí hiệu $ là kiểu viết rút gọn của hàm JQuery. Nhưng $ cũng là cách viết rút gọn của nhiều thư viện JS khác (vd ProtoTypeJS). Để tránh nhầm lẫn giữa các biến $ của Jquery khai báo global và tránh xung đột giữa các biến $ của thư viện JS khác nếu bạn dùng nhiều thư viện JS cùng lúc thì bạn nên đặt mọi thứ vào scope.
Và đoạn code trên đã làm thế cho chúng ta :)))
Lexical Scope
Khi function A nằm trong function B mà trong function A có biến tham chiếu tới function B thì ta nói function A có lexical scope, lexical scope của function A chính là scope của function A cộng với scope của function B.
VD:
function init() { var name = 'hungdv'; // name is a local variable created by init function displayName() { // displayName() is the inner function, a closure alert(name); // use variable declared in the parent function } displayName(); } init();
Closure là tập hợp gồm function và lexical scope của function đó.
Vì lexical scope là khái niệm đi với function nên có thể nói closure là function có lexical scope.
Ví dụ:
var createCallBack = function() { //First function var firstVar = 1; return function() { //Second function console.log("Log firstVar in second function:", firstVar); var secondVar = 2; return function() { //Third function console.log("Log secondVar in third function:", secondVar); } } }
Khi bạn khai báo một hàm trong hàm mà hàm đó có biến tham chiếu tới scope cha, ông thì hàm đó được gọi là closure. Ví dụ trên có second function có biến firstVar là biến nằm trong scope của hàm cha là first function nên second function là closure. Tương tự third function cũng là một closure.
Closure sử dụng biến là con trỏ tới biến thuộc scope cha chứ không phải copy biến của scope cha vào scope của mình. Ví dụ:
function say() { var num = 42; var say = function() { console.log(num); } num++; return say; } var sayNumber = say(); sayNumber(); // logs 43
Đoạn code trên sẽ log 43 chứ không phải 42 chứng tỏ closure sử dụng biến num chính là biến num của scope hàm say() thứ nhất. Khi biến num của scope này thay đổi thì kết quả in ra màn hình cũng thay đổi theo.
VD1:
function sayHello(name) { var text = 'Hello ' + name; var say = function() { console.log(text); } say(); } sayHello('Joe'); // Hello Joe
VD2:
function sayHello2(name) { var text = 'Hello ' + name; // Local variable var say = function() { console.log(text); } return say; } var say2 = sayHello2('Bob'); say2(); // logs "Hello Bob"
VD3:
function say667() { // Local variable that ends up within closure var num = 42; var say = function() { console.log(num); } num++; return say; } var sayNumber = say667(); sayNumber(); // logs 43
VD4:
var gLogNumber, gIncreaseNumber, gSetNumber; function setupSomeGlobals() { var num = 42; // Store some references to functions as global variables gLogNumber = function() { console.log(num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; } } setupSomeGlobals(); gIncreaseNumber(); gLogNumber(); // 43 gSetNumber(5); gLogNumber(); // 5 var oldLog = gLogNumber;// here will copy function and context setupSomeGlobals(); gLogNumber(); // 42 oldLog() // 5
VD5:
function buildList(list) { var result = []; for (var i = 0; i < list.length; i++) { var item = 'item' + i; result.push( function() {console.log(item + ' ' + list[i])} ); } return result; } var fnlist = buildList([1,2,3]); //Using j only to help prevent confusion -- could use i. for (var j = 0; j < fnlist.length; j++) { fnlist[j](); }
Đoạn code trên sẽ in ra "item2 undefined" 3 lần vì cả 3 closure đều sử dụng chung một tham chiếu tới item và biến i (Lúc này item đã là item2 và i đã có gía trị là 3).
Để đoạn code trên chạy theo ý muốn của bạn, chỉ cần đơn giản sửa closure sao cho mỗi closure sử dụng một scope riêng. Một trong vài cách đó là sử dụng từ khóa let giúp biến được đóng trong scope của dấu {} mà không cần nằm trong hàm:
function buildList(list) { var result = []; for (let i = 0; i < list.length; i++) { let item = 'item' + i; result.push( function() {console.log(item + ' ' + list[i])} ); } return result; } var fnlist = buildList([1,2,3]); //Using j only to help prevent confusion -- could use i. for (var j = 0; j < fnlist.length; j++) { fnlist[j](); }
VD6:
function sayAlice() { var say = function() { console.log(alice); } var alice = 'Hello Alice'; return say; } sayAlice()();// logs "Hello Alice"
Mọi biến trong js khi khai báo sẽ được đưa lên đầu scope (giá trị ban đầu là undefined ) và được gán giá trị tại câu lệnh gán của biến đó (variable hoisting). ví dụ:
function testHoisting() { console.log("a1:", a); // log undefined, not error var a = 3; console.log("a2:", a); // log 3 } testHoisting();
VD7:
function newClosure(someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push(num); console.log('num: ' + num + '; anArray: ' + anArray.toString() + '; ref.someVar: ' + ref.someVar + ';'); } } obj = {someVar: 4}; fn1 = newClosure(4, obj); fn2 = newClosure(5, obj); fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4; fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4; obj.someVar++; fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5; fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
Link tham khảo:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
https://medium.com/dailyjs/i-never-understood-javascript-closures-9663703368e8
http://doctrina.org/JavaScript:Why-Understanding-Scope-And-Closures-Matter.html
http://stackoverflow.com/questions/111102/how-do-javascript-closures-work