Tiếp nối bài Chinh phục HOF, Clorsures, Currying trong Javascript thì trong bài này chúng ta sẽ tìm hiểu về Scope và cơ chế Hoisting nổi tiếng trong Javascript. Cảnh báo trước là bài khá dài đấy, các bác thắt dây an toàn và lên đường nào.

1. Scope là gì

Scope là phạm vi  cho phép truy cập của biến. Javascript hiện nay có 3 loại Scope đó là Global Scope, Function Scope, Block Scope.

Global Scope

Biến được khai báo global (bên ngoài bất cứ function nào) thì có Global Scope

var company = "xdevclass";

// Code tại đây có thể xử dụng biến company

function myFunction() {
  // Code tại đây cũng có thể xử dụng biến company
}

Biến global có thể được truy cập từ bất cứ đâu trong chương trình Javascript

Function Scope

// Code ở đây không thể sử dụng biến carName

function myFunction() {
  var carName = "Volvo";
  // code ở đây CÓ THỂ sử dụng biến carName
}

// Code ở đây không thể sử dụng biến carName

Biến được khai báo cục bộ (bên trong một function) thì có Function Scope

Block Scope

Biến được khai báo với var không thể có Block Scope. Vì thế biến được khai báo trên trong block { } có thể được truy cập từ bên ngoài block

{
  var x = 2;
}
// x CÓ THỂ được dùng tại đây

Trước ES2015 (ES6) Javascript không có Block Scope.

Từ ES6 trở đi biến được khai báo bằng letconstBlock Scope. Vì thế các biến bên ngoài block { } không thể được truy cập bên ngoài block

{
  let x = 2;
}
// x KHÔNG THỂ truy cập tại đây

2. undefined vs ReferenceError

Trước khi bắt đầu thì hãy xem đoạn code này

console.log(typeof variable); // Output: undefined

Và chúng ta có lưu ý đầu tiên

Trong Javascript, một biến chưa được khai báo thì sẽ được gắn giá trị mặc định là undefined tại thời điểm thực thi code và type của nó là undefined

Và lưu ý thứ hai là

console.log(variable); // Output: ReferenceError: variable is not defined

Trong Javascript, Lỗi ReferenceError được quăng ra khi bạn cố truy cập vào một biến chưa được khai báo trước đó.

Vì sự đa dạng trong cách xử lý biến nên Javascript sẽ làm cho nhiều người bối rối. Chúng ta sẽ soi kĩ hơn ở phần dưới.

3. Hoisting là gì

Hoisting là một cơ chế Javascript nơi mà các biến và function khi khai báo sẽ được đưa lên trên cùng của scope trước khi code thực thi.

Điều này có nghĩa là bất kể các hàm và biến được khai báo ở đâu đi nữa thì chúng đều được chuyển lên đầu phạm vi của chúng.

Lưu ý là nó chỉ di chuyển khai báo. Còn việc gán giá trị thì vẫn giữ nguyên. Đây là lưu ý quan trọng, nếu bạn chưa hiểu thì hãy đọc tiếp!

4. Biến Hoisting

Một vòng đời bình thường của biến trong Javascript là
Khai báo => Gán giá trị => Sử dụng => Giải phóng

Ví dụ:

var a;
a = 100;
a + 30;

Tuy nhiên, vì Javascript cho phép chúng ta vừa khai báo vừa gián giá trị cho biến. Vậy nên đây là cách mà mọi người thường làm

var a = 100;

Vì Javascript đưa khai báo biến lên trên cùng nên ta sẽ có hiện tượng như thế này

console.log(hoist); // Output: undefined

var hoist = 'The variable has been hoisted.';

Có thể bạn dự đoán kết quả là ReferenceError: hoist is not defined vì chúng ta đang cố truy cập đến một biến mà chưa được khai báo trước đó nhưng thay vào đó là undefined .

Vậy điều gì đã xảy ra?

Javascript đã nâng khai báo biến lên trên cùng. Đây là những gì xảy ra bên trong đoạn code bên trên khi Javascript chạy

var hoist;

console.log(hoist); // Output: undefined
hoist = 'The variable has been hoisted.';

Nếu bạn thắc mắc đưa khai báo lên trên cùng, vậy trên cùng là ở đâu. Trên cùng là vị trí cao nhất của scope hiện tại. Ví dụ

function hoist() {
  console.log(message);
  var message='Hoisting is all the rage!'
}

hoist();

Thì sẽ tương đương như thế này

function hoist() {
  var message;
  console.log(message);
  message='Hoisting is all the rage!'
}

hoist(); // Ouput: undefined

Khai báo var message sẽ nằm trên cùng trong scope function hoist() .

Để tránh sự tự do đến khó hiểu này, hãy khai báo và khởi tạo biến trước khi sử dụng

function hoist() {
  var message='Hoisting is all the rage!'
  return (message);
}

hoist(); // Ouput: Hoisting is all the rage!

Bonus thêm: Tất cả các biến không được khai báo sẽ không tồn tại. Nhưng khi gán giá trị cho một biến không được khai báo, nó sẽ trở thành biến global.

function hoist() {
  a = 20;
  var b = 100;
}
hoist();
console.log(a); 
/* 
Có thể truy cập a như một biến global bên ngoài function hoist()
Output: 20
*/

console.log(b); 
/*
Vì đã được khai báo biến, nó sẽ được đẩy khai báo lên trên cùng của scope function hoist().
Vì thế biến b bị giới hạn bên trong scope nên chúng ta không thể truy cập từ bên ngoài
Output: ReferenceError: b is not defined
*/

Vì sự phức tạp này mà ES5 ra đời một chế độ có tên là strict mode

5. Strict Mode

Một chức năng khá hay của ES5 là strict mode ( tức là chế độ nghiêm ngặt ). Chế độ này yêu cầu chúng ta cần phải khai báo biến trước khi sử dụng.

Để bật tính năng này, chúng ta chỉ cần thêm một đoạn string vào file hoặc function

'use strict';

// OR
"use strict";

Khám phá nhé

"use strict";

hoist = "Hoisted";
console.log(hoist); // Output: ReferenceError: hoist is not defined

Chúng ta có thể thấy nếu quên việc khai báo biến hoist thì Javascript sẽ báo lỗi ngay Reference error . Nếu không dùng use strict thì code vẫn chạy và bạn sẽ log ra là Hoisted như bình thường.

Lưu ý là use strict mode hoạt động khác nhau ở các trình duyệt khác nhau. Hãy cẩn thận khi dùng use strict nhé.

6. Hàm Hoisting

Hàm trong Javascript có thể được chia ra làm 2 loại là

  1. Khai báo hàm (Function Declaration)
  2. Biểu thức hàm (Function Expression)

Khai báo hàm

Với cách này thì function được Hoisting. Và Javascript cho phép chúng ta gọi một hàm trước khi hàm đó được khai báo.

hoisted(); // Output: "This function has been hoisted."

function hoisted() {
  console.log('This function has been hoisted.');
};

Biểu thức hàm

Tuy nhiên với cách khai báo function kiểu này thì sẽ không được hoisting

expression(); //Output: "TypeError: expression is not a function

var expression = function() {
  console.log('Will this work?');
};

Hãy thử cách này xem

expression(); // Ouput: TypeError: expression is not a function

var expression = function hoisting() {
  console.log('Will this work?');
};

Giải thích: Biến var expression vẫn được hoisting và được đẩy lên trên cùng của scope nhưng chỉ là khai báo mà thôi, nó không được gán cho hàm! Vì thế nó sẽ ném ra lỗi TypeError .

7. Thứ tự Hoisting

Đây là điều quan trọng khi khai báo các hàm và biến trong Javascript. Thứ tự Hoisting được sắp xếp giảm dần như dưới đây.

  1. Khai báo biến
  2. Khai báo hàm
  3. Gán biến

Ví dụ hàm double được Hoisting lên cùng, ngay sau đó là phép gán biến double. Vì thế biến double sẽ đè lên hàm double và cuối cùng type của doublenumber.

var double = 22;

function double(num) {
  return (num*2);
}

console.log(typeof double); // Output: number

Ví dụ dưới đây cho thấy khai báo hàm được Hoisting dưới khai báo biến. Vì thế type của double sẽ là number

var double;

function double(num) {
  return (num*2);
}

console.log(typeof double); // Output: function

Dù cho bạn có đảo ngược vị trí khai báo trong code đi chăng nữa thì trình thông dịch Javascript vẫn cho ra double là một function

Haiz…. Bạn đã quá mệt mỏi và đau đầu với Hoisting chưa. Nếu rồi thì ta sẽ đi đến ES6 – sự giải cứu chúng ta khỏi các vấn đề nhọc nhằn mà Hoisting mang lại cũng như phiên bản cũ của Javascript gây ra.

8. ES6

ECMAScript 6,  ECMAScript 2015 còn được gọi là ES6. Đây là phiên bản Javascript có nhiều cải tiến, nhất là tiêu chuẩn trong việc khai báo và khởi tạo biến.

let

Trước khi bắt đầu, lưu ý là các biến được khai báo bằng từ khóa let sẽ thuộc Block Scope, không thuộc Function Scope. Vì thế phạm vi biến sẽ bị ràng buộc trong block chứ không phải trong hàm.

Ví dụ dưới đây cho thấy sự chặt chẽ của let

console.log(hoist); // Output: ReferenceError: hoist is not defined 
let hoist = 'The variable has been hoisted.';

Nếu ta dùng var để khai báo thì sẽ sẽ log ra undefined. Tuy nhiên với let,  es6 không cho phép chúng ta dùng biến mà chưa được khai báo, trình thông dịch sẽ báo lỗi Reference .

Điều này đảm bảo rằng chúng ta luôn luôn khai báo các biến rồi mới được sử dụng.

Nếu bạn khai báo mà không gán giá trị thì sẽ cho ra kết quả là undefined. Lần này thì logic rồi 😀

let hoist;

console.log(hoist); // Output: undefined
hoist = 'Hoisted'

Nếu var cho phép khai báo lại thì letconst chỉ được khai báo 1 lần trong phạm vi của scope

var x = 1;
var x = 2;
console.log(x); // Output: 2
let y = 1;
let y = 2; // throws SyntaxError: Identifier y has already been declared
const z = 1;
const z = 2; // throws SyntaxError: Identifier z has already been declared

Quả là chặt chẽ phải không  😎

Lưu ý: Nếu bạn hỏi letconst có được Hoist hay không thì câu trả lời là có. letconst được Hoist nhưng bạn sẽ không thể truy cập chúng trước khi chúng thực sự được khai báo.

const

const là từ khóa được giới thiệu trong es6 cho phép khai báo hằng số bất biến. Ví thế biến sẽ không bị thi đổi giá trị dù thế nào đi nữa.

const PI = 3.142;
PI = 22/7; // Let's reassign the value of PI
console.log(PI); // Output: TypeError: Assignment to constant variable.

Nhưng hãy lưu ý với các object. Vì const không cho phép thay đổi giá trị mà biến tham chiếu đến, chứ không phải là các thuộc tính trong object.

const obj = {
  name: "Java"
};
obj.name = "Javascript";
console.log(obj.name); // Javascript

Lưu ý thêm nữa là khi khai báo với const bạn phải gán giá trị lúc khai báo.

const PI;
console.log(PI); // Ouput: SyntaxError: Missing initializer in const declaration
PI=3.142;

9. Tóm lại

  • Scope là từ khóa ám chỉ phạm vi hoạt động của biến. Có 3 loại Scope là Global Scope, Function Scope và Block Scope.
  • Hoisting là cơ chế của Javascript cho phép đưa tất cả các khai báo lên trên cùng của Scope.
  • Từ nay hãy dùng letconst thay cho var để tránh Hoisting lằng nhằng và giúp code chặc chẽ hơn.

Dài quá rồi .Thật sự nếu các bạn đã đọc đến đây mình tin rằng các bạn đã hiểu được rất rõ về HoistingScope rồi (bác nào chưa hiểu thì đọc lại lần nữa đi nhé :mrgreen: ). Cám ơn mọi người đã theo dõi, hẹn gặp lại tại phần tiếp theo với chủ đề lập trình hướng đối tượng trong Javascript. 😎

Tham khảo

Understanding Hoisting in JavaScript

JavaScript Let