Избегать новых операторов в JavaScript - лучший способ

16

Предупреждение: это длинный пост.

Давайте будем простыми. Я хочу избежать необходимости добавлять префикс оператора new каждый раз, когда я вызываю конструктор в JavaScript. Это потому, что я склонен забывать об этом, и мой код плохо работает.

Простой способ обойти это ...

function Make(x) {
  if ( !(this instanceof arguments.callee) )
  return new arguments.callee(x);

  // do your stuff...
}

Но мне нужно это, чтобы принять переменную №. аргументов, как это ...

m1 = Make();
m2 = Make(1,2,3);
m3 = Make('apple', 'banana');

Первым немедленным решением, похоже, является метод apply:

function Make() {
  if ( !(this instanceof arguments.callee) )
    return new arguments.callee.apply(null, arguments);

  // do your stuff
}

Однако это НЕПРАВИЛЬНО - новый объект передается applyметоду, а НЕ нашему конструктору arguments.callee.

Теперь у меня есть три решения. Мой простой вопрос: какой из них кажется лучшим. Или, если у вас есть лучший метод, скажите это.

Первый - использовать eval()для динамического создания кода JavaScript, который вызывает конструктор.

function Make(/* ... */) {
  if ( !(this instanceof arguments.callee) ) {
    // collect all the arguments
    var arr = [];
    for ( var i = 0; arguments[i]; i++ )
      arr.push( 'arguments[' + i + ']' );

    // create code
    var code = 'new arguments.callee(' + arr.join(',') + ');';

    // call it
    return eval( code );
  }

  // do your stuff with variable arguments...
}

Второе - у каждого объекта есть __proto__свойство, которое является «секретной» ссылкой на его объект-прототип. К счастью, это свойство доступно для записи.

function Make(/* ... */) {
  var obj = {};

  // do your stuff on 'obj' just like you'd do on 'this'
  // use the variable arguments here

  // now do the __proto__ magic
  // by 'mutating' obj to make it a different object

  obj.__proto__ = arguments.callee.prototype;

  // must return obj
  return obj;
}

Третье - это что-то похожее на второе решение.

function Make(/* ... */) {
  // we'll set '_construct' outside
  var obj = new arguments.callee._construct();

  // now do your stuff on 'obj' just like you'd do on 'this'
  // use the variable arguments here

  // you have to return obj
  return obj;
}

// now first set the _construct property to an empty function
Make._construct = function() {};

// and then mutate the prototype of _construct
Make._construct.prototype = Make.prototype;

  • eval Решение кажется неуклюжим и идет со всеми проблемами «зла Эвал».

  • __proto__ решение нестандартное, и «Великий браузер mIsERY» не соблюдает его.

  • Третье решение кажется слишком сложным.

Но со всеми вышеупомянутыми тремя решениями мы можем сделать что-то подобное, что мы не можем иначе ...

m1 = Make();
m2 = Make(1,2,3);
m3 = Make('apple', 'banana');

m1 instanceof Make; // true
m2 instanceof Make; // true
m3 instanceof Make; // true

Make.prototype.fire = function() {
  // ...
};

m1.fire();
m2.fire();
m3.fire();

Таким образом, вышеприведенные решения дают нам «истинные» конструкторы, которые принимают переменную no. аргументов и не требуютnew . Что ты думаешь об этом?

-- ОБНОВИТЬ --

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

treecoder
источник
2
или просто выдавайте ошибку, когда плохо изучите трассировку стека и вы можете исправить код
ratchet freak
2
Я думаю, что этот вопрос лучше задать при переполнении стека или проверке кода . Кажется, что это скорее код, но не концептуальный вопрос.
Адам Лир
1
@greengit вместо того, чтобы выдавать ошибку, используйте jslint. Он предупредит вас, если вы сделали Make()без, newпотому что Make
написан с большой
1
Так что подождите - вы ищете лучший способ сделать это или вы просто ищете кого-то, кто даст вам код, чтобы вы могли создавать объекты с переменным аргументом без него new? Потому что, если это последнее, вы, вероятно, спрашиваете не на том сайте. Если это первое, вы, возможно, захотите не отклонять предложения относительно использования новых и быстрого обнаружения ошибок ... Если ваше приложение действительно "тяжелое", последнее, что вам нужно, - это какой-то перегруженный механизм конструирования, чтобы замедлить его. new, несмотря на все неудачи, это довольно быстро.
Shog9
5
По иронии судьбы, попытка «умно» обрабатывать ошибки программиста сама по себе является причиной многих «плохих частей» JavaScript.
Даниэль Пратт

Ответы:

19

в первую очередь arguments.callee , устарела в ES5 строго, поэтому мы не используем его. Реальное решение довольно простое.

Вы не используете newвообще.

var Make = function () {
  if (Object.getPrototypeOf(this) !== Make.prototype) {
    var o = Object.create(Make.prototype);
    o.constructor.apply(o, arguments);
    return o;
  }
  ...
}

Это правильная боль в заднице, верно?

Пытаться enhance

var Make = enhance(function () {
  ...
});

var enhance = function (constr) {
  return function () {
    if (Object.getPrototypeOf(this) !== constr.prototype) {
      var o = Object.create(constr.prototype);
      constr.apply(o, arguments);
      return o;
    }
    return constr.apply(this, arguments);
  }
}

Теперь, конечно, для этого требуется ES5, но каждый использует ES5-прокладку верно?

Вы также можете быть заинтересованы в альтернативных шаблонах JS OO

В качестве альтернативы вы можете заменить второй вариант

var Make = function () {
  var that = Object.create(Make.prototype);
  // use that 

  return that;
}

Если вам нужна собственная шайба ES5, Object.createто это действительно легко

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
Raynos
источник
Да, правда - но вся магия здесь из-за Object.create. Как насчет pre ES5? ES5-Shim перечисляет Object.createкак ДУБОВНЫЙ.
Treecoder
@greengit, если вы прочитаете это снова, ES5 shim заявляет, что второй аргумент Object.create - ДУБОВНЫЙ. первый в порядке.
Райнос
1
Да, я читал это. И я думаю (ЕСЛИ) они там используют какие-то __proto__вещи, тогда мы все еще находимся на том же уровне. Потому что до ES5 нет более простого способа поменять прототип. Но в любом случае, ваше решение кажется наиболее элегантным и перспективным. +1 за это. (мой предел голосования достигнут)
Treecoder
1
Спасибо @psr. И @Raynos your shim Object.create- в значительной степени мое третье решение, но, конечно, оно менее сложное и выглядит лучше, чем мое.
Treecoder
37

Давайте будем простыми. Я хочу избежать необходимости добавлять префикс оператора new каждый раз, когда я вызываю конструктор в JavaScript. Это потому, что я склонен забывать об этом, и мой код плохо работает.

Очевидный ответ будет не забывать new ключевое слово.

Вы меняете структуру и значение языка.

Что, на мой взгляд, и ради будущих разработчиков вашего кода - ужасная идея.

CaffGeek
источник
9
+1 Кажется странным спорить о вредных привычках программирования. Конечно, некоторые правила подсветки синтаксиса / регистрации могут обеспечить предотвращение склонных к ошибкам шаблонов / возможных опечаток.
Стоив
Если бы я нашел библиотеку, в которой все конструкторы не заботились о том, используете ли вы ее newили нет, я бы обнаружил, что она более удобна в обслуживании.
Джек,
@ Джек, но это будет гораздо сложнее и тоньше, чтобы найти ошибки. Просто посмотрите на все ошибки, вызванные тем, что javascript "не заботится", если вы включаете ;операторы end. (Автоматическая вставка точки с запятой)
CaffGeek
@CaffGeek «Javascript не заботится о точках с запятой» не является точным - существуют случаи, когда использование точки с запятой и ее отсутствие незначительно отличаются, а в некоторых случаях это не так. Это проблема. Решение, представленное в принятом ответе, является с точностью до наоборот - с его помощью во всех случаях использование newили отсутствие семантически идентичны . Там нет ни одного тонких случаев , когда этот инвариант сломан. Вот почему это хорошо, и почему вы хотите его использовать.
Джек,
14

Самое простое решение - просто запомнить newи выдать ошибку, чтобы сделать очевидным, что вы забыли.

if (Object.getPrototypeOf(this) !== Make.prototype) {
    throw new Error('Remember to call "new"!');
}

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

Джош К
источник
Вызывающе избегайте .__proto__этого дьявола
Райнос
3

Я на самом деле написал пост об этом. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

function Ctor() {
    if (!(this instanceof Ctor) {
        return new Ctor(); 
    }
    // regular construction code
}

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

Отказ от ответственности Я не использую это в своем коде, я только разместил это для дидактического значения. Я обнаружил, что забыть newэто легко обнаружить. Как и другие, я не думаю, что нам это нужно для большей части кода. Если вы не пишете библиотеку для создания наследования JS, в этом случае вы можете использовать из одного места, и вы уже будете использовать другой подход, чем прямое наследование.

Хуан Мендес
источник
Потенциально скрытая ошибка: если у меня есть, var x = new Ctor();а потом и x, как thisи у меня var y = Ctor();, это не будет работать так , как ожидалось.
luiscubal
@luiscubal Не уверен, что вы говорите "позже сделайте x as this", вы можете опубликовать jsfiddle, чтобы показать потенциальную проблему?
Хуан Мендес
Ваш код немного более надежен, чем я изначально думал, но мне удалось придумать (несколько запутанный, но все еще верный
luiscubal
@luiscubal Я понимаю вашу точку зрения, но она действительно запутанная. Вы предполагаете, что это нормально, чтобы позвонить Ctor.call(ctorInstance, 'value'). Я не вижу действительного сценария того, что вы делаете. Чтобы построить объект, вы используете var x = new Ctor(val)либо var y=Ctor(val). Даже если бы был допустимый сценарий, я утверждаю, что у вас могут быть конструкторы без использования new Ctor, а не то, что у вас могут быть конструкторы, работающие с использованием. Ctor.callСм. Jsfiddle.net/JHNcR/2
Хуан Мендес,
0

Как насчет этого?

/* thing maker! it makes things! */
function thing(){
    if (!(this instanceof thing)){
        /* call 'new' for the lazy dev who didn't */
        return new thing(arguments, "lazy");
    };

    /* figure out how to use the arguments object, based on the above 'lazy' flag */
    var args = (arguments.length > 0 && arguments[arguments.length - 1] === "lazy") ? arguments[0] : arguments;

    /* set properties as needed */
    this.prop1 = (args.length > 0) ? args[0] : "nope";
    this.prop2 = (args.length > 1) ? args[1] : "nope";
};

/* create 3 new things (mixed 'new' and 'lazy') */
var myThing1 = thing("woo", "hoo");
var myThing2 = new thing("foo", "bar");
var myThing3 = thing();

/* test your new things */
console.log(myThing1.prop1); /* outputs 'woo' */
console.log(myThing1.prop2); /* outputs 'hoo' */

console.log(myThing2.prop1); /* outputs 'foo' */
console.log(myThing2.prop2); /* outputs 'bar' */

console.log(myThing3.prop1); /* outputs 'nope' */
console.log(myThing3.prop2); /* outputs 'nope' */

РЕДАКТИРОВАТЬ: я забыл добавить:

«Если ваше приложение действительно« тяжелое », последнее, что вам нужно, это какой-то перегруженный механизм конструирования, чтобы замедлить его»

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

кодировщик
источник
Придумывание значений для отсутствующих аргументов конструктора подвержено ошибкам. Если они отсутствуют, оставьте свойства неопределенными. Если они необходимы, выведите ошибку, если они отсутствуют. Что если prop1 должен был быть bool? Проверка «нет» на правдивость будет источником ошибок.
JBRWilkinson