Hướng dẫn function scope javascript - phạm vi chức năng javascript

MỞ ĐẦU

Javascript có các khái niệm liên quan tới

// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
1 và không khái niệm nào là
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
2 đối với những newbie mới làm quen với Javascript (hay kể cả những dev lâu năm). Bài viết này hướng tới những bạn mong muốn tìm hiểu sâu hơn về
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
1 sau khi đã "đối mặt" với các từ khóa như
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
4

Scope là gì?

Trong Javascript,

// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
1 hay
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
6, đề cập đến ngữ cảnh của đoạn code.
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
7 có thể định nghĩa là toàn cục (
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
8) hoặc cục bộ (
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
9). Nắm rõ
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
1 trong Javascrip là chía khóa để viết những đoạn code rõ ràng,
// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
1, hiểu được các biến/hàm này có thể truy cập đến không hay giúp cho đoạn code của bạn dễ manitain, dễ debug hơn. Khi xét scope của variable/function, ta thường đặt câu hỏi: nó thuộc scope A hay scope B ???

Global Scope

Trước khi bắt đầu viết một dòng code, chúng ta đang nằm trong cái mà được gọi là

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
2(
// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
3). Nếu ta định nghĩa biến, biến đó là toàn cục:

// global scope
var name = 'Duy Buffet';

Global scope là bạn tốt nhất và cũng là cơn ác mộng tồi tệ nhất!!! Nếu không nắm rõ mình đang nằm trong scope nào, chắc chắn ta sẽ gặp vấn đề với global scope (thường là xung đột namespace). Người ta cứ nói rằng việc dùng Global scope là rất dở, nhưng không phải trong mọi trường hợp. Ta cần sử dụng nó để tạo ra các Modules/APIs được truy cập bởi các scope khác. Ví dụ: trong jQuery, ta

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
4 một
// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
5 bằng class name như sau:

jQuery('.myClass');

Ở đây, ta đang truy cập đến namespace jQuery trong global scope. Khái niệm

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
6 đôi khi có thể dùng thay thế cho
// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
1, nhưng chủ yếu là đề cập đến scope có level cao nhất. Trong trường hợp này, jQuery nằm trong global scope đồng thời cũng là namespace cho thư viện jQuery.

Local scope

Local scope đề cập tới bất kỳ scope nào được xác định qua

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
3. Thường có một phạm vi truy cập toàn cục (global scope) duy nhất và mối function lại định nghĩa phạm vi truy cập cục bộ (
// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
9) của riêng nó. Nếu định nghĩa một function và tạo các biến bên trong nó, các biến này được gọi là biến cục bộ. Ví dụ:

// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);

Biến

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
0 có phạm vi truy cập là
// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};
9 và nó sẽ không thể được truy cập bởi scope cha, do đó dẫn đến kết quả là
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
2

Function scope

Tất cả các scope trong Js không được tạo bởi vòng lặp

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
3 hoặc
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
4, hay các lệnh rẽ nhánh
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
5 hoặc
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
6 mà bởi
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
7. Công thức là: tạo functions = tạo scope mới. Ví dụ:

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};

Lexical Scope

Khi nhìn thấy một function nằm trong một function khác, function trong có quyền truy cập tới scope của function bên ngoài, đó gọi là

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
8 hay
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
9 - còn được gọi là
var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
0. Ví dụ:

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};

Ở đây có thể nhận thấy rằng,

var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
1 mới chỉ được định nghĩa chứ chưa được gọi. Thứ tự gọi cũng có ảnh hưởng đến các biến:

var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`

Làm việc với Lexical scope cũng khá là dễ dàng, bật cứ biến/object/ function được định nghĩa trong parent scope, đều có thể được truy cập bởi các scope con nhỏ hơn. Ví dụ:

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};

Chú ý, Lexical scope không hoạt động theo chiều ngược lại, tức là biến/object/function định nghĩa trong scope con thì ko thể truy cập bởi scope cha.

// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};

Scope Chain

Mình có đoạn code như sau:

function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
var text = "in gloal";

Theo các bạn đoạn code trên in ra

var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
2 hay
var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
3 ??? Đáp án là
// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
2. Vì trong một scope, nếu ta truy cập giá trị một biến, mà không tìm thấy biến đó trong scope hiện tại thì nó sẽ tìm ở scope cha (chính là cái mà scope chain muốn đề cập). Trong
var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
5 không có biến
var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
6, do vậy nó sẽ ngược lên scope cha để tìm biến text. Tuy dòng khai báo
var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
6 nằm ở cuối cùng, tuy nhiên do
var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`
8 trong Js, nên mọi khai báo sẽ được chuyển lên đầu scope:

var text;
function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
text = "in gloal";

như vậy function b sẽ cố in ra biến b lúc chưa có giá trị nên kết quả là

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};
2

Closures là gì?

Closure có mối quan hệ chặt chẽ với Lexical Scope. Ví dụ tiêu biểu về cách thức hoạt động của closure đó là khi 1 function trả về tham chiếu tới 1 function.

jQuery('.myClass');
0

Khái niệm

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
0 làm cho scope của ta không thể tiếp cận được public scope. Chỉ gọi function sẽ không thực hiện gì bởi nó trả về kết quả là tham chiếu tới function.

jQuery('.myClass');
1

Để method hoạt động ta cần gán nó vào biến rồi mới thực thi:

jQuery('.myClass');
2

Không nhất thiết phải trả về function mới được gọi là closure. Đơn giản chỉ cần truy cập tới biến nằm ngoài Lexical scope cũng là closure

Scope và ‘this’

Mỗi scope lại bind giá trị khác nhau cho

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 tùy thuộc vào vị trí nó được gọi tới. Mặc định thì
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 bind đến object toàn cục nhất
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
3. Ta cùng xem cách gọi hàm khác nhau cho ra kết quả của
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 khác nhau:

jQuery('.myClass');
3

Có trường hợp dù trong cùng một function, scope vẫn có thể thay đổi và giá trị của

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 cũng bị thay đổi:

jQuery('.myClass');
4

Ở đây ta đã tạo ra một scope mới mà không được gọi tới bởi event handler, nên mặc định giá trị của

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 là
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
3 object. Để lấy được giá trị
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 trong context của event handler mà không phải object window, ta có thể cache
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 lại và refer đến nó theo lexical binding:

jQuery('.myClass');
5

Thay đổi scope với .call(), .apply() and .bind()

Đôi khi bạn cần điều chỉnh scope cho phù hợp với mục đích sử dụng:

jQuery('.myClass');
6

Giá trị của

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 ở đây không refer tới các element như ta mong muốn. Ta có thể thay đổi scope theo cách sau đây

.call() và .apply()

// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
1 và
// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
2 cho phép ta bind đúng giá trị của
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 bằng cách truyền một scope vào một function. Bây giờ mình sẽ sửa lại đoạn code bên trên để this bind đến đúng các phần từ của mảng:

jQuery('.myClass');
7

Ở đây, mình đang truyền element trong mỗi vòng lặp

// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
4 vào để thay đổi scope của function ===>
var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};
1 = iterated element.
// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
2 có chức năng tương tự, chỉ khác cách truyển tham số, trong khi
// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
7 nhận các tham số riêng lẻ cách nhau bởi dấu
// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
8, còn
// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};
9 lại nhận một mảng.

.bind()

Không như ví dụ bên trên,

function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
var text = "in gloal";
0 không gọi tới function, nó bind giá trị trước khi function được gọi tới. Phương thức này được giới thiệu trong
function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
var text = "in gloal";
1. Như đã biết, ta không thể truyền tham số vào tham chiếu của function:

jQuery('.myClass');
8

Có thể fix bằng cách:

jQuery('.myClass');
9

Nhưng một lần nữa ở đây đã làm thay đổi scope và đồng thời vô tình tạo ra một function vô dụng có thể làm ảnh hưởng đến performance nếu ta đặt nó trong vòng lặp và binding event listener. Đó là lúc

function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
var text = "in gloal";
0 tỏa sáng và giải quyết vấn đề, vẫn đảm bảo truyền được tham số, nhưng function chưa được gọi ngay lúc đó:

// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);
0

KẾT LUẬN

Trên đây mình đã giới thiệu sơ qua một số vấn đề về scope trong Javascript. Hi vọng bài viết phần nào hữu ích cho những người đang bắt đầu tìm hiểu Javascript.

Nguồn tham khảo

  1. https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/