Marketing đã thay đổi OOP trong JavaScript như thế nào

theanh

Administrator
Nhân viên
Mặc dù tên của JavaScript được đặt theo ngôn ngữ Java, nhưng hai ngôn ngữ này lại hoàn toàn khác nhau. JavaScript có nhiều điểm chung hơn với LispScheme, chia sẻ các tính năng như hàm hạng nhất và phạm vi từ vựng.

JavaScript cũng mượn tính kế thừa nguyên mẫu của nó từ ngôn ngữ Self. Cơ chế kế thừa này có lẽ là điều mà nhiều — nếu không muốn nói là hầu hết — các nhà phát triển không dành đủ thời gian để hiểu, chủ yếu là vì nó không phải là yêu cầu bắt đầu làm việc với JavaScript. Đặc điểm đó có thể được coi là một lỗi thiết kế hoặc một nét thiên tài. Nói như vậy, bản chất nguyên mẫu của JavaScript đã được tiếp thị và ẩn sau lớp mặt nạ "Java dành cho web". Chúng tôi sẽ giải thích thêm về điều đó khi chúng tôi tiếp tục.

JavaScript không tự tin vào bản chất nguyên mẫu của chính nó, vì vậy nó cung cấp cho các nhà phát triển các công cụ để tiếp cận ngôn ngữ mà không cần phải chạm vào nguyên mẫu. Đây là một nỗ lực để mọi nhà phát triển dễ dàng hiểu được, đặc biệt là những người đến từ các ngôn ngữ dựa trên lớp, chẳng hạn như Java, và sau này trở thành một trong những kẻ thù lớn nhất của JavaScript trong nhiều năm tới: Bạn không cần phải hiểu cách JavaScript hoạt động để viết mã bằng JavaScript.

Lập trình hướng đối tượng cổ điển là gì?​

Lập trình hướng đối tượng (OOP) cổ điển xoay quanh khái niệm về lớp và thể hiện và được sử dụng rộng rãi trong các ngôn ngữ như Java, C++, C# và nhiều ngôn ngữ khác. Lớp là bản thiết kế hoặc mẫu để tạo đối tượng. Nó định nghĩa cấu trúc và hành vi của các đối tượng thuộc về lớp đó và đóng gói các thuộc tính và phương thức. Mặt khác, các đối tượng là các thể hiện của lớp. Khi bạn tạo một đối tượng từ một lớp, về cơ bản bạn đang tạo một thể hiện cụ thể kế thừa cấu trúc và hành vi được xác định trong lớp đó đồng thời cũng cung cấp cho mỗi đối tượng một trạng thái riêng.

OOP có nhiều khái niệm cơ bản, nhưng chúng ta sẽ tập trung vào thừa kế, một cơ chế cho phép một lớp tiếp nhận các thuộc tính và phương thức của một lớp khác. Điều này tạo điều kiện thuận lợi cho việc tái sử dụng mã và tạo ra một hệ thống phân cấp các lớp.


Prototypal OOP trong JavaScript là gì?​

Tôi sẽ giải thích các khái niệm đằng sau prototypal OOP trong Javascript, nhưng để có lời giải thích sâu hơn về cách thức hoạt động của nguyên mẫu, MDN có tổng quan tuyệt vời về chủ đề này.

OOP nguyên mẫu khác với OOP cổ điển, dựa trên các lớp và thể hiện. Trong OOP nguyên mẫu, không có lớp, chỉ có đối tượng và chúng được tạo trực tiếp từ các đối tượng khác.

Nếu chúng ta tạo một đối tượng, đối tượng đó sẽ có một thuộc tính tích hợp được gọi là prototype giữ tham chiếu đến nguyên mẫu đối tượng "cha" của nó để chúng ta có thể truy cập các phương thức và thuộc tính của nguyên mẫu. Đây là thứ cho phép chúng ta truy cập các phương thức như .sort() hoặc .forEach() từ bất kỳ mảng nào vì mỗi mảng đều kế thừa các phương thức từ đối tượng Array.prototype.

Bản thân nguyên mẫu là một đối tượng, vì vậy nguyên mẫu sẽ có nguyên mẫu riêng. Điều này tạo ra một chuỗi các đối tượng được gọi là chuỗi nguyên mẫu. Khi bạn truy cập một thuộc tính hoặc phương thức trên một đối tượng, JavaScript sẽ tìm kiếm nó trước tiên trên chính đối tượng đó. Nếu không tìm thấy, nó sẽ duyệt qua chuỗi nguyên mẫu cho đến khi tìm thấy thuộc tính hoặc đạt đến đối tượng cấp cao nhất. Nó thường kết thúc bằng Object.prototype, có nguyên mẫu null, biểu thị kết thúc của chuỗi.

Một sự khác biệt quan trọng giữa OOP cổ điển và nguyên mẫu là chúng ta không thể thao tác động vào định nghĩa lớp sau khi một đối tượng được tạo. Nhưng với các nguyên mẫu JavaScript, chúng ta có thể thêm, xóa hoặc thay đổi các phương thức và thuộc tính từ nguyên mẫu, ảnh hưởng đến các đối tượng ở cuối chuỗi.
“Các đối tượng kế thừa từ các đối tượng. Còn gì có thể hướng đối tượng hơn thế nữa?”

Douglas Crockford


Sự khác biệt trong JavaScript là gì? Spoiler: Không có​

Vì vậy, trên lý thuyết, sự khác biệt rất đơn giản. Trong OOP cổ điển, chúng ta khởi tạo các đối tượng từ một lớp và một lớp có thể kế thừa các phương thức và thuộc tính từ một lớp khác. Trong OOP nguyên mẫu, các đối tượng có thể kế thừa các thuộc tính và phương thức từ các đối tượng khác thông qua nguyên mẫu của chúng.

Tuy nhiên, trong JavaScript, không có một sự khác biệt nào ngoài cú pháp. Bạn có thể phát hiện ra sự khác biệt giữa hai đoạn mã sau không?
Mã:
// Với các lớpclass Dog { constructor(name, color) { this.name = name; this.color = color; } bark() { return `Tôi là một chú chó ${this.color} và tên tôi là ${this.name}.`; }}const myDog = new Dog("Charlie", "brown");console.log(myDog.name); // Charlieconsole.log(myDog.bark()); // Tôi là một chú chó nâu và tên tôi là Charlie.
Mã:
// Với các nguyên mẫufunction Dog(name, color) { this.name = name; this.color = color;}Dog.prototype.bark = function () { return `Tôi là một chú chó ${this.color} và tên tôi là ${this.name}.`;};const myDog = new Dog("Charlie", "brown");console.log(myDog.name); // Charlieconsole.log(myDog.bark()); // Tôi là một chú chó nâu và tên tôi là Charlie.
Không có sự khác biệt nào và JavaScript sẽ thực thi cùng một mã, nhưng ví dụ sau trung thực về những gì JavaScript đang làm dưới lớp vỏ, trong khi ví dụ trước ẩn nó đằng sau cú pháp sugar.

Tôi có vấn đề gì với cách tiếp cận cổ điển không? Có và không. Có thể lập luận rằng cú pháp cổ điển cải thiện khả năng đọc bằng cách đưa tất cả mã liên quan đến lớp vào trong phạm vi khối. Mặt khác, nó gây hiểu lầm và khiến hàng nghìn nhà phát triển tin rằng JavaScript có các lớp thực sự khi một lớp trong JavaScript không khác gì bất kỳ đối tượng hàm nào khác.
Vấn đề lớn nhất của tôi không phải là giả vờ rằng các lớp thực sự tồn tại mà là các nguyên mẫu đừng.

Hãy xem xét đoạn mã sau:
Mã:
class Dog { constructor(name, color) { this.name = name; this.color = color; } bark() { return `Tôi là một chú chó ${this.color} và tên tôi là ${this.name}.`; }}const myDog = new Dog("Charlie", "brown");Dog.prototype.bark = function () { return "Tôi thực sự chỉ là một đối tượng khác có nguyên mẫu!";};console.log(myDog.bark()); // Tôi thực sự chỉ là một đối tượng khác có nguyên mẫu!"
Khoan đã, chúng ta vừa truy cập vào nguyên mẫu lớp sao? Đúng vậy, vì lớp không tồn tại! Chúng chỉ là các hàm trả về một đối tượng (gọi là hàm xây dựng) và tất yếu là chúng có nguyên mẫu, nghĩa là chúng ta có thể truy cập vào thuộc tính .prototype của nó.

Có vẻ như JavaScript cố gắng ẩn nguyên mẫu của mình. Nhưng tại sao?

Có manh mối trong lịch sử JavaScript​

Vào tháng 5 năm 1995, Netscape đã mời người sáng tạo ra JavaScript Brendan Eich tham gia vào một dự án triển khai ngôn ngữ kịch bản vào trình duyệt Netscape. Ý tưởng chính là triển khai ngôn ngữ Scheme vào trình duyệt do cách tiếp cận tối thiểu. Kế hoạch đã thay đổi khi Netscape ký kết thỏa thuận với Sun Microsystems, những người sáng tạo ra Java, để triển khai Java trên web. Ngay sau đó, Brendan Eich và người sáng lập Sun Microsystems Bill Joy nhận thấy nhu cầu về một ngôn ngữ mới. Một ngôn ngữ dễ tiếp cận đối với những người không chỉ tập trung vào lập trình. Một ngôn ngữ dành cho cả nhà thiết kế đang cố gắng tạo trang web và nhà phát triển giàu kinh nghiệm đến từ Java.

Với mục tiêu này, JavaScript đã được tạo ra trong 10 ngày làm việc chăm chỉ dưới cái tên ban đầu là Mocha. Nó sẽ được đổi thành LiveScript để tiếp thị như một tập lệnh thực thi "trực tiếp" trong trình duyệt nhưng vào tháng 12 năm 1995, cuối cùng nó sẽ được đặt tên là JavaScript để được tiếp thị cùng với Java. Thỏa thuận này với Sun Microsystems đã buộc Brendan phải điều chỉnh ngôn ngữ dựa trên nguyên mẫu của mình thành Java. Theo Brendan Eich, JavaScript được coi là "ngôn ngữ phụ trợ của Java" và bị thiếu hụt rất nhiều kinh phí so với nhóm Java:
"Tôi đã suy nghĩ suốt thời gian đó, ngôn ngữ này nên như thế nào? Nó có dễ sử dụng không? Liệu cú pháp có giống ngôn ngữ tự nhiên hơn không? [...] Vâng, tôi muốn làm điều đó, nhưng ban quản lý của tôi nói, "Làm cho nó trông giống Java."
Ý tưởng của Eich cho JavaScript là triển khai các hàm hạng nhất của Scheme — một tính năng cho phép gọi lại các sự kiện của người dùng — và OOP dựa trên các nguyên mẫu từ Self. Ông ấy đã bày tỏ điều này trước đây trên blog:
“Tôi không tự hào, nhưng tôi vui vì đã chọn các hàm hạng nhất Scheme-ish và các nguyên mẫu Self-ish làm thành phần chính.”
Bản chất nguyên mẫu của JavaScript vẫn được giữ nguyên nhưng sẽ được che khuất cụ thể đằng sau một lớp vỏ Java. Các nguyên mẫu có thể vẫn được giữ nguyên vì Eich đã triển khai các nguyên mẫu Self ngay từ đầu và sau đó chúng không thể thay đổi, chỉ có thể ẩn đi. Chúng ta có thể tìm thấy một lời giải thích hỗn hợp trong một bình luận cũ trên blog của ông:
“Thật trớ trêu khi JS không thể có lớp vào năm 1995 vì nó sẽ cạnh tranh với Java. Nó bị hạn chế bởi cả thời gian và vai trò phụ trợ.”
Dù bằng cách nào, JavaScript đã trở thành ngôn ngữ dựa trên nguyên mẫu và là ngôn ngữ phổ biến nhất cho đến nay.

Giá như JavaScript chấp nhận các nguyên mẫu của nó​

Trong quá trình vội vã giữa việc tạo ra JavaScript và việc áp dụng rộng rãi, đã có một số quyết định thiết kế đáng ngờ khác liên quan đến các nguyên mẫu. Trong cuốn sách của mình, JavaScript: The Good Parts, Crockford giải thích những phần xấu xung quanh JavaScript, chẳng hạn như biến toàn cục và sự hiểu lầm xung quanh nguyên mẫu.

Như bạn có thể thấy, bài viết này được lấy cảm hứng từ cuốn sách của Crockford. Mặc dù tôi không đồng ý với nhiều ý kiến của ông về những phần xấu của JavaScript, nhưng điều quan trọng cần lưu ý là cuốn sách được xuất bản vào năm 2008 khi ECMAScript 4 (ES4) là phiên bản ổn định của JavaScript. Nhiều năm đã trôi qua kể từ khi xuất bản và JavaScript đã thay đổi đáng kể trong thời gian đó. Sau đây là các tính năng mà tôi nghĩ có thể đã được lưu khỏi ngôn ngữ nếu JavaScript chỉ chấp nhận các nguyên mẫu của nó.

Giá trị this trong các bối cảnh khác nhau​

Từ khóa this là một trong những thứ JavaScript thêm vào để trông giống Java. Trong Java và OOP cổ điển nói chung, this tham chiếu đến phiên bản hiện tại mà phương thức hoặc hàm tạo đang được gọi, chỉ vậy thôi. Tuy nhiên, trong JavaScript, chúng ta không có cú pháp lớp cho đến ES6 nhưng vẫn kế thừa từ khóa this. Vấn đề của tôi với this là nó có thể là bốn thứ khác nhau tùy thuộc vào nơi được gọi!
1. this trong Mẫu gọi hàm​
Khi this được gọi bên trong một lệnh gọi hàm, nó sẽ được liên kết với đối tượng toàn cục. Nó cũng sẽ được liên kết với đối tượng toàn cục nếu nó được gọi từ phạm vi toàn cục.
Mã:
console.log(this); // windowfunction myFunction() { console.log(this);}myFunction(); // window
Ở chế độ nghiêm ngặt và thông qua mẫu gọi hàm, this sẽ không xác định.
Mã:
function getThis() { "use strict"; return this;}getThis(); // undefined
2. this Trong Mẫu Gọi Phương Thức​
Nếu chúng ta tham chiếu một hàm như là thuộc tính của đối tượng, this sẽ được liên kết với đối tượng cha của nó.
Mã:
const dog = { name: "Sparky", bark: function () { console.log(`Woof, my name is ${this.name}.`); },};dog.bark(); // Woof, my name is Sparky.
Các hàm mũi tên không có this riêng của chúng, thay vào đó, chúng kế thừa this từ phạm vi cha của chúng khi tạo.
Mã:
const dog = { name: "Sparky", bark: () => { console.log(`Woof, my name is ${this.name}.`); },};dog.bark(); // Woof, my name is undefined.
Trong trường hợp này, this được liên kết với đối tượng toàn cục thay vì dog, do đó this.nameundefined.
3. Mẫu gọi hàm tạo​
Nếu chúng ta gọi một hàm có tiền tố new, một đối tượng rỗng mới sẽ được tạo và this sẽ được liên kết với đối tượng đó.
Mã:
function Dog(name) { this.name = name; this.bark = function () { console.log(`Woof, tên tôi là ${this.name}.`); };}const myDog = new Dog("Coco");myDog.bark(); // Woof, tên tôi là Coco.
Chúng ta cũng có thể sử dụng this từ nguyên mẫu của hàm để truy cập các thuộc tính của đối tượng, điều này có thể cung cấp cho chúng ta lý do hợp lệ hơn để sử dụng nó.
Mã:
function Dog(name) { this.name = name;}Dog.prototype.bark = function () { console.log(`Woof, tên tôi là ${this.name}.`);};const myDog = new Dog("Coco");myDog.bark(); // Woof, tên tôi là Coco.
4. Mẫu gọi apply
Cuối cùng, mỗi hàm kế thừa một phương thức apply từ nguyên mẫu hàm có hai tham số. Tham số đầu tiên là giá trị sẽ được liên kết với this bên trong hàm và tham số thứ hai là một mảng sẽ được sử dụng làm tham số hàm.
Mã:
// Liên kết `this` với một đối tượng khácfunction bark() { console.log(`Woof, my name is ${this.name}.`);}const myDog = { name: "Milo",};bark.apply(myDog); // Woof, my name is Milo.// Sử dụng tham số mảngconst numbers = [3, 10, 4, 6, 9];const max = Math.max.apply(null, numbers);console.log(max); // 10
Như bạn thấy, this có thể là bất cứ thứ gì và ngay từ đầu không nên có trong JavaScript. Các cách tiếp cận như sử dụng bind() là giải pháp cho một vấn đề thậm chí không nên tồn tại. May mắn thay, this hoàn toàn có thể tránh được trong JavaScript hiện đại và bạn có thể tự cứu mình khỏi nhiều rắc rối nếu bạn học cách né tránh nó; một lợi thế mà người dùng lớp ES6 không thể tận hưởng.

Crockford có một giai thoại hay về chủ đề này trong cuốn sách của ông:
“Đây là một đại từ chỉ định. Chỉ cần có this trong ngôn ngữ khiến ngôn ngữ khó nói hơn. Giống như lập trình cặp với Abbott và Costello vậy.”
“Nhưng nếu chúng ta muốn tạo một hàm tạo, chúng ta sẽ cần sử dụng this.” Không nhất thiết! Trong ví dụ sau, chúng ta có thể tạo một hàm tạo không sử dụng this hoặc new để hoạt động.
Mã:
function counterConstructor() { let counter = 0; function getCounter() { return counter; } function up() { counter += 1; return counter; } function down() { counter -= 1; return counter; } return { getCounter, up, down, };}const myCounter = counterConstructor();myCounter.up(); // 1myCounter.down(); // 0
Chúng ta vừa tạo một hàm tạo mà không sử dụng this hoặc new! Và nó đi kèm với một cú pháp đơn giản. Một nhược điểm mà bạn có thể thấy là các đối tượng được tạo từ counterConstructor sẽ không có quyền truy cập vào nguyên mẫu của nó, vì vậy chúng ta không thể thêm các phương thức hoặc thuộc tính từ counterConstructor.prototype.

Nhưng chúng ta có cần điều này không? Tất nhiên, chúng ta sẽ cần sử dụng lại mã của mình, nhưng có những cách tiếp cận tốt hơn mà chúng ta sẽ thấy sau.

Tiền tố new

Trong JavaScript: The Good Parts, Crockford lập luận rằng chúng ta không nên sử dụng tiền tố new chỉ vì không có gì đảm bảo rằng chúng ta sẽ nhớ sử dụng nó trong các hàm dự định. Tôi nghĩ rằng đây là một lỗi dễ phát hiện và cũng có thể tránh được bằng cách viết hoa các hàm dựng mà bạn định sử dụng với new. Và ngày nay, các trình kiểm tra lỗi sẽ cảnh báo chúng ta khi chúng ta gọi một hàm viết hoa mà không có new hoặc ngược lại.

Một lập luận tốt hơn chỉ đơn giản là việc sử dụng new buộc chúng ta phải sử dụng this bên trong các hàm dựng hoặc "lớp" của mình, và như chúng ta đã thấy trước đó, tốt hơn hết là chúng ta nên tránh sử dụng this ngay từ đầu.

Nhiều cách để truy cập vào các nguyên mẫu​

Vì những lý do lịch sử mà chúng ta đã xem xét, chúng ta có thể hiểu tại sao JavaScript không chấp nhận các nguyên mẫu của nó. Mở rộng ra, chúng ta không có công cụ để trộn lẫn với các nguyên mẫu một cách đơn giản như chúng ta muốn, mà thay vào đó là những nỗ lực gian xảo để thao túng chuỗi nguyên mẫu. Mọi thứ trở nên tệ hơn khi trong tài liệu, chúng ta có thể đọc được các thuật ngữ chuyên ngành khác nhau xung quanh các nguyên mẫu.
Sự khác biệt giữa [[Prototype]], __proto__.prototype
Để trải nghiệm đọc dễ chịu hơn, chúng ta hãy xem xét sự khác biệt giữa các thuật ngữ này.
  • [[Prototype]] là một thuộc tính nội bộ giữ tham chiếu đến nguyên mẫu của đối tượng. Nó được đặt trong dấu ngoặc vuông đôi, nghĩa là nó thường không thể được truy cập bằng ký hiệu thông thường.
  • __proto__ có thể tham chiếu đến hai thuộc tính có thể:Nó có thể tham chiếu đến một thuộc tính từ bất kỳ đối tượng Object.prototype nào hiển thị thuộc tính [[Prototype]] ẩn. Nó đã lỗi thời và hoạt động kém.
  • Nó có thể tham chiếu đến một thuộc tính tùy chọn mà chúng ta có thể thêm khi tạo một đối tượng theo nghĩa đen. Nguyên mẫu của đối tượng sẽ trỏ đến giá trị mà chúng ta cung cấp cho nó.
[*] .prototype là một thuộc tính dành riêng cho các hàm hoặc lớp (trừ các hàm mũi tên). Khi được gọi bằng tiền tố new, nguyên mẫu của đối tượng được khởi tạo sẽ trỏ đến .prototype của hàm.
Bây giờ chúng ta có thể thấy tất cả các cách chúng ta có thể sửa đổi nguyên mẫu trong JavaScript. Sau khi xem xét, chúng ta sẽ nhận thấy tất cả chúng đều thiếu sót ở ít nhất một khía cạnh nào đó.

Sử dụng Thuộc tính theo nghĩa đen __proto__ khi Khởi tạo​

Khi tạo một đối tượng JavaScript bằng cách sử dụng các đối tượng theo nghĩa đen, chúng ta có thể thêm thuộc tính __proto__. Đối tượng được tạo sẽ trỏ [[Prototoype]] của nó tới giá trị được cung cấp trong __proto__. Trong ví dụ trước, các đối tượng được tạo từ hàm tạo của chúng ta không có quyền truy cập vào nguyên mẫu của hàm tạo. Chúng ta có thể sử dụng thuộc tính __proto__ khi khởi tạo để thay đổi điều này mà không cần sử dụng this hoặc new.
Mã:
function counterConstructor() { let counter = 0; function getCounter() { return counter; } function up() { counter += 1; return counter; } function down() { counter -= 1; return counter; } return { getCounter, up, down, __proto__: counterConstructor.prototype, };}
Ưu điểm của việc liên kết nguyên mẫu của đối tượng mới với hàm tạo là chúng ta có thể mở rộng các phương thức của nó từ nguyên mẫu của hàm tạo. Nhưng sẽ có ích gì nếu chúng ta cần sử dụng this một lần nữa?
Mã:
const myCounter = counterConstructor();counterConstructor.prototype.printDouble = function () { return this.getCounter() * 2;};myCounter.up(); // 1myCounter.up(); // 2myCounter.printDouble(); // 4
Chúng ta thậm chí còn không sửa đổi giá trị nội bộ count mà thay vào đó in nó thành double. Vì vậy, một phương thức setter sẽ cần thiết để thao tác trạng thái của nó từ bên ngoài khai báo hàm khởi tạo ban đầu. Tuy nhiên, chúng ta đang làm phức tạp quá mức mã của mình vì chúng ta có thể chỉ cần thêm một phương thức double vào bên trong hàm của mình.
Mã:
function counterConstructor() { let counter = 0; function getCounter() { return counter; } function up() { counter += 1; return counter; } function down() { counter -= 1; return counter; } function double() { counter = counter * 2; return counter; } return { getCounter, up, down, double, };}const myCounter = counterConstructor();myCounter.up(); // 1myCounter.up(); // 2myCounter.double(); // 4
Sử dụng __proto__ là quá mức cần thiết trong thực tế.

Điều quan trọng cần lưu ý là __proto__ chỉ được sử dụng khi khởi tạo một đối tượng mới thông qua một đối tượng theo nghĩa đen. Sử dụng trình truy cập __proto__ trong Object.prototype.__proto__ sẽ thay đổi [[Prototoype]] của đối tượng sau khi khởi tạo, làm gián đoạn nhiều quá trình tối ưu hóa được thực hiện ngầm bởi các công cụ JavaScript. Đó là lý do tại sao Object.prototype.__proto__ hoạt động kém và đã lỗi thời.
Object.create()
Object.create() trả về một đối tượng mới có [[Prototype]] là đối số đầu tiên của hàm. Nó cũng có một đối số thứ hai cho phép bạn xác định các thuộc tính bổ sung cho các đối tượng mới. Tuy nhiên, việc tạo một đối tượng bằng cách sử dụng một đối tượng theo nghĩa đen linh hoạt và dễ đọc hơn. Do đó, cách sử dụng thực tế duy nhất của nó là tạo một đối tượng không có nguyên mẫu bằng cách sử dụng Object.create(null) vì tất cả các đối tượng được tạo bằng các đối tượng theo nghĩa đen đều được tự động liên kết đến Object.prototype.
Object.setPrototypeOf()
Object.setPrototypeOf() lấy hai đối tượng làm đối số và sẽ đột biến chuỗi nguyên mẫu từ đối số trước thành đối số sau. Như chúng ta đã thấy trước đó, việc chuyển đổi nguyên mẫu của đối tượng sau khi khởi tạo là không hiệu quả, vì vậy hãy tránh bằng mọi giá.

Đóng gói và các lớp riêng tư​

Lập luận cuối cùng của tôi chống lại các lớp là thiếu tính riêng tư và đóng gói. Ví dụ, hãy lấy cú pháp lớp sau:
Mã:
class Cat { constructor(name) { this.name = name; } meow() { console.log(`Meow! My name is ${this.name}.`); }}const myCat = new Cat("Gala");myCat.meow(); // Meow! My name is Gala.myCat.name = "Pumpkin";myCat.meow(); // Meow! My name is Pumpkin.
Chúng ta không có bất kỳ quyền riêng tư nào! Tất cả các thuộc tính đều là công khai. Chúng ta có thể thử giảm thiểu điều này bằng các closure:
Mã:
class Cat { constructor(name) { this.getName = function () { return name; }; } meow() { console.log(`Meow! My name is ${this.name}.`); }}const myCat = new Cat("Gala");myCat.meow(); // Meow! My name is undefined.
Ồ, giờ thì this.name undefined nằm ngoài phạm vi của constructor. Chúng ta phải đổi this.name thành this.getName() để nó có thể hoạt động bình thường.
Mã:
class Cat { constructor(name) { this.getName = function () { return name; }; } meow() { console.log(`Meow! My name is ${this.getName()}.`); }}const myCat = new Cat("Gala");myCat.meow(); // Meow! Tên tôi là Gala.
Đây chỉ là một đối số, vì vậy bạn có thể tưởng tượng được mã của chúng ta sẽ lặp lại không cần thiết như thế nào khi chúng ta thêm nhiều đối số hơn. Bên cạnh đó, chúng ta vẫn có thể sửa đổi các phương thức đối tượng của mình:
Mã:
myCat.meow = function () { console.log(`Meow! ${this.getName()} is a bad kitten.`);};myCat.meow(); // Meow! Gala là một chú mèo con hư.
Chúng ta có thể lưu và triển khai quyền riêng tư tốt hơn nếu sử dụng các hàm tạo của riêng mình và thậm chí làm cho các phương thức của chúng ta không thể thay đổi bằng cách sử dụng Object.freeze()!
Mã:
function catConstructor(name) { function getName() { return name; } function meow() { console.log(`Meow! My name is ${name}.`); } return Object.freeze({ getName, meow, });}const myCat = catConstructor("Loaf");myCat.meow(); // Meow! Tên tôi là Loaf.
Và việc cố gắng sửa đổi các phương thức của đối tượng sẽ thất bại một cách thầm lặng.
Mã:
myCat.meow = function () { console.log(`Meow! ${this.getName()} is a bad Kitten.`);};myCat.meow(); // Meow! Tên tôi là Loaf.
Và vâng, tôi biết về đề xuất gần đây cho các trường lớp riêng tư. Nhưng liệu chúng ta có thực sự cần nhiều cú pháp mới hơn nữa khi chúng ta có thể thực hiện điều tương tự bằng cách sử dụng các hàm dựng tùy chỉnh và closures không?

Vậy, Lớp hay Nguyên mẫu trong JavaScript?​

Trong cuốn sách gần đây hơn của Crockford, JavaScript hoạt động như thế nào (PDF), chúng ta có thể thấy một tùy chọn tốt hơn là sử dụng Nguyên mẫu hoặc Lớp để tái sử dụng mã: Thành phần!
Như Crockford đã nói trong cuốn sách gần đây hơn của mình:
nstead of same as except we can get a little bit of this and a little bit of that.”

— Douglas Crockford, JavaScript hoạt động như thế nào

Thay vì một hàm tạo hoặc lớp kế thừa từ một hàm khác, chúng ta có thể có một tập hợp các hàm tạo và kết hợp chúng khi cần để tạo ra một đối tượng chuyên biệt.
Mã:
function speakerConstructor(name, message) { function talk() { return `Xin chào, tên tôi là ${name} và tôi muốn nói điều gì đó: ${message}.`; } return Object.freeze({ talk, });}function loudSpeakerConstructor(name, message) { const {talk} = speakerConstructor(name, message); function yell() { return talk().toUpperCase(); } return Object.freeze({ talk, yell, });}const mySpeaker = loudSpeakerConstructor("Juan", "Bạn trông đẹp đấy!");mySpeaker.talk(); // Xin chào, tên tôi là Juan và tôi muốn nói điều gì đó: Bạn trông đẹp đấy!mySpeaker.yell(); // CHÀO, TÊN TÔI LÀ JUAN VÀ TÔI MUỐN NÓI MỘT ĐIỀU: BẠN TRÔNG ĐẸP QUÁ!
Không cần thisnew và các lớp hoặc nguyên mẫu, chúng ta đạt được một hàm tạo có thể tái sử dụng với quyền riêng tư và đóng gói đầy đủ.

Kết luận​

Đúng vậy, JavaScript được tạo ra trong 10 ngày vội vã; đúng vậy, nó đã bị ảnh hưởng bởi tiếp thị; và đúng vậy, nó có một tập hợp dài các phần vô dụng và nguy hiểm. Tuy nhiên, nó là một ngôn ngữ đẹp và thúc đẩy rất nhiều sự đổi mới đang diễn ra trong phát triển web ngày nay, vì vậy rõ ràng nó đã làm được điều gì đó tốt!
Thật không may, quyết định có ý thức này để bám vào những phần tốt không chỉ dành riêng cho JavaScript OOP vì, giữa sự vội vã ra đời, ngôn ngữ này đã mang đến rất nhiều tính năng đáng ngờ khác mà chúng ta không nên sử dụng. Có lẽ chúng ta có thể giải quyết chúng trong một bài viết trong tương lai, nhưng trong thời gian chờ đợi, chúng ta sẽ phải thừa nhận sự hiện diện của chúng và đưa ra quyết định có ý thức để tiếp tục học và hiểu ngôn ngữ để biết những phần nào nên sử dụng và những phần nào nên bỏ qua.

Tài liệu tham khảo​

 
Back
Bên trên