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:
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
Chuỗi phạm vi.
function foo() {
function bar() {
function baz() {
// chain: baz -> bar -> foo -> global
}
}
}
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
, row
và add
. 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 a
và b
.
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:
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 ở đó.
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.
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
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
Đ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. –