Thứ ba, 15/09/2020 | 00:00 GMT+7

Hiểu Hoisting trong JavaScript

Trong hướng dẫn này, ta sẽ tìm hiểu cách cơ chế kéo nổi tiếng xảy ra trong JavaScript. Trước khi đi sâu vào, ta hãy tìm hiểu kỹ thuật nâng hạ là gì.

Hoisting là một cơ chế JavaScript nơi các biến và khai báo hàm được chuyển lên đầu phạm vi của chúng trước khi thực thi mã.

Không thể tránh khỏi, điều này nghĩa là dù hàm và biến được khai báo ở đâu, chúng đều được chuyển lên đầu phạm vi của chúng dù phạm vi của chúng là toàn cục hay local .

Tuy nhiên, lưu ý là cơ chế nâng chỉ di chuyển phần khai báo. Các bài tập được giữ nguyên.

Nếu bạn đã bao giờ tự hỏi tại sao bạn có thể gọi các hàm trước khi viết chúng vào mã của bạn , thì hãy đọc tiếp!

undefined so với ReferenceError

Trước khi bắt đầu một cách nghiêm túc, ta hãy cân nhắc về một vài điều.

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

Điều này đưa ta đến điểm lưu ý đầu tiên:

Trong JavaScript, một biến chưa khai báo được gán giá trị không xác định khi thực thi và cũng thuộc loại không xác định.

Điểm thứ hai của ta là:

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

Trong JavaScript, một ReferenceError được đưa ra khi cố gắng truy cập vào một biến chưa được khai báo trước đó.

Hành vi của JavaScript khi xử lý các biến trở nên có nhiều sắc thái do quá trình lưu trữ. Ta sẽ xem xét vấn đề này sâu hơn trong các phần tiếp theo.

Các biến số

Sau đây là vòng đời JavaScript và chỉ báo về trình tự trong đó việc khai báo và khởi tạo biến xảy ra.

biến-cẩu

Tuy nhiên, vì JavaScript cho phép ta khai báo và khởi tạo các biến đồng thời, đây là mẫu được sử dụng nhiều nhất:

var a = 100; 

Tuy nhiên, điều quan trọng cần nhớ là ở chế độ nền, JavaScript khai báo một cách tôn giáo sau đó khởi tạo các biến của ta .

Như ta đã đề cập trước đây, tất cả các khai báo biến và hàm đều được đưa lên đầu phạm vi của chúng. Tôi cũng nên thêm rằng các khai báo biến được xử lý trước khi bất kỳ mã nào được thực thi.

Tuy nhiên, ngược lại, các biến chưa được khai báo không tồn tại cho đến khi mã gán chúng được thực thi.
Do đó, việc gán giá trị cho một biến chưa được khai báo sẽ ngầm tạo ra nó như một biến toàn cục khi việc gán được thực thi. Điều này nghĩa là , tất cả các biến chưa được khai báo đều là các biến toàn cục.

Để chứng minh hành vi này, hãy xem phần sau:

function hoist() {   a = 20;   var b = 100; }  hoist();  console.log(a);  /*  Accessible as a global variable outside hoist() function Output: 20 */  console.log(b);  /* Since it was declared, it is confined to the hoist() function scope. We can't print it out outside the confines of the hoist() function. Output: ReferenceError: b is not defined */ 

Vì đây là một trong những điểm bất thường về cách JavaScript xử lý các biến, nên luôn khai báo các biến dù chúng nằm trong phạm vi hàm hay toàn cục . Điều này mô tả rõ ràng cách trình thông dịch sẽ xử lý chúng tại thời điểm chạy.

ES5

var

Phạm vi của một biến được khai báo với từ khóa varngữ cảnh thực thi hiện tại của nó. Đây là hàm bao quanh hoặc cho các biến được khai báo bên ngoài bất kỳ hàm nào, toàn cục .
Hãy xem một vài ví dụ để xác định điều này nghĩa là gì:

biến toàn cục

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

Ta mong đợi kết quả của log là: ReferenceError: hoist is not defined , nhưng thay vào đó, kết quả của nó là undefined .

Tại sao điều này đã xảy ra?

Khám phá này đưa ta đến gần hơn với việc quấn lấy con mồi của bạn .

JavaScript đã nâng khai báo biến. Đây là mã ở trên trông như thế nào đối với trình thông dịch:

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

Do đó, ta có thể sử dụng các biến trước khi khai báo chúng. Tuy nhiên, ta phải cẩn thận vì biến được khởi tạo với giá trị không xác định.
Lựa chọn tốt nhất là khai báo và khởi tạo biến của ta trước khi sử dụng.

Biến phạm vi hàm

Như ta đã thấy ở trên, các biến trong phạm vi toàn cục được đưa lên đầu phạm vi. Tiếp theo, hãy xem cách các biến trong phạm vi hàm được lưu trữ.

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

Hãy phỏng đoán có học thức về kết quả của ta có thể là gì.

Nếu bạn đoán, undefined bạn đúng. Nếu bạn không, đừng lo lắng, ta sẽ sớm đi đến tận cùng vấn đề này.

Đây là cách trình thông dịch xem đoạn mã trên:

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

Khai báo biến, var message có phạm vi là hàm hoist() , được đưa lên đầu hàm.

Để tránh cạm bẫy này, ta phải đảm bảo khai báokhở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! 

chế độ nghiêm ngặt

Nhờ một tiện ích của version JavaScript es5 được gọi là chế độ nghiêm ngặt, ta có thể cẩn thận hơn về cách khai báo các biến của bạn .
Bằng cách bật chế độ nghiêm ngặt , ta chọn tham gia một biến thể JavaScript bị hạn chế sẽ không chấp nhận việc sử dụng các biến trước khi chúng được khai báo.

Chạy mã của ta ở chế độ nghiêm ngặt:

  1. Loại bỏ một số lỗi JavaScript im lặng bằng cách thay đổi chúng thành các lỗi ném rõ ràng sẽ được trình thông dịch loại bỏ.
  2. Sửa các lỗi khiến các công cụ JavaScript khó thực hiện tối ưu hóa.
  3. Cấm một số cú pháp có thể được xác định trong các version JavaScript trong tương lai.

Ta bật chế độ nghiêm ngặt bằng cách đặt trước file hoặc chức năng của ta với

'use strict';  // OR "use strict"; 

Hãy kiểm tra nó ra.

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

Ta có thể thấy rằng thay vì giả định ta đã bỏ lỡ việc khai báo biến của bạn , use strict đã ngăn ta theo dõi bằng cách ném ra một Reference error một cách rõ ràng. Hãy thử nó mà không cần sử dụng nghiêm ngặt và xem điều gì sẽ xảy ra.

Tuy nhiên, chế độ nghiêm ngặt hoạt động khác nhau trong các trình duyệt khác nhau, vì vậy bạn nên thực hiện kiểm tra tính năng kỹ trước khi dựa vào nó trong production .

ES6

ECMAScript 6 , ECMAScript 2015 còn gọi là ES6 là version mới nhất của tiêu chuẩn ECMAScript, như bài viết này, tháng 1 năm 2017 và giới thiệu một số thay đổi đối với es5.

Mối quan tâm của ta là những thay đổi trong tiêu chuẩn ảnh hưởng như thế nào đến việc khai báo và khởi tạo các biến JavaScript.

để cho

Trước khi ta bắt đầu, cần lưu ý các biến được khai báo với từ khóa let là phạm vi khối chứ không phải phạm vi hàm. Điều đó quan trọng, nhưng nó không nên làm phiền ta ở đây. Tuy nhiên, ngắn gọn thì nó chỉ nghĩa là phạm vi của biến bị ràng buộc với khối mà nó được khai báo chứ không phải hàm mà nó được khai báo.

Hãy bắt đầu bằng cách xem xét hành vi của từ khóa let .

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

Giống như trước đây, đối với từ khóa var , ta mong đợi kết quả của log là undefined . Tuy nhiên, kể từ khi let es6 không mất lòng về ta sử dụng các biến chưa được khai báo, người phiên dịch rõ ràng spits ra một Reference lỗi.

Điều này đảm bảo ta luôn khai báo các biến của bạn trước.

Tuy nhiên, ta vẫn phải cẩn thận ở đây. Việc triển khai như sau sẽ dẫn đến Reference error undefined thay vì Reference error .

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

Do đó, để thận trọng hơn, ta nên khai báo sau đó gán các biến của bạn cho một giá trị trước khi sử dụng chúng.

hăng sô

Từ khóa const được giới thiệu trong es6 để cho phép các biến bất biến . Đó là, các biến có giá trị không thể được sửa đổi sau khi được gán.

Với const , cũng giống như let , biến được nâng lên trên cùng của khối.

Hãy xem điều gì sẽ xảy ra nếu ta cố gắng gán lại giá trị được gắn vào một biến const .

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

Khai báo biến const thay đổi như thế nào? Ta hãy xem xét.

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

Giống như từ khóa let , thay vì thoát âm thầm với một từ khóa undefined , trình thông dịch sẽ cứu ta bằng cách đưa Reference error một cách rõ ràng.

Điều tương tự cũng xảy ra khi sử dụng const trong các hàm.

function getCircumference(radius) {   console.log(circumference)   circumference = PI*radius*2;   const PI = 22/7; }  getCircumference(2) // ReferenceError: circumference is not defined 

Với const , es6 tiến xa hơn. Trình thông dịch ném ra một lỗi nếu ta sử dụng một hằng số trước khi khai báo và khởi tạo nó.

Người liên hệ của ta cũng nhanh chóng thông báo cho ta về trọng tội này:

PI was used before it was declared, which is illegal for const variables. 

Trên phạm vi global ,

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

Do đó, một biến hằng số phải được khai báo và khởi tạo trước khi sử dụng.


Như một phần mở đầu cho phần này, điều quan trọng cần lưu ý là thực sự, JavaScript chứa các biến được khai báo với es6 let và const. Sự khác biệt trong trường hợp này là cách nó khởi tạo chúng.
Các biến được khai báo với letconst vẫn chưa được khởi tạo khi bắt đầu thực thi trong khi các biến được khai báo với var được khởi tạo với giá trị không xác định .

Chức năng cẩu

Các hàm JavaScript có thể được phân loại lỏng lẻo như sau:

  1. Khai báo hàm
  2. Biểu thức hàm

Ta sẽ điều tra xem việc nâng hạ bị ảnh hưởng như thế nào bởi cả hai loại chức năng.

Khai báo hàm

Đây là những hình thức sau và được nâng hoàn toàn lên trên cùng. Bây giờ, ta có thể hiểu tại sao JavaScript cho phép ta gọi một hàm dường như trước khi khai báo nó.

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, biểu thức hàm không được nâng lên.

expression(); //Output: "TypeError: expression is not a function  var expression = function() {   console.log('Will this work?'); }; 

Hãy thử kết hợp khai báo hàm và biểu thức.

expression(); // Ouput: TypeError: expression is not a function  var expression = function hoisting() {   console.log('Will this work?'); }; 

Như ta có thể thấy ở trên, var expression khai báo biến được lưu trữ nhưng nó được gán cho một hàm thì không. Do đó, intepreter ném một TypeError vì nó coi expression là một biến chứ không phải một hàm .

Thứ tự ưu tiên

Điều quan trọng cần ghi nhớ khi khai báo các hàm và biến JavaScript.

  1. Phép gán biến được ưu tiên hơn khai báo hàm
  2. Khai báo hàm được ưu tiên hơn khai báo biến

Khai báo hàm được lưu trên các khai báo biến nhưng không được gán biến.

Ta hãy xem hành vi này có những tác động nào.

Phép gán biến trên khai báo hàm

var double = 22;  function double(num) {   return (num*2); }  console.log(typeof double); // Output: number 

Khai báo hàm trên khai báo biến

var double;  function double(num) {   return (num*2); }  console.log(typeof double); // Output: function 

Ngay cả khi ta đảo ngược vị trí của các khai báo, trình thông dịch JavaScript vẫn sẽ coi là một hàm double .

Các lớp nâng

Các lớp JavaScript cũng có thể được phân loại lỏng lẻo như sau:

  1. Khai báo lớp
  2. Biểu thức lớp

Khai báo lớp

Giống như các đối tác chức năng của chúng, các khai báo lớp JavaScript được lưu trữ. Tuy nhiên, chúng vẫn chưa được giám sát cho đến khi đánh giá.
Điều này nghĩa là bạn phải khai báo một lớp trước khi có thể sử dụng nó.

 var Frodo = new Hobbit(); Frodo.height = 100; Frodo.weight = 300; console.log(Frodo); // Output: ReferenceError: Hobbit is not defined  class Hobbit {   constructor(height, weight) {     this.height = height;     this.weight = weight;   } } 

Tôi chắc rằng bạn đã nhận thấy rằng thay vì nhận được một lỗi undefined ta nhận được Reference error . Bằng chứng đó khẳng định vị trí của ta rằng các khai báo lớp được lưu trữ.

Nếu bạn đang chú ý đến chiếc linter của bạn , nó sẽ cung cấp cho ta một mẹo hữu ích.

Hobbit was used before it is declared, which is illegal for class variables 

Vì vậy, cho đến khi khai báo lớp, để truy cập khai báo lớp, bạn phải khai báo trước.

class Hobbit {   constructor(height, weight) {     this.height = height;     this.weight = weight;   } }  var Frodo = new Hobbit(); Frodo.height = 100; Frodo.weight = 300; console.log(Frodo); // Output: { height: 100, weight: 300 } 

Biểu thức lớp

Giống như các đối tác hàm của chúng, các biểu thức lớp không được kéo lên.

Đây là một ví dụ với biến thể không được đặt tên hoặc ẩn danh của biểu thức lớp.

var Square = new Polygon(); Square.height = 10; Square.width = 10; console.log(Square); // Output: TypeError: Polygon is not a constructor  var Polygon = class {   constructor(height, width) {     this.height = height;     this.width = width;   } }; 

Đây là một ví dụ với một biểu thức lớp được đặt tên.

var Square = new Polygon(); Square.height = 10; Square.width = 10; console.log(Square); // Output: TypeError: Polygon is not a constructor   var Polygon = class Polygon {   constructor(height, width) {     this.height = height;     this.width = width;   } };  

Cách chính xác để làm điều đó là như sau:

var Polygon = class Polygon {   constructor(height, width) {     this.height = height;     this.width = width;   } };  var Square = new Polygon(); Square.height = 10; Square.width = 10; console.log(Square); 

Cảnh báo trước

Có một chút tranh luận được đưa ra về việc liệu các biến let, const và các lớp trong Javascript es6 có thực sự được nâng, được kéo gần hay không. Một số người cho rằng chúng thực sự được cẩu lên nhưng chưa được làm sạch trong khi một số người cho rằng chúng hoàn toàn không được cẩu.

Kết luận

Hãy tóm tắt những gì ta đã học được cho đến nay:

  1. Trong khi sử dụng es5 var , việc cố gắng sử dụng các biến chưa được khai báo sẽ dẫn đến việc biến được gán giá trị không xác định khi lưu trữ.
  2. Trong khi sử dụng es6 letconst , việc sử dụng các biến không được khai báo sẽ dẫn đến Lỗi tham chiếu vì biến vẫn chưa được khởi tạo khi thực thi.

Vì thế,

  1. Ta nên tạo thói quen khai báo và khởi tạo các biến JavaScript trước khi sử dụng.
  2. Sử dụng chế độ nghiêm ngặt trong JavaScript es5 có thể giúp hiển thị các biến chưa được khai báo.

Tôi hy vọng bài viết này sẽ là một phần giới thiệu tốt về khái niệm lưu trữ trong JavaScript và thúc đẩy sự quan tâm của bạn về sự tinh tế của ngôn ngữ JavaScript.


Tags:

Các tin liên quan

Lời hứa của JavaScript dành cho người giả
2020-09-15
Sao chép các đối tượng trong JavaScript
2020-09-15
Cách sử dụng API tìm nạp JavaScript để lấy dữ liệu
2020-09-15
Cách mã hóa và giải mã chuỗi với Base64 trong JavaScript
2020-09-15
Toán tử đơn nguyên JavaScript: Đơn giản và hữu ích
2020-09-15
5 Mẹo để Viết Điều kiện Tốt hơn trong JavaScript
2020-09-15
Hiểu Vòng lặp sự kiện, Gọi lại, Hứa hẹn và Không đồng bộ / Chờ đợi trong JavaScript
2020-09-10
Bốn phương pháp để tìm kiếm thông qua các mảng trong JavaScript
2020-09-09
Sử dụng phương thức Array.find trong JavaScript
2020-09-09
split () Phương thức chuỗi trong JavaScript
2020-09-09