Назначение методов прототипа * внутри * функция-конструктор - почему бы и нет?

Стилистически, я предпочитаю эту структуру:

var Filter = function( category, value ){
  this.category = category;
  this.value = value;

  // product is a JSON object
  Filter.prototype.checkProduct = function( product ){
    // run some checks
    return is_match;
  }

};

К этой структуре:

var Filter = function( category, value ){
  this.category = category;
  this.value = value;
};// var Filter = function(){...}

Filter.prototype.checkProduct = function( product ){
  // run some checks
  return is_match;
}

Функционально, существуют ли какие-либо недостатки для структурирования моего кода таким образом? Будет ли добавление прототипического метода к объекту-прототипу внутри тела функции конструктора (т.е. До закрытия оператора выражения функции конструктора) вызывает непредвиденные проблемы с определением области видимости?

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

Ответ 1

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

Да, есть недостатки и неожиданные проблемы.

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

  • В некоторых случаях возникают непредсказуемые проблемы. См. Пример Counter в конце моего ответа для явного примера. Если вы ссылаетесь на локальную переменную конструктора из метода прототипа, то ваш первый пример создает потенциально неприятную ошибку в вашем коде.

Есть и другие (более мелкие) отличия. Ваша первая схема запрещает использование прототипа вне конструктора, как в:

Filter.prototype.checkProduct.apply(someFilterLikeObject, ...)

И, конечно, если кто-то использовал:

Object.create(Filter.prototype) 

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


С точки зрения производительности во время выполнения (производительность вызывающих методов на объекте) вам было бы лучше:

var Filter = function( category, value ){
  this.category = category;
  this.value = value;

  // product is a JSON object
  this.checkProduct = function( product ){
    // run some checks
    return is_match;
  }

};

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


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

var Counter = function(initialValue){
  var value = initialValue;

  // product is a JSON object
  Counter.prototype.get = function() {
      return value++;
  }

};

var c1 = new Counter(0);
var c2 = new Counter(10);
console.log(c1.get());    // outputs 10, should output 0

Демонстрация проблемы: http://jsfiddle.net/jfriend00/c7natr3d/

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

Ответ 2

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

Стилистически, я предпочитаю эту структуру

Вероятно, вам нравится чистый encapsulation, который предлагает эта нотация - все, что принадлежит классу, должным образом "привязано" к нему {}. (конечно, ошибочность заключается в том, что он привязан к каждому прогону функции конструктора).

Я предлагаю вам воспользоваться шаблонами (раскрывающимися) которые предлагает JavaScript. Вы получаете гораздо более явную структуру, отдельное объявление конструктора, частные переменные класса и все правильно инкапсулированные в блок:

var Filter = (function() {

    function Filter(category, value) { // the constructor
        this.category = category;
        this.value = value;
    }

    // product is a JSON object
    Filter.prototype.checkProduct = function(product) {
        // run some checks
        return is_match;
    };

    return Filter;
}());

Ответ 3

Первый пример кода типа пропускает цель прототипа. Вы будете воссоздавать метод checkProduct для каждого экземпляра. Хотя он будет определен только на прототипе и не будет потреблять память для каждого экземпляра, он все равно займет время.

Если вы хотите инкапсулировать класс, вы можете проверить существование метода перед тем, как указать метод checkProduct:

if(!Filter.prototype.checkProduct) {
  Filter.prototype.checkProduct = function( product ){
    // run some checks
    return is_match;
  }
}

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

Ответ 4

Самый большой недостаток вашего кода - это закрытие возможности переопределить ваши методы.

Если я пишу:

Filter.prototype.checkProduct = function( product ){
    // run some checks
    return different_result;
}

var a = new Filter(p1,p2);

a.checkProduct(product);

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

Ответ 5

В первом примере прототип Filter не заполняется функциями до тех пор, пока Filter не будет вызван хотя бы один раз. Что, если кто-то попытается наследовать Filter прототипом? Используя либо nodejs '

function ExtendedFilter() {};
util.inherit(ExtendedFilter, Filter);

или Object.create:

function ExtendedFilter() {};
ExtendedFilter.prototype = Object.create(Filter.prototype);

всегда заканчивается пустым прототипом в цепочке прототипов, если забыл или не знал, чтобы сначала вызвать Filter.

Ответ 6

Просто FYI, вы тоже не можете сделать это:

 function Constr(){

    const privateVar = 'this var is private';

    this.__proto__.getPrivateVar = function(){
       return privateVar;
    };

 }

причина в том, что Constr.prototype === this.__proto__, поэтому у вас будет такое же неправильное поведение.