Что может сделать система прототипов JavaScript за пределами имитации классической системы классов?

Система прототипов выглядит гораздо более гибкой, чем традиционная система классов, но люди, похоже, чувствуют, что довольны так называемыми "лучшими практиками", которые имитируют традиционную систему классов:

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

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

new foo()

Должны быть другие вещи, которые прототипная система может делать со всей гибкостью.

Используются ли для прототипной системы вне классов подражания? Какие вещи могут создавать прототипы, какие классы не могут, или их нет?

Ответ 1

Система прототипов предлагает увлекательную модель metaprogramming путем реализации наследования через стандартные объекты. Конечно, это в основном используется, чтобы выразить установленную и простую концепцию классов экземпляров, но без классов как неизменяемых структур уровня языка, которые нуждаются в конкретном синтаксисе для их создания. Используя простые объекты, все, что вы можете сделать с объектами (и вы можете делать все), теперь можете делать с "классами" - это гибкость, о которой вы говорите.

Эта гибкость тогда много используется для расширения и изменения классов программным образом, используя только данные возможности объектной мутации JavaScript:

  • mixins и черты для множественного наследования
  • прототипы могут быть изменены после того, как объекты, наследующие их, были созданы
  • функции более высокого порядка и декораторы методов могут быть легко использованы при создании прототипов.

Конечно, сама модель прототипа более мощная, чем просто реализация классов. Эти функции используются довольно редко, поскольку концепция класса очень полезна и широко распространена, поэтому фактические полномочия наследования прототипов не являются хорошо известными (и не оптимизированы в JS-машинах: -/)

  • Отключение прототипов существующих объектов может быть использовано для значительного изменения их поведения. (полная поддержка с ES6 Reflect.setPrototypeOf)
  • несколько шаблонов разработки программного обеспечения могут быть реализованы непосредственно с объектами. Примерами являются flyweight pattern со свойствами, цепочка обязанностей включая динамические цепочки, о, и, конечно, шаблон .

    Хорошим примером для последнего является объекты-опции с значениями по умолчанию. Каждый создает их с помощью

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

    но более динамичным подходом было бы использовать

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

Ответ 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'

Это также означает, что вы можете продлить прототип после того, как объект унаследовал от него, но эти изменения будут применены.

И, лично, я считаю, что прототипы действительно удобны и просты, методы укладки внутри объекта действительно привлекательны для меня;)

Ответ 4

В JavaScript нет такой концепции класса. Здесь все есть объект. И все объекты в JavaScript отступают от Object. Свойство prototype помогает в наследовании, когда мы разрабатываем приложение объектно-ориентированным способом. В прототипе больше возможностей, чем класс в традиционной объектно-ориентированной структуре.

В прототипе вы можете добавить свойства функции, которые написаны кем-то другим.

Например,

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

Использование в наследовании:

Вы можете использовать наследование путем использования свойства прототипа. Здесь вы можете использовать наследование с JavaScript.

В традиционной системе классов вы не можете изменять, как только класс определен. Но вы можете сделать в JavaScript с прототипом системы.