2015-02-28 18 views
65

Hệ thống nguyên mẫu trông linh hoạt hơn nhiều so với hệ thống lớp học truyền thống, nhưng mọi người dường như cảm thấy hài lòng với cái gọi là "thực hành tốt nhất", mà bắt chước hệ thống lớp học truyền thống:Hệ thống nguyên mẫu JavaScript có thể làm gì ngoài việc bắt chước một hệ thống lớp học cổ điển?

function foo() { 
    // define instance properties here 
} 

foo.prototype.method = //define instance method here 

new foo() 

Phải có những thứ khác rằng một hệ thống nguyên mẫu có thể làm với tất cả sự linh hoạt.

Có sử dụng cho hệ thống nguyên mẫu bên ngoài các lớp học bắt chước không? Những loại nguyên mẫu nào có thể là nguyên mẫu mà lớp nào không thể, hoặc là không có gì?

+0

liên quan nếu không trùng lặp: [Có một số thư viện JavaScript sử dụng các khía cạnh động của hệ thống nguyên mẫu không?] (Http://stackoverflow.com/q/10609822/1048572) – Bergi

+0

Tôi đoán sẽ dễ dàng hơn trong việc thêm động và bằng cách sử dụng nguyên mẫu, bạn có thể mở rộng "lớp học" của bạn rất dễ dàng. – theonlygusti

+4

Điều đáng nói đến là một hệ thống lớp học cổ điển cũng có thể bắt chước mô hình thừa kế nguyên mẫu. –

Trả lời

46

Hệ thống nguyên mẫu cung cấp mô hình thu hút metaprogramming, bằng cách triển khai kế thừa thông qua các đối tượng chuẩn. Tất nhiên, điều này chủ yếu được sử dụng để thể hiện khái niệm được thiết lập và đơn giản về các lớp thể hiện, nhưng không có các lớp như cấu trúc bất biến ở mức độ ngôn ngữ cần cú pháp cụ thể để tạo ra chúng. Bằng cách sử dụng các đối tượng đơn giản, tất cả những gì bạn có thể làm với các đối tượng (và bạn có thể làm mọi thứ) bây giờ bạn có thể làm với "các lớp" - đây là tính linh hoạt mà bạn nói.

Sự linh hoạt này sau đó được sử dụng rất nhiều để mở rộng và thay đổi các lớp học lập trình, chỉ sử dụng các khả năng cho đối tượng đột biến của JavaScript:

  • mixins và đặc điểm cho đa kế thừa
  • nguyên mẫu có thể được sửa đổi sau đối tượng được kế thừa từ chúng đã được tạo ra
  • các hàm trang trí và phương pháp đặt hàng cao hơn có thể được sử dụng dễ dàng trong việc tạo ra các nguyên mẫu

Tất nhiên, mô hình nguyên mẫu chính nó mạnh hơn là chỉ thực hiện các lớp. Các tính năng này được sử dụng khá hiếm khi, vì khái niệm lớp rất hữu ích và phổ biến, do đó, sức mạnh thực sự của thừa kế nguyên mẫu không nổi tiếng (và không được tối ưu hóa tốt trong các công cụ JS: - /)

  • chuyển đổi nguyên mẫu của các đối tượng hiện có có thể được sử dụng để thay đổi hành vi của chúng một cách đáng kể. (hỗ trợ đầy đủ với ES6 Reflect.setPrototypeOf)
  • một vài mẫu kỹ thuật phần mềm có thể được triển khai trực tiếp với các đối tượng. Ví dụ là flyweight pattern với các thuộc tính, một chain of responsibilities bao gồm chuỗi động, oh và dĩ nhiên là prototype pattern.

    Ví dụ tốt cho mẫu cuối cùng sẽ là đối tượng tùy chọn có mặc định. Tất cả mọi người tạo ra chúng bằng cách sử

    var myOptions = extend({}, defaultOptions, optionArgument); 
    

    nhưng một cách tiếp cận năng động hơn sẽ được sử dụng

    var myOptions = extend(Object.create(defaultOptions), optionArgument); 
    
+0

Có lợi thế nào mở rộng 'myOptions' bằng cách sử dụng cách tiếp cận năng động hơn không? Tôi có nghĩa là, thường là một đối tượng cấu hình vẫn giống hệt nhau trong suốt một cuộc gọi chức năng. – Kay

+0

@Kay: Nó sẽ nhỏ hơn (ít bộ nhớ hơn) và nên được tạo nhanh hơn, đặc biệt là với các đối tượng mặc định lớn. Ngoài ra, thay đổi mặc định sẽ tự động lan truyền – Bergi

11

Tôi nghĩ rằng hệ thống kế thừa nguyên chủng cho phép một bổ sung nhiều năng động hơn các phương pháp/tài sản.

Bạn có thể dễ dàng mở rộng các lớp học được viết bởi những người khác, ví dụ là tất cả các plugin jQuery trên mạng, và bạn cũng có thể dễ dàng thêm vào các lớp học có nguồn gốc, thêm các chức năng tiện ích để chuỗi, mảng và, tốt, bất cứ điều gì.

Ví dụ:

// I can just add whatever I want to anything I want, whenever I want 
String.prototype.first = function(){ return this[0]; }; 

'Hello'.first() // == 'H' 

Bạn cũng có thể sao chép các phương pháp từ các lớp khác,

function myString(){ 
    this[0] = '42'; 
} 
myString.prototype = String.prototype; 

foo = new myString(); 
foo.first() // == '42' 

Nó cũng có nghĩa là bạn có thể kéo dài một nguyên mẫu sau một đối tượng đã thừa hưởng từ nó, nhưng những thay đổi sẽ được áp dụng.

Và, cá nhân tôi, tôi thấy nguyên mẫu để được thực sự thuận tiện và đơn giản, đặt phương pháp ra trong một đối tượng được thực sự hấp dẫn đối với tôi;)

32

Trở lại trong tháng sáu năm 2013 tôi đã trả lời một câu hỏi về các benefits of prototypal inheritance over classical. Kể từ đó, tôi đã dành rất nhiều thời gian suy nghĩ về thừa kế, cả nguyên mẫu lẫn cổ điển và tôi đã viết rộng rãi về số prototype-classisomorphism.

Có, việc sử dụng chính của thừa kế prototypal là mô phỏng các lớp. Tuy nhiên, nó có thể được sử dụng nhiều hơn là chỉ mô phỏng các lớp. Ví dụ, các chuỗi mẫu thử nghiệm rất giống với các chuỗi phạm vi.

Đồng nhất nguyên mẫu-Phạm vi cũng là

Nguyên mẫu và phạm vi trong JavaScript có nhiều điểm chung. Có ba loại chuỗi phổ biến trong JavaScript:

  1. Chuỗi nguyên mẫu.

    var foo = {}; 
    var bar = Object.create(foo); 
    var baz = Object.create(bar); 
    
    // chain: baz -> bar -> foo -> Object.prototype -> null 
    
  2. Chuỗi phạm vi.

    function foo() { 
        function bar() { 
         function baz() { 
          // chain: baz -> bar -> foo -> global 
         } 
        } 
    } 
    
  3. Chuỗi phương pháp.

    var chain = { 
        foo: function() { 
         return this; 
        }, 
        bar: function() { 
         return this; 
        }, 
        baz: function() { 
         return this; 
        } 
    }; 
    
    chain.foo().bar().baz(); 
    

Trong số ba, chuỗi mẫu và chuỗi phạm vi là giống nhất. Trong thực tế, bạn có thể đính kèm một chuỗi nguyên mẫu vào một chuỗi phạm vi bằng cách sử dụng câu lệnh notoriouswith.

function foo() { 
    var bar = {}; 
    var baz = Object.create(bar); 

    with (baz) { 
     // chain: baz -> bar -> Object.prototype -> foo -> global 
    } 
} 

Vì vậy, việc sử dụng tính đồng cấu phạm vi nguyên mẫu là gì? Một cách sử dụng trực tiếp là mô hình hóa các chuỗi phạm vi sử dụng các chuỗi nguyên mẫu. Đây là chính xác những gì tôi đã làm cho ngôn ngữ lập trình của riêng tôi Bianca, mà tôi đã thực hiện trong JavaScript.

đầu tiên tôi được xác định phạm vi toàn cầu của Bianca, Populating nó với một loạt các chức năng toán học hữu ích trong một tập tin aptly tên global.js như sau:

var global = module.exports = Object.create(null); 

global.abs = new Native(Math.abs); 
global.acos = new Native(Math.acos); 
global.asin = new Native(Math.asin); 
global.atan = new Native(Math.atan); 
global.ceil = new Native(Math.ceil); 
global.cos = new Native(Math.cos); 
global.exp = new Native(Math.exp); 
global.floor = new Native(Math.floor); 
global.log = new Native(Math.log); 
global.max = new Native(Math.max); 
global.min = new Native(Math.min); 
global.pow = new Native(Math.pow); 
global.round = new Native(Math.round); 
global.sin = new Native(Math.sin); 
global.sqrt = new Native(Math.sqrt); 
global.tan = new Native(Math.tan); 

global.max.rest = { type: "number" }; 
global.min.rest = { type: "number" }; 

global.sizeof = { 
    result: { type: "number" }, 
    type: "function", 
    funct: sizeof, 
    params: [{ 
     type: "array", 
     dimensions: [] 
    }] 
}; 

function Native(funct) { 
    this.funct = funct; 
    this.type = "function"; 
    var length = funct.length; 
    var params = this.params = []; 
    this.result = { type: "number" }; 
    while (length--) params.push({ type: "number" }); 
} 

function sizeof(array) { 
    return array.length; 
} 

Lưu ý rằng tôi đã tạo ra phạm vi toàn cầu sử dụng Object.create(null). Tôi đã làm điều này bởi vì phạm vi toàn cầu không có bất kỳ phạm vi cha mẹ nào.

Sau đó, đối với mỗi chương trình, tôi đã tạo phạm vi chương trình riêng biệt chứa các định nghĩa cấp cao nhất của chương trình. Mã được lưu trữ trong một tệp có tên analyzer.js quá lớn để vừa với một câu trả lời.Dưới đây là ba dòng đầu tiên của tệp:

var parse = require("./ast"); 
var global = require("./global"); 
var program = Object.create(global); 

Như bạn có thể thấy, phạm vi toàn cầu là cấp độ gốc của phạm vi chương trình. Do đó, program được kế thừa từ global, giúp tra cứu biến phạm vi đơn giản như tra cứu thuộc tính đối tượng. Điều này làm cho thời gian chạy của ngôn ngữ đơn giản hơn nhiều.

Phạm vi chương trình chứa các định nghĩa cấp cao nhất của chương trình. Ví dụ, hãy xem xét các chương trình nhân ma trận sau đó được lưu trữ trong tập tin matrix.bianca:

col(a[3][3], b[3][3], i, j) 
    if (j >= 3) a 
    a[i][j] += b[i][j] 
    col(a, b, i, j + 1) 

row(a[3][3], b[3][3], i) 
    if (i >= 3) a 
    a = col(a, b, i, 0) 
    row(a, b, i + 1) 

add(a[3][3], b[3][3]) 
    row(a, b, 0) 

Các định nghĩa cấp cao nhất là col, rowadd. Mỗi hàm trong số này có phạm vi hàm riêng của nó cũng như được thừa hưởng từ phạm vi chương trình. Mã cho rằng có thể được tìm thấy trên line 67 of analyzer.js:

scope = Object.create(program); 

Ví dụ, phạm vi chức năng của add có các định nghĩa cho các ma trận ab.

Do đó, bên cạnh các lớp mẫu cũng hữu ích cho việc lập mô hình phạm vi chức năng.

Nguyên mẫu để lập mô hình các loại dữ liệu đại số

Lớp học không phải là loại trừu tượng duy nhất có sẵn. Trong dữ liệu ngôn ngữ lập trình hàm được mô hình hóa bằng cách sử dụng algebraic data types.

Ví dụ tốt nhất của một kiểu dữ liệu đại số là một danh sách:

data List a = Nil | Cons a (List a) 

định nghĩa dữ liệu này chỉ đơn giản có nghĩa là một danh sách của một thể hoặc là một danh sách rỗng (tức là Nil) hoặc nếu không giá trị loại “ a ” được chèn vào danh sách (ví dụ: Cons a (List a)). Ví dụ, sau đây là danh sách tất cả:

Nil       :: List a 
Cons 1 Nil     :: List Number 
Cons 1 (Cons 2 Nil)   :: List Number 
Cons 1 (Cons 2 (Cons 3 Nil)) :: List Number 

Loại biến a trong định nghĩa dữ liệu cho phép parametric polymorphism (tức là nó cho phép trong danh sách để tổ chức bất kỳ loại giá trị). Ví dụ: Nil có thể chuyên về danh sách các số hoặc danh sách các boolean vì nó có loại List a trong đó a có thể là bất kỳ thứ gì.

Điều này cho phép chúng ta tạo ra các chức năng tham số như length:

length :: List a -> Number 
length Nil  = 0 
length (Cons _ l) = 1 + length l 

Chức năng length có thể được sử dụng để tìm chiều dài của danh sách bất kỳ không phụ thuộc vào loại giá trị nó chứa vì length chức năng đơn giản không quan tâm đến các giá trị của danh sách.

Ngoài đa hình tham số hầu hết các ngôn ngữ lập trình chức năng cũng có một số dạng ad-hoc polymorphism. Trong đa hình ad-hoc, một thực hiện cụ thể của một hàm được chọn tùy thuộc vào loại biến đa hình.

Ví dụ: toán tử + trong JavaScript được sử dụng cho cả kết hợp chuỗi và chuỗi phụ thuộc vào loại đối số.Đây là một dạng đa hình ad-hoc.

Tương tự, trong các ngôn ngữ lập trình chức năng, hàm map thường bị quá tải. Ví dụ: bạn có thể triển khai map khác nhau cho danh sách, triển khai khác nhau cho các bộ, v.v. Loại lớp là một cách để triển khai đa hình đặc biệt. Ví dụ, lớp Functor loại cung cấp map chức năng:

class Functor f where 
    map :: (a -> b) -> f a -> f b 

Chúng tôi sau đó tạo ra các trường hợp cụ thể của Functor với nhiều loại dữ liệu khác nhau:

instance Functor List where 
    map :: (a -> b) -> List a -> List b 
    map _ Nil  = Nil 
    map f (Cons a l) = Cons (f a) (map f l) 

Nguyên mẫu trong JavaScript cho phép chúng ta mô hình cả hai loại dữ liệu đại số và tính đa hình ad-hoc. Ví dụ, đoạn code trên có thể được dịch one-to-one JavaScript như sau:

var list = Cons(1, Cons(2, Cons(3, Nil))); 
 

 
alert("length: " + length(list)); 
 

 
function square(n) { 
 
    return n * n; 
 
} 
 

 
var result = list.map(square); 
 

 
alert(JSON.stringify(result, null, 4));
<script> 
 
// data List a = Nil | Cons a (List a) 
 

 
function List(constructor) { 
 
    Object.defineProperty(this, "constructor", { 
 
     value: constructor || this 
 
    }); 
 
} 
 

 
var Nil = new List; 
 

 
function Cons(head, tail) { 
 
    var cons = new List(Cons); 
 
    cons.head = head; 
 
    cons.tail = tail; 
 
    return cons; 
 
} 
 

 
// parametric polymorphism 
 

 
function length(a) { 
 
    switch (a.constructor) { 
 
    case Nil: return 0; 
 
    case Cons: return 1 + length(a.tail); 
 
    } 
 
} 
 

 
// ad-hoc polymorphism 
 

 
List.prototype.map = function (f) { 
 
    switch (this.constructor) { 
 
    case Nil: return Nil; 
 
    case Cons: return Cons(f(this.head), this.tail.map(f)); 
 
    } 
 
}; 
 
</script>

Mặc dù lớp có thể được sử dụng để mô hình đa hình ad-hoc là tốt, tất cả các chức năng quá tải cần phải xác định ở một nơi. Với nguyên mẫu, bạn có thể xác định chúng bất cứ nơi nào bạn muốn.

Kết luận

Như bạn thấy, nguyên mẫu là rất linh hoạt. Có, chúng chủ yếu được sử dụng để mô hình hóa các lớp. Tuy nhiên, chúng có thể được sử dụng cho rất nhiều thứ khác.

Một số trong những điều khác mà nguyên mẫu có thể được sử dụng cho:

  1. Tạo persistent data structures với chia sẻ cấu trúc.

    Ý tưởng cơ bản của việc chia sẻ cấu trúc là thay vì sửa đổi một đối tượng, tạo ra một đối tượng mới mà kế thừa từ đối tượng gốc và thực hiện bất kỳ thay đổi bạn muốn. Prototypal thừa kế vượt trội ở đó.

  2. Như những người khác đã đề cập, nguyên mẫu là động. Do đó, bạn có thể hồi tiếp thêm các phương thức mẫu thử nghiệm mới và chúng sẽ tự động có sẵn trên tất cả các phiên bản của mẫu thử nghiệm.

Hy vọng điều này sẽ hữu ích.

+1

A (quá) dài nhưng thú vị đọc :-) Tuy nhiên, bằng cách giải thích các đẳng cấp nguyên mẫu-isomorphism nó bỏ lỡ điểm của câu hỏi imo - OP đã biết làm thế nào những công việc, ông muốn để biết cái gì ngoài đó. Các tính năng duy nhất mà bạn đề cập là các nguyên mẫu có thể được sử dụng để triển khai một chuỗi phạm vi (ví dụ rất thú vị) và cho phép thêm phương thức vào bất cứ nơi nào bạn muốn (có vẻ như cần thiết để triển khai ADT). – Bergi

+1

Sẽ đọc các bài viết về cấu trúc dữ liệu liên tục ngay bây giờ. Tôi tự hỏi làm thế nào nguyên mẫu có thể được sử dụng để thực hiện chúng mà không bị rò rỉ dữ liệu cũ. – Bergi

+0

Bạn chính xác. Tôi sẽ cắt giảm độ dài của câu trả lời bằng cách loại bỏ đẳng cấu nguyên mẫu. –

0

Trong JavaScript, không có khái niệm như vậy Class. Ở đây mọi thứ đều là đối tượng. Và tất cả các đối tượng trong JavaScript đều bị mất từ ​​Object. Các tài sản nguyên mẫu giúp trong kế thừa, Khi chúng ta đang phát triển ứng dụng theo hướng đối tượng. Có nhiều tính năng hơn trong nguyên mẫu, hơn Class trong cấu trúc hướng đối tượng truyền thống.

Trong nguyên mẫu, bạn có thể thêm thuộc tính vào chức năng được viết bởi người khác.

Ví dụ:

Array.prototype.print=function(){ 
    console.log(this); 
} 

sử dụng trong Inheritance:

Bạn có thể sử dụng thừa kế bởi bằng cách sử dụng thuộc tính prototype. Here là cách bạn có thể sử dụng kế thừa với JavaScript.

Trong hệ thống Class truyền thống, bạn không thể sửa đổi khi lớp được xác định. Nhưng trong bạn có thể làm trong JavaScript với hệ thống nguyên mẫu.

Các vấn đề liên quan