Frontender Magazine

Программирование классами в веб-приложениях

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

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

Олег Лукавенко (один из моих работодателей)

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

Как корабль назовешь, так он и поплывёт

Разрабатывая клиентское веб-приложение, необходимо заранее подумать о том, каким образом члены одной команды разработчиков будут взаимодействовать с проектом и, что более важно, необходимо заранее предвидеть как будет выглядеть код приложения через несколько лет. Плохо, когда архитектура приложения хранится в голове одного человека. Люди непостоянны и порой даже непредсказуемы. Хорошо, когда архитектура приложения понятна с первого взгляда (и задокументирована). На сегодняшний день в вопросе архитектуры веб-приложений на клиенте хорошо зарекомендовал себя Backbone. Но я бы не стал утверждать, что Backbone — это новаторское решение. Как раз наоборот. Разработчики Backbone обратили внимание мирового сообщества на то, что всегда было в JavaScript, а именно на возможность программирования классами.

JavaScript устроен таким образом, что все в нем является объектами. Строки, массивы, числа — это объекты JavaScript. По этой причине понятие классы в JavaScript никогда не применялось или применялось разве что в разговорной речи. В JavaScript более распространено такое понятие как функции-конструкторы, но для удобства я буду называть их классами.

Далее я постараюсь раскрыть тему статьи на примере 3-х вымышленных фронтенд-разработчиков, которые выполняют одну и ту же задачу, а именно: вычислить скидку для товара и вывести цену на HTML страницу.

Junior, Middle and Senior developers

Посмотрим что же напишет «Junior developer»:

var discount = 0.10;
$('.products li').each(function() {
    var price = $(this).find('.price').html();
    var discountedPrice = price - (price * discount);
    $(this).find('.price').html(discountedPrice);
});

CodePen

С одной стороны задача выполнена, но тут мы имеем массу явных минусов, которые затянут проект в черную дыру:

  1. Данные берутся прямо из HTML страницы;
  2. Нет модели данных;
  3. Строковая математика;
  4. Код тяжелый для зрительного восприятия;
  5. Чтобы понять назначение отдельной части кода надо прочитать весь код целиком (затрата времени и умственных ресурсов);
  6. Код не расширяемый;
  7. Высокая зависимость от HTML шаблона.

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

var discountByTypes = {
    cheap: 0.20,
    expensive: 0.10
};
$('.products li').each(function() {
    var price = $(this).find('.price').html();
    var type = $(this).data('type');

    if (type === 'cheap') {
        var discountedPrice = price - (price * discountByTypes.cheap);
    }

    if (type === 'expensive') {
        var discountedPrice = price - (price * discountByTypes.expensive);
    }

    $(this).find('.price').html(discountedPrice);
});

CodePen

Код стал более запутанным и еще более сложным для восприятия. Уже на этом этапе можно сказать, что через 2 месяца активной жизни веб-приложения с кодом будет невозможно работать, а главное — неприятно. Ведь работа должна доставлять удовольствие. Только представьте, если строк такого кода будет 10 000. К тому же, как поведет себя скрипт, если мы изменим шаблон? Он перестанет работать, и нам придется рефакторить весь код.

Теперь посмотрим как справится с задачей «Middle developer».

var productDiscount = {

    discountByTypes: {
        cheap: 0.20,
        expensive: 0.10
    },

    getDiscoutedPrice: function(price, discount) {
        return price - (price * discount);
    },

    getDiscountedByType: function(price, type) {
        return this.getDiscoutedPrice(price, this.discountByTypes[type]);
    },

    setPrice: function(elem, price, type) {
        var discountedPrice = this.getDiscountedByType(price, type);
        elem.html(discountedPrice);
    },

    init: function() {
        $('.products li').each(function() {
            var price = $(this).find('.price').html(),
                type = $(this).data('type');

            productDiscount.setPrice($(this).find('.price'), price, type);
        });
    }
}

productDiscount.init();

CodePen

Уже намного лучше. «Middle developer» создал объект productDiscount, каждый метод которого имеет свое уникальное имя. Также он сделал функцию-инициализатор объекта init(). Что изменилось по сравнению с вариантом джуниора?

  1. Код стал проще для восприятия;
  2. Разделение кода на блоки;
  3. Наименование методов говорят сами за себя. Теперь не нужно читать весь код чтобы понять (изменить) отдельную его часть.
  4. Код можно расширять новыми методами и свойствами.

Данный вариант значительно лучше. Хороший код всегда читается как открытая книга. Чем больше код похож на человеческую речь, тем лучше. Это и отличает языки высокого уровня от низкоуровневых языков. Но у нас по-прежнему нет модели данных! Мы оперируем данными с веб-страницы и используем строковую математику. Что нам ответит на это «Senior developer»? Он будет программировать классами, разделив при этом данные от представления. Вы конечно же знаете о таких конструкциях, как new Object(), new String() и т. д. Но, к великому сожалению, не все знают, что могут создавать самостоятельно подобные функции-конструкторы и работать с их экземплярами.

Когда мы создаем, например, new Object(), то получаем на вооружение не только сам объект, но и все его свойства и методы. Мы можем создать десятки тысяч экземпляров объекта, а методы и свойства у него всегда будут одинаковыми (далее я буду называть это модель). Если добавить новый метод в модель, у всех 10 000 экземпляров он появится тоже. Если нам потребуется добавить новое свойство, то экземпляры его унаследуют. Данный подход раскрывает перед нами поистине безграничные возможности. Модель — это сердце веб-приложения.

Теперь давайте посмотрим, как «Senior developer» решит поставленную задачу.

function Product(config) {
    var config = config || {};

    this.getDiscountedPrice = function() {
        return config.price - (config.price * config.discount);
    }

    return this;
}

Мы только что создали новый класс Product. Такой небольшой, понятный и лаконичный фрагмент кода открывает перед нами большие горизонты. Теперь мы можем создавать экземпляры продукта до тех пор, пока не закончится оперативная память и все они будут содержать методы getDiscountedPrice и writeDiscountedPriceTo. Такой подход не только исключает вероятность повторения кода, но и обладает рядом других преимуществ, о которых я расскажу далее.

Тут же хочется напомнить об общепринятых в мировом сообществе правилах:

  1. Наименование функции-конструктора (класса) всегда начинаются с заглавной буквы;
  2. Экземпляры класса начинаются строчной буквой;
  3. Постарайтесь во всех классах использовать переменную «config» для конфигурирования класса

Создадим несколько экземпляров класса Product:

// Создаем экземпляры класса Product и передаем им конфиг

var firstProduct = new Product({
    title: 'Шапка ушанка',
    price: 1899,
    discount: 0.10,
    type: 'expensive'
});

var secondProduct = new Product({
    title: 'Валенки',
    price: 399,
    discount: 0.10,
    type: 'cheap'
});

var thirdProduct = new Product({
    title: 'Варежки',
    price: 199,
    discount: 0.10,
    type: 'cheap'
});

typeof firstProduct.getDiscountedPrice // function
typeof secondProduct.getDiscountedPrice // function
typeof thirdProduct.getDiscountedPrice // function

Как видно из примера, «Senior» создал 3 продукта через конструкцию new Product(). Имейте ввиду, что «Senior» не объявлял метод getDiscountedPrice для экземпляров, но, тем не менее, наши экземпляры содержат его. Теперь firstProduct, secondProduct и thirdProduct также содержат метод getDiscountedPrice.

Теперь немного расширим нашу модель.

function Product(config) {
    var config = config || {};

    this.template = '<li><h3><%=title%></h3><span><%=price%></span></li>'; // NEW

    this.appendTo = function(elem) { // NEW
        var parsedTpl = this.template
            .replace('<%=title%>', this.getTitle())
            .replace('<%=price%>', this.getDiscountedPrice());

        elem.append(parsedTpl);
    }

    this.getTitle = function() { // NEW
        return config.title;
    }

    this.getDiscountedPrice = function() {
        return config.price - (config.price * config.discount);
    }

    return this;
}

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

Отлично. Мы только что добавили новый метод getTitle в нашу модель, который будет возвращать название товара. Также мы добавили шаблон модели «template» и возможность добавить модель на HTML страницу при помощи метода appendTo. Обратите внимание на то, что мы добавили эти методы и свойство только в одном месте, когда писали функцию-конструктор Product, но, несмотря на это, у всех экземпляров класса эти методы тоже появятся. Не правда ли здорово?

Мы отделили модель данных от представления и сделали код расширяемым. Мы больше не зависим от шаблона, и теперь можем не бояться потерять данные, изменяя шаблон. Это принцип MVC, который мы используем на фронтенде.

Не могу не обратить внимание и на структурированность кода. Он настолько понятный, так легко читается, что разобраться в нем не составляет никакого труда. Код не режет глаза как это было с примером джуниора. С таким кодом приятно работать продолжительное время. Синтаксис кода приближен к человеческой речи. Если метод называется «getPrice», значит он возвращает цену, если называется «getDiscountedPrice», значит цену со скидкой. Проще не бывает! По этой причине мы можем пригласить в проект нового программиста и обучить его, затратив минимальное количество времени.

Старайтесь придумывать для методов и свойств короткие и понятные имена, говорящие сами за себя. Это сделает ваш код понятным.

CodePen

Этот процесс бесконечный. Мы можем расширять класс Product сколько угодно. Также мы можем, например, создать новый класс ProductCollection, который будем наполнять экземплярами Product через метод add.

function ProductCollection(config) {
    var config = config,
        storage = [],
        template = '<ul class="product"></ul>';

    this.add = function(product) {
        storage.push(product);
    }

    this.appendTo = function(elem) {
        var products = $(template);
        storage.forEach(function(one) {
            one.appendTo(products); // Тут one является экземпляром класса Product и, соответственно содержит все его методы
        });
        elem.append(products);
    }

    this.getProductsByType = function(type) {
        // Напишите метод самостоятельно. Должен возвращать массив товаров с типом, переданным в type
    }

    return this;
}

var collection = new ProductCollection();
collection.add(firstProduct);
collection.add(secondProduct);
collection.add(thirdProduct);

Теперь в нашей коллекции содержится 3 товара. Мы можем написать методы для работы с целыми коллекциями данных. Попробуйте самостоятельно добавить в ProductCollection новый метод getProductsByType.

CodePen

Способы хранения классов

Каждый класс обычно помещается в файл с наименованием класса. Например, product.class.js или productCollection.class.js. Для удобства файлы размещаются в папочку «model» или «core». Это позволяет использовать системный подход и не запутаться, когда количество классов достигает десятков или сотен.

Классы пишутся только один раз, приложение пишется в отдельном файле, например app.js. Структура нашего приложения будет выглядеть так:

Cтруктура приложения

В реальных условиях вам, вероятно, не захочется хранить фрагменты HTML кода в .js файлах, как мы делали это в свойстве template. И это хорошо. Тогда можно будет расширить дерево каталогов и добавить view, в который мы будем хранить вьюшки наших классов:

Структура приложения

Для программирования классами удобно использовать менеджер проектов на Node.js. Из наиболее известных можно выделить Gulp и Grunt. Данные инструменты позволят вам собирать классы в один файл для дальнейшего использования в проекте.

Удобство работы в команде. Каждый из разработчиков вашей команды может написать новый класс и разместить его в каталоге model без всякой путаницы, а вы и ваши коллеги потом будете использовать все методы и свойства этого класса.

Также хотелось бы отметить высокую мобильность такого подхода. Достаточно один раз написать класс, чтобы потом использовать его в большом количестве проектов. В идеале каждый класс должен быть независимым и иметь возможность безболезненного импорта в другие проекты с незначительными корректировками.

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

seniorDeveloper.zip

Прототипы и наследование

На фоне всего вышесказанного хочется также вспомнить о прототипах в JavaScript. В примерах в данной публикации мы создавали классы Product и ProductCollection. С точки зрения JavaScript данные классы ничем не отличаются от нативных классов, таких как Number, String и т.п. Они имеют одинаковые права на существование. Разница только в том, что Number и String есть в JavaScript по умолчанию, а Product и ProductCollection мы создали самостоятельно. Из этого следует, что наши классы тоже имеют прототипы.

Говоря простым языком, прототип — это возможность обратиться напрямую к функции-конструктору для того, чтобы расширить ее новыми методами и свойствами извне самой функции. Чтобы лучше понять принцип работы прототипов в рамках данной темы, предлагаю вновь вернуться к классу Product. Допустим, в праздничные дни наши продажи вырастают в 2 раза, и мы хотим увеличить цену товаров на 10%. Для этого нам понадобится новый метод, который мы можем добавить двумя способами:

  1. Написать метод напрямую в функции-конструкторе;
  2. Добавить метод через прототип.

Происходит это следующим образом:

var product = new Product({
    price: 199,
    discount: 0.20,
    type: 'cheap'
});

Product.prototype.increasePrice = function(percent) {
    var price = this.getPrice();
    return price + (price * percent);
}

product.increasePrice(0.10); // 218.9

Я нарочно добавил новый метод после создания экземпляра product. Несмотря на то, что экземпляр создавался выше по коду, он все равно получит метод increasePrice. Объяснение очень простое — экземпляры всегда наследуют методы класса, даже если мы добавили их в другом месте. Таким образом, при помощи прототипов мы можем расширять классы динамически. С таким же успехом мы могли бы добавить метод increasePrice во время создания класса:

...
    this.increasePrice = function(percent) {
        var price = this.getPrice();
        return price + (price * percent);
    }
...

Попробуйте добавить метод increasePrice сначала внутри функции конструктора, затем до создания экзмепляра и в конце после него. Результат будет всегда один и тот же — класс Product обретет этот метод.

CodePen

Расширение нативных классов

Как я уже говорил выше, пользовательские классы имеют одинаковые права с нативными. Из это следует только одно — их мы тоже можем расширить. Опять же, как мы можем расширить класс?

  1. Написать метод напрямую в функции-конструкторе;
  2. Добавить метод через прототип.

Попробуем расширить нативный String новым методом через его прототип. Допустим, мы хотим иметь такой метод, который смог бы удалять лишние пробелы из строки. Нам нужно выполнить trim и заменить 2 и более пробела на 1.

// Добавляем в String новый метод. Обратите внимание, что название классов всегда пишутся с заглавной буквы.

String.prototype.clearing = function() {
    return this.replace(/\s+/g, ' ').trim();
}

var string = '    Клара    у Карла украла      кораллы   ';

string; //    Клара    у Карла украла      кораллы   
string.clearing(); // Клара у Карла украла кораллы

Оператор this внутри метода всегда ссылается на текущий экземпляр. В данном случае:

this === ' Клара у Карла украла кораллы '.

По умолчанию класс String не обладает методом clearing, но мы расширили его через прототип и теперь можем использовать во всех строках. Вообще во всех. Потому что любая строка является всего-навсего экземпляром класса String.

CodePen

JavaScript нас ни в чем не ограничивает. Мы вообще можем удалить или заменить нативные методы классов. Например, по умолчанию String имеет метод split. Давайте попробуем его удалить.

console.info('Slpit works'.split(' ')); // ["Slpit", "works"]

String.prototype.split = undefined;

try {
    "Where is our method split?".split();
} catch(e) {
    console.info('Ugh, it was deleted!');
    console.error(e);
}

Если мы попытаемся выполнить такой код, то сразу поймаем «exception»:

TypeError: "Where is our method split?".split is not a function

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

Сделать плохо на CodePen

Добавляйте методы для извлечения емайлов и телефонов из String, получение квадрата от Number, уникальные переборы Array — никаких ограничений для полета вашей творческой мысли. Вместо того, чтобы писать функции, гораздо более интересно расширять классы новыми методами и использовать их в веб приложении, получая от процесса огромное удовольствие.

Подведем итоги

  1. Это нативный JavaScript и его фундаментальные принципы;
  2. Мы получаем модель данных и возможность управлять ей;
  3. Данные не зависят от шаблона;
  4. Мы можем изменять шаблон (HTML) и не бояться потерять данные;
  5. Нам больше не требуется строковая математика;
  6. Классы можно импортировать в другие проекты;
  7. Код легкий для восприятия;
  8. Код легко читается благодаря синтаксису, приближенному к человеческой речи;
  9. Код расширяется до бесконечности без риска попасть в черную дыру;
  10. Код легко документируется. Достаточно лишь описать класс и его методы;
  11. Низкий порог входа в проект других разработчиков

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Евгений Вьюшин

Комментарии (78 комментариев, если быть точным)

Автар пользователя
a-wart

Очередная статья, где автор открыл для себя функции-конструкторы и разобрался в прототипном наследовании, это конечно, хорошо, но есть но: → запутал всех, обозвав функции-конструкторы сначала классами, а потом моделью.  → зачем-то на пути к идеальному решению несколько раз изменил исходные условия, добавил клиентскую шаблонизацию и рассказал про как хорошо размещать класс в каталоге модель.

прочитав заголовок, я ждал описания ES6 классов, ну или хотя бы небольшой статьи про сахар class в CoffeeScript. В общем, рассказать про конструкторы, создание объектов и прототипы это хорошо, называть это популистским термином «программирование классами в веб-приложениях» — плохо. Плохо, потому что в js на текущий момент НЕТ классов, но есть прототипы. Плохо путать людей и называть синее мягким.

Автар пользователя
SilentImp

Материалы про классы в es6 будут несколько позднее. Что касается терминологии — мне кажется, что точность терминологии начинает играть роль, когда ты уже знаешь какие могут быть подходы к ООП в js. А когда ты им вообще не пользуешься — большой роли это не играет.

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

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

Автар пользователя
LeusMaximus

А как лучше упростить создание экземпляров класса http://prntscr.com/7kfceh ? Чтобы не писать для каждого продукта отдельно?

Автар пользователя
vyushin

@LeusMaximus, лучше написать новый метод в ProductCollection, который будет добавлять продукты массивом.

Я сделал пример на CodePen. http://codepen.io/vyushin/pen/oXoBqx

Автар пользователя
ogonkov

Что это за сеньор, пихающий шаблоны в модель?

Автар пользователя
vyushin

@ogonkov, примеры максимально простые, чтобы лучше понять тему. Клиентская шаблонизация - это отдельная большая тема.

Конечно в боевых условиях модель и шаблоны надо разделять.

Автар пользователя
vyushin

@SilentImp, я тоже редко встречаю подобный подход в повседневной разработке! У меня пока нет точного ответа почему так происходит, но это правда грустно. По большей части это встречается во фреймворках и API. Например Backbone, Ext JS, Ymaps API и т. п.

Такой подход медленнее на старте, но зато потом легче поддерживать приложение.

Автар пользователя
fpinger

JavaScript устроен таким образом, что все в нем является объектами.

Например null или undefined? Мало того: значения типов string, number и boolean то же не являются объектами, а только ведут себя так иногда (такая встроенная магия).

var bar = "Вася"; bar.foo = "Пертрович" ; // Скрытая магия: // bar = new String(bar); // bar.foo = "Пертрович" ; // bar = bar.toString(); // А следоватешльно console.log(bar); // Вася console.log(bar.foo); // undefined

Использовать String, Number и Boolean вообще стоит ограничено, если вообще стоит. То есть если ты решил заняться разработкой на JS, то до всяких имитаций ООП, используя прототипное наследование, нужно ознакомится с http://bonsaiden.github.io/JavaScript-Garden/ru/

Автар пользователя
vyushin

@fpinger,

Например null или undefined?

Постараюсь выразить свою мысль в коде:

javascript console.log(typeof null) // object console.log(typeof new Boolean(true)) // object console.log(typeof new Number(1)) // object console.log(typeof new String('1')) // object console.log(typeof new Array([1, 2])) // object

http://codepen.io/anon/pen/oXooOY

Автар пользователя
a-wart

@vyushin typeof'у ж нельзя верить. null это не объект. Результат typeof null == "object" — это официально признанная ошибка в языке, которая сохраняется для совместимости. На самом деле null — это не объект, а отдельный тип данных. И в целом, согласен с @fpinger — использовать Number String и Boolean в принципе антипаттерн, неспроста мдн и прочие js garden’ы настоятельно рекомендуют использовать эти примитивы в обход конструкторов

Автар пользователя
vyushin

@a-wart, JS слаботипизированный язык:

javascript 1 == new Number(1) // true 1 === new Number(1) // false

Вы говорите о типах, а я говорю про объекты. Не путайте, прошу вас. Это разные понятия.

Автар пользователя
a-wart

@vyushin вы утверждаете, что «всё в js является объектами». мы говорим: нет, не всё. например, null вы говорите: console.log(typeof null) // object мы говорим: это признанная ошибка языка и на самом деле null это не объект. вы говорите, что мы что-то путаем. я не вижу путаницы в своих рассуждениях, к сожалению, только в ваших.

и раз уж вы применяете в своём примере typeof то тоже говорите о типах.

Автар пользователя
vyushin

@a-wart, Вы правы, успокойтесь. В JS не всё является объектами, например (), {}, [] в JS не являются объектами. Запятые, символы +, -, =, : в JS не являются объектами. Я пропустил какой-то символ? :)

Автар пользователя
a-wart

@vyushin я спокоен, не переживайте. Боюсь, вы пропустили целостное понимание того, о чём пытаетесь писать. В остальном претензий не имею.

Автар пользователя
DeLaGuardo

Уточнения ради, все-таки {} и [] в JS являются обьектами

Автар пользователя
vyushin

@DeLaGuardo, а так }{, ][ ? ))

Автар пользователя
DeLaGuardo

@vyshin, Вы что-то пытаетесь этим доказать? Не понимаю, правда, что :)

Автар пользователя
DeLaGuardo

В JavaScript более распространено такое понятие как функции-конструкторы, но для удобства я буду называть их классами.

Итак, ООП... базовыми понятиями ООП являются наследование и инкапсуляция. Сделайте пожалуйста функцию-конструктор, которая будет наследована от другой функции-конструктора частично переопределяя ее функционал. Потом "для удобства" называйте ее классом, пока вы этого не сделаете - пожалуйста, даже не так - умоляю!! не смешивайте эти два понятия.

Автар пользователя
fpinger

@vyshin, если вас задела фраза:

То есть если ты решил заняться разработкой на JS

То прошу извинения, но она к абстрактному начинающему разработчику на JS, а не лично к вам. Я, думаю, она повлияла на градус эмоций.

Если null объект, то покажите мне его родословную, как объекта :)

Автар пользователя
vyushin

@DeLaGuardo, это базовые вещи. :) Функции-конструкторы поддерживают наследование через прототип.

Автар пользователя
DeLaGuardo

@vyushin, частичное, я говорил про частичное наследование

Автар пользователя
vyushin

@DeLaGuardo, опять же через прототип. :) Можно расширить один прототип другим прототипом, можно каким-то конкретным методом. Пожалуйста, в JS это есть. Пользуйтесь на здоровье. :)

Автар пользователя
vyushin

@fpinger, Если вам не по душе моя терминология, то я не стану вас переубеждать. :) Кто смог понять идею статьи целиком, не придираясь к отдельным конкретным словам, тому респект и уважуха, кто её не понял - очень жаль.

Автар пользователя
fpinger

@vyshin, я не против понятного кода, но при этом я за правильное понимание JS. :)

Автар пользователя
SilentImp

@vyushin В JS есть простые типы, такие как число или строка, но есть и объекты — число и строка. Если попытаться скажем использовать метод объекта строка для переменной простого типа строка, то из неё будет создан объект типа строка и метод будет вызван.

Про типы, кстати, достаточно подробно написано в спецефикации ECMA. И правила их преобразования тоже.

Автар пользователя
SilentImp

Так что да, «все в JS — объект» это преувеличение с формальной точки зрения.

Автар пользователя
vyushin

@SilentImp, конечно преувеличение. Ведь символы }, {, [, ], +, -, = и т. д. не являются объектами. :)

Автар пользователя
fpinger

@vyushin, литералы могут объявлять как объекты, так и не объекты. Но ведь речь не об этом.

Автар пользователя
vyushin

@fpinger, вы изучаете английский? Так вот, в английском языке одни и те же слова имеют разный смысл в зависимости от контекста. Тут то же самое. Перестаньте придираться к словам и смотрите на контекст. Что за детский сад?

Автар пользователя
fpinger

@vyushin, кстати относительно объектного подхода. А вы пробовали функциональный? В JS он позволяет писать ещё более понятный код, чем объектно ориентированный. :)

Автар пользователя
iamstarkov

@fpinger 👍 за фп! мне нравится ramda, а вам?

Автар пользователя
fpinger

@iamstarkov, спасибо за информацию. Добавил в тудушку посмотреть. Честно. я в теме фп недавно.

Автар пользователя
alexeyraspopov

Сделайте пожалуйста функцию-конструктор, которая будет наследована от другой функции-конструктора частично переопределяя ее функционал. Потом "для удобства" называйте ее классом, пока вы этого не сделаете - пожалуйста, даже не так - умоляю!! не смешивайте эти два понятия.

``` function Parent() {}

Parent.prototype.someMethodA = function(){};

Parent.prototype.someMethodB = function(){};

function Child() {}

Child.prototype = Object.create(Parent.prototype);

Child.prototype.someMethodA = function(){}; ```

Теперь можно называть классом, да? :)

Автар пользователя
ditransler

Статья мне понравилась. Было интересно посмотреть, как одна и та же задача выполняется с помощью разных подходов (в зависимости от уровня разработчика). Себя смело причисляю к разряду Junior'ов.

Поводу высказывания "JavaScript устроен таким образом, что все в нем является объектами." и дальнейшего обсуждения хочу сказать, что уже не в первом источнике говорится об обратном. Например, в "Eloquent JavaScript" (http://eloquentjavascript.net/04_data.html#h_mT4YQfwHp6):

"Values of type string, number, and Boolean are not objects, and though the language doesn’t complain if you try to set new properties on them, it doesn’t actually store those properties. The values are immutable and cannot be changed."

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

Автар пользователя
vyushin

@ditransler, спасибо. :) Если кому-то нравится копать глубоко, то это личное дело каждого. Некоторым вообще нравится на ассемблере писать. Да ради Бога! Пусть хоть на голове стоят.

Есть натуральное число 9. Ну да - это число. Есть строка "I'm a string!". Да - это строка. Есть булев тип и т. д. Да, да, да. Это всё так. Это типы и они есть. Никто не говорил, что типов не существует и что типы - это объекты. Не надо придираться к слову "всё", я вас очень прошу.

javascript 0; // это число "I'm a string"; // Это строка true; // Булев тип

Речь не о том. Со всеми этими типами так или иначе мы взаимодействуем, как с объектами. Не всегда (а то опять скажете, что слишком обобщаю), понятное дело. Это само собой разумеется! Если мы работаем с ними, как с объектами, то имеем полное право назвать их объектами. И я буду называть их ОБЪЕКТАМИ. По тому что я работаю с ними, как с ОБЪЕКТАМИ и рассказываю о них, как об ОБЪЕКТАХ.

jvascript var str = "I'm a string"; // Это строка. Тип "string". Но у нее есть методы. Значит это объект. str.toUpperCase(); // Вызываем метод ОБЪЕКТА String

Другой пример:

jvascript var num = 671; // Это натуральное число. Тип "number". Но у него есть методы. Значит это объект. num.toString(); // Вызываем метод ОБЪЕКТА Number

Есть и третий пример:

javascript var str = "I'm a string"; str.substr(1, 2); // Передаваемые аргументы 1 и 2 не являются объектами. Это целые числа.

Какой вывод мы можем сделать? В JavaScript представление типов может быть как в натуральном виде, так и в виде объектов. Вот и разобрались.

Автар пользователя
KarelWintersky

Здравствуйте. У меня глупый вопрос :) Senior developer создал очень интересную конструкцию. А потом вызвал её для трех продуктов. Создал три экземпляра объекта.

А что делать, если у нас 50 товаров? Создавать для каждого экземпляр: ... = new Product({ ... }); ? А если 500? А если 10000 ?

Ой, что у нас с потреблением памяти-то будет твориться...

Да, я теперь понимаю, почему современные веб-страницы так дико тормозят на моем стареньком ноуте! Упоение технологиями без оглядки на реальность.

Автар пользователя
vyushin

@KarelWintersky, хороший вопрос и правильный. Выше в комментах я говорил о методе addProducts для добавления товаров одним массивом. Это хардкодный вариант просто для наглядности. В реальности лучше подгружать этот массив аяксом с сервера в json или XML формате.

В случае, когда у нас много товаров мы можем сделать пагинацию, аякс скроллинг и т. п., подгружая данные динамически.

Автар пользователя
sergiivelykodnyi

По поводу "Объектов" и "Необъектов". Я думаю что лучше вещи называть своими именами, чтобы не сбивать с толку людей, которые только начали изучать JS. Строка это строка, она не есть объект, но если мы на ней вызываем метод объекта String, то парсер автоматически, на лету, создает объект обвертку, чтобы выполнить метод, но с самой строкой ничего не происходит, она остается неизменной. То что, со строкой можно обращаться как с объектом, это еще не значит, то ее можно называть объектом.

Автар пользователя
iamstarkov

срочно необходимо понизить градус дискуссии someone-is-wrong

Автар пользователя
KarelWintersky

@vyushin,

В реальности лучше подгружать этот массив аяксом с сервера в json или XML формате.

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

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

В реальности лучше подгружать этот массив аяксом с сервера в json или XML формате.

Отлично :) Но ведь мы потом все равно проходим по этому массиву и импортируем данные в память, пусть и в цикле. Да, код страницы меньше. Но я говорю про потребление оперативки и cpu-time.

Как жить с таким?

Вопросы пагинации оставим в стороне: "Клиент должен видеть все доступные ему товары, никакой пагинации" - я такое слышал не раз.

Аякс-скроллинг - не панацея. Да, изначально на странице товаров мало и мало объектов. А когда мы пролистаем вниз? Мы же не можем убрать объекты из памяти только потому, что они оказались за пределами browser.viewport ?!

Похожая ситуация получается, когда мы просматриваем архив (ре)постов тумблер-блога. Но там то, хоть и используется аякс-скроллинг - просто добавляются элементы в DOM. Просто элементы, просто в DOM (хоть и превьюшки картинок/постов). Тем не менее, на каком-то этапе инстанс хрома улетает далеко за 800 мегов.

Нормально ли это? Не думаю. Что с этим делать? Не знаю.

Автар пользователя
alexeyraspopov

Как жить с таким?

Мы* решаем проблемы бизнеса, а не дрочим на байты.

*ответственные разработчики.

Автар пользователя
iamstarkov

байты, а особенно много-много байтов это проблемы клиентов, а соответственно и проблемы бизнеса

Автар пользователя
vyushin

@alexeyraspopov, сняли с языка выражение "байтодротство". :)

@KarelWintersky, желаю вам удачи в экономии байтов и ЦПУ. Родина вас не забудет.

Автар пользователя
KarelWintersky

@iamstarkov :+1: Осталось провести тесты и посрамить оппонентов.

@vyushin, кто с ассемблера начинал, всегда помнит о байтах и ЦПУ.

Автар пользователя
vyushin

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

Автар пользователя
KarelWintersky

@vyushin, вы отстаиваете позицию "подход сениора более качественный и занимает меньше времени разработки"? :)

Автар пользователя
sergiivelykodnyi

Все зависит от того для чего и кого создается приложение. Если это бизнес, то там все очень просто, нет клиентов, нет бизнеса. Например, не зря же написаны JS библиотеки для поддержки старых, и горячо любимых всеми разработчиками, браузеров ІE.

Автар пользователя
vyushin

@KarelWintersky, не совсем. Я считаю, что подход сениора более медленный на старте, но в долгосрочной перспективе полностью оправдывает себя. Тем более для команды разработчиков.

Автар пользователя
KarelWintersky

Мы оперируем данными с веб-страницы и используем строковую математику.

Мне кажется, подход миддла некорректен. Подозреваю, автор совершил эту ошибку намеренно - внёс селектор внутрь init()

javascript init: function(selector) { $(selector).each(function() { ... }); }

и productDiscount.init('.products li' );

не зря же написаны JS библиотеки для поддержки старых, и горячо любимых всеми разработчиками, браузеров ІE.

Совсем недавно друг допиливал внутренний портал для одного крупного российского банка. С большим трудом ему удалось убедить заказчика, что IE6 поддерживать не надо. В ответ его обязали поддерживать IE7. Почему? Потому что IE7 внутри локальной сети банка - _стандарт_. Мнение разработчика никого не интересует.

Автар пользователя
KarelWintersky

@iamstarkov: сильно мешает .comments form textarea { resize: none; }

Автар пользователя
fpinger

Скатились в преждевременную оптимизацию круглого коня в вакууме :)

А задача изначально мутная. Зачем рассчитывать цену со скидкой на стороне клиента? Другой динамики не вижу, как-то: вывод тут же оригинальной цены; выбор условий покупки и следовательно динамический процент скидки: нет аякса для днеобходимости динамической сборки. В таком случае лучшая оптимизация - вообще не использовать JS. Нет кода на стороне клиента - нет проблем оптимизации. Это как бы по данному круглому коню :)

Автар пользователя
bionicle12

Особо порадовало про "код сразу стал легко читаем" и чем дальше, тем сидишь и думаешь, штаааа. Для меня так наборот все стало китайской грамотой. Аж выбесило такое упрощение, которое из простого действия, сделало запуск баллистической ракеты. Не люблю когда упрощение сопутствует фразам учите матчасть,базовую шляпу. Упрощение это когда для дебилов понятно...

Автар пользователя
bionicle12

И да, я не дочитал, кинул в закладки, когда-нить вернусь чтобы осилить. А вы вот неправы. Плохие педагоги. Нехорошие. Как и все педагоги в этой стране )) Один я в пальто.

Автар пользователя
kadymov

Не понравилось, очередное разжевывание. Манкипатчинг - зло.

Автар пользователя
tonkado

Спасибо за статью, лично мне понравилось. Просто и понятно. Оцениваю свой уровень между junior & middle. Если отфильтровать брюзжание в комментариях, то есть интересные моменты.

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

Автар пользователя
vyushin

@bionicle12, согласен с вами на все 100%. :) Людям без понимания ООП будет сложновато. Но это не повод бросать статью!

Тут другая штука интересная! :) По этому принципу строятся множество фреймворков. Я уже упоминал выше Ext JS, Backbone и т. п. Освоив этот принцип один раз потом легче разобраться в любом из фреймворков, да и вообще в чем угодно... ООП используется и в других языках. Например PHP, Java. Везде подход очень похож, даже синтаксис имеет лишь незначительные различия.

Так что изучение данного принципа будет большим шагом вперед

Автар пользователя
vyushin

@tonkado, благодарствую. :)

Критика тоже полезна, так что пусть эти толстопузые, высокомерные и тщеславные увольни и дальше брюзжат :)

P. S. Не имею ввиду никого конкретного. Выражаюсь художественным, абстрагированным языком с двусмысленными понятиями.

Автар пользователя
KarelWintersky

Ах-ах. Несмотря на то, что подход в целом интересный. И я даже кажется нашел задачу, в которой именно такой подход: - с набором данных - с методами, обрабатывающими данные в JS - с методами, общающимися с сервером и меняющими набор данных

окажется более эффективным и выгодным с точки зрения общей архитектуры кода...

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

Автар пользователя
KarelWintersky

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

Автар пользователя
vyushin

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

Автар пользователя
dzharii

Программировать классами это все равно что строить кирпичами :)

Автар пользователя
KarelWintersky

@vyushin как говорят в наших интернетах - "слив зощитан". А теперь разъясню: предложение "пойти посмотреть демки фреймворков" - это равнозначно посылу на три буквы. Потому что демки синтетические и очень далеки от реальной жизни. Хотя бы тем, что не показывают отрицательные стороны методологии, выпячивая положительные.

Да-да, "веб-сервер на ноде в 10 строк" и звучит и выглядит очень круто, но лично мне совершенно непонятно, в чем же профит?

Автар пользователя
tonkado

@KarelWintersky, у меня есть хороший пример. Если скачать браузерное расширение Яндекс.Диска и посмотреть его код, то вы приятно удивитесь. Для удобства выкладываю уже распакованное расширение: https://yadi.sk/d/2bIfW1fAhZzg3

Советую начать с просмотра файла /background/main.min.js

Автар пользователя
tonkado

Сори, вот актуальная ссылка: https://yadi.sk/d/tvLxtfCpha2aY

Автар пользователя
iamstarkov

Советую начать с просмотра файла /background/main.min.js

зачем смотреть минифицированную часть?

Автар пользователя
tonkado

@iamstarkov, что за странный вопрос? ) я обратил внимание не на минифицированную часть, а на основной файл расширения который написан с применением ООП — на примере реального проекта.

Из минифицированного кода сделать обычный можно одним кликом, например JSFormat для Sublime Text.

Автар пользователя
iamstarkov

Советую начать с просмотра файла /background/main.min.js @ я обратил внимание не на минифицированную часть,

рилли?

Автар пользователя
tonkado

Не нужно к словам придираться. Для кого-то проблема привести минифицированную версию к нормальному виду?

Автар пользователя
tonkado

Рилли. Основной файл расширения — main.***.js

Автар пользователя
fpinger

Отформатировать минифицированный код онлаин можно тут http://jsbeautifier.org/

Автар пользователя
KarelWintersky

Спасибо, поучительно.

Автар пользователя
krontill

Привет. Я сделал такой метод в коллекции

 
this.getProductsSearch = function(str) {
                if(str.replace(/\s+/g, ' ').trim() == ''){
                    storage.forEach(function (item) {
                        jomresJquery('#product_id-' + item.getId()).show();
                    });
                } else {
                    storage.forEach(function (item) {
                        if(~item.getId().indexOf(str) || ~item.getTitle().toLowerCase().indexOf(str.toLowerCase())){
                            jomresJquery('#product_id-' + item.getId()).show();
                        } else {
                            jomresJquery('#product_id-' + item.getId()).hide();
                        }
                    });
                }
            };
 

Это по фен-шую или так делают нубы? Или надо сделать чтобы метод возвращал коллекцию, потом я очищаю на странице старую коллекцию например через innerHTML = '' и с помощью appendTo добавляю новую? Еще думаю сортировку делать, но там вроде так просто как с поиском не получится, а надо пилить как я описал выше? Или всё х*ня и начинать по новой, тогда как?

Автар пользователя
ogonkov

@krontill смешана логика с представлением, не круто.

Автар пользователя
aleks-ba

Идея предоставить три варианта решения (джуниора, мидла, сеньора) гениальна! Вот если бы все задачи разбирались подобным образом

Автар пользователя
DangelZM

В примере:

String.prototype.clearing = function() { return this.replace(/\s+/g, ' ').trim(); }

скрыта опасность, если уже создавать методы в прототипе, то писать с проверкой

String.prototype.clearing = String.prototype.clearing || function() { return this.replace(/\s+/g, ' ').trim(); }

Автар пользователя
Pentaprizm

Поясните нубу зачем в конструкторе явно возвращать return this; ?? Ведь вызов конструктора по умолчанию и возвращает новый объект с данными, записанными в конструкторе посредством this.

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

Автар пользователя
zerkms

@Pentaprizm незачем, в JS это не имеет вообще никакого смысла. А программистов, знакомых с языками, в которых есть конструкторы - это введёт в ступор.

Автар пользователя
vyushin

@Pentaprizm, Согласен, что можно не указывать return из конструктора, но можно и указать — это не является ошибкой.

На мой взгляд хорошим тоном является, когда функция или метод что-либо возвращает и когда это явно видно. Я всегда пишу ф-ции и методы так, что они что-нибудь да возвращают. Если нет каких-то конкретных данных, то я возвращаю this. Это у меня выработано до автоматизма, так что в конструкторах тоже пишу return.