Javascript khi nào sử dụng nguyên mẫu


93

Tôi muốn hiểu khi nào thì thích hợp để sử dụng các phương thức nguyên mẫu trong js. Chúng có nên được sử dụng luôn không? Hoặc có những trường hợp sử dụng chúng không được ưu tiên và / hoặc phải chịu một hình phạt về hiệu suất?

Khi tìm kiếm xung quanh trang web này về các phương pháp phổ biến cho không gian tên trong js, có vẻ như hầu hết sử dụng cách triển khai không dựa trên nguyên mẫu: chỉ đơn giản là sử dụng một đối tượng hoặc một đối tượng hàm để đóng gói một không gian tên.

Đến từ một ngôn ngữ dựa trên lớp, thật khó để không thử vẽ các đối số và nghĩ rằng các nguyên mẫu giống như "các lớp" và các triển khai không gian tên mà tôi đã đề cập giống như các phương thức tĩnh.

Câu trả lời:


133

Nguyên mẫu là một tối ưu hóa .

Một ví dụ tuyệt vời về việc sử dụng chúng tốt là thư viện jQuery. Mỗi khi bạn lấy một đối tượng jQuery bằng cách sử dụng $('.someClass'), đối tượng đó có hàng tá "phương thức". Thư viện có thể đạt được điều đó bằng cách trả về một đối tượng:

return {
   show: function() { ... },
   hide: function() { ... },
   css: function() { ... },
   animate: function() { ... },
   // etc...
};

Nhưng điều đó có nghĩa là mọi đối tượng jQuery trong bộ nhớ sẽ có hàng chục vị trí được đặt tên chứa các phương thức giống nhau, lặp đi lặp lại.

Thay vào đó, các phương thức đó được xác định trên một nguyên mẫu và tất cả các đối tượng jQuery "kế thừa" nguyên mẫu đó để đạt được tất cả các phương thức đó với chi phí thời gian chạy rất ít.

Một phần cực kỳ quan trọng trong cách jQuery làm đúng là điều này được ẩn với lập trình viên. Nó hoàn toàn được coi là một tối ưu hóa, không phải là thứ mà bạn phải lo lắng khi sử dụng thư viện.

Vấn đề với JavaScript là các hàm khởi tạo thường yêu cầu người gọi phải nhớ đặt tiền tố cho chúng newhoặc nếu không thì chúng thường không hoạt động. Không có lý do chính đáng cho điều này. jQuery làm đúng bằng cách giấu điều vô nghĩa đó đằng sau một hàm thông thường $, vì vậy bạn không cần quan tâm đến cách các đối tượng được triển khai.

Để bạn có thể thuận tiện tạo một đối tượng với một nguyên mẫu được chỉ định, ECMAScript 5 bao gồm một chức năng tiêu chuẩn Object.create. Một phiên bản được đơn giản hóa rất nhiều của nó sẽ trông như thế này:

Object.create = function(prototype) {
    var Type = function () {};
    Type.prototype = prototype;
    return new Type();
};

Nó chỉ lo việc viết một hàm khởi tạo và sau đó gọi nó bằng new.

Khi nào bạn tránh các nguyên mẫu?

Một so sánh hữu ích là với các ngôn ngữ OO phổ biến như Java và C #. Chúng hỗ trợ hai loại kế thừa:

  • kế thừa giao diện , trong đó bạn implementmột interfacelớp cung cấp triển khai duy nhất của riêng nó cho mọi thành viên của giao diện.
  • thực hiện kế thừa, nơi bạn có extendmột classcung cấp triển khai mặc định của một số phương pháp.

Trong JavaScript, kế thừa nguyên mẫu là một kiểu kế thừa triển khai . Vì vậy, trong những tình huống mà (trong C # hoặc Java), bạn sẽ bắt nguồn từ một lớp cơ sở để đạt được hành vi mặc định, sau đó bạn thực hiện các sửa đổi nhỏ thông qua ghi đè, thì trong JavaScript, kế thừa nguyên mẫu có ý nghĩa.

Tuy nhiên, nếu bạn đang ở trong tình huống mà bạn đã sử dụng giao diện trong C # hoặc Java, thì bạn không cần bất kỳ tính năng ngôn ngữ cụ thể nào trong JavaScript. Không cần phải khai báo rõ ràng một cái gì đó đại diện cho giao diện và không cần đánh dấu các đối tượng là "triển khai" giao diện đó:

var duck = {
    quack: function() { ... }
};

duck.quack(); // we're satisfied it's a duck!

Nói cách khác, nếu mỗi "loại" đối tượng có định nghĩa riêng về "phương thức", thì sẽ không có giá trị nào trong việc kế thừa từ một nguyên mẫu. Sau đó, nó phụ thuộc vào số lượng phiên bản bạn phân bổ của mỗi loại. Nhưng trong nhiều thiết kế mô-đun, chỉ có một trường hợp của một kiểu nhất định.

Và trên thực tế, nhiều người đã cho rằng kế thừa thực hiện là xấu xa . Nghĩa là, nếu có một số hoạt động phổ biến cho một kiểu, thì có lẽ sẽ rõ ràng hơn nếu chúng không được đưa vào một lớp cơ sở / siêu lớp, mà thay vào đó chỉ được hiển thị như các hàm thông thường trong một số mô-đun, mà bạn truyền (các) đối tượng vào. bạn muốn chúng hoạt động.


1
Lời giải thích hay. Sau đó, bạn có đồng ý rằng, vì bạn coi nguyên mẫu là một sự tối ưu hóa, nên chúng luôn có thể được sử dụng để cải thiện mã của bạn? Tôi tự hỏi nếu có những trường hợp sử dụng nguyên mẫu không có ý nghĩa hoặc thực sự phải chịu một hình phạt về hiệu suất.
OPL

Trong phần tiếp theo của bạn, bạn đề cập rằng "nó phụ thuộc vào số lượng trường hợp bạn phân bổ của mỗi loại". Nhưng ví dụ bạn tham khảo không sử dụng nguyên mẫu. Đâu là khái niệm cấp phát một thể hiện (bạn vẫn sử dụng "mới" ở đây)? Ngoài ra: giả sử phương thức quack có một tham số - liệu mỗi lệnh gọi của duck.quack (param) có khiến một đối tượng mới được tạo trong bộ nhớ (có thể không liên quan nếu nó có một tham số hay không)?
opl

3
1. Ý của tôi là nếu có một số lượng lớn các trường hợp của một loại vịt, thì việc sửa đổi ví dụ này sẽ rất hợp lý để quackhàm ở dạng nguyên mẫu, trong đó nhiều trường hợp vịt được liên kết với nhau. 2. Cú pháp nghĩa đen của đối tượng { ... }tạo ra một thể hiện (không cần sử dụng newvới nó). 3. Kêu gọi bất kỳ chức năng JS gây ít nhất một đối tượng được tạo ra trong bộ nhớ - nó được gọi là các argumentsđối tượng và lưu trữ các đối số được truyền trong các cuộc gọi: developer.mozilla.org/en/JavaScript/Reference/...
Daniel Earwicker

Cảm ơn tôi đã chấp nhận câu trả lời của bạn. Nhưng tôi vẫn hơi nhầm lẫn với quan điểm của bạn (1): Tôi không hiểu ý của bạn về "số lượng lớn các trường hợp của một loại vịt". Giống như bạn đã nói ở (3) mỗi khi bạn gọi một hàm JS, một đối tượng sẽ được tạo trong bộ nhớ - vì vậy ngay cả khi bạn chỉ có một loại vịt, bạn sẽ không phân bổ bộ nhớ mỗi khi bạn gọi một hàm vịt (trong trường hợp nào sẽ luôn có ý nghĩa khi sử dụng một nguyên mẫu)?
opl

11
+1 Việc so sánh với jQuery là lời giải thích ngắn gọn và rõ ràng đầu tiên về thời điểm & lý do sử dụng các nguyên mẫu mà tôi đã đọc. Cảm ơn rât nhiều.
GFoley83

46

Bạn nên sử dụng các nguyên mẫu nếu bạn muốn khai báo một phương thức "không tĩnh" của đối tượng.

var myObject = function () {

};

myObject.prototype.getA = function (){
  alert("A");
};

myObject.getB = function (){
  alert("B");
};

myObject.getB();  // This works fine

myObject.getA();  // Error!

var myPrototypeCopy = new myObject();
myPrototypeCopy.getA();  // This works, too.

@keatsKelleher nhưng chúng ta có thể tạo một phương thức không tĩnh cho đối tượng bằng cách chỉ định nghĩa phương thức bên trong hàm khởi tạo bằng thisví dụ this.getA = function(){alert("A")}phải không?
Amr Labib

17

Một lý do để sử dụng prototypeđối tượng tích hợp sẵn là nếu bạn sẽ sao chép nhiều lần một đối tượng sẽ chia sẻ chức năng chung. Bằng cách đính kèm các phương thức vào nguyên mẫu, bạn có thể tiết kiệm các phương thức nhân bản được tạo cho mỗi newtrường hợp. Nhưng khi bạn đính kèm một phương thức vào prototype, tất cả các phiên bản sẽ có quyền truy cập vào các phương thức đó.

Giả sử bạn có một Car()lớp / đối tượng cơ sở .

function Car() {
    // do some car stuff
}

sau đó bạn tạo nhiều Car()phiên bản.

var volvo = new Car(),
    saab = new Car();

Bây giờ, bạn biết mỗi chiếc xe sẽ cần phải lái, bật máy, v.v. Thay vì đính kèm một phương thức trực tiếp vào Car()lớp (chiếm bộ nhớ trên mỗi phiên bản được tạo), bạn có thể đính kèm các phương thức vào nguyên mẫu thay thế (chỉ tạo các phương thức một lần), do đó cấp quyền truy cập vào các phương thức đó cho cả phương thức mới volvosaab.

// just mapping for less typing
Car.fn = Car.prototype;

Car.fn.drive = function () {
    console.log("they see me rollin'");
};
Car.fn.honk = function () {
    console.log("HONK!!!");
}

volvo.honk();
// => HONK!!!
saab.drive();
// => they see me rollin'

2
thực sự điều này là không chính xác. volvo.honk () sẽ không hoạt động vì bạn đã thay thế hoàn toàn đối tượng nguyên mẫu chứ không phải mở rộng nó. Nếu bạn làm điều gì đó như thế này, nó sẽ hoạt động như bạn mong đợi: Car.prototype.honk = function () {console.log ('HONK');} volvo.honk (); // 'HONK'
29er

1
@ 29er - theo cách tôi đã viết ví dụ này, bạn đúng. Thứ tự không quan trọng. Nếu tôi vẫn giữ nguyên ví dụ này, Car.prototype = { ... }thì phải đến trước khi gọi một new Car()như minh họa trong jsfiddle này: jsfiddle.net/mxacA . Đối với lập luận của bạn, đây sẽ là cách chính xác để làm điều đó: jsfiddle.net/Embnp . Điều buồn cười là, tôi không nhớ trả lời câu hỏi này =)
hellatan

@hellatan, bạn có thể khắc phục điều đó bằng cách đặt hàm tạo: Xe thành vì bạn đã ghi đè thuộc tính nguyên mẫu bằng một đối tượng theo nghĩa đen.
Josh Bedo

@josh cảm ơn vì đã chỉ ra điều đó. Tôi đã cập nhật câu trả lời của mình để tôi không ghi đè nguyên mẫu bằng một đối tượng theo nghĩa đen, vì nó đáng lẽ phải có ngay từ đầu.
hellatan

12

Đặt các hàm trên một đối tượng nguyên mẫu khi bạn định tạo nhiều bản sao của một loại đối tượng cụ thể và tất cả chúng đều cần chia sẻ các hành vi chung. Làm như vậy, bạn sẽ tiết kiệm được một số bộ nhớ bằng cách chỉ có một bản sao của mỗi chức năng, nhưng đó chỉ là lợi ích đơn giản nhất.

Thay đổi phương thức trên các đối tượng nguyên mẫu hoặc thêm phương thức, ngay lập tức thay đổi bản chất của tất cả các bản sao của (các) kiểu tương ứng.

Bây giờ chính xác lý do tại sao bạn làm tất cả những điều này chủ yếu là một chức năng của thiết kế ứng dụng của riêng bạn và những thứ bạn cần làm trong mã phía máy khách. (Một câu chuyện hoàn toàn khác sẽ là mã bên trong máy chủ; dễ tưởng tượng hơn nhiều khi thực hiện mã "OO" quy mô lớn hơn ở đó.)


vậy khi tôi khởi tạo một đối tượng mới bằng các phương thức nguyên mẫu (thông qua từ khóa mới), thì đối tượng đó không nhận được bản sao mới của mỗi hàm (chỉ là một con trỏ)? Nếu đúng như vậy, tại sao bạn không muốn sử dụng một nguyên mẫu?
opl

như @marcel, d'oh ... =)
hellatan,

@opi vâng, bạn nói đúng - không có bản sao nào được tạo ra. Thay vào đó, các ký hiệu (tên thuộc tính) trên đối tượng nguyên mẫu chỉ được sắp xếp tự nhiên "ở đó" như là các phần ảo của mỗi đối tượng cá thể. Lý do duy nhất khiến mọi người không muốn bận tâm đến điều đó là những trường hợp các đối tượng tồn tại trong thời gian ngắn và khác biệt, hoặc nơi không có nhiều "hành vi" để chia sẻ.
Pointy

3

Nếu tôi giải thích trong thuật ngữ dựa trên lớp thì Person là lớp, walk () là phương thức Nguyên mẫu. Vì vậy, walk () sẽ chỉ tồn tại sau khi bạn khởi tạo đối tượng mới với điều này.

Vì vậy, nếu bạn muốn tạo các bản sao của đối tượng như Person u có thể tạo nhiều người dùng thì Prototype là giải pháp tốt vì nó tiết kiệm bộ nhớ bằng cách chia sẻ / kế thừa cùng một bản sao hàm cho từng đối tượng trong bộ nhớ.

Trong khi tĩnh không phải là trợ giúp lớn trong trường hợp như vậy.

function Person(){
this.name = "anonymous";
}

// its instance method and can access objects data data 
Person.prototype.walk = function(){
alert("person has started walking.");
}
// its like static method
Person.ProcessPerson = function(Person p){
alert("Persons name is = " + p.name);
}

var userOne = new Person();
var userTwo = new Person();

//Call instance methods
userOne.walk();

//Call static methods
Person.ProcessPerson(userTwo);

Vì vậy, với phương thức này giống như phương thức instance hơn của nó. Cách tiếp cận của đối tượng giống như các phương thức Tĩnh.

https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.