Tôi biết rằng câu trả lời này trễ 3 năm nhưng tôi thực sự nghĩ rằng các câu trả lời hiện tại không cung cấp đủ thông tin về cách thừa kế nguyên mẫu tốt hơn so với thừa kế cổ điển .
Trước tiên, hãy xem các đối số phổ biến nhất mà các lập trình viên JavaScript nêu để bảo vệ tính kế thừa nguyên mẫu (Tôi đang lấy các đối số này từ nhóm câu trả lời hiện tại):
- Thật đơn giản.
- Nó mạnh mẽ.
- Nó dẫn đến mã nhỏ hơn, ít dự phòng hơn.
- Nó năng động và do đó tốt hơn cho các ngôn ngữ động.
Bây giờ tất cả các đối số đều hợp lệ, nhưng không ai bận tâm giải thích lý do tại sao. Nó giống như nói với một đứa trẻ rằng học toán là quan trọng. Chắc chắn là có, nhưng đứa trẻ chắc chắn không quan tâm; và bạn không thể tạo ra một đứa trẻ như Toán bằng cách nói rằng nó quan trọng.
Tôi nghĩ vấn đề với sự kế thừa nguyên mẫu là nó được giải thích từ quan điểm của JavaScript. Tôi yêu JavaScript, nhưng kế thừa nguyên mẫu trong JavaScript là sai. Không giống như thừa kế cổ điển, có hai kiểu thừa kế nguyên mẫu:
- Các mẫu nguyên mẫu của thừa kế nguyên mẫu.
- Các mẫu xây dựng của thừa kế nguyên mẫu.
Thật không may, JavaScript sử dụng mô hình hàm tạo của kế thừa nguyên mẫu. Điều này là do khi JavaScript được tạo, Brendan Eich (người tạo ra JS) muốn nó trông giống như Java (có sự kế thừa cổ điển):
Và chúng tôi đã đẩy nó như một đứa em trai với Java, vì một ngôn ngữ bổ sung như Visual Basic là C ++ trong các gia đình ngôn ngữ của Microsoft vào thời điểm đó.
Điều này là xấu bởi vì khi mọi người sử dụng các hàm tạo trong JavaScript, họ nghĩ rằng các hàm tạo kế thừa từ các hàm tạo khác. Cái này sai. Trong các đối tượng thừa kế nguyên mẫu kế thừa từ các đối tượng khác. Các nhà xây dựng không bao giờ đi vào hình ảnh. Đây là điều khiến hầu hết mọi người bối rối.
Những người từ các ngôn ngữ như Java, vốn có tính kế thừa cổ điển, thậm chí còn bối rối hơn bởi vì mặc dù các nhà xây dựng trông giống như các lớp mà họ không hành xử giống như các lớp. Như Douglas Crockford đã nêu:
Sự thiếu quyết đoán này nhằm mục đích làm cho ngôn ngữ dường như quen thuộc hơn với các lập trình viên được đào tạo bài bản, nhưng không thực hiện được, như chúng ta có thể thấy từ các lập trình viên Java có ý kiến rất thấp về JavaScript. Mẫu constructor của JavaScript không thu hút đám đông cổ điển. Nó cũng che khuất bản chất nguyên mẫu thực sự của JavaScript. Kết quả là, có rất ít lập trình viên biết sử dụng ngôn ngữ một cách hiệu quả.
Có bạn có nó. Thẳng từ miệng ngựa.
Kế thừa nguyên mẫu thực sự
Kế thừa nguyên mẫu là tất cả về các đối tượng. Các đối tượng kế thừa các thuộc tính từ các đối tượng khác. Thats tất cả để có nó. Có hai cách tạo đối tượng bằng cách sử dụng kế thừa nguyên mẫu:
- Tạo một đối tượng hoàn toàn mới.
- Nhân bản một đối tượng hiện có và mở rộng nó.
Lưu ý: JavaScript cung cấp hai cách để sao chép một đối tượng - phân quyền và ghép nối . Do đó, tôi sẽ sử dụng từ "bản sao" để chỉ riêng việc thừa kế thông qua ủy quyền và từ "bản sao" để chỉ dành riêng cho thừa kế thông qua ghép nối.
Nói đủ. Hãy xem một số ví dụ. Nói rằng tôi có một vòng tròn bán kính 5
:
var circle = {
radius: 5
};
Chúng ta có thể tính diện tích và chu vi của vòng tròn từ bán kính của nó:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Bây giờ tôi muốn tạo một vòng tròn bán kính khác 10
. Một cách để làm điều này sẽ là:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
Tuy nhiên, JavaScript cung cấp một cách tốt hơn - ủy quyền . Các Object.create
chức năng được sử dụng để làm điều này:
var circle2 = Object.create(circle);
circle2.radius = 10;
Đó là tất cả. Bạn vừa thực hiện kế thừa nguyên mẫu trong JavaScript. Điều đó không đơn giản sao? Bạn lấy một đối tượng, nhân bản nó, thay đổi bất cứ điều gì bạn cần và xin chào - bạn đã có cho mình một đối tượng hoàn toàn mới.
Bây giờ bạn có thể hỏi, "Làm thế nào đơn giản? Mỗi lần tôi muốn tạo một vòng tròn mới, tôi cần sao chép circle
và gán thủ công cho nó một bán kính". Giải pháp là sử dụng một chức năng để thực hiện việc nâng vật nặng cho bạn:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
Trong thực tế, bạn có thể kết hợp tất cả những điều này thành một đối tượng theo nghĩa đen như sau:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Kế thừa nguyên mẫu trong JavaScript
Nếu bạn nhận thấy trong chương trình trên, create
hàm sẽ tạo một bản sao circle
, gán một cái mới radius
cho nó và sau đó trả về nó. Đây chính xác là những gì một nhà xây dựng làm trong JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
Mẫu constructor trong JavaScript là mẫu nguyên mẫu được đảo ngược. Thay vì tạo một đối tượng, bạn tạo một hàm tạo. Các new
từ khóa liên kết với this
con trỏ bên trong constructor để một bản sao của prototype
các nhà xây dựng.
Nghe có vẻ khó hiểu? Đó là vì mẫu constructor trong JavaScript làm phức tạp mọi thứ một cách không cần thiết. Đây là điều mà hầu hết các lập trình viên cảm thấy khó hiểu.
Thay vì nghĩ về các đối tượng kế thừa từ các đối tượng khác, họ nghĩ về các nhà xây dựng kế thừa từ các nhà xây dựng khác và sau đó trở nên hoàn toàn bối rối.
Có một loạt các lý do khác tại sao nên tránh mô hình hàm tạo trong JavaScript. Bạn có thể đọc về chúng trong bài đăng trên blog của tôi ở đây: Con constructor vs Prototypes
Vì vậy, những lợi ích của thừa kế nguyên mẫu so với thừa kế cổ điển là gì? Chúng ta hãy đi qua các đối số phổ biến nhất một lần nữa, và giải thích tại sao .
1. Kế thừa nguyên mẫu là đơn giản
CMS nêu trong câu trả lời của mình:
Theo tôi lợi ích chính của thừa kế nguyên mẫu là sự đơn giản của nó.
Hãy xem xét những gì chúng ta vừa làm. Chúng tôi tạo ra một đối tượng circle
có bán kính là 5
. Sau đó, chúng tôi nhân bản nó và cho bản sao bán kính 10
.
Do đó, chúng ta chỉ cần hai điều để làm cho kế thừa nguyên mẫu hoạt động:
- Một cách để tạo một đối tượng mới (ví dụ: đối tượng bằng chữ).
- Một cách để mở rộng một đối tượng hiện có (ví dụ
Object.create
).
Ngược lại thừa kế cổ điển phức tạp hơn nhiều. Trong thừa kế cổ điển, bạn có:
- Các lớp học.
- Vật.
- Giao diện.
- Các lớp học trừu tượng.
- Lớp cuối cùng.
- Các lớp cơ sở ảo.
- Nhà xây dựng.
- Phá hủy.
Bạn có được ý tưởng. Vấn đề là sự kế thừa nguyên mẫu dễ hiểu hơn, dễ thực hiện hơn và dễ lý luận hơn.
Như Steve Yegge đã đặt nó trong bài viết trên blog cổ điển của mình " Portrait of a N00b ":
Siêu dữ liệu là bất kỳ loại mô tả hoặc mô hình của một cái gì đó khác. Các ý kiến trong mã của bạn chỉ là một mô tả ngôn ngữ tự nhiên của tính toán. Điều làm cho siêu dữ liệu siêu dữ liệu là nó không thực sự cần thiết. Nếu tôi có một con chó với một số giấy tờ phả hệ, và tôi mất giấy tờ, tôi vẫn có một con chó hoàn toàn hợp lệ.
Trong cùng một nghĩa các lớp chỉ là siêu dữ liệu. Các lớp học không bắt buộc phải thừa kế. Tuy nhiên, một số người (thường là n00bs) thấy các lớp thoải mái hơn khi làm việc. Nó mang lại cho họ một cảm giác an toàn sai lầm.
Vâng, chúng tôi cũng biết rằng các loại tĩnh chỉ là siêu dữ liệu. Chúng là một loại bình luận chuyên biệt nhắm vào hai loại độc giả: lập trình viên và người biên dịch. Các loại tĩnh kể một câu chuyện về tính toán, có lẽ là để giúp cả hai nhóm độc giả hiểu được ý định của chương trình. Nhưng các kiểu tĩnh có thể bị loại bỏ trong thời gian chạy, vì cuối cùng chúng chỉ là các bình luận cách điệu. Chúng giống như giấy tờ phả hệ: nó có thể làm cho một loại tính cách không an toàn nào đó vui hơn về con chó của họ, nhưng con chó chắc chắn không quan tâm.
Như tôi đã nói trước đó, các lớp học mang lại cho mọi người cảm giác an toàn sai lầm. Ví dụ, bạn nhận được quá nhiều NullPointerException
s trong Java ngay cả khi mã của bạn hoàn toàn dễ đọc. Tôi thấy sự kế thừa cổ điển thường cản trở việc lập trình, nhưng có lẽ đó chỉ là Java. Python có một hệ thống kế thừa cổ điển tuyệt vời.
2. Kế thừa nguyên mẫu là mạnh mẽ
Hầu hết các lập trình viên đến từ một nền tảng cổ điển cho rằng kế thừa cổ điển mạnh hơn kế thừa nguyên mẫu bởi vì nó có:
- Biến riêng.
- Đa thừa kế.
Yêu cầu này là sai. Chúng ta đã biết rằng JavaScript hỗ trợ các biến riêng tư thông qua các bao đóng , nhưng còn nhiều kế thừa thì sao? Các đối tượng trong JavaScript chỉ có một nguyên mẫu.
Sự thật là sự kế thừa nguyên mẫu hỗ trợ kế thừa từ nhiều nguyên mẫu. Kế thừa nguyên mẫu đơn giản có nghĩa là một đối tượng kế thừa từ một đối tượng khác. Thực tế có hai cách để thực hiện kế thừa nguyên mẫu :
- Đoàn hoặc thừa kế khác biệt
- Nhân bản vô tính hoặc kế thừa
Có JavaScript chỉ cho phép các đối tượng ủy quyền cho một đối tượng khác. Tuy nhiên, nó cho phép bạn sao chép các thuộc tính của một số lượng đối tượng tùy ý. Ví dụ _.extend
, chỉ làm điều này.
Tất nhiên nhiều lập trình viên không coi đây là sự kế thừa thực sự bởi vì instanceof
và isPrototypeOf
nói khác đi. Tuy nhiên, điều này có thể được khắc phục dễ dàng bằng cách lưu trữ một loạt các nguyên mẫu trên mọi đối tượng kế thừa từ một nguyên mẫu thông qua ghép nối:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Do đó, thừa kế nguyên mẫu cũng mạnh mẽ như thừa kế cổ điển. Trong thực tế, nó mạnh hơn nhiều so với thừa kế cổ điển bởi vì trong thừa kế nguyên mẫu, bạn có thể tự tay chọn các thuộc tính để sao chép và các thuộc tính nào được bỏ qua từ các nguyên mẫu khác nhau.
Trong thừa kế cổ điển, không thể (hoặc ít nhất là rất khó) để chọn thuộc tính nào bạn muốn thừa kế. Họ sử dụng các lớp cơ sở ảo và giao diện để giải quyết vấn đề kim cương .
Tuy nhiên, trong JavaScript rất có thể bạn sẽ không bao giờ nghe về vấn đề kim cương vì bạn có thể kiểm soát chính xác các thuộc tính bạn muốn thừa kế và từ nguyên mẫu nào.
3. Kế thừa nguyên mẫu ít dư thừa
Điểm này khó giải thích hơn một chút vì kế thừa cổ điển không nhất thiết dẫn đến mã thừa. Trong thực tế thừa kế, cho dù cổ điển hay nguyên mẫu, được sử dụng để giảm sự dư thừa trong mã.
Một lập luận có thể là hầu hết các ngôn ngữ lập trình có tính kế thừa cổ điển được gõ tĩnh và yêu cầu người dùng khai báo rõ ràng các loại (không giống như Haskell có kiểu gõ tĩnh). Do đó điều này dẫn đến mã dài hơn.
Java nổi tiếng với hành vi này. Tôi nhớ rất rõ Bob Nystrom đã đề cập đến giai thoại sau đây trong bài đăng trên blog của mình về Pratt Parsers :
Bạn phải yêu thích mức độ quan liêu "tăng gấp bốn lần" của Java ở đây.
Một lần nữa, tôi nghĩ đó chỉ là do Java hút rất nhiều.
Một đối số hợp lệ là không phải tất cả các ngôn ngữ có thừa kế cổ điển đều hỗ trợ nhiều kế thừa. Một lần nữa Java xuất hiện trong tâm trí. Có Java có giao diện, nhưng điều đó là không đủ. Đôi khi bạn thực sự cần nhiều sự kế thừa.
Vì thừa kế nguyên mẫu cho phép nhiều kế thừa, mã yêu cầu nhiều kế thừa sẽ ít dư thừa hơn nếu được viết bằng cách sử dụng thừa kế nguyên mẫu thay vì trong một ngôn ngữ có thừa kế cổ điển nhưng không có nhiều kế thừa.
4. Kế thừa nguyên mẫu là năng động
Một trong những lợi thế quan trọng nhất của thừa kế nguyên mẫu là bạn có thể thêm các thuộc tính mới vào các nguyên mẫu sau khi chúng được tạo. Điều này cho phép bạn thêm các phương thức mới vào một nguyên mẫu sẽ tự động được cung cấp cho tất cả các đối tượng ủy quyền cho nguyên mẫu đó.
Điều này là không thể trong kế thừa cổ điển bởi vì một khi một lớp được tạo, bạn không thể sửa đổi nó trong thời gian chạy. Đây có lẽ là lợi thế lớn nhất của kế thừa nguyên mẫu so với thừa kế cổ điển, và nó nên được đặt lên hàng đầu. Tuy nhiên tôi thích tiết kiệm tốt nhất cho đến cuối cùng.
Phần kết luận
Vấn đề thừa kế nguyên mẫu. Điều quan trọng là giáo dục các lập trình viên JavaScript về lý do tại sao từ bỏ mô hình xây dựng của kế thừa nguyên mẫu để ủng hộ mô hình nguyên mẫu của kế thừa nguyên mẫu.
Chúng ta cần bắt đầu dạy JavaScript một cách chính xác và điều đó có nghĩa là chỉ cho các lập trình viên mới cách viết mã bằng cách sử dụng mẫu nguyên mẫu thay vì mẫu xây dựng.
Không chỉ dễ dàng hơn để giải thích sự kế thừa nguyên mẫu bằng cách sử dụng mẫu nguyên mẫu, mà còn giúp các lập trình viên tốt hơn.
Nếu bạn thích câu trả lời này thì bạn cũng nên đọc bài đăng trên blog của tôi về " Tại sao các vấn đề kế thừa nguyên mẫu ". Tin tôi đi, bạn sẽ không phải thất vọng.