Kế thừa nguyên mẫu
Ngoại trừ null
và undefined
, mỗi loại dữ liệu gốc đều có một mẫu gốc (prototype), một trình bao bọc đối tượng tương ứng cung cấp các phương thức để xử lý các giá trị. Khi một phương thức hoặc lượt tra cứu thuộc tính được gọi trên một đối tượng gốc, JavaScript sẽ gói đối tượng gốc ở chế độ nền và gọi phương thức hoặc thực hiện lượt tra cứu thuộc tính trên đối tượng trình bao bọc.
Ví dụ: một chuỗi cố định không có phương thức riêng, nhưng bạn có thể gọi phương thức .toUpperCase()
trên chuỗi đó 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à kiểu 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 gốc bằng các hàm khởi tạo này, thay vì chỉ xác định các dữ liệu đó theo giá trị của chúng. Ví dụ: việc sử dụng hàm khởi tạo String
sẽ tạo một đối tượng chuỗi, chứ không phải một chuỗi cố định: 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 các 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 các đối tượng đó. Ví dụ: mặc dù việc xác định 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ả 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 các toán tử số học trên các đối tượng đó giống như trên số cố định:
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 hiếm khi cần sử dụng các hàm khởi tạo này vì tính năng kế thừa nguyên mẫu tích hợp sẵn của JavaScript có nghĩa là các hàm khởi tạo này không mang lại lợi ích thực tế. Việc tạo các dữ liệu 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 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ể làm phức tạp việc sử dụng các toán tử so sánh nghiêm ngặt:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
Tự động chèn dấu chấm phẩy (ASI)
Trong khi phân tích cú pháp một 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 trường hợp bị thiếu dấu chấm phẩy. Nếu gặp một mã thông báo không được phép, trình phân tích cú pháp JavaScript sẽ cố gắng thêm dấu chấm phẩy trước mã thông báo đó để khắc phục lỗi cú pháp tiềm ẩn, miễn là một hoặc nhiều điều kiện sau đây là đúng:
- Mã thông báo đó được phân tách với mã thông báo trước đó bằng 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ệnhdo
…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 đây 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ể tính đến 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à một nỗ lực sửa lỗi, chứ không phải một loại tính linh hoạt về 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 phải 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 quản lý cách viết JavaScript đã phát triển vượt xa mọi thứ được xem xét trong quá trình thiết kế ban đầu của ngôn ngữ này. Mọi thay đổi mới đối với hành vi dự kiến của JavaScript đều phải tránh gây ra lỗi trong các trang web cũ.
ES5 giải quyết một số vấn đề lâu nay về ngữ nghĩa JavaScript mà không làm gián đoạn các phương thức triển khai hiện có bằng cách giới thiệu "chế độ nghiêm ngặt", một cách để chọn 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ố hành động "không an toàn" hoặc tính năng không dùng nữa, gửi lỗi rõ ràng thay vì lỗi "im lặng" phổ biến và cấm sử dụng 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 khiến nhà phát triển có nhiều khả năng "làm ô nhiễm" phạm vi toàn cục khi khai báo biến, bất kể ngữ cảnh chứa, bằng cách bỏ qua từ khoá var
:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
Các 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 vào hành vi đó, dù là do nhầm lẫn hay cố ý. Thay vào đó, JavaScript hiện đại ngăn chặn vấn đề 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 ngữ cảnh của các tính năng ngôn ngữ mới mà không làm hỏng các phương thứ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 chuỗi cố định.
Mã cố định mẫu (use strict
) sẽ không hoạt động. Bạn cũng phải thêm "use strict"
trước mọi mã có thể thực thi trong ngữ cảnh dự kiến. Nếu không, trình thông dịch sẽ bỏ qua.
(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
Theo tham chiếu, theo giá trị
Bất kỳ biến nào, 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ảng, tập hợp hoặc bản đồ, đều có thể chứa một giá trị gốc hoặc giá trị tham chiếu.
Khi một giá trị gốc được gán từ biến này sang biến khác, công cụ JavaScript sẽ tạo một bản sao của giá trị đó và gán giá trị đó cho biến.
Khi bạn chỉ định một đối tượng (phiên bản lớp, mảng và hàm) cho một biến, thay vì tạo một bản sao mới của đối tượng đó, biến sẽ chứa một tham chiếu đến vị trí lưu trữ của đối tượng trong bộ nhớ. Do đó, việc thay đổi đối tượng được tham chiếu bằng một biến sẽ thay đổi đối tượng được tham chiếu, chứ không chỉ thay đổi giá trị mà biến đó chứa. Ví dụ: nếu bạn khởi tạo một biến mới bằng 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 quan trọng đối với việc thực hiện các phép so sánh nghiêm ngặt, vì sự bằng nhau nghiêm ngặt giữa các đối tượng đòi hỏi cả hai biến phải tham chiếu đến cùng một đối tượng để đánh giá thành true
. Các đối tượng này không thể tham chiếu đến các đối tượng khác nhau, ngay cả khi các đối tượng đó giống hệt nhau về cấu trúc:
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 một cách rõ ràng trong quá trình phát triển. Mặc dù thông tin chi tiết về các 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 để xử lý các giá trị tham chiếu.
Có hai "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 – các giá trị gốc và tham chiếu đến đối tượng – vì dung lượng cố định cần thiết để lưu trữ dữ liệu này có thể được phân bổ 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 này có thể thay đổi trong quá trình thực thi. Bộ nhớ được giải phóng bằng một quy trình có tên là "thu gom rác". Quy trình này sẽ 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 có mô hình thực thi "đồng bộ", nghĩa là mỗi lần chỉ có thể thực thi một tác vụ. Ngữ cảnh thực thi tuần tự này được gọi là luồng chính.
Luồng chính được chia sẻ bởi các tác vụ khác của trình duyệt, 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ư làm nổi bật 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 tác 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.
Một số tác vụ có thể được thực thi trong các luồng trong nền có tên là Trình chạy web, với một số hạn chế:
- Luồng worker chỉ có thể hoạt động trên các tệp JavaScript độc lập.
- Các lỗi này làm giảm nghiêm trọng hoặc không cho phép truy cập vào cửa sổ trình duyệt và giao diện người dùng.
- Các luồng 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 khiến chúng trở nên lý tưởng cho các tác vụ tập trung, tốn nhiều tài nguyên mà nếu không thì 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ý "bối 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ỉ gọi là "ngăn xếp"). Khi một tập lệnh được thực thi lần đầu tiên, trình thông dịch JavaScript sẽ tạo một "ngữ cảnh thực thi toàn cục" và đẩy ngữ cảnh đó vào ngăn xếp lệnh gọi, với các câu lệnh bên trong ngữ cảnh toàn cục đó được thực thi lần lượt, từ trên xuống dưới. Khi gặp lệnh gọi hàm trong khi thực thi ngữ cảnh toàn cục, trình thông dịch 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 toàn cục và thực thi ngữ cảnh thực thi hàm.
Mỗi khi 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 theo nguyên tắc "vào sau, ra trước", nghĩa là lệnh gọi hàm gần đây nhất (ở vị trí cao nhất trong ngăn xếp) sẽ được thực thi và tiếp tục cho đến khi giải quyết xong. 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 cho quá trình thực thi. Các hàm 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 mẹ, đồ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ộ bao gồm các 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ộ hẹn 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 gián đoạn đột ngột 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" do sự kiện điều khiển, bao gồm "vòng lặp sự kiện" và "hàng đợi gọi lại" (đôi khi được gọi là "hàng đợi thông báo").
Khi một tác vụ không đồng bộ được thực thi 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, chứ không phải ở đầ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à trình phản ứng, 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 lệnh gọi lại. Nếu có tác vụ trong hàng đợi lệnh 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ụ từ hàng đợi lệnh gọi lại sẽ được đẩy vào ngăn xếp từng tác vụ một để thực thi.