Уникнути нового оператора в JavaScript - кращий спосіб


16

Попередження: Це довгий пост.

Давайте будемо просто. Я хочу уникати префіксації нового оператора кожного разу, коли я викликаю конструктор у 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');

Першим негайним рішенням здається метод "застосувати", як цей ...

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

  // do your stuff
}

Однак це WRONG - новий об'єкт передається 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__ рішення є нестандартним, і "Великий браузер МІСЕРІ" це не шанує.

  • Третє рішення видається надто складним.

Але з усіма перерахованими вище трьома рішеннями ми можемо зробити щось подібне, що не можемо інакше ...

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();

Настільки ефективно наведені вище рішення дають нам "справжні" конструктори, які приймають змінну "ні". аргументів і не вимагають new. Що ти береш за це?

- ОНОВЛЕННЯ -

Деякі казали "просто кинь помилку". Моя відповідь така: ми робимо важкий додаток з 10+ конструкторами, і я думаю, що було б набагато смішніше, якби кожен конструктор міг «розумно» впоратися з цією помилкою, не кидаючи повідомлення про помилки на консоль.


2
або просто киньте помилку, коли погано вивчіть стек-трек, і ви зможете виправити код
храповик урод

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-shim правильно?

Можливо, вас також зацікавлять альтернативні схеми 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;
};

Так правда - але вся магія тут - через Object.create. Що щодо попереднього ES5? ES5-Shim Object.createвказаний як ДУБІОЗНАЙ.
treecoder

@greengit, якщо ви прочитаєте його ще раз, ES5 shim заявляє, що другий аргумент для Object.create є ДУБІОЗАЦІЙНИМ. перший - добре.
Райнос

1
Так, я це прочитав. І я думаю (ЯКЩО) вони використовують якусь__proto__ річ, тоді ми все ще в тій же самій точці. Тому що перед ES5 немає простішого способу мутувати прототип. Але все одно ваше рішення здається найелегантнішим і перспективнішим. +1 для цього. (мій ліміт голосування досягнуто)
treecoder

1
Дякую @psr. І @Raynos, твоя Object.createshim - це майже моє третє рішення, але менш складне і краще виглядає, ніж моє, звичайно.
treecoder

37

Давайте будемо просто. Я хочу уникати префіксації нового оператора кожного разу, коли я викликаю конструктор у JavaScript. Це тому, що я, як правило, це забуваю, і мій код погано викручується.

Очевидною відповіддю було б не забути newключове слово.

Ви змінюєте структуру та значення мови.

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


9
+1 Мабуть, дивним чином не розбирати мову про шкідливі звички кодування. Безумовно, що політика підсвічування / реєстрації синтаксису може призвести до уникнення шаблонів / помилок, сприйнятливих до помилок.
Стоїть

Якби я знайшов бібліотеку, де всі конструктори не хвилювались, використовували ви її newчи ні, я вважаю, що це більш рентабельне.
Джек

@Jack, але це знайде набагато важче і тонкіше знайти помилки. Просто огляньте всі помилки, спричинені JavaScript "не дбайливо", якщо ви включите ;оператори до кінця. (Автоматичне введення
крапки з

@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 як 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' */

EDIT: я забув додати:

"Якщо ваш додаток по-справжньому" важкий ", останнє, що ви хочете, - це якийсь перекритий механізм побудови, щоб уповільнити його"

Я абсолютно згоден - коли створюється "річ" вище без "нового" ключового слова, це повільніше / важче, ніж з ним. Помилки - твій друг, тому що вони говорять тобі, що не так. Більше того, вони кажуть вашим колегам-розробникам що вони роблять не так.


Винахід значення для відсутніх аргументів конструктора схильний до помилок. Якщо вони відсутні, залиште властивості невизначеними. Якщо вони суттєві, киньте помилку, якщо вони відсутні. Що робити, якщо prop1 повинен був бути bool? Тестування "ні" на правдивість стане джерелом помилок.
JBRWilkinson
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.