Nội dung bài viết
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 let
và const
có Block 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à
- Khai báo hàm (Function Declaration)
- 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.
- Khai báo biến
- Khai báo hàm
- 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 double
là number
.
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ì let
và const
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 let
và const
có được Hoist hay không thì câu trả lời là có. let
và const
đượ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
let
vàconst
thay chovar
để 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ề Hoisting và Scope rồi (bác nào chưa hiểu thì đọc lại lần nữa đi nhé ). 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. 😎
Cảm ơn đã cho mình biết về hoisting trong js
Cảm ơn bác nhé 💪