Phụ lục

Di truyền nguyên mẫu

Ngoại trừ nullundefined, mỗi loại dữ liệu gốc đều có một nguyên mẫu, một trình bao bọc đối tượng tương ứng cung cấp các phương thức để làm việc với các giá trị. Khi một lệnh tìm kiếm phương thức hoặc thuộc tính được gọi trên một nguồn dữ liệu gốc, JavaScript sẽ gói dữ liệu gốc phía sau hậu trường và gọi phương thức này hoặc thực hiện hoạt động tra cứu thuộc tính trên đối tượng trình bao bọc.

Ví dụ: giá trị cố định dạng chuỗi không có phương thức riêng, nhưng bạn có thể gọi phương thức .toUpperCase() trên giá trị đó nhờ trình bao bọc đối tượng String tương ứng:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

Đây được gọi là tính kế thừa nguyên mẫu – kế thừa các thuộc tính và phương thức từ hàm khởi tạo tương ứng của một giá trị.

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object { … }

Bạn có thể tạo các dữ liệu nguyên gốc bằng cách sử dụng các hàm khởi tạo này, thay vì chỉ xác định chúng theo giá trị. Ví dụ: việc sử dụng hàm khởi tạo String sẽ tạo ra một đối tượng chuỗi chứ không phải giá trị cố định kiểu chuỗi: một đối tượng không chỉ chứa giá trị chuỗi mà còn chứa tất cả các thuộc tính và phương thức kế thừa của hàm khởi tạo.

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

Trong hầu hết trường hợp, các đối tượng thu được sẽ hoạt động như các giá trị mà chúng ta đã dùng để xác định. Ví dụ: mặc dù việc xác định một giá trị số bằng hàm khởi tạo new Number sẽ dẫn đến một đối tượng chứa tất cả các phương thức và thuộc tính của nguyên mẫu Number, nhưng bạn có thể sử dụng toán tử trên các đối tượng đó giống như đối với giá trị cố định dạng số:

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

Bạn rất hiếm khi cần sử dụng những hàm khởi tạo này, vì tính kế thừa nguyên mẫu tích hợp sẵn của JavaScript nghĩa là các hàm này không mang lại lợi ích thiết thực. Việc tạo dữ liệu nguyên gốc bằng hàm khởi tạo cũng có thể dẫn đến kết quả không mong muốn vì kết quả là một đối tượng chứ không phải là một giá trị cố định đơn giản:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

Điều này có thể khiến việc sử dụng các toán tử so sánh nghiêm ngặt trở nên phức tạp:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

Chèn dấu chấm phẩy tự động (ASI)

Trong khi phân tích cú pháp tập lệnh, trình thông dịch JavaScript có thể sử dụng một tính năng có tên là chèn dấu chấm phẩy tự động (ASI) để cố gắng sửa các bản sao của dấu chấm phẩy bị bỏ qua. Nếu trình phân tích cú pháp JavaScript gặp một mã thông báo không được cho phép, thì trình phân tích cú pháp JavaScript sẽ cố thêm dấu chấm phẩy trước mã thông báo đó để sửa lỗi cú pháp có thể xảy ra, miễn là một hoặc nhiều điều kiện sau đây xảy ra:

  • Mã thông báo đó được phân tách với mã thông báo trước đó bằng một dấu ngắt dòng.
  • Mã thông báo đó là }.
  • Mã thông báo trước đó là ) và dấu chấm phẩy được chèn sẽ là dấu chấm phẩy kết thúc của câu lệnh do...while.

Để biết thêm thông tin, hãy tham khảo các quy tắc của ASI.

Ví dụ: việc bỏ qua dấu chấm phẩy sau các câu lệnh sau sẽ không gây ra lỗi cú pháp do ASI:

const myVariable = 2
myVariable + 3
> 5

Tuy nhiên, ASI không thể giải thích cho nhiều câu lệnh trên cùng một dòng. Nếu bạn viết nhiều câu lệnh trên cùng một dòng, hãy nhớ phân tách các câu lệnh đó bằng dấu chấm phẩy:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

ASI là nỗ lực sửa lỗi, không phải là một loại linh hoạt cú pháp được tích hợp vào JavaScript. Hãy nhớ sử dụng dấu chấm phẩy khi thích hợp để bạn không dựa vào dấu chấm phẩy để tạo mã chính xác.

Chế độ nghiêm ngặt

Các tiêu chuẩn chi phối cách viết JavaScript đã phát triển vượt xa những tiêu chuẩn được cân nhắc trong thời gian đầu của ngôn ngữ này. Mọi thay đổi mới về hành vi dự kiến của JavaScript phải tránh gây ra lỗi trên các trang web cũ.

ES5 giải quyết một số vấn đề lâu dài về ngữ nghĩa JavaScript mà không làm hỏng các hoạt động triển khai hiện có bằng cách đưa ra "chế độ nghiêm ngặt", một cách chọn sử dụng một bộ quy tắc ngôn ngữ hạn chế hơn cho toàn bộ tập lệnh hoặc một hàm riêng lẻ. Để bật chế độ nghiêm ngặt, hãy sử dụng giá trị cố định kiểu chuỗi "use strict", theo sau là dấu chấm phẩy, trên dòng đầu tiên của tập lệnh hoặc hàm:

"use strict";
function myFunction() {
  "use strict";
}

Chế độ nghiêm ngặt ngăn chặn một số thao tác "không an toàn" hoặc tính năng không dùng nữa, gửi các lỗi rõ ràng thay cho các lỗi "ẩn" phổ biến và cấm sử dụng các cú pháp có thể xung đột với các tính năng ngôn ngữ trong tương lai. Ví dụ: các quyết định thiết kế ban đầu liên quan đến phạm vi biến (variable phạm vi) khiến các nhà phát triển có nhiều khả năng "thăm dò" nhầm phạm vi toàn cục khi khai báo một biến (bất kể ngữ cảnh có chứa nội dung gì) bằng cách bỏ qua từ khoá var:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

Môi trường thời gian chạy JavaScript hiện đại không thể khắc phục hành vi này mà không có nguy cơ làm hỏng bất kỳ trang web nào dựa trên hành vi này, dù là do nhầm lẫn hay cố ý. Thay vào đó, JavaScript hiện đại ngăn chặn việc này bằng cách cho phép nhà phát triển chọn chế độ nghiêm ngặt cho công việc mới và chỉ bật chế độ nghiêm ngặt theo mặc định trong bối cảnh các tính năng ngôn ngữ mới mà chúng sẽ không phá vỡ các triển khai cũ:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

Bạn phải viết "use strict" dưới dạng giá trị cố định kiểu chuỗi. Giá trị cố định của mẫu (use strict) sẽ không hoạt động. Bạn cũng phải đưa "use strict" vào trước bất kỳ mã có thể thực thi nào trong ngữ cảnh dự định. Nếu không, trình phiên dịch sẽ bỏ qua nó.

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

Tham chiếu, theo giá trị

Mọi biến, bao gồm cả các thuộc tính của đối tượng, tham số hàm và các phần tử trong một mảng, tập hợp hoặc bản đồ, đều có thể chứa giá trị nguyên gốc hoặc giá trị tham chiếu.

Khi một giá trị gốc được gán từ biến này cho biến khác, công cụ JavaScript sẽ tạo bản sao của giá trị đó và chỉ định giá trị đó cho biến.

Khi bạn chỉ định một đối tượng (thực thể lớp, mảng và hàm) cho một biến, thay vì tạo bản sao mới của đối tượng đó, biến đó sẽ chứa tham chiếu đến vị trí được lưu trữ của đối tượng trong bộ nhớ. Do đó, việc thay đổi đối tượng được tham chiếu bởi một biến sẽ làm thay đổi đối tượng đang được tham chiếu, chứ không chỉ giá trị trong biến đó. Ví dụ: nếu bạn khởi tạo một biến mới với một biến chứa tham chiếu đối tượng, sau đó sử dụng biến mới để thêm một thuộc tính vào đối tượng đó, thì thuộc tính và giá trị của thuộc tính đó sẽ được thêm vào đối tượng ban đầu:

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

Điều này không chỉ quan trọng đối với việc thay đổi đối tượng mà còn đối với việc thực hiện các phép so sánh nghiêm ngặt, vì đẳng thức nghiêm ngặt giữa các đối tượng yêu cầu cả hai biến tham chiếu đến cùng một đối tượng để đánh giá đến true. Chúng không thể tham chiếu nhiều đối tượng, ngay cả khi các đối tượng đó có cấu trúc giống hệt nhau:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Phân bổ bộ nhớ

JavaScript sử dụng tính năng quản lý bộ nhớ tự động, nghĩa là bộ nhớ không cần được phân bổ hoặc giải phóng rõ ràng trong quá trình phát triển. Mặc dù thông tin chi tiết về phương pháp quản lý bộ nhớ của công cụ JavaScript nằm ngoài phạm vi của mô-đun này, nhưng việc hiểu cách phân bổ bộ nhớ sẽ cung cấp ngữ cảnh hữu ích để làm việc với các giá trị tham chiếu.

Có 2 "khu vực" trong bộ nhớ: "ngăn xếp" và "vùng nhớ khối xếp". Ngăn xếp lưu trữ dữ liệu tĩnh — giá trị gốc và thông tin tham chiếu đến đối tượng — vì có thể phân bổ không gian cố định cần thiết để lưu trữ dữ liệu này trước khi tập lệnh thực thi. Vùng nhớ khối xếp lưu trữ các đối tượng cần không gian được phân bổ động vì kích thước của các đối tượng đó có thể thay đổi trong quá trình thực thi. Bộ nhớ được giải phóng bằng một quá trình có tên là "gom rác", một quá trình xoá các đối tượng không có tham chiếu khỏi bộ nhớ.

Luồng chính

Về cơ bản, JavaScript là một ngôn ngữ đơn luồng với mô hình thực thi "đồng bộ", tức là chỉ có thể thực thi một tác vụ tại một thời điểm. Ngữ cảnh thực thi tuần tự này được gọi là luồng chính.

Luồng chính được các tác vụ khác của trình duyệt chia sẻ, chẳng hạn như phân tích cú pháp HTML, kết xuất và kết xuất lại các phần của trang, chạy ảnh động CSS và xử lý các hoạt động tương tác của người dùng từ đơn giản (như đánh dấu văn bản) đến phức tạp (như tương tác với các phần tử biểu mẫu). Các nhà cung cấp trình duyệt đã tìm ra cách tối ưu hoá các nhiệm vụ do luồng chính thực hiện, nhưng các tập lệnh phức tạp hơn vẫn có thể sử dụng quá nhiều tài nguyên của luồng chính và ảnh hưởng đến hiệu suất tổng thể của trang.

Bạn có thể thực thi một số tác vụ trong luồng trong nền được gọi là Trình chạy web, với một số hạn chế như sau:

  • Các luồng Worker chỉ có thể hoạt động trên các tệp JavaScript độc lập.
  • Các ứng dụng này đã bị giảm đáng kể hoặc không có quyền truy cập vào cửa sổ trình duyệt và giao diện người dùng.
  • Các lớp này bị hạn chế về cách giao tiếp với luồng chính.

Những hạn chế này lý tưởng cho các tác vụ tập trung, tốn nhiều tài nguyên mà có thể chiếm luồng chính.

Ngăn xếp lệnh gọi

Cấu trúc dữ liệu dùng để quản lý "ngữ cảnh thực thi" (mã đang được thực thi tích cực) là một danh sách có tên là ngăn xếp lệnh gọi (thường chỉ là "ngăn xếp"). Khi tập lệnh được thực thi lần đầu tiên, trình thông dịch JavaScript sẽ tạo "ngữ cảnh thực thi chung" và đẩy tập lệnh đó vào ngăn xếp lệnh gọi, cùng với các câu lệnh bên trong ngữ cảnh chung đó được thực thi lần lượt, từ trên xuống dưới. Khi trình phiên dịch gặp một lệnh gọi hàm trong khi thực thi ngữ cảnh chung, hệ thống sẽ đẩy "ngữ cảnh thực thi hàm" cho lệnh gọi đó lên đầu ngăn xếp, tạm dừng ngữ cảnh thực thi chung và thực thi ngữ cảnh thực thi hàm.

Mỗi lần một hàm được gọi, ngữ cảnh thực thi hàm cho lệnh gọi đó sẽ được đẩy lên đầu ngăn xếp, ngay phía trên ngữ cảnh thực thi hiện tại. Ngăn xếp lệnh gọi hoạt động trên cơ sở "vào trước, ra trước", nghĩa là lệnh gọi hàm gần đây nhất (cao nhất trong ngăn xếp) sẽ được thực thi và tiếp tục cho đến khi được giải quyết. Khi hàm đó hoàn tất, trình thông dịch sẽ xoá hàm khỏi ngăn xếp lệnh gọi và ngữ cảnh thực thi chứa lệnh gọi hàm đó sẽ trở thành mục cao nhất trong ngăn xếp và tiếp tục thực thi.

Các ngữ cảnh thực thi này thu thập mọi giá trị cần thiết để thực thi. Các thuộc tính này cũng thiết lập các biến và hàm có sẵn trong phạm vi của hàm dựa trên ngữ cảnh gốc, đồng thời xác định và đặt giá trị của từ khoá this trong ngữ cảnh của hàm.

Vòng lặp sự kiện và hàng đợi gọi lại

Việc thực thi tuần tự này có nghĩa là các tác vụ không đồng bộ có chứa hàm gọi lại (chẳng hạn như tìm nạp dữ liệu từ máy chủ, phản hồi hoạt động tương tác của người dùng hoặc chờ bộ tính giờ được đặt bằng setTimeout hoặc setInterval) sẽ chặn luồng chính cho đến khi tác vụ đó hoàn tất hoặc đột ngột làm gián đoạn ngữ cảnh thực thi hiện tại tại thời điểm ngữ cảnh thực thi của hàm gọi lại được thêm vào ngăn xếp. Để giải quyết vấn đề này, JavaScript quản lý các tác vụ không đồng bộ bằng cách sử dụng "mô hình đồng thời" theo hướng sự kiện tạo thành "vòng lặp sự kiện" và "hàng đợi gọi lại" (đôi khi còn gọi là "hàng đợi thông báo").

Khi thực thi một tác vụ không đồng bộ trên luồng chính, ngữ cảnh thực thi của hàm gọi lại sẽ được đặt trong hàng đợi gọi lại, không ở đầu ngăn xếp lệnh gọi. Vòng lặp sự kiện là một mẫu đôi khi được gọi là phản hồi. Quá trình này liên tục thăm dò trạng thái của ngăn xếp lệnh gọi và hàng đợi gọi lại. Nếu có các tác vụ trong hàng đợi gọi lại và vòng lặp sự kiện xác định rằng ngăn xếp lệnh gọi trống, thì các tác vụ trong hàng đợi gọi lại sẽ được đẩy lần lượt vào ngăn xếp để được thực thi.