JavaScript Patterns – Các khái niệm cơ bản

Bài viết này dành cho những lập trình viên đã có chút quen thuộc với ngôn ngữ lập trình JavaScript nói riêng cũng như những khái niệm cơ bản trong lập trình nói chung. Mục đích là cung cấp một lối tiếp cận với JavaScript “như nó vốn thế”, chứ không theo lối “giống như ở” một ngôn ngữ lập trình khác nào đó.

Bài viết này đặt vấn đề rằng chúng ta đang tiếp cận với JavaScript theo những phiên bản của chuẩn ES rất cổ xưa, 5, hay thậm chí là 3. Sẽ có một vài chi tiết không còn đúng. Nhưng không có nghĩa là những khái niệm trong bài viết này là vô ích, trên thực tế chúng là nền móng cho những khái niệm trong các chuẩn ES hiện đại.

Hướng đối tượng

JavaScript là ngôn ngữ lập trình hướng đối tượng, phần lớn những gì bạn bắt gặp trong mã JavaScript là đối tượng. Chỉ có năm kiểu nguyên thủy: number, string, boolean, nullundefined. Và ba kiểu đầu tiên có kiểu vỏ bọc ở dạng đối tượng của chúng.

Các hàm cũng là các đối tượng, chúng cũng có thuộc tính và phương thức.

Chỉ thị đơn giản nhất mà bạn thực hiện với một ngôn ngữ lập trình là khai báo một biến. Trong JavaScript, khi khai báo một biến chính là đang làm việc với một đối tượng. Thứ nhất, biến được khai báo sẽ trở thành một thuộc tính của một đối tượng được gọi là Activation Object (hoặc Global Object nếu biến đó được khai báo tại phạm vi toàn cục).

var a = "hello";
//window is the global VariableObject
window.a; //hello

Thứ hai, bản thân biến đó thật ra cũng rất giống với một đối tượng, nó có thuộc tính của nó, những thuộc tính quy định khả năng sửa được, xóa được, hay liệt kê được vào câu lệnh for..in. (tham khảo JavaScript Data_structures – Objects).

Bản thân đối tượng trong JavaScript không hề phức tạp. Chúng đơn thuần là tập hợp của các thuộc tính được đặt tên, một danh sách các cặp key – value, gần như tương đồng với mảng ở các ngôn ngữ lập trình khác. Value có thể là kiểu nguyên thủy, hoặc là đối tượng, cũng có nghĩa rằng có thể là hàm, trong trường hợp đó thuộc tính được gọi là phương thức.

Thuộc tính của đối tượng hầu như có thể được sửa, bất kỳ lúc nào. Bạn có thể tự do thêm, xóa, sửa đổi thuộc tính của mọi đối tượng. Nếu muốn bổ sung các quy chế và quyền hạn, bạn có thể sử dụng các mẫu thiết kế phù hợp để triển khai.

Và, điều cuối cùng, có hai loại đối tượng chính:

  • Native, được định nghĩa bởi chuẩn ES. Có thể là đối tượng xây dựng sẵn, chẳng hạn Array, Date; hoặc cũng có thể do lập trình viên chỉ thị tạo ra (var o = {};).
  • Host, được tạo ra bởi môi trường thực thi, chẳng hạn môi trường trình duyệt, với đối tượng window và các đối tượng DOM của nó.

Không có khái niệm Lớp

Bạn phải chấp nhận điều đó. Bạn đã học về khái niệm “lớp” ở đâu đó trong các ngôn ngữ khác, và bạn sẽ phải học một điều rằng JavaScript không có khái niệm về lớp và JaveScript chỉ có khái niệm về đối tượng.

Không có khái niệm lớp thì tốt thôi, bạn sẽ phải viết ít mã hơn. Bạn có thể tạo đối tượng mà không cần tạo lớp. Cứ thử nghĩ đến một trường hợp cần tạo ra một đối tượng rỗng ruột và sau đó định nghĩa dần các thuộc tính và phương thức cho nó, và thử nghĩ đến mã tương tự ở ngôn ngữ Java.

var o = {};

Luật của Gang of Fours viết trong cuốn Design Patterns: “ưu tiên cấu thành đối tượng thay vì kế thừa lớp” mang ý nghĩa rằng nếu bạn có thể tạo ra những đối tượng với các thành phần mà đang có sẵn xung quanh thì sẽ tốt hơn so với việc phải tạo một chuỗi phân cấp kế thừa cha-con dài đằng đẵng. JavaScript không có khái niệm lớp thành ra cấu thành đối tượng là việc duy nhất bạn có thể làm, và như thế thì tốt thôi.

Nguyên mẫu

JavaScript có khái niệm kế thừa, mặc dù vậy do không có khái niệm về lớp nên kế thừa chỉ đơn thuần là một cách để tái sử dụng mã. Kế thừa có thể được thực hiện bằng nhiều cách khác nhau, và một cách thông dụng là sử dụng Nguyên Mẫu (Prototype).

Nguyên mẫu là một đối tượng (không thể là cái gì khác được cả). Cơ bản thì mọi hàm (cũng là một đối tượng) mà bạn định nghĩa sẽ có một thuộc tính tên là prototype trỏ đến một đối tượng rỗng. Đối tượng này gần giống như đối tượng được tạo bằng cú pháp {} hay new Object(), ngoại trừ một chuyện là constructor của nó chính là hàm bạn định nghĩa, chứ không phải hàm Object() định nghĩa sẵn. Các thuộc tính và phương thức được thêm vào đối tượng prototype sẽ được sử dụng bởi những đối tượng thừa kế prototype.

Môi trường

Về cơ bản, trong lập trình, môi trường là tập hợp của tất cả những giá trị và hàm mà có thể được chương trình sử dụng. JavaScript cần một môi trường để có thể thực thi. Môi trường trình duyệt là một môi trường quen thuộc, nhưng không phải là duy nhất, chẳng hạn môi trường trong Node.

Phạm vi hoạt động

JavaScript sử dụng hàm để tạo ra cơ chế về phạm vi hoạt động của biến. Biến được tạo ra trong hàm thì là biến địa phương của hàm và không khả dụng tại phạm vi bên ngoài hàm. Bên cạnh đó, biến toàn cục là biến được tạo ra ở bên ngoài bất kỳ hàm nào, hoặc là biến được sử dụng mà không có bất kỳ lệnh khai báo nào.

Mỗi một môi trường thực thi JavaScript có một đối tượng toàn cục mà có thể được truy cập thông qua từ khóa this nằm bên ngoài bất kỳ hàm nào. Mỗi một biến toàn cục do bạn tạo ra sẽ trở thành một thuộc tính của đối tượng toàn cục này. Để thuận tiện hơn, các trình duyệt bổ sung cho đối tượng toàn cục một thuộc tính tên là window mà trỏ tới chính đối tượng đó.

this;
// Window...
window;
// Window...
this.window;
// Window...
o = {};
// {}
this.o;
// {}

Vấn đề của các biến toàn cục

Vấn đề của các biến toàn cục là chúng khả dụng trên phạm vi toàn bộ chương trình hay trên toàn trang web, và luôn có rủi ro xung đột tên gọi mà không hề có sự báo trước. Tên biến toàn cục mà bạn đặt ra có thể đã được một thư viện, mã quảng cáo, mã phân tích, mã theo dõi, mã của extension trình duyệt… sử dụng. Nếu bạn ghi đè tên biến, những chương trình đó sẽ gặp lỗi.

Rất nhiều các mẫu thiết kế cho JavaScript được tạo ra là để giảm thiểu số lượng các biến toàn cục mà bạn cần tạo ra. Và một trong số những cách quan trọng nhất là luôn khai báo biến với từ khóa khai báo (var, hay let ở các chuẩn ES mới).

Tại sao lại thế? Điều đó đến từ hai đặc điểm của JavaScript. Thứ nhất, bạn có thể sử dụng biến mà không cần khai báo. Thứ hai, bất kỳ biến nào được sử dụng mà không qua khai báo sẽ trở thành thuộc tính của đối tượng toàn cục. Vậy nên nếu không cẩn thận, bạn có thể sẽ tạo tao biến toàn cục mà không hề hay biết.

function sum(x, y) {
  result = x + y;
  return result;
}

sum (1, 1);
// 2

result;
// 2

Hàm sum ở trên đã có hiệu ứng phụ. Nó đã tạo thêm một biến toàn cục result. Để hạn chế biến toàn cục, từ khóa var cần phải được sử dụng:

function sum(x, y) {
  var result = x + y;
  return result;
}

sum (1, 1);
// 2

result;
// Uncaught ReferenceError: result is not defined

Một ví dụ khác có cơ chế hoàn toàn tương tự, nhưng khó nhận ra hơn:

function foo() {
  var a = b = 0;
  // ...
}

…cần phải được viết thành:

function foo() {
  var a, b;
  // ...
  a = b = 0;
}

Truy cập đối tượng toàn cục, và chuyện this này this nọ

Đối tượng toàn cục có thể được truy cập bằng cách gọi this ở bên ngoài bất kỳ hàm nào. Tại trình duyệt, đối tượng toàn cục cũng có thể truy cập qua biến toàn cục window. Và tồn tại một cách truy cập khác:

var global = (function() {
  return this;
})();

Cơ chế ở đây là, ở trong bất kỳ hàm nào được gọi ở trạng thái nó là một hàm (chứ không phải ở trạng thái một constructor với từ khóa new), từ khóa this luôn trỏ tới đối tượng toàn cục.

Hoisting – bẫy kéo

Nếu bạn từng nghe về một truyền thuyết của ngôn ngữ C, đó là tất cả các lệnh khai báo biến đều phải đặt ở phần trên cùng của hàm, thì đây chính là câu chuyện tương tự.

Trong JavaScript, tất cả các biến được tạo ra bởi từ khóa var, cho dù ở bất kỳ vị trí nào của hàm, cũng sẽ hoạt động y hệt như là được khai báo ở đầu hàm. Không nhận thức được cơ chế này có thể tạo ra rất nhiều lỗi logic khó hiểu cho bạn.

// antipattern
myname = "global"; // global variable
function f() {
  alert(myname); // "undefined"
  var myname = "local";
  alert(myname); // "local"
}
f();

Bạn mong muốn alert đầu tiên cho bạn global, nhưng không phải như thế. Câu lệnh var myname = "local" có thể coi như hai câu lệnh: var myname; myname = "local", và phần mã khai báo thì bị bẫy kéo lên đầu hàm. Hàm trên thực chất hoạt động như sau:

myname = "global"; // global variable
function f() {
  var myname;
  alert(myname); // "undefined"
  myname = "local";
  alert(myname); // "local"
}
f();

Tổng kết

Trên đây là những điểm lưu ý rất chung khi viết mã JavaScript. Bạn có thể tìm được những diễn giải chi tiết về mỗi vấn đề, nhưng về cơ bản chúng hoạt động đơn giản như được mô tả trong bài viết này.

Loading

2 Replies to “JavaScript Patterns – Các khái niệm cơ bản”

  1. Dear anh,

    Em là BBT từ trang Blog TopDev.

    Nhận thấy trang của mình có đăng tải những chia sẻ về kiến thức lập trình rất hữu ích, em xin phép được repost tại trang Blog để truyền tải rộng rãi hơn trong cộng đồng lập trình viên. Bài viết sẽ để tên tác giả và dẫn nguồn đầy đủ.

    Trong quá trình hợp tác, TopDev sẽ lan truyền bài viết trên các trang social media, tiếp nhận ý kiến bạn đọc cũng như lan tỏa hình ảnh của tác giả đến với cộng động lập trình viên do TopDev xây dựng. Ngoài ra, nếu trong tương lai anh có nhu cầu tuyển dụng, TopDev cũng sẽ hỗ trợ hết sức tìm kiếm những ứng viên phù hợp.

    Mong nhận được phản hồi từ anh,

    Trân trọng.

    1. Dear bạn từ TopDev,

      Cảm ơn bạn đã quan tâm, bài viết trên blog của mình không gắn bản quyền thương mại nên các bạn có thể repost nếu cảm thấy phù hợp nhé.

      Chúc các bạn thành công,
      Trân trọng.

Leave a Reply to _ Cancel reply

Your email address will not be published. Required fields are marked *