Pull to refresh

Comments 22

Это нехорошо:
if (this.constructor.name !== 'Calculator') { throw “No way, buddy!”; }

Если уж так делать, то можно:
function Calculator() {
if (this.constructor !== Calculator) { throw “No way, buddy!”; }

А лучше так:
function Calculator() {
if (! (this instanceof Calculator)) { throw new TypeError(“No way, buddy!”); }
Для начала давайте для ясности определимся какая цель была у этого кода.
Я понял его, как желание определить, вызывается ли функция в качестве конструктора. Все три способа так или иначе делают это, но первый полагается на некоторое строковое значение, в то время как второй и третий на ссылку. С моей точки зрения лучше полагаться на объекты выполняемого приложения, доступные по ссылкам, нежели на строки, которые являются данными.

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

Кстати, в вашем примере вот эта строка:
NamedFunction.name = "Good bye";
не сработает, потому как name это readonly-свойство функции.

Спасибо, вы правы. Действительно read-only. Но мы подошли к тому, что мой способ и поломать-то нельзя. (фиддл подправил).
Сенсей, вы не находите, что тогда все три способа тождественны, и ни один из них ни хуже, ни лучше?
У instanceof есть преимущества:
  • проходится по цепочке прототипов
  • работает даже когда this == null
Сенсей, вы не находите, что тогда все три способа тождественны, и ни один из них ни хуже, ни лучше?


Нет, я не нахожу. Объявление надо врапнуть в анонимную функцию, тогда точно никто не переопределит.

var IsInstance = (function () {
  return function IsInstance() { 
   console.log(1);
    if (! (this instanceof IsInstance)) throw "No way!";
  }
})();

var A = IsInstance;
function IsInstance() {};
var a = new A;


Способ с именем — дурацкий.
Ништяк. Я срочно побежал везде оборачивать все конструкторы лямбдами. Теперь никто из команды точно не сможет ни случайно пропустить кейворд new, ни нацистким способом похачить и без того обернутые в лямбду модули.
Основная ваша проблема в том, что вы не соблюдаете DRY. Каждый раз повторять проверку — явный признак быдлокодинга.

Кстати, бросать строку вместо эксепшина — это плохая практика. Никакого стектрейса, просто голая ошибка. Кто так делает вообще?
Как мило. Разговор, вижу, пошел по понятиям. Самоутверждатесь, товарищ? — Предлагаю минусануть еще и этот коментарий и успокоиться.
О чём вы? Какие понятия? Просто лёгкий ликбез.
К слову, у вас будет ещё проблем — когда захотите унаследоваться, то не сможете вызвать конструктор:

function Child () {
  Parent.call(this); // fail
}
Кстати, есть ещё интересная конструкция, которая позволяет вызывать функцию как конструктор без ключевого слова new (она тоже основана на проверке instanceof).
Быстрым загугливанием нашёл здесь: js-bits.blogspot.ru/2010/08/constructors-without-using-new.html
Может быть и на хабре освещался этот вопрос.

Это своеобразный «иной» подход к той же проблеме: вместо ошибки мы просто позволяем вызывать функцию без слова new с тем же эффектом.
Защита нужна не от вторжения, а от невнимательности.
Не могли бы вы объяснить, почему же так нехорошо одно и чем же так прекрасно другое и третье?
Промахнулся веткой. Этот вопрос был к StreetStrider.
Для выборки нескольких элементов приходится в начале строки указывать нестандартный селектор "[]", возвращающий массив, а не jQuery-объект. В обычных селекторах тоже возвращается DOM element. Возможно я не прав, но если уж работать с jQuery, то почему бы не использовать все возможности этой библиотеки?

Чем плодить кучу проверок и exception-ов, лучше переложить большую часть на плечи jQuery, а обработку самих ошибок оставить пользователю. Это даст намного больше гибкости и возможностей.

Например с теми же селекторами: пользователь указывает любые селекторы, которые в скрипте пытаются найтись внутри элемента-контейнера jQuery('selector', container). Если селектор неправильный, либо такого элемента не существует — вернётся пустой объект jQuery, а не ошибка. И программист, при необходимости, сам проверит есть ли элемент. Тогда вместо кода

Скрытый текст
  // Extract jQuery DOM parts from a container using a map of selectors.
  priv.extractParts = function (sourceContainer, selectors) {
    var domParts = {};

    if (!priv.isElement(sourceContainer)) {
      throw "POE10: arguments should be 1st — HTML DOM container and 2nd — selectors map";
    }

    if (!$.isPlainObject(selectors)) {
      selectors = {};
    }


    $.each(selectors, function (name, selector) {
      var found,
        findMultiple = false;

      if (!/^[A-z\.]+$/.test(name)) {
        throw "POE12: incorrect selector name `" + name + "`";
      }

      if ($.isArray(selector) &&
        selector.length === 2 &&
        $.type(selector[0]) === 'string' &&
        $.isFunction(selector[1])) {

        domParts[name] = {};
        $(sourceContainer).find(selector[0]).each(function () {
          var id = selector[1].call(this, this);
          if (id) {
            if (domParts[name][id]) {
              throw "POE13: duplicate identifier `" + id + "` in DOM part namespace `"+ name +"`";
            }
            domParts[name][id] = this;
          }
        });
      }

      else if ($.isPlainObject(selector)) {
        domParts[name] = priv.extractParts(sourceContainer, selector);
      }

      else if (typeof selector === 'string') {
        findMultiple = selector.indexOf('[]') === 0;
        if (findMultiple) {
          selector = selector.replace('[]', '');
        }
        found = $(sourceContainer).find(selector);
        if (found.length === 0) {
          throw "POE14: DOM parts weren't found for selector `" + name + "`";
        } else if (found.length > 1) {
          if (findMultiple) {
            found = Array.prototype.slice.call(found);
          } else {
            throw "POE15: multiple DOM parts found for selector `" + name + "`";
          }
        } else {
          found = found[0];
        }
        domParts[name] = found;
      }

      else {
        throw "POE16: invalid selector value";
      }

    });

    return domParts;
  };



можно легко использовать

Скрытый текст
priv.extractParts = function(container, selectors){
    var domParts = {};
    $.each(selectors, function(name, selector){
        domParts[name] = (typeof name == 'string') ?
            $(selector, container) : priv.extractParts(container, selector);
    });
    return domParts;
};



То же самое и с проверками $.isPlainObject — достаточно использовать $.extend, чтобы гарантировать, что в результате всегда будет объект.

Небольшая переделка Demo
<script type="text/html" id="tmplCalculator">
  <table cellspacing="0" cellpadding="0">
      <caption><%= caption %></caption>
      <tr>
          <td colspan="5"><p></p></td>
      </tr>
      <tr>
          <td><input class="calc-button" type="button" value="1"/></td>
          <td><input class="calc-button" type="button" value="2"/></td>
          <td><input class="calc-button" type="button" value="3"/></td>
          <td><input class="calc-button" type="button" value="/"/></td>
          <td><input class="calc-func calc-cancel" type="button" title="Cancel" value="C"/></td>
      </tr>
      <tr>
          <td><input class="calc-button" type="button" value="4"/></td>
          <td><input class="calc-button" type="button" value="5"/></td>
          <td><input class="calc-button" type="button" value="6"/></td>
          <td><input class="calc-button" type="button" value="*"/></td>
          <td><input class="calc-func calc-undo" type="button" title="Undo" value="←"/></td>
      </tr>
      <tr>
          <td><input class="calc-button" type="button" value="7"/></td>
          <td><input class="calc-button" type="button" value="8"/></td>
          <td><input class="calc-button" type="button" value="9"/></td>
          <td><input class="calc-button" type="button" value="-"/></td>
          <td rowspan="2"><input class="calc-func calc-equals" type="button" value="="/></td>
      </tr>
      <tr>
          <td colspan="2"><input class="calc-button calc-0" type="button" value="0"/></td>
          <td><input class="calc-button" type="button" value="."/></td>
          <td><input class="calc-button" type="button" value="+"/></td>
      </tr>
  </table>
</script>


;
(function ($) {

    function Calculator() {

        $.turnToPageObject(this, {
            template: $('#tmplCalculator').html(),
            containerClass: 'calc',
            context: {
                caption: "Demo Calculator"
            },
            selectors: {
                "cancel": '.calc-cancel',
                "equals": '.calc-equals',
                "undo": '.calc-undo',
                "led": 'p'
            }
        });

        var history = [],
        led = $(this.DOM.led);

        $(this.DOM.container).on('click', '.calc-button', function(){
            history.push(led.text());
            led.append(' ' + $(this).val());
        });

        $(this.DOM.cancel).click(function(){
            history = [];
            led.empty();
        });

        $(this.DOM.equals).click(function(){
            history.push(led.text());
            try {
                led.text((eval(led.text().replace(/ /g, '')) + '').replace(/./g, "$& "));
            } catch(e) {
                led.text(history.pop());
            }
        });

        $(this.DOM.undo).click(function(){
            led.text(history.pop() || '');
        });
    }
    window.Calculator = Calculator;

})(jQuery);

небольшая опечатка:

Скрытый текст
priv.extractParts = function(container, selectors){
    var domParts = {};
    $.each(selectors, function(name, selector){
        domParts[name] = (typeof selector == 'string') ?
            $(selector, container) : priv.extractParts(container, selector);
    });
    return domParts;
};

Я понял вашу идею. Смотрите, extractParts сделана такой для того, чтобы 1) возвращать не jQuery объекты, а DOM-элементы и 2) не допустить невалидные селекторы, чтобы разработчик как-раз не возился с обработкой ошибок. То есть, в текущей реализации результат предсказуем.

Недостаток способа, который вы предлагаете также в том, что в нем нет фичи selectorName: [ realSelector, getKeyFromElementFunction ].
1) возвращать не jQuery объекты, а DOM-элементы

а чем плохи jQuery объект? Всё равно используется библиотека jQuery, так зачем обращаться к DOM элементам? К тому же объект jQuery может работать и как массив — в большинстве случаев работа происходит не с единичными объектами, а именно с множеством объектов, полученных по определённым селекторам. Даже в вашем демо калькулятора работу с кнопками удобнее было бы организовать, если бы возвращался jQuery объект.

2) не допустить невалидные селекторы, чтобы разработчик как-раз не возился с обработкой ошибок

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

Скрытый текст
priv.extractParts = function(container, selectors){
    var domParts = {};
    $.each(selectors, function(name, selector){
        if(typeof selector == 'string'){
            domParts[name] = $(selector, container);
            if(domParts[name].length == 0){
                throw 'Incorrect selector "'+selector+'" or element "'+name+'" is not found';
            }
        } else {
            domParts[name] = priv.extractParts(container, selector);
        }
    });
    return domParts;
};


Обычно, все баги фронт-енда выявляются ещё на этапе разработки. На своей практике я редко сталкивался с ошибками в хардкодном хтмл-е и селекторах для него.

нет фичи selectorName: [ realSelector, getKeyFromElementFunction ]

а какой смысл в этой функции?
а чем плохи jQuery объект?
Скажу вам, что изначально, когда придумалась extractParts, она возвращала именно jQ объекты, но постепенно мы пришли к тому, что у этого есть недостатки.

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

Еще, мы пришли к тому, что форма записи $(calc.DOM.container).show() лучше, так как явно указывает человеку, вникающему в код, что вот он обернутый в jQuery дом-элемент.

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

<script type="text/html" id="tmplTableWidget">
<table>
  <tr>
    <th></th>
    <th>2011</th>
    <th>2012</th>
    <th>2013</th>
  </tr>
  <tr data-metric="newUsers">
    <th>New Users</th>
    <td data-year="2011"></td>
    <td data-year="2012"></td>
    <td data-year="2013"></td>
  </tr>
  <tr data-metric="uniqueUsers">
    <th>Unique Users</th>
    <td data-year="2011"></td>
    <td data-year="2012"></td>
    <td data-year="2013"></td>
  </tr>
  <!-- и т.д. -->
</table>
</script>

После превращения

$.turnToPageObject(this, {
  template: $('#tmplTableWidget').html(),
  selectors: {
    newUsers: [ 'tr[data-metric=newUsers]>td', function () { return $(this).attr('data-year'); } ],
    uniqueUsers: [ 'tr[data-metric=uniqueUsers]>td', function () { return $(this).attr('data-year'); } ]
  }
});

this.DOM будет выглядет вот так:

this.DOM.newUsers.2011
this.DOM.newUsers.2012
this.DOM.newUsers.2013
this.DOM.uniqueUsers.2011
this.DOM.uniqueUsers.2012
this.DOM.uniqueUsers.2013

Когда понадобится обновить метрики согласно какому-то вновь прибывшему json, мы все-равно пробежимся по нему циклом, но вместо jQuery-пасты а-ля $(this).find('tr[data-metric='+metric+']>td[data-year='+year+']).text(value) запишем $(this.DOM[metric][year]).text(value), что отработает куда быстрее.
Возможно Ваш способ действительно удобнее, но, имхо, не стоит подобных усилий. Проверять на существование элемента/ключа массива придётся в любом случае, иначе Ваш вариант кода сгенерирует exception. Так что я бы не стал лишний раз усложнять код. Однако моё дело лишь подсказать возможное «другое» решение.

Сорри, ответил не в ту ветку.
Sign up to leave a comment.

Articles