Почему прототипирование JavaScript? - PullRequest
40 голосов
/ 10 января 2011

Это может показаться грамматически неправильным и, возможно, безумным вопросом, но вот что я имею в виду: пытаясь понять концепцию prototype в JavaScript, я натолкнулся на примеры, которые были несколько более или менее сложными версиями следующего :

//Guitar function constructor
function Guitar(color, strings) {
    this.color = color;
    this.strings = strings;
}
//Create a new instance of a Guitar
var myGuitar = new Guitar('Black', ['D', 'A', 'D', 'F', 'A', 'E']);
//Adding a new method to Guitar via prototype
Guitar.prototype.play = function (chord) {
    alert('Playing chord: ' + chord);
};
//Now make use of this new method in a pre-declared instance
myGuitar.play('D5');

Итак, к моей проблеме: какого черта ты хочешь это сделать? Почему бы вам просто не включить функцию play в Guitar для начала? Зачем объявлять экземпляр, а затем начинать добавлять методы позже? Единственная причина, которую я вижу, это то, что вы хотели, чтобы myGuitar не имел доступа к play при его первоначальном создании, но я не могу привести ни одного примера, объясняющего причину, по которой вы хотели бы что-то подобное.

Кажется, что было бы разумнее сделать это:

function Guitar(color, string) {
    this.color = color;
    this.strings = strings;
    this.play = function (chord) {
        alert('Playing chord: ' + chord);
    };
}
var myGuitar = new Guitar('White', ['E', 'A', 'D', 'G', 'B', 'E']);
myGuitar.play('E7#9');

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

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

Редактировать: Некоторые ответы:

  • Когда я сказал "Зачем объявлять экземпляр, а потом начинать добавлять методы позже?" Я более критично относился ко всем примерам, которые я вижу, которые разыгрываются в порядке моего первого примера. Когда этот порядок изменяется, как в ответе Хармена ниже, он имеет немного больше визуального смысла. Однако это не меняет того факта, что в том же духе, что и в моем первом примере, вы можете создать пустой конструктор функции объекта, объявить 100 экземпляров этого объекта, а затем только потом определять, что на самом деле является исходным объектом . , передав ему методы и свойства через prototype. Возможно, это обычно делается таким образом, чтобы намекнуть на идею копирования и ссылки, изложенную ниже.
  • Основываясь на нескольких ответах, вот мое новое понимание: если вы добавите все свои свойства и методы в конструктор функции объекта, а затем создадите 100 экземпляров этого объекта, вы получите 100 копий всех свойств и методов. Вместо этого, если вы добавите все свои свойства и методы в prototype конструктора функции объекта, а затем создадите 100 экземпляров этого объекта, вы получите 100 ссылок на одну (1) копию объекта свойства и методы. Это, очевидно, быстрее и эффективнее, и именно поэтому используется prototype (кроме изменения таких вещей, как String и Image, как упомянуто ниже). Итак, почему бы не сделать это:

(маркированные списки нарушают любой код сразу после них, по-видимому, поэтому я должен добавить строку отдельного текста здесь)

function Guitar(color, strings) {
    this.prototype.color = color;
    this.prototype.strings = strings;
    this.prototype.play = function (chord) {
        alert('Playing chord: ' + chord);
    };
}
var myGuitar = new Guitar('Blue', ['D', 'A', 'D', 'G', 'B', 'E']);
myGuitar.play('Dm7');

Ответы [ 10 ]

24 голосов
/ 10 января 2011

Итак, к моей проблеме: какого черта ты хочешь это сделать? Почему бы вам сначала не включить функцию воспроизведения в гитаре? Зачем объявлять экземпляр, а затем начинать добавлять методы позже?

Javascript не является «классическим» языком наследования. Он использует прототип наследования. Просто так оно и есть. В этом случае правильный способ создания метода в «классе» - поместить метод в прототип. Обратите внимание, что я помещаю «класс» в кавычки, поскольку, строго говоря, JS не имеет понятия «класс». В JS вы работаете с объектами, которые определены как функции.

Вы можете объявить метод в функции, которая определяет Guitar, однако, когда вы делаете это, каждая новая гитара получает свою собственную копию метода play. Помещение его в прототип более эффективно в среде исполнения, когда вы начинаете создавать гитары. Каждый экземпляр использует один и тот же метод воспроизведения, но контекст / область задаются при вызове, поэтому он действует как правильный метод экземпляра, к которому вы привыкли в своем классическом языке наследования.

Обратите внимание на разницу. В приведенном вами примере «почему бы не так» каждый раз, когда вы создаете новую гитару, вам нужно создать новый метод воспроизведения, такой же, как и любой другой метод воспроизведения. Однако, если игра идет по прототипу, все гитары заимствуют один и тот же прототип, поэтому все они используют один и тот же код для игры. Разница между x количеством гитар, каждая из которых имеет идентичный код игры (таким образом, у вас есть x копий игры) по сравнению с x количеством гитар, играющих в одной игре код (1 копия игры независимо от того, сколько гитар). Конечно, компромисс заключается в том, что во время выполнения игра должна быть связана с объектом, для которого она вызывается, но в javascript есть методы, которые позволяют вам делать это очень эффективно и легко (а именно методы call и apply ) * * тысяча двадцать-один

Многие фреймворки javascript определяют свои собственные утилиты для создания «классов». Как правило, они позволяют вам писать код, подобный тому, который вы хотели бы увидеть. За кулисами они помещают функции в прототип для вас.


РЕДАКТИРОВАТЬ - в ответ на ваш обновленный вопрос, почему нельзя сделать

function Guitar() {
    this.prototype.play = function()....
}

это связано с тем, как javascript создает объекты с ключевым словом 'new'. См. Второй ответ здесь - в основном, когда вы создаете экземпляр, javascript создает объект, а затем назначает свойства прототипа. Так что this.prototype.play на самом деле не имеет смысла; на самом деле, если вы попробуете это, вы получите ошибку.

12 голосов
/ 19 августа 2011

Как примечание перед началом - я использую здесь ECMAScript вместо JavaScript, поскольку ActionScript 1 и 2 демонстрируют точно такое же поведение во время выполнения.

Те из нас, кто работает в более «традиционном» объектно-ориентированном мире (читайте Java / C # / PHP), считают идею расширения класса во время выполнения почти полностью чужой. Я имею в виду, серьезно, это должен быть мой ОБЪЕКТ. Мой ОБЪЕКТ будет идти вперед и делать вещи, которые были поставлены перед Детские классы EXTEND Other CLASSES . У него очень структурированное, прочное, каменное чувство. И, по большей части, это работает, и это работает достаточно хорошо. (И это одна из причин, по которой Гослинг спорил, и я думаю, что большинство из нас согласились бы довольно эффективно, что оно так хорошо подходит для массивных систем)

ECMAScript, с другой стороны, следует гораздо более примитивной концепции ООП. В ECMAScript наследование классов - это, поверьте или нет, гигантский шаблон декоратора. Но это не просто шаблон декоратора, который, как вы могли бы сказать, присутствует в C ++ и Python (и вы можете легко сказать, что это декораторы). ECMAScript позволяет назначать прототип класса экземпляру.

Представьте, что это происходит в Java:

class Foo {
    Foo(){}
}

class Bar extends new Foo() {
    // AAAHHHG!!!! THE INSANITY!
}

Но это именно то, что доступно в ECMAScript (я думаю, что Io также допускает что-то подобное, но не цитируйте меня).

Причина, по которой я сказал, что это примитивно, заключается в том, что философия дизайна такого типа тесно связана с тем, как Маккарти использовал Lambda Calculus для реализации Lisp. Это больше связано с идеей closures, чем, скажем, с Java OOP.

Итак, в свое время Алонзо Черч написал The Calculi Lambda Conversion, основополагающую работу в Lambda Calculus. В ней он предлагает два способа взглянуть на функции с несколькими аргументами. Во-первых, их можно рассматривать как функции, которые принимают синглетоны, кортежи, тройки и т. Д. В основном f (x, y, z) следует понимать как f, которая принимает параметр (x, y, z). (Кстати, по моему скромному мнению, это является основным стимулом для структуры списков аргументов Python, но это гипотеза).

Другое (и для наших целей (и, если честно, целей Церкви) более важное) определение было подхвачено Маккарти. Вместо этого f (x, y, z) следует переводить в f (x g (y h (z))). Разрешение самого внешнего метода может исходить из ряда состояний, которые были сгенерированы внутренними вызовами функций. Это сохраненное внутреннее состояние является самой основой замыкания, которое, в свою очередь, является одной из основ современного ООП. Замыкания позволяют передавать закрытые исполняемые состояния между различными точками.

Предоставлена ​​книга «Земля Лиспа»:

; Can you tell what this does? It it is just like your favorite 
; DB’s sequence!
; (getx) returns the current value of X. (increment) adds 1 to x 
; The beauty? Once the let parens close, x only exists in the 
; scope of the two functions! passable enclosed executable state!
; It is amazingly exciting!
(let (x 0)
  ; apologies if I messed up the syntax
  (defun increment ()(setf x (+ 1 x)))
  (defun getx ()(x)))

Теперь, какое это имеет отношение к ECMAScript против Java? Хорошо, когда объект создается в ECMAScript, он может почти точно следовать этому шаблону:

 function getSequence()
{
     var x = 0;
     function getx(){ return x }
     function increment(){ x++ }
     // once again, passable, enclosed, executable state
     return { getX: getX, increment:increment}
}

И вот здесь начинается поступление прототипа. Наследование в ECMAScript означает «начать с объекта A и добавить к нему». Оно не копирует его. Он принимает это волшебное состояние, и ECMAScript добавляет его. И это самый источник и вершина того, почему должно учитывать MyClass.prototype.foo = 1.

Относительно того, почему вы добавляете методы «по факту». По большей части это сводится к стилевым предпочтениям. Все, что происходит внутри первоначального определения, делает не более того же типа украшения, что происходит снаружи.

По большей части стилистически выгодно поместить все ваши определения в одном месте, но иногда это невозможно. Например, расширения jQuery работают на основе идеи непосредственного добавления прототипа объекта jQuery. Библиотека Prototype на самом деле имеет специализированный способ расширения определений классов, который она использует последовательно.

Если я правильно помню Prototype.js, это примерно так:

 var Sequence = function(){}

 // Object.extend takes all keys & values from the right object and
 // adds them to the one on the left.
 Object.extend( Sequence.prototype, (function()
 {
     var x = 0;
     function getx(){ return x }
     function increment(){ x++ }
     return { getX: getX, increment:increment}
  })());

Что касаетсяИспользование ключевого слова prototype внутри исходного определения, ну, в большинстве случаев это не сработает, потому что «this» относится к экземпляру определяемого объекта (во время его создания).Если у экземпляра также нет свойства «prototype», this.prototype обязательно будет неопределенным!

Поскольку все this внутри исходного определения будут экземплярами этого объекта, изменяя thisбыло бы достаточно.Но, (и я улыбаюсь, когда говорю это, потому что он идет вместе с прототипом) каждый this имеет свойство constructor.

 // set the id of all instances of this “class”. Event those already 
 // instantiated...
 this.constructor.prototype.id = 2
 console.log( this.id );
3 голосов
/ 19 августа 2011

JavaScript - это язык-прототип, довольно редкая порода.Это вовсе не произвольно, это требование языка, который оценивается в реальном времени и способен к «eval», динамическим модификациям и REPL.

Прототипическое наследование можно понять по сравнению с объектно-ориентированным программированиемоснованные на определениях «живых» классов во время выполнения вместо статических предопределенных.

Редактировать: также полезно использовать другое объяснение, украденное по следующей ссылке.В объектно-ориентированном языке (Class -> Object / Instance) все возможные свойства любого данного X перечислены в Class X, и экземпляр заполняет свои собственные конкретные значения для каждого из них.В прототипическом наследовании вы описываете только различия между существующей ссылкой на живой X и подобным, но другим живым Y, и Master Copy .

* нет1014 *

Прежде всего вам нужно понять контекст.JavaScript - это интерпретируемый язык, который выполняется и может быть изменен в реальной среде.Сама внутренняя структура программы может быть изменена во время выполнения.Это накладывает различные ограничения и преимущества на любой скомпилированный язык или даже на язык CLR, такой как .Net.

Понятие "eval" / REPL требует динамической типизации переменных.Вы не можете эффективно редактировать в реальном времени среду, в которой вы должны иметь предопределенные монолитные структуры наследования на основе классов.Это бессмысленно, вы также можете просто скомпилировать в сборку или байт-код.

Вместо этого у нас есть прототипное наследование, где вы связываете свойства INSTANCE объекта.Концепция заключается в том, что если вы находитесь в полностью живой среде, классы (статические, предопределенные конструкции) неоправданно ограничивают.Классы построены на ограничениях, которые не существуют в JavaScript.

При этой стратегии JavaScript в основном опирается на то, что все «живое». Ничто не является запретным, нет никаких «определенных и готовых» классов, к которым вы никогда не сможете прикоснуться.Среди переменных, которые более святы, чем ваш код, нет «одного истинного шотландца», потому что все подчиняется тем же правилам, что и код, который вы решили написать сегодня.

Последствия этого очевидны, а также в значительной степени основаны на людях.Это подталкивает разработчиков языка к использованию легких, эффективных прикосновений в предоставлении нативных объектов.Если они плохо справляются со своей задачей, то моб просто узурпирует платформу и восстановит свою собственную (прочитайте источник MooTools, он буквально переопределяет / переопределяет все, начиная с Function и Object).Вот как обеспечивается совместимость с такими платформами, как старые версии Internet Explorer.Это продвигает библиотеки, которые являются мелкими и узкими, плотно функциональными.Глубокое наследование приводит к тому, что наиболее часто используемые части (легко) выбираются из вишни и становятся главной библиотекой.Широкие библиотеки приводят к разрыву, когда люди выбирают, какие части им нужны, потому что откусить кусочек легко, а не невозможно, как в большинстве других сред.

Концепция микробиблиотеки уникально процветает в JavaScript, и онаАбсолютно можно проследить до основ языка.Он поощряет эффективность и краткость с точки зрения потребления человеком способами, которые не поддерживает ни один другой язык (который я знаю).

3 голосов
/ 10 января 2011

Если вы не используете прототип, каждый раз, когда вы вызываете конструктор Guitar, вы создаете новую функцию.Если вы создаете много объектов Guitar, вы заметите разницу в производительности.

Другой причиной использования прототипов является эмуляция классического наследования.

var Instrument = {
    play: function (chord) {
      alert('Playing chord: ' + chord);
    }
};

var Guitar = (function() {
    var constructor = function(color, strings) {
        this.color = color;
        this.strings = strings;
    };
    constructor.prototype = Instrument;
    return constructor;
}());

var myGuitar = new Guitar('Black', ['D', 'A', 'D', 'F', 'A', 'E']);
myGuitar.play('D5');

В этом примере расширяется гитараИнструмент, и, следовательно, имеет функцию воспроизведения.Вы также можете переопределить функцию «Play» инструмента на гитаре, если хотите.

2 голосов
/ 10 января 2011

Первый метод, который вы даете, быстрее, и он действительно начинает иметь смысл, когда вы пишете его в другом порядке:

//Guitar function constructor
function Guitar(color, strings) {
  this.color = color;
  this.strings = strings;
}

Guitar.prototype.play = function (chord) {
  alert('Playing chord: ' + chord);
};

var myGuitar = new Guitar('Black', ['D', 'A', 'D', 'F', 'A', 'E']);

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

Для доказательства см. этот тест скорости по вопросу, очень похожему на этот.


И, возможно, эта альтернативная версия имеет для вас еще больший смысл:

function Guitar(){
  // constructor
}

Guitar.prototype = {
  play: function(a){
    alert(a);
  },

  stop: function(){
    alert('stop it');
  }
};
1 голос
/ 10 января 2011

Он основан на шаблоне креативного дизайна Prototype. Эта ссылка на Википедию имеет хорошее обсуждение.

http://en.wikipedia.org/wiki/Prototype_pattern

1 голос
/ 10 января 2011

У вас уже есть много хороших ответов, поэтому я не рассматриваю все ваши пункты.

Зачем объявлять экземпляр, а потом начинать добавлять методы позже?

Это не правильно.Объект-прототип существует независимо от каждого экземпляра.Это свойство объекта функции (функция конструктора).
Когда вы создаете новый экземпляр, он «наследует» все свойства от прототипа (фактически он имеет ссылку на него),

На самом деле это имеет смысл, если вы думаете об объектах и ​​ссылках: лучше (с точки зрения памяти) поделиться одной ссылкой на объект, чем каждый экземпляр, имеющий свою собственную копию объекта (объект в этом случае будет функцией play).

Относительно того, почему он основан на прототипе: Вы также можете спросить, почему существуют разные языковые парадигмы (функциональные, oo, декларативные).Нет только одного правильного способа что-то сделать.

1 голос
/ 10 января 2011

Javascript основан на прототипе . Когда в Риме, делайте, как римляне, когда в JS, используйте прототип наследования.

Это более эффективно, потому что метод становится наследуемым для каждого объекта. Если бы это не был прототипный метод, то каждый экземпляр этого объекта имел бы свой собственный play метод. Зачем идти по неэффективному и нетрадиционному маршруту в JS, если мы можем пойти по эффективному и естественному маршруту в JS?

1 голос
/ 10 января 2011

С одной стороны, вы можете использовать прототип для расширения объектов, встроенных в язык JavaScript (например, String). Я предпочитаю второй пример для пользовательских объектов.

0 голосов
/ 16 июня 2017

Бро, позвольте мне спросить вас об одном: что, если у вас есть гитара, Casio, скрипка и вы хотите играть одинаковые аккорды на каждом из этих музыкальных инструментов.

Так что я думаю, почему бы нам не оставить одну функциюplay_chord отдельно и используйте эту (play_chord) функцию с любым из вышеперечисленных инструментов, вместо этого используя каждую функцию внутри гитары, casio или скрипки.

, поэтому, наконец, когда нам нужна функция, которая может быть частью другого конструктора, мы должны определитьэту конкретную функцию внутри прототипа и использовать соответственно:)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...