Демонстрация преимуществ наследования Javascript по сравнению с моделью на основе классов - PullRequest
3 голосов
/ 10 октября 2011

Может ли кто-нибудь предоставить конкретный пример, показывающий наследование прототипа Javascript, который демонстрирует, когда выгодно использовать его по сравнению с традиционной классовой (классической) моделью?

Другие вопросы, которые я встречал (например, Классическое наследование прототипов против * , Почему JavaScript был реализован с использованием наследования прототипов? , наследование на основе прототипов и наследование на основе классов) дает только обсуждение на высоком уровне, а не конкретный пример (желательно тот, который вы использовали в рабочем коде).

Ответы [ 5 ]

2 голосов
/ 16 октября 2011

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

Создание подклассов экземпляров объектов

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

// Magic code
function child(src) {
    function Child() {};
    Child.prototype = src;
    return new Child;
}

// Base object
var default_options = {
    color: 'red',
    size:  'large',
    font:  'arial'
};

// Child object
var my_options = child(default_options);
my_options.size = 'small';
my_options.font = 'verdana';

my_options.color == 'red';
default_options.font == 'arial';

В браузерах, поддерживающих __proto__, это может быть еще проще:

var my_options = {
    size: 'small',
    font: 'verdana'
};

// When applying options:
my_options.__proto__ = default_options;
my_options.color == 'red';

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

my_options.__proto__ = OptionsProcessor.prototype;

Расширение существующих объектов

Конечно, настоящая причина, по которой наследование JavaScript так велико, заключается в том, что вы собираетесь иметь дело со средой, которая уже достаточно хорошо создана с тысячами объектов, которые вы можете улучшить. Скажем, вы хотите использовать element.querySelectorAll в старом браузере. С классическим наследованием вам не повезло, но с наследованием JavaScript это просто:

(HTMLElement || Object).prototype.querySelectorAll = function(selector) { ... }

Этот тип полифилла имеет большое преимущество перед чем-то вроде jQuery, потому что вы можете использовать стандартный код во всем приложении и импортировать JavaScript только тогда, когда он нам нужен.

Функции перенастройки

Допустим, мы хотим знать каждый раз, когда используется querySelectorAll, в случае, если мы хотим заменить его более быстрой функцией для более простых запросов. Мы можем перехватить функцию и вывести ее на консоль каждый раз, когда она вызывается:

var oldFunction = HTMLElement.prototype.querySelectorAll;
HTMLElement.prototype.querySelectorAll = function(selector) {
    console.log(selector);
    oldFunction.prototype.apply(this, arguments);
};

Применение методов к другим классам

В JavaScript много функций, похожих на массивы. arguments не является массивом. Ни один не document.getElementsByTagName('div'). Это означает, что если мы хотим получить первые 5 элементов из массива, мы не можем использовать list.slice(0, 5). Однако вы можете применить Array.prototype.slice к объекту list:

var divs = document.getElementsByTagName('div');
var first5divs = Array.prototype.slice.call(divs, 0, 5);
2 голосов
/ 14 октября 2011

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

В этом примере, используемом на рабочей странице, jQuery не будет анимироваться ввсе в браузерах ltIE8.Поскольку это происходит только на одной конкретной странице, не имело бы смысла взламывать ядро ​​jQuery, которое также загружается на другие страницы (не говоря уже о том, что его нужно размещать и не загружать из Google).Вместо этого я сделал блок условного сценария ltIE8, который модифицирует метод cur встроенного прототипа fx, который устраняет проблему, при которой он возвращает значение NaN для шагов анимации:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<!--[if lt IE 8]>
<script type="text/javascript">
jQuery.fx.prototype.cur = function() {
var parsed, r;
    if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
        return this.elem[ this.prop ];
    }
r = this.elem.style[this.prop] || jQuery.css( this.elem, this.prop );
return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed;
};
</script>
<![endif]-->

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

var Person = function Person( name, age ){ //Declare the constructor
this.name = name || "John";
this.age = age || "20";

this.instanceId = "person"+this.constructor._getId(); //Create unique instance id
this.constructor._addInstance( this ); //Make this instance accessible from Person
}

.Inherits( Animal, Monkey )

.Class({

     "static _instances": {},

     "static _curId": 0,

     "static _getId": function(){
     return this._curId++; //In static methods "this" refers to the Person constructor, not to an instance.
     },

     "static _addInstance": function( instance ) {
     this._instances[instance.instanceId] = instance;
     },

     "static alias byId": "getInstanceById", //Inline alias for Person.getInstanceById === Person.byId

     "static getInstanceById": function( id ){
     return ( id in this._instances ) && this._instances[id];
     },

     "alias greet": "sayHello", //alias for the instance method

     "alias sayHi": "sayHello",

     sayHello: function(){
     return "hello from "+this.name;
     },

     eat: function(){
     return this.__super__( "eat", "pizza" ); //Super method call, not really useful in this particular implementation
     }
})

.Implements( whatever ); //emulating interfaces, whatever should be an object that describes how the methods must be implemented


//Instantiating and such works like regular js
var mike = new Person( "mike" );

mike.greet(); //"hello from mike"
mike.sayHi(); //"hello from mike"
mike.sayHello(); //"hello from mike"

mike === Person.byId( "person0" ); //true

Нет реализации для создания префиксных методов _underscoreна самом деле недоступен снаружи, потому что накладные расходы не будут стоить этого на js-тяжелой странице.Множественное наследование и супер методы работают только для последнего поколения.

2 голосов
/ 11 октября 2011

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

  1. Прототипическое наследование может использоваться для имитации классического наследования. Это надмножество классического наследования. Наоборот не возможно. Это связано с тем, что в классическом наследовании классы могут наследовать только от других классов. Однако в прототипном наследовании любой объект может наследоваться от любого другого объекта (а в JavaScript все является объектом).
  2. В JavaScript каждый объект имеет внутреннее свойство proto, которое указывает на его объект-прототип (объект Object имеет прототип null). Поскольку JavaScript является динамическим, вы можете вносить изменения в объект-прототип, и эти изменения будут отражены на каждом объекте, внутреннее свойство proto которого указывает на этот прототип (даже после создания объекта). Таким образом, его можно использовать для расширения функциональности группы объектов.

Есть много других причин. Я буду обновлять его по мере возможности.

Вот код Java, демонстрирующий классическое наследование:

public class Employee {
    public String name;
    public String dept;
    public Employee () {
        this("", "general");
    }
    public Employee (String name) {
        this(name, "general");
    }
    public Employee (String name, String dept) {
        this.name = name;
        this.dept = dept;
    }
}

public class WorkerBee extends Employee {
    public String[] projects;
    public WorkerBee () {
        this(new String[0]);
    }
    public WorkerBee (String[] projs) {
        projects = projs;
    }
}

public class Engineer extends WorkerBee {
    public String machine;
    public Engineer () {
        dept = "engineering";
        machine = "";
    }
    public Engineer (String mach) {
        dept = "engineering";
        machine = mach;
    }
}

Вот эквивалентный код JavaScript:

function Employee (name, dept) {
    this.name = name || "";
    this.dept = dept || "general";
}

function WorkerBee (projs) {
    this.projects = projs || [];
}
WorkerBee.prototype = new Employee;

function Engineer (mach) {
    this.dept = "engineering";
    this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
1 голос
/ 17 октября 2011

Я думаю, что главная причина того, что мы не видим больше прототипа, заключается в том, что синтаксис по умолчанию в Javascript - это псевдоклассическая аберрация вместо более благоприятного Object.create. Если вы хотите по-настоящему увидеть прототипный блеск, ищите места, где используется эта функция. Следующий пример взят из инструментария Dojo:


Предостережение : Я как бы изменил свое мнение о том, насколько хорош этот вид кода с тех пор, как я первоначально написал этот ответ. Хотя основная идея остается в силе, вы должны быть осторожны, если у вас есть методы, которые изменяют свойства экземпляра ("this"). Это потому, что если вы вызываете метод в объекте делегата через делегатор, то вы можете в конечном итоге установить переменные в делегаторе, а не в делегате, и это может нарушить некоторые инварианты, если кто-то в итоге получит доступ к делегату, непосредственно последнему.

Вся идея в том, что у вас на 100% все в порядке.


Dojo определяет общий интерфейс store (с такими методами, как get (), add () и т. Д.), Который можно использовать, например, для абстрагирования REST API от сервера. Мы хотели бы создать функцию Cache , которая получает любое хранилище данных и возвращает новую версию, которая кэширует любые вызовы метода get () (это позволяет нам отделить кэширование от поведения хранилища при реализации фактический get ())

Первая идея заключается в использовании факта, что Javascript является высоко динамичным, для замены метода get:

//not the actual implementation. Things get more complicated w/ async code.
var oldGet = store.get;
store.get = function(id){
    if(!cache[id]){ cache[id] = oldGet(id); }
    return cache[id];
}

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

Вторая идея заключается в создании более надежного решения с использованием делегирования:

function Cache(obj){
    this.obj = obj;
}
Cache.prototype = {
    get: function(){
        //do things involving this.obj...
    }
};

Это выглядит многообещающе, пока вы не вспомните, что полученный объект Cache должен реализовать интерфейс хранилища. Мы можем попробовать добавить все методы вручную:

Cache.prototype = {
    //...
    put: function(){ return this.obj.apply(this, arguments); },
    //...
}

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

Что ж, способ сделать это "автоматическое делегирование" - это наследование, но в этом случае оно изначально кажется бесполезным, поскольку вам нужно будет создать новый подкласс кэша для каждого возможного класса хранилища, или вам понадобится какой-то вид Необычный миксин множественного наследования. Введите прототип наследования, чтобы сохранить день. Мы можем легко создать новый объект, который добавляет функциональность к старому , не изменяя его и не манипулируя классом hieharchies

dojo.store.Cache = function(masterStore, cachingStore, options){
    //...
    return dojo.delegate(masterStore, {
        //... 
        get: function(id, directives){
            //...
        }
        //...
    }
}

Где dojo.delegate - это функция, которая создает новый объект со всеми свойствами во втором аргументе и прототип которого будет первым аргументом.


Теоретические несоответствия JS: наследование прототипов может использоваться еще более агрессивно в сценариях с еще большим делегированием в таком языке, как Self, который допускает несколько прототипов, а также прямой доступ и модификацию прототипов во время выполнения. Например, можно реализовать шаблон State из GoF, делегировав все подходящие методы прототипу и меняя прототип при каждом изменении состояния.

1 голос
/ 11 октября 2011

На мой взгляд, прототип гораздо более гибкий.

  1. Я могу сделать только один экземпляр моего класса Bar наследуемым от Foo вместо всех экземпляров Bar.

  2. Я могу решить, что больше не хочу, чтобы Bar наследовал от Foo, установив для моего Bar.prototype значение null или другое значение объекта.

  3. Я могу в любой момент решить, что хочу, чтобы Bar наследовал от Array вместо Foo.

Однако классические языки имеют свои преимущества. Как лучше инкапсуляция. С прототипом вам нужно сделать много магии закрытия, чтобы свойства вашего объекта «действовали» как частные свойства.

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