Как стать автором
Обновить

Комментарии 60

Очень-очень крутая статья! Спасибо за «линивую инициацию», не знал… И ребята, давайте не будем разводить срач, данный подход имеет право на жизнь, верно?
пожалуйста ))) рад стараться :)
мало того. он мощнее. но несколько опаснее.
Прототипное наследование в JS неполноценно, т.к. отсутствует способ как клонировать объект, так и связать его с прототипом.
Т.е. в «классовом» варианте у нас есть класс, и далее с помощью new мы можем наштамповать сколько угодно объектов (инстанцирование).
В прототипном же варианте у нас есть прототип, и… И дальше-то что? Мы не можем ни прописать ссылку на прототип в явном виде, ни склонировать этот самый прототип для последующих изменений, а должны использовать new или Object.create, попутно извращаясь над «прототипной функцией»-псевдоконструктором, что по сути является замаскированным использованием классов.
Вот если бы можно было сделать так:

var obj1= {… };
var obj2=clone(obj1);
obj2.newMethod=...;

или хотя бы вот так:

var obj1={… };
var obj2={ newMethod: function() {...} };
obj2.setPrototype(obj1);

вот тогда можно было бы говорить о прототипном наследовании. А так — в JS используется обычная классовая система, только с чрезвычайно уродливым негуманоидным синтаксисом. И хорошо, что создатели стандарта наконец-то это заметили.
что по вашему такое «клон»? Мне кажется, на эту роль подходят объекты, производимые Object.create
первый ваш пример будет работать, если сделать:
var clone = Object.create;
Спасибо, так действительно работает. Жаль, что этой возможности не было в стандарте с самого начала — тогда не потребовалось бы городить огород с function.prototype и new.
Ну так имплементация Object.create есть в книге Крокфорда (http://stackoverflow.com/questions/2766057/what-is-happening-in-crockfords-object-creation-technique), не?
Мало того, и второй вариант работать будет в движках, поддерживающих __proto__, если ваш 'setPrototype' реализует obj2.__proto__=obj1. Свойство __proto__ включено в спецификацию ES6, да и setPrototypeOf там присутствует. Хотя изменять прототип существующего объекта — еще то извращение.
Не могу для себя прояснить вопрос, чем Object.setPrototypeOf(obj) лучше obj.setPrototype()?
setPrototype более привычно и объектно-ориентированно, почему расширяют Object а не Object.prototype?
Лично я бы ничего против лишних методов Object.prototype не имел, если бы объекты и ассоциативные массивы в js небыли бы одним и тем же. А так — задаешь пустой хэш нотацией {}, а в нем уже несколько, хоть неперечислимых и в прототипе, но свойств, уже присутствует. И обратное — методы прототипа перекрываются свойствами объекта.
// Пример 1:
// телефонный справочник
var phone={
  'Вася':'+7987654',
  'Петя':'+7654321'
}
function number(name){
  if(name in phone)return phone[name];
  return 'Пользователь отсутствует в базе'
}
console.log(number('Вася'));          //=>'+7987654'
console.log(number('hasOwnProperty'));//=>function hasOwnProperty() { [native code] }
// Пример 2:
var phone={
  'Вася':'+7987654',
  'Петя':'+7654321'
}
function number(name){
  if(phone.hasOwnProperty(name))return phone[name];
  return 'Пользователь отсутствует в базе'
}
console.log(number('Вася'));          //=>'+7987654'
phone['hasOwnProperty']='+7666666';   //=>Добавляем нового абонента
console.log(number('Вася'));          //=>Упс
Это и хорошо — можно перебивать. А если нужен каноничный метод, то:
Object.hasOwnProperty.call(phone, name)

JS вариативный. Здорово же.
В любом другом случае — здорово, но не в случае с объектами-коллекциями. Лишняя громоздкая проверка на каждое обращение к элементу. Есть, конечно, Object.create(null), но тогда прощай ie8- (можно эмулировать через фрейм, но изврат) и простая запись коллекции фигурными скобками.
спасибо, хороший пример.
НЛО прилетело и опубликовало эту надпись здесь
В последней 15-ой ревизии спецификации ES6, псевдо-свойство __proto__ вынесли в приложение B, это значит что его имплементация в небраузерных движках опцианальна.
Между тем Object.prototype.__proto__ находится в приложение F (свалка)
Последние от ECMA уже на Java syntax похожь)
То есть они прилепили классы в ECMA Script 6, но до сих пор осталось вот это сумасшествие: this.name = name||"Unnamed"?

Нда…
нет, не осталось, это я забыл исправить. спасибо :)
a классы еще не прилепили, все еще обсуждается
super не особо вписывается в идеологию JS:
super.method() вызывается в контексте this, a не в контексте super, как следовало бы ожидать.
Разве что, можно предположить что super это прокси с ловушкой get, которая возвращает super[method].bind(this).
Что значит «еще не прилепили»? Давно в working draft и оттуда уже явно не выпилят — на них многое завязано.
Както я это пропустил… Ок. «прилепили» :)
Насколько мне известно, реализации классов создаются только лишь для добавления синтаксического сахара.
«Традиционное» наследование:
A = function() {};
A.prototype = { a: 3, constructor: A };
inherits(A,B); // некая функция, наследующая А от В

Наследование, реализованное в виде классов:
A = Class(B, { constructor: function() {}, a: 3 });

Как видно, во втором случае мы получаем компактность и очевидность того, что класс А наследуется от класса В. Иначе, при достаточно крупной функции-конструкторе и\или при увесистом прототипе, нам придется скроллить вниз, дабы понять, наследует ли класс А что-либо.

С чистым Object.create (без оберток) cитуация еще хуже, если нам требуется запустить конструктор и добавить свойств:
A = function() {};
A.prototype = { a: 3 };
a = Object.create(B.prototype);
A.call(a);
for( var i in A.prototype ) {
  a[ i ] = A.prototype[ i ];
}

Все крики о том, что надо использовать прототипы, а не классы, считаю бессмысленными, так как, в итоге, реализации классов всё равно используют прототипы и являются не более чем синтаксическим сахаром.
Я говорю не сколько о том, что использовать прототипы, а о том что не использовать функции- конструкторы
Как, в таком случае, будет выглядеть большое приложение, хотя бы на 100 тыс. строк? Я действительно не понимаю. Ваш вариант выглядит, так скажем, грязновато.
Перепутал порядок. A.call(a) идет конечно же, после for.
А for этот зачем вообще? Это множественное наследование?
Дабы поместить в объект общие для всех подобных объектов свойства и методы.
И не обращайте внимания, я действительно не очень внимательно прочел статью в первый раз, ожидая типичную ругань на обертки, стараюсь исправиться ниже.
Вы, наверное, статью не читали. А, ну да, для чего читать «бессмысленное». В ней описывается как можно обойтись без функций со свойством prototype (над которыми все обертки и строятся).
Сам не люблю подобные комментарии, поэтому перечитал статью. Несмотря на это, я всё еще не понимаю, как это должно выглядеть в крупном проекте.

В случае обычного конструктора, мы можем определить нужные нам свойства, передав аргументы при инициализации:
A = function( x, y ) { this.x = x; this.y = y; };
a = new A(1,2);

В вашем случае, придется устанавливать свойства вручную, после каждой инициализации.
a = Object.create(proto);
a.x = 1;
a.y = 2;

Теперь, имея горсть объектов, желая не помещать x и y в каждый из них, вам придется перелопатить все инициализации, убрав последние две строки кода, которого я показал. Но можно сделать и так:
a = Object.create(proto);
a.init(1,2)

или
a = Object.create(proto).init(1,2);

Но, тогда, чем это лучше запуска конструктора?

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

Сам я — огромный любитель штук, типа «о, можно еще и так сделать!», но с опытом приходит понимание того, что любой подход требует проверки на проекте, и часто получается так, что прикольная штука может только навредить. Если покажете какой-нибудь хотя-бы небольшой проект и он не будет похож на кашу, я признаю свою ошибку и извинюсь.
У Object.create 2 параметра. 2ой как раз и задает свойства инстанса, но в формате дескрипторов, что не очень удобно.
Object.create(proto,{x:{value:1},y:{value:2}})

Можно это упростить, написав хелперы:
// Object.getOwnPropertyDescriptors есть в ES6
var getDescs=Object.getOwnPropertyDescriptors||function(obj){
  return Object.getOwnPropertyNames(obj).reduce(function(rez,k){
    rez[k]=Object.getOwnPropertyDescriptor(obj,k);
    return rez},{})
}
function create(obj,props){
  return Object.create(obj,getDescs(props))
}

// Используем
create(proto,{x:1,y:2});
я считаю что
a.width = 1; a.height = 2; a.size = 3; a.draggable = true; a.editable = true; ...

более понятно чем
a.init(1,2,3,true,true ...)

ну a лучше всего будет
a.setValues({width: 1, height: 2, size: 3, draggable: true, editable: true ...})

тут метод setValues общий, из Object.prototype, в отличии от вашего init, который для каждого класса нужно писать отдельный.

Не совсем понял что вы имеете ввиду. Что зачем перелопатитъ надо?
Перелопатить, если захотите не использовать свойства x и y из моего примера (например, захотите инкапсулировать эти аргументы).
Плохо в js с инкапсуляцией :(
Если делать это через замыкания в конструкторе, то методы, обращающиеся к свойству, прийдется тоже создавать в конструкторе, и положить их в прототип уже не получится.

А насчет перелопатить, этого прийдется делать не больше чем изменять аргументы конструктору (во всех вызовах). Тут большой разницы я не вижу.
А используя get-ттеры и set-ттеры можно добиться инкапсуляции? По крайней мере есть параметр enumerable который позволяет «скрывать» свойства при использовании в циклах.
можно, в не strict коде, используя deprecated caller
Поправочка: не можно, а возможно :)
И еще, по поводу «длинного метода»: большой метод легко делится на мелкие.
легко-нелегко, а делить его или не делить решается программистом. А с ленивой инициализацией делить придется обязательно всегда.
Во втором примере ошибка.
Попытка вызвать donald.quack() после
var donald = new TalkingDuck("Donald");
вызывает бесконечную рекурсию.
Спасибо, исправил.
Все это разбивается о реальность — V8 использует hidden классы и вся инициализация должна производиться в конструкторе, а не где-то еще. Я автор ecma5 реализации в Котлине (https://github.com/develar/kotlin) и там сейчас не используется оператор new, а просто вызов функции и вот такой подход с Object.create. Но в итоге оказалось, что плюсов никаких и нет, смысла не использовать оператор new нет. А минусы есть — код хуже оптимизируется V8.
Я нашел решение — __proto__. Он оказался даже быстрее конструкторов с оператором new.
Создал фреймворк из 3х строчек кода :)
github.com/quadroid/clonejs-nano
Еще один «эксперт» пришел в JS из правильных ООП языков и решил похвастаться.

К чему вся эта статья и сравнение идеологий и идиом, если автор абсолютно не представляет чем класс отличается от объекта с точки зрения хранения его в памяти. Нельзя объекты JS называть классами, это намного более масштабируемые структуры.
К чему вообще весь этот мир? Кругом тлен и безысходность. Да? :)
ECMA Script 6 называет эти объекты классами, почему мне нельзя?
Как мои знания или незнания тонкостей реализации транслятора JS влияют на суть статьи? В ней же обсуждаются идеологии и идиомы, как вы успели заметить.

P.S.
В JS я пришел из бейсика.
Не обращайте внимания, просто alexDark считает себя умнее вас и что все, априори, должны знать то, что описано в публикации. Осознав это он решил завуалированно сообщить свои мысли сообществу. Посему ему можно рекомендовать к прочтению этот замечательный перевод: habrahabr.ru/post/178747/

p.s. За статью спасибо, вынес для себя кое-что.
прототип является классом и экземпляром объединенными в одну сущность, грубо говоря, Singleton'ом

wat???
Часто начинаешь понимать что в JS и объекты не нужны. :)
Прототипное наследование может даже в чём то хорошо но все разработчики интерпретаторов и JIT оптимизаторов орут, что именно из-за этого происходит проседание по скорости. Гугл вон даже из-за этого Dart начал делать.

Классическое ООП на классах не сильно сковывает руки (хотя есть такое) но зато даёт возможность написать в разы более производительных интерпретаторов/компиляторов.

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

Всё чаще начинают писать тяжёлое ПО на JS (игры и т.д.) и все эти проблемы вылезают. В итоге мы видим всякие Float32Array и прочие радости.
Так, ну давайте по порядку. Во первых, Object.create это очень медленно, по сравнению с конструктором. Проверьте.
Во вторых, концепт о приватных свойствах есть и уже часть будующей спецификации ES6. Подробности можно посмотреть тут — webreflection.blogspot.ru/2013/03/simulating-es6-symbols-in-es5.html
Опять же, вы можете сказать, что это не реальные приватные свойства. Однако они будут. Плюс, ещё возможность совместимости в ES5.
Мне больше вот так нравится:
var obj = {
	get lazy() {
		delete this.lazy;
		return this.lazy = "Лениво инициализированное свойство " + this.name;
	},
	//set lazy(value) {
	//	delete this.lazy;
	//	return this.lazy = value;
	//},
	name: "БезИмени"
};

(лично мне set ни разу не был нужен)
Но, к сожалению, такое нельзя использовать в прототипах.
Да, так красивее. Почему нельзя в прототипах?
Геттер так не удаляется. А если бы и удалился – это сломало бы прототип (и все будущие и еще не инициализированные экземпляры).
function F(i) {
	this.i = i;
}
F.prototype = {
	get lazy() {
		delete this.lazy;
		return this.lazy = "Lazy: " + this.i;
	}
};
var f1 = new F(1);
var f2 = new F(2);
alert(f1.lazy + "\n" + f2.lazy); // 1 и 2
f1.i = f2.i = 3;
alert(f1.lazy + "\n" + f2.lazy); // 3 и 3


А еще в Firefox 3.6 и более старых выдавало ошибку
setting a property that has only a getter
:)
Ну, а вот так уже все в порядке:
function F(i) {
	this.i = i;
}
F.prototype = {
	get lazy() {
		var value = "Lazy: " + this.i;
		Object.defineProperty(this, "lazy", {
			value: value,
			writable: true,
			enumerable: true
		});
		return value;
	}
};
var f1 = new F(1);
var f2 = new F(2);
alert(f1.lazy + "\n" + f2.lazy);
f1.i = f2.i = 3;
alert(f1.lazy + "\n" + f2.lazy);
Геттер так не удаляется
а зачем ему удалятся из прототипа? Инстансы работают как надо, чем вам не нравится 1 и 2 и 3 и 3?
ok. я понял
Я склонен воспринимать ООП всего лишь как еще одну систему контрактов между создаваемыми сущностями.

Если у нас есть фабрика, которая при одинаковых вводных производит одинаковые объекты, то есть императивно навязывает этот контракт всем создаваемым объектам, эта фабрика и есть класс.

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

Если все эти уровни композиции будут забиваться на уровне готового синтаксиса, то кому-то станет проще, (особенно разработчикам IDE хе-хе). Меньше будет возможности из-за недопонимания как работает собственный код встрелить себе в ногу и всякие for-of служат приблизительно тому же. Понимание же при этом может как появиться, так и не появиться.
абстракция является одной из основных концепций программирования. поэтому что угодно можно написать на чем угодно. в С тоже можно программировать в ООП стиле, думая про классы, наследование и т.п. и в JavaScript ни кто не мешает эмулировать классы через прототипы и клонирование.

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

Например, если не рассматривать только задачу создания UI, то обычно классы нужны для того, чтобы описать иерархию между элементами, например, Rectangle->Element->Container->Window, когда весь интерфейс делается вручную с нуля или в рамках UI библиотеки какой-то. А тут вроде как у нас браузер предоставляет уже все элементы типа input, div и т.д. С другой стороны, их надо как-то расширять, значит наверное всё-таки нужно?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории