Trang chủ Development

Scope, Closure, This và tổ chức bộ nhớ trong Javascript

Đối với những bạn lập trình Web nói chung và lập trình Javascript nói riêng thì những kiến thức về Scope, Closure là cần phải nắm rõ. Với mình thì Javascrip là một ngôn ngữ lập trình khó, nếu không nắm rõ cách tổ chức bộ nhớ thì sẽ gặp phải những Bug rất nan giải.

Nhiều người ban đầu dùng Javascript thì sẽ thấy rất dễ, càng làm vào sâu hơn sẽ thấy phức tạp dần với các vấn đề khó trong Javascript là Scope, Closure hay từ khóa this. Về bản chất, các bạn chỉ cần nắm rõ Scope và một số chú ý là sẽ tránh được một số lỗi mà không biết vì sao lỗi.

Sau đây mình xin đi vào cụ thể từng mục:

1. Scope trong javascript là gì?

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? Đơn giản vì Javascrip 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 Javascrip đã 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");

}

}

Javascrip 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.

2. 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 messagevà 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.

- Function Invocation (Gọi thực thi hàm)

- Function Invocation xảy ra khi gọi một hàm nào đó bằng cách sử dụng dấu (). Cách gọi thực thi hàm như thế được gọi là Function Invocation Pattern.

Ví dụ:

var add = function(num1, num2) {

console.log(num1 num2);

}

add(2, 3); // 5

Lại xét ví dụ sau:

var createCallBack = function() { //First function

console.log("Here is in first function");

return function() { //Second function

console.log("Here is in second function");

return function() { //Third function

console.log("Here is in third function");

}

}

}

createCallBack;// caller 1

createCallBack();// caller 2

window.onload = createCallBack; // caller 3

window.onload = createCallBack(); // caller 4

window.onload = createCallBack()();// caller 5

Nhớ là gọi caller 1 tới caller 5 từng lượt một chứ không phải gọi cùng lúc như trên nhé.

Theo bạn tương ứng với mỗi câu lệnh caller 1 -> caller 5 thì kết quả in ra console là gì?. Nếu bạn thử kiểm tra từng câu lệnh trên bằng trình duyệt thì hãy gọi từng câu lệnh một chứ đừng gọi cả 5 câu lệnh cùng lúc như trên vì js là xử lý bất đồng bộ (assynchronus) nên sẽ không in ra đúng thứ tự cho bạn đâu.

• caller 1 sẽ chẳng in ra cho bạn cái gì cả, vì hàm createCallBack đã được gọi thực thi đâu.

• caller 2 sẽ in ra cho bạn "Here is in first function"

• caller 3 sẽ in ra cho bạn "Here is in first function". Nhiều người sẽ thắc mắc vì sao caller 1 và caller 3 trông giống nhau mà kết qủa lại khác nhau?

Caller 3 là hàm createCallBack được truyền vào dưới dạng callback của event (sự kiện trên trình duyệt) nên khi event đó (ở vd trên là window.onload) được trigger thì hàm callback truyền vào sẽ tự động được thực thi.

Mọi hàm callback được truyền vào event thì sẽ được tự động thực thi ngay khi event đó trigger.

• caller 4 sẽ in ra 2 dòng "Here is in first function" và "Here is in second function". Như bạn thấy caller 4 chỉ là thêm dấu () vào sau caller 3, nghĩa là nó thực thi caller 3 xong rồi thực thi tiếp hàm mà caller 3 trả về (vd trên là second function)

• caller 5 giải thích tương tự caller 4, kết qủa in ra là "Here is in first function", "Here is in second function" và "Here is in third function".

Thế còn thế này thì sao?:

var createCallBack = function() { //First function

console.log("first function");

return function() { //Second function

console.log("second function");

return function() { //Third function

console.log("third function");

}

}

}

window.onload = function() { createCallBack; }; // caller 6

window.onload = function() { createCallBack(); }; // caller 7

Nhắc lại là mọi hàm Callback được truyền vào event thì sẽ được tự động thực thi ngay khi event đó trigger.

Nghĩa là 2 function caller 6 và caller 7 trên đều được thực thi ngay khi window load, thực thi ngay ở đây nghĩa là thực thi nội dung trong dấu {} của 2 function truyền vào cho event window.onload.

• caller 6 sẽ không in ra cái gì cả vì hàm createCallBack không được gọi thực thi.

• caller 7 sẽ in ra "first function" vì hàm createCallBack được gọi thực thi bằng cách thêm dấu ()

3. Closures trong javascript là gì?

Closure là hàm có tham chiếu tới biến nằm ở scope ngoài hàm đó.

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.

Khi một closure được tạo nó sẽ có 2 thành phần là nội dung hàm (function body) và bối cảnh (context), context chính là nơi mà closure được tạo ra.

Nếu closure tham chiếu tới biến ở hàm cha thì context chính là scope của hàm cha, nếu closure tham chiếu tới hàm ông thì context chính là scope của hàm ông.

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 numcủa scope hàm say() thứ nhất. Khi biến num của context này thay đổi thì kết quả in ra màn hình cũng thay đổi theo.

Từ khóa "this" với Function Invocation.

Xét ví dụ:

var value = 500; //Global variable

var obj = {

value: 0,

increment: function() {

this.value ;

var innerFunction = function() {

alert(this.value);

}

innerFunction(); //Function invocation pattern

}

}

obj.increment(); //Method invocation pattern

Bạn nghĩ đoạn code trên sẽ alert ra kết qủa nào?

Gọi hàm kiểu Function invocation pattern (gọi trực tiếp bằng cách thêm dấu () ) thì từ khóa this trong hàm đó luôn là global object (window).

Vì vậy đoạn code trên sẽ alert ra 500 chứ không phải 1 như nhiều người nhầm lẫn.

Để khắc phục lỗi trên thì có một cách đơn giản là copy biến this vào that khi khai báo hàm và sử dụng biến that này.

var value = 500; //Global variable

var obj = {

value: 0,

increment: function() {

var that = this;

this.value ;// or that.value

var innerFunction = function() {

alert(that.value);

}

innerFunction(); //Function invocation pattern

}

}

obj.increment();// 1

4. Constructor Invocation Pattern.

Constructor Invocation Pattern là cách gọi hàm bằng cách thêm từ khóa new phía trước.

Ví dụ:

var createCallBack = function() { //First function

console.log("first function");

return new function() { //Second function

console.log("second function");

return function() { //Third function

console.log("third function");

}

}

}

window.onload = createCallBack;

Ví dụ trên giống với ví dụ ở phía trên với lời gọi caller 3. Nhưng lời gọi này ở phía trên chỉ in ra "first function" còn ở ví dụ này là "first function" và "second function". Vì sao?

Câu trả lời nằm ở từ khóa new đặt trước second function.

Từ khóa new chứng tỏ second function đã được gọi thực thi ngay (Constructor Invocation Pattern).

Ví dụ trên có thể được viết lại như sau:

var createCallBack = function() { //First function

console.log("first function");

var secondFunction = function() { //Second function

console.log("second function");

return function() { //Third function

console.log("third function");

}

}

return secondFunction();

}

window.onload = createCallBack;

Hàm được gọi bằng kỹ thuật Constructor Invocation Pattern (dùng từ khóa new) sẽ trả về:

• Nếu hàm có return các kiểu đơn như number, string, boolean, null hoặc undefined thì giá trị trả về sẽ bị bỏ đi và trả về this (là object được tạo ra từ từ khóa new).

• Nếu hàm có return là một object (là mọi thứ trừ các kiểu đơn), thì object này sẽ được return thay vì this.

Với ví dụ trên thì second function sẽ return third function.

Một số ví dụ kiểm tra.

Ví dụ 1:

function sayHello(name) {

var text = 'Hello ' name;

var say = function() { console.log(text); }

say();

}

sayHello('Joe'); // Hello Joe

Ví dụ 2:

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"

Ví dụ 3:


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

Ví dụ 4:

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

Ví dụ 5:

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 hoặc context 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]();

}

Ví dụ 6: 

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();

Ví dụ 7:

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;

VCCloud sưu tầm

Theo blog.daovanhung.com

>> Tìm hiểu thêm: Sự khác biệt của 2 cách sử dụng Resolve/Reject trong Promise