Система прототипов выглядит гораздо более гибкой, чем традиционная система классов, но люди, похоже, чувствуют, что довольны так называемыми "лучшими практиками", которые имитируют традиционную систему классов:
Должны быть другие вещи, которые прототипная система может делать со всей гибкостью.
Используются ли для прототипной системы вне классов подражания? Какие вещи могут создавать прототипы, какие классы не могут, или их нет?
Ответ 2
Еще в июне 2013 года я ответил на вопрос о преимуществах прототипного наследования по сравнению с классическим. С тех пор я много времени размышлял над наследованием, как прототипным, так и классическим, и я много писал о прототипе fooobar.com/questions/4789/... изоморфизм.
Да, первичное использование прототипального наследования заключается в моделировании классов. Однако его можно использовать намного больше, чем просто моделировать классы. Например, цепи прототипов очень похожи на цепочки областей.
Изоморфизм прототипа-области, а также
Прототипы и области применения JavaScript имеют много общего. В JavaScript существует три общих типа цепочек:
-
Цепочки прототипов.
var foo = {};
var bar = Object.create(foo);
var baz = Object.create(bar);
// chain: baz -> bar -> foo -> Object.prototype -> null
-
Цепочки областей.
function foo() {
function bar() {
function baz() {
// chain: baz -> bar -> foo -> global
}
}
}
-
Цепочки методов.
var chain = {
foo: function () {
return this;
},
bar: function () {
return this;
},
baz: function () {
return this;
}
};
chain.foo().bar().baz();
Из трех цепей прототипа и цепей видимости наиболее схожи. На самом деле вы можете присоединить цепочку прототипов к цепочке видимости с помощью пресловутого with
.
function foo() {
var bar = {};
var baz = Object.create(bar);
with (baz) {
// chain: baz -> bar -> Object.prototype -> foo -> global
}
}
Итак, что такое использование изоморфизма прототипа-области? Одно прямое использование заключается в моделировании цепей областей с использованием прототипных цепей. Это именно то, что я сделал для своего собственного языка программирования Bianca, который я реализовал в JavaScript.
Сначала я определил глобальную область Bianca, заполнив ее кучей полезных математических функций в файле, точно названном global.js как следующим образом:
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;
}
Обратите внимание, что я создал глобальную область с помощью Object.create(null)
. Я сделал это, потому что глобальная область не имеет родительской области.
После этого для каждой программы я создал отдельную программную область, в которой содержатся определения верхнего уровня программы. Код хранится в файле с именем analyzer.js, который слишком велик, чтобы вписаться в один ответ. Вот первые три строки файла:
var parse = require("./ast");
var global = require("./global");
var program = Object.create(global);
Как вы можете видеть, глобальная область действия является родителем области программы. Следовательно, program
наследует от global
, делая просмотр переменной области таким же простым, как поиск свойства объекта. Это значительно упрощает время выполнения языка.
В объем программы входят определения верхнего уровня программы. Например, рассмотрим следующую программу умножения матриц, которая хранится в файле 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)
Определения верхнего уровня: col
, row
и add
. Каждая из этих функций имеет собственную область действия, которая наследуется от области программы. Код для этого можно найти на строке 67 анализатора .js:
scope = Object.create(program);
Например, область функций add
имеет определения для матриц a
и b
.
Следовательно, помимо классов прототипы также полезны для моделирования областей функций.
Прототипы для моделирования типов алгебраических данных
Классы - это не единственный доступный тип абстракции. В языках функционального программирования данные моделируются с использованием алгебраических типов данных.
Лучший пример типа алгебраических данных - это список:
data List a = Nil | Cons a (List a)
Это определение данных просто означает, что список a может быть либо пустым списком (т.е. Nil
), либо значением типа "a", вставленным в список (т.е. Cons a (List a)
). Например, следующие списки:
Nil :: List a
Cons 1 Nil :: List Number
Cons 1 (Cons 2 Nil) :: List Number
Cons 1 (Cons 2 (Cons 3 Nil)) :: List Number
Переменная типа a
в определении данных позволяет параметрический полиморфизм (т.е. позволяет списку сохранять любой тип значения). Например, Nil
может быть специализирован для списка чисел или списка логических элементов, поскольку он имеет тип List a
, где a
может быть чем угодно.
Это позволяет нам создавать параметрические функции, такие как length
:
length :: List a -> Number
length Nil = 0
length (Cons _ l) = 1 + length l
Функция length
может использоваться для поиска длины любого списка независимо от типа значений, которые он содержит, потому что функция length
просто не заботится о значениях списка.
В дополнение к параметрическому полиморфизму большинство языков функционального программирования также имеют некоторую форму ad-hoc polymorphism. В ad-hoc-полиморфизме выбирается одна конкретная реализация функции в зависимости от типа полиморфной переменной.
Например, оператор +
в JavaScript используется как для сложения, так и для конкатенации строк в зависимости от типа аргумента. Это форма ad-hoc-полиморфизма.
Аналогично, в языках функционального программирования функция map
обычно перегружена. Например, у вас может быть другая реализация map
для списков, другая реализация для наборов и т.д. Классы типов - это один из способов реализации ad-hoc-полиморфизма. Например, класс Functor
предоставляет функцию map
:
class Functor f where
map :: (a -> b) -> f a -> f b
Затем мы создаем конкретные экземпляры Functor
для разных типов данных:
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)
Прототипы в JavaScript позволяют нам моделировать как алгебраические типы данных, так и ad-hoc-полиморфизм. Например, приведенный выше код можно перевести один-на-один на JavaScript следующим образом:
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>
Ответ 3
Я думаю, что прототипная система наследования допускает гораздо более динамическое добавление методов/свойств.
Вы можете легко расширить классы, написанные другими людьми, например, все плагины jQuery, и вы также можете легко добавить к родным классам, добавить функции утилиты в строки, массивы и, ну, что угодно.
Пример:
// I can just add whatever I want to anything I want, whenever I want
String.prototype.first = function(){ return this[0]; };
'Hello'.first() // == 'H'
Вы также можете копировать методы из других классов,
function myString(){
this[0] = '42';
}
myString.prototype = String.prototype;
foo = new myString();
foo.first() // == '42'
Это также означает, что вы можете продлить прототип после того, как объект унаследовал от него, но эти изменения будут применены.
И, лично, я считаю, что прототипы действительно удобны и просты, методы укладки внутри объекта действительно привлекательны для меня;)