Có hai mô hình để triển khai các lớp và các thể hiện trong JavaScript: cách tạo mẫu và cách đóng. Cả hai đều có ưu điểm và nhược điểm, và có rất nhiều biến thể mở rộng. Nhiều lập trình viên và thư viện có các cách tiếp cận khác nhau và các hàm tiện ích xử lý lớp để viết lên một số phần xấu nhất của ngôn ngữ.
Kết quả là trong công ty hỗn hợp, bạn sẽ có một mớ hỗn độn của metaclass, tất cả đều cư xử hơi khác nhau. Tệ hơn nữa, hầu hết các tài liệu hướng dẫn JavaScript là khủng khiếp và phục vụ một số loại thỏa hiệp giữa để bao quát tất cả các cơ sở, khiến bạn rất bối rối. (Có lẽ tác giả cũng nhầm lẫn. Mô hình đối tượng của JavaScript rất khác với hầu hết các ngôn ngữ lập trình và ở nhiều nơi được thiết kế tồi.)
Hãy bắt đầu với cách nguyên mẫu . Đây là bản gốc JavaScript nhất mà bạn có thể nhận được: có tối thiểu mã trên không và instanceof sẽ hoạt động với các phiên bản của loại đối tượng này.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Chúng ta có thể thêm các phương thức vào thể hiện được tạo bằng new Shape
cách viết chúng vào phần prototype
tra cứu của hàm tạo này:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Bây giờ để phân lớp nó, nhiều như bạn có thể gọi JavaScript là phân lớp nào. Chúng tôi làm điều đó bằng cách thay thế hoàn toàn prototype
tài sản ma thuật kỳ lạ đó :
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
trước khi thêm các phương thức vào nó:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Ví dụ này sẽ hoạt động và bạn sẽ thấy mã giống như nó trong nhiều hướng dẫn. Nhưng bạn ơi, điều đó new Shape()
thật xấu xí: chúng ta đang tạo ra lớp cơ sở mặc dù không có hình dạng thực tế nào được tạo ra. Nó xảy ra để hoạt động trong trường hợp đơn giản này vì JavaScript rất cẩu thả: nó cho phép không có đối số nào được truyền vào, trong trường hợp này x
và y
trở thành undefined
và được gán cho nguyên mẫu this.x
và this.y
. Nếu hàm constructor đang làm bất cứ điều gì phức tạp hơn, nó sẽ nằm thẳng trên mặt của nó.
Vì vậy, những gì chúng ta cần làm là tìm cách tạo một đối tượng nguyên mẫu có chứa các phương thức và các thành viên khác mà chúng ta muốn ở cấp độ lớp, mà không gọi hàm xây dựng của lớp cơ sở. Để làm điều này, chúng ta sẽ phải bắt đầu viết mã trợ giúp. Đây là cách tiếp cận đơn giản nhất mà tôi biết:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Điều này chuyển các thành viên của lớp cơ sở trong nguyên mẫu của nó sang một hàm xây dựng mới mà không làm gì cả, sau đó sử dụng hàm tạo đó. Bây giờ chúng ta có thể viết đơn giản:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
thay vì new Shape()
sai Bây giờ chúng ta có một tập hợp nguyên thủy chấp nhận được cho các lớp được xây dựng.
Có một vài tinh chỉnh và tiện ích mở rộng mà chúng ta có thể xem xét theo mô hình này. Ví dụ ở đây là một phiên bản đường cú pháp:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
Cả hai phiên bản đều có nhược điểm là chức năng của hàm tạo không thể được kế thừa, vì nó có trong nhiều ngôn ngữ. Vì vậy, ngay cả khi lớp con của bạn không thêm gì vào quá trình xây dựng, nó vẫn phải nhớ gọi hàm tạo cơ sở với bất kỳ đối số nào mà cơ sở muốn. Điều này có thể được tự động hóa một chút bằng cách sử dụng apply
, nhưng bạn vẫn phải viết ra:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Vì vậy, một phần mở rộng phổ biến là phá vỡ các công cụ khởi tạo thành chức năng của chính nó chứ không phải là chính hàm tạo. Hàm này sau đó có thể kế thừa từ cơ sở tốt:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Bây giờ chúng ta vừa có cùng một hàm khởi tạo hàm cho mỗi lớp. Có lẽ chúng ta có thể chuyển nó ra chức năng của trình trợ giúp riêng của mình để chúng ta không phải tiếp tục gõ nó, ví dụ thay vì Function.prototype.subclass
xoay tròn và để Hàm của lớp cơ sở nhổ ra các lớp con:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... bắt đầu trông hơi giống các ngôn ngữ khác, mặc dù với cú pháp hơi vụng về. Bạn có thể rắc thêm một vài tính năng nếu bạn thích. Có thể bạn muốn makeSubclass
lấy và nhớ một tên lớp và cung cấp một mặc định toString
bằng cách sử dụng nó. Có thể bạn muốn làm cho hàm tạo phát hiện khi nó vô tình được gọi mà không có new
toán tử (điều này thường sẽ dẫn đến việc gỡ lỗi rất khó chịu):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Có lẽ bạn muốn vượt qua tất cả các thành viên mới và đã makeSubclass
thêm họ vào nguyên mẫu, để tiết kiệm bạn phải viết Class.prototype...
khá nhiều. Rất nhiều hệ thống lớp làm điều đó, ví dụ:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Có rất nhiều tính năng tiềm năng mà bạn có thể xem là mong muốn trong một hệ thống đối tượng và không ai thực sự đồng ý với một công thức cụ thể.
Cách đóng cửa , sau đó. Điều này tránh được các vấn đề về thừa kế dựa trên nguyên mẫu của JavaScript, bằng cách không sử dụng tính kế thừa nào cả. Thay thế:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Bây giờ mỗi phiên bản đơn lẻ Shape
sẽ có bản sao riêng của toString
phương thức (và bất kỳ phương thức nào khác hoặc các thành viên lớp khác mà chúng tôi thêm vào).
Điều tệ hại về mọi trường hợp có bản sao riêng của từng thành viên trong lớp là nó kém hiệu quả hơn. Nếu bạn đang xử lý một số lượng lớn các thể hiện được phân lớp, kế thừa nguyên mẫu có thể phục vụ bạn tốt hơn. Ngoài ra, việc gọi một phương thức của lớp cơ sở là hơi khó chịu như bạn có thể thấy: chúng ta phải nhớ phương thức đó là gì trước khi hàm tạo của lớp con ghi đè lên nó, hoặc nó bị mất.
[Ngoài ra vì không có quyền thừa kế ở đây, instanceof
nhà điều hành sẽ không làm việc; bạn sẽ phải cung cấp cơ chế riêng để đánh hơi lớp nếu bạn cần. Mặc dù bạn có thể đánh lừa các đối tượng nguyên mẫu theo cách tương tự như với kế thừa nguyên mẫu, nhưng nó hơi khó và không thực sự đáng để instanceof
làm việc.]
Điểm hay của mọi cá thể có phương thức riêng là phương thức đó có thể bị ràng buộc với thể hiện cụ thể sở hữu nó. Điều này hữu ích vì cách ràng buộc kỳ lạ của JavaScript this
trong các cuộc gọi phương thức, có kết quả là nếu bạn tách một phương thức khỏi chủ sở hữu của nó:
var ts= mycircle.toString;
alert(ts());
sau đó this
bên trong phương thức sẽ không phải là đối tượng Circle như mong đợi (nó thực sự sẽ là window
đối tượng toàn cầu , gây ra sự cố gỡ lỗi rộng rãi). Trong thực tế này thường xảy ra khi một phương pháp được thực hiện và giao cho một setTimeout
, onclick
hoặc EventListener
nói chung.
Với cách thức nguyên mẫu, bạn phải bao gồm một bao đóng cho mỗi lần gán như vậy:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
hoặc, trong tương lai (hoặc bây giờ nếu bạn hack Function.prototype), bạn cũng có thể làm điều đó với function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
nếu các thể hiện của bạn được thực hiện theo cách đóng, ràng buộc được thực hiện miễn phí bằng cách đóng trên biến đối tượng (thường được gọi that
hoặc self
, mặc dù cá nhân tôi sẽ khuyên chống lại cái sau vì self
đã có nghĩa khác, trong JavaScript). 1, 1
Mặc dù vậy, bạn không nhận được các đối số trong đoạn trích trên miễn phí, vì vậy bạn vẫn sẽ cần một lần đóng cửa khác hoặc bind()
nếu bạn cần làm điều đó.
Có rất nhiều biến thể về phương pháp đóng cửa quá. Bạn có thể muốn bỏ qua this
hoàn toàn, tạo một cái mới that
và trả lại nó thay vì sử dụng new
toán tử:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Cách nào là đúng cách? Cả hai. Tốt nhất"? Điều đó phụ thuộc vào tình hình của bạn. FWIW Tôi có xu hướng tạo nguyên mẫu cho kế thừa JavaScript thực sự khi tôi đang thực hiện công cụ OO mạnh mẽ và đóng cửa cho các hiệu ứng trang bỏ đi đơn giản.
Nhưng cả hai cách đều khá phản trực quan đối với hầu hết các lập trình viên. Cả hai đều có nhiều biến thể lộn xộn tiềm năng. Bạn sẽ gặp cả hai (cũng như nhiều sơ đồ ở giữa và thường bị hỏng) nếu bạn sử dụng mã / thư viện của người khác. Không có ai trả lời chung chung. Chào mừng bạn đến với thế giới tuyệt vời của các đối tượng JavaScript.
[Đây là phần 94 của Tại sao JavaScript không phải là ngôn ngữ lập trình yêu thích của tôi.]