12/08/2018, 17:53

Javascript - Hỏi đáp về javascript [Phần 2]

Introduction Quay trở lại với series hỏi đáp về javascript, hôm nay chúng ta sẽ đi qua các câu hỏi, các khái niệm về scope , lexical scope , hoisting , function , IIFE , function expression , function declaration , closure . Let's get started. Scope C10. Scope là gì? Scope là tập ...

Introduction

Quay trở lại với series hỏi đáp về javascript, hôm nay chúng ta sẽ đi qua các câu hỏi, các khái niệm về scope, lexical scope, hoisting, function, IIFE, function expression, function declaration, closure. Let's get started.

Scope

C10. Scope là gì?

Scope là tập các rules để lưu trữ variable sao cho ta có thể truy xuất nó sau này. Hơi khó hiểu phải không, bạn cứ tưởng tượng thế này. Scope chính là các đất nước trên thế giới, mỗi đất nước đều có tập các rules để phân biệt với nhau (biên giới, tọa độ, quốc kì, ...), khi này người dân (variables) thỏa mãn các điều kiện (rules) thì sẽ thuộc đất nước đó. Nhưng trong đất nước thì cũng có các scope nhỏ hơn, ví dụ Hà Nội chẳng hạn, một người thuộc scope Hà Nội (người Hà Nội) thì cũng thuộc scope Việt Nam (vì Hà Nội nằm trong Việt Nam). Nhưng nếu mà lỡ một variable nào đó lạc sang scope Trung Quốc (chả liên quan gì Việt Nam ngoài thi thoảng sang xin tí đất), thì javascript sẽ tìm cách báo lỗi: "Có thằng vượt biên, có thằng vượt biên".

Mỗi ngôn ngữ thì sẽ implement một loại scope (như mình nói ở trên, là tập các quy tắc, rules) khác nhau. Ví dụ Việt Nam và Trung Quốc sử dụng chung 1 scope có 1 rule là "cấm vượt biên" nên nếu có variable vượt biên sẽ gây lỗi, nhưng Châu Âu thì lại implement 1 scope khác, "Sang thoải con gà mái rule", nên khi vượt biên thì sẽ không bị gì. Javascript và đa phần các ngôn ngữ đều sử dụng lexical scope. Vậy thì ...

C11. Thế nào là lexical scope?

Tôi tin là khi mà các bạn đi phỏng vấn hay đọc các bài post về javascript, thi thoảng các bạn sẽ gặp khái niện này: lexical scope. Muốn hiểu khái niệm này thì cần đi sâu hơn vào cách javascript compile và transpile code một chút.

Khi compile code, javascript engine sẽ trải qua 3 giai đoạn

  1. Tokenizing / Lexing: chuyển string (code) thành các tokens. Ví dụ statement sau var a = 2; sẽ được chuyển thành var, a, =, 2, ;
  2. Parsing: chuyển các tokens thành một AST (Abstract syntax tree) đại diện cho cấu trúc ngữ nghĩa của chương trình. Ví dụ từ tập các tokens trên, tôi sẽ build ra 1 tree có root node là VariableDeclaration, nó sẽ có 2 child node là Identifier có value là a và AssignmentExpression, node này có 1 node con là NumericLiteral có value là 2.
  3. Code - Generation: chuyển AST thành executable code. var a = 2 sẽ được chuyển thành các machine instructions: tạo variable a, assign cho giá trị = 2, rồi lưu vào memory.

Lúc trước tôi có nói compiler của javascript không giống các ngôn ngữ compiled thông thường, và ngoài 3 bước trên thì nó còn thực hiện thêm 1 vài bước mà chỉ chúa mới biết. Nhưng trọng tâm là giải thích cho các bạn hiểu về lexical scope nên tôi không đề cập tới. Vậy là ta hiểu thêm 1 chút về compiler và các steps của nó, bạn có thấy quen quen không. lexical scope , lexing. Vâng, chuẩn cmnr. lexical scope chính là scope được quyết định ở giai đoạn lexing, lúc compile, chứ không phải lúc execute. Và để dễ hiểu hơn, lexical scope là scope mà ta chỉ cần nhìn nó thế nào, thì scope của nó là thế đấy. Ví dụ:

var a = 2;
function print() {
    console.log(a); // 2
    var b = 100;
}
print();
console.log(b); //ReferenceError: b is not defined

Làm thế nào để không chạy mà cũng biết nó bị lỗi, hãy nghe tôi: NHÌN, vâng, chỉ cần nhìn thôi, không cần biết lúc chạy code thế nào, miễn là bạn code nó như thế, nhìn nó như thế, thì scope của nó sẽ là vậy. var a = 2; tức là nó ở global scope, thế thì dùng nó ở đâu mà chả được, còn var b = 100; nằm ở scope của function print, hay còn gọi là local variable. Nhưng chúng ta lại có ý định dùng nó ở scope bên ngoài. ERRRRRRRRRR - Vượt biên - Báo lỗi.

C12. Áp dụng kiến thức về lexical scope. Đoạn code sau in ra gì:

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

function bar() {
    var a = 3;
    foo();
}

var a = 'Ahihi';
bar();

=> Đoạn code sẽ in ra 2. Và nếu ta bỏ đoạn code var a = 2; trong function foo thì nó sẽ in ra ... Ahihi. Có lẽ, tôi cũng không cần phải giải thích nhỉ.

C13. Thế nào là hoisting?

Nhắc tới, scope, lexical scope mà quên mất hoisting là không được. Nhưng trước khi giải thích khái niệm hoisting. Tôi sẽ giải đáp câu C9 của post trước. Câu ấy như sau:

C9. Đoạn code sau in ra gì:

console.log(a);
var a = 100;

Các bạn làm về javascript được bao lâu rồi, nếu trên 1 năm mà trả lời là 100 thì xin lỗi, các bạn nên CHẠY NGAY ĐI, nộp đơn xin nghỉ việc đi là vừa. Đùa thôi, thi thoảng bị hỏi lại câu này, tôi vẫn trả lời là Error chứ còn lăn tăn cm gì nữa. Thực ra đáp án của nó là: => undefined Tại sao lại là undefined?? Hãy đọc lại post trước của tôi, 3 trường hợp undefined, bạn nghĩ câu C9 rơi vào trường hợp nào mà lại ra undefined. Là trường hợp đầu tiên, biến a đã được khai báo, nhưng chưa được assign value, nên nó giữ giá trị undefined. wtf, khai báo lúc sau mà. Chắc các bạn đang thốt lên vậy đúng không.

Trong javascript tồn tại một khái niệm là hoisting. Miễn là ta khai báo variable, nó sẽ được sử dụng ở bất cứ đâu trong lexical scope. Đó, rõ ràng chưa, ta đã khai báo var a = 100; ngay sau lệnh console.log(a);. Điều này cũng tương tự việc dù bạn đang sống ở Sài Gòn, nhưng vẫn đăng kí được sổ hộ khẩu Hà Nội vậy. Có thể các bạn sẽ thấy ngoài tác dụng gây thêm vài cái bug thì nó không được tích sự gì cả. Đây cũng chính là lý do, trong ES6, nếu các bạn khai báo với let hoặc const thì hoisting không còn tạc dụng nữa

console.log(a); //ReferenceError
let a = 'ahihi'; // the same with const

C14. Function scope vs Block scope?

Tôi đã có nhắc về sự khác biệt này trong một bài về ES6 - The Good Part (Phần 1). Scope trong javascript là Function scope. Tức là một khi bạn khai báo một variable trong function, scope cuả nó sẽ là nằm bên trong function đấy (Như kiểu sinh ra ở Việt Nam thì là dân Việt Nam luôn ấy). Trong ES6 thì người ta thêm 1 rule nữa, tạo ra 1 scope là Block scope, miễn là bạn khai báo variable giữa 2 giấu ngoặc nhọn { } thì scope của nó là nằm trong 2 dấu ngoặc ấy. Điều này sẽ tốt cho bạn khi khai báo ở trong if, while, for, tên biến sẽ không bị pollute ra bên ngoài, mà lại tốt cho việc garbage collection.

function run() {
    if (100 > 20) {
        var m = 100;
        console.log(m, "meters !!");
    }
    console.log("you ran ", m, "meters");
}
run();  //100meters !! 
            //you ran 100 meters;

Tôi đã đảo thứ tự khai báo variable m đi một chút, nhưng function run() vẫn hoạt động êm ru, sao lại thế? Bởi var là function scope, nên dù khai báo bên trong mệnh đề if nó vẫn được vô tư sử dụng bên ngoài, miễn là còn nằm trong function (không thể hư cấu hơn             </div>
            
            <div class=

0