Unikanie nowego operatora w JavaScript - lepszy sposób

16

Ostrzeżenie: To jest długi post.

Uprośćmy to. Chcę uniknąć konieczności dodawania nowego operatora za każdym razem, gdy wywołuję konstruktor w JavaScript. Jest tak, ponieważ zwykle zapominam o tym, a mój kod źle się psuje.

Prostym sposobem na obejście tego jest ...

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

  // do your stuff...
}

Ale potrzebuję tego, aby zaakceptować zmienną nr. takich argumentów ...

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

Pierwszym natychmiastowym rozwiązaniem wydaje się być metoda „zastosuj” w ten sposób ...

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

  // do your stuff
}

Jest to NIEPRAWIDŁOWE - nowy obiekt jest przekazywany do applymetody, a NIE do naszego konstruktora arguments.callee.

Teraz wymyśliłem trzy rozwiązania. Moje proste pytanie brzmi: które wydaje się najlepsze. Lub, jeśli masz lepszą metodę, powiedz to.

Po pierwsze - użyj eval()do dynamicznego tworzenia kodu JavaScript, który wywołuje konstruktor.

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...
}

Po drugie - każdy obiekt ma __proto__właściwość, która jest „tajnym” łączem do jego obiektu prototypowego. Na szczęście ta właściwość jest zapisywalna.

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;
}

Po trzecie - jest to coś podobnego do drugiego rozwiązania.

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 rozwiązanie wydaje się niezdarne i wiąże się ze wszystkimi problemami „ewolucji zła”.

  • __proto__ rozwiązanie jest niestandardowe, a „Great Browser of mIsERY” go nie honoruje.

  • Trzecie rozwiązanie wydaje się zbyt skomplikowane.

Ale dzięki wszystkim powyższym trzem rozwiązaniom możemy zrobić coś takiego, czego nie możemy inaczej ...

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

Tak skutecznie powyższe rozwiązania dają nam „prawdziwe” konstruktory, które akceptują zmienną nr. argumentów i nie wymagają new. Jakie jest twoje zdanie na ten temat?

-- AKTUALIZACJA --

Niektórzy powiedzieli „po prostu wyrzuć błąd”. Moja odpowiedź brzmi: robimy ciężką aplikację z ponad 10 konstruktorami i myślę, że byłoby znacznie bardziej przydatne, gdyby każdy konstruktor mógł „mądrze” poradzić sobie z tym błędem bez wyświetlania komunikatów o błędach na konsoli.

treecoder
źródło
2
lub po prostu wyrzuć błąd, gdy jest zły, sprawdź stacktrace i możesz naprawić kod
maniak ratchet
2
Myślę, że to pytanie lepiej byłoby zadać w przypadku przepełnienia stosu lub recenzji kodu . Wydaje się, że jest to raczej pytanie skoncentrowane na kodzie niż pytanie koncepcyjne.
Adam Lear
1
@greengit zamiast rzucać błąd użyj jslint. Będzie cię ostrzec, jeśli nie Make()bez newponieważ Marka jest aktywowane i dlatego zakłada, że jest to konstruktor
Raynos
1
Więc czekaj - szukasz lepszego sposobu na osiągnięcie tego, czy po prostu kogoś, kto da ci kod, abyś mógł tworzyć obiekty z argumentami zmiennymi bez new? Ponieważ jeśli to jest to drugie, prawdopodobnie pytasz na niewłaściwej stronie. Jeśli jest to pierwsze, możesz nie odrzucić sugestii dotyczących korzystania z nowych i wykrywania błędów tak szybko ... Jeśli Twoja aplikacja jest naprawdę „ciężka”, ostatnią rzeczą, jakiej chcesz, jest jakiś nadmierny mechanizm konstrukcyjny, który ją spowolni. new, dla całego tego, co dostaje, jest dość szybkie.
Shog9
5
Jak na ironię, próba „inteligentnego” radzenia sobie z błędami programisty jest sama odpowiedzialna za wiele „złych części” JavaScript.
Daniel Pratt

Odpowiedzi:

19

Po pierwsze arguments.calleejest przestarzałe w ES5 strict, więc go nie używamy. Prawdziwe rozwiązanie jest raczej proste.

W ogóle nie używasz new.

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

To właściwy ból w dupie, prawda?

Próbować 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);
  }
}

Teraz oczywiście wymaga to ES5, ale wszyscy używają podkładki ES5, prawda?

Możesz być także zainteresowany alternatywnymi wzorami OO js

Na marginesie możesz zastąpić opcję drugą

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

  return that;
}

Jeśli chcesz mieć własną Object.createpodkładkę ES5, jest to naprawdę łatwe

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
Raynos
źródło
Tak, prawda - ale cała ta magia jest spowodowana Object.create. Co z wersją wcześniejszą niż ES5? ES5-Shim podaje się Object.createjako WĄTPLIWE.
treekoder
@greengit, jeśli przeczytasz go ponownie, shim ES5 podaje drugi argument Object.create jest DUBIOUS. pierwszy jest w porządku.
Raynos,
1
Tak, przeczytałem to. I myślę, że (JEŚLI) używają __proto__tam czegoś, to wciąż jesteśmy w tym samym punkcie. Ponieważ przed ES5 nie ma łatwiejszego sposobu mutowania prototypu. W każdym razie Twoje rozwiązanie wydaje się najbardziej eleganckie i wybiegające w przyszłość. +1 za to. (mój limit głosowania został osiągnięty)
treecoder
1
Dzięki @psr. I @Raynos twój Object.createpodkładka to właściwie moje trzecie rozwiązanie, ale mniej skomplikowane i lepiej wyglądające niż moje.
treekoder
37

Uprośćmy to. Chcę uniknąć konieczności dodawania nowego operatora za każdym razem, gdy wywołuję konstruktor w JavaScript. Jest tak, ponieważ zwykle zapominam o tym, a mój kod źle się psuje.

Oczywistą odpowiedzią byłoby, nie zapomnij new słowa kluczowego.

Zmieniasz strukturę i znaczenie języka.

Co, moim zdaniem, i ze względu na przyszłych opiekunów twojego kodu, to okropny pomysł.

CaffGeek
źródło
9
+1 Dziwne wydaje się kłótnie języka z powodu złych nawyków programistycznych. Z pewnością niektóre zasady podświetlania / sprawdzania składni mogą wymuszać unikanie podatnych na błędy wzorców / prawdopodobnych literówek.
Stoive,
Gdybym znalazł bibliotekę, w której wszystkie konstruktory nie dbałyby o to, czy ją użyłeś, newczy nie, uważałbym to za łatwiejsze do utrzymania.
Jack
@Jack, ale wprowadzi znacznie trudniejsze i subtelniejsze sposoby na znalezienie błędów. Wystarczy rozejrzeć się po wszystkich błędach spowodowanych przez javascript „nie dbający”, jeśli uwzględni ;się instrukcje zakończenia. (Automatyczne wstawianie
średnika
@CaffGeek „JavaScript nie dba o średniki” nie jest dokładny - zdarzają się przypadki, gdy użycie średnika i nieużywanie go jest nieco inne, i są przypadki, w których nie są. To jest problem. Rozwiązanie przedstawione w przyjętej odpowiedzi jest dokładnie odwrotne - z nim we wszystkich przypadkach użycie newlub nie jest semantycznie identyczne . Nie żadne subtelne przypadki, w których to niezmienna jest zepsuty. Dlatego jest dobry i dlaczego chcesz go używać.
Jack
14

Najłatwiejszym rozwiązaniem jest zapamiętanie newi zgłoszenie błędu, aby było oczywiste, że zapomniałeś.

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

Cokolwiek robisz, nie używaj eval. Odwróciłbym się od korzystania z niestandardowych właściwości, na przykład __proto__dlatego, że są one niestandardowe, a ich funkcjonalność może ulec zmianie.

Josh K.
źródło
Zdecydowanie unikać .__proto__to diabeł
Raynos
3

Napisałem o tym post. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

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

Możesz nawet go uogólnić, abyś nie musiał dodawać tego na szczycie każdego konstruktora. Możesz to zobaczyć odwiedzając mój post

Oświadczenie Nie używam tego w moim kodzie, opublikowałem go tylko dla wartości dydaktycznej. Przekonałem się, newże łatwo zauważyć błąd. Podobnie jak inni, nie sądzę, że tak naprawdę potrzebujemy tego w przypadku większości kodów. Chyba że piszesz bibliotekę do tworzenia dziedziczenia JS, w takim przypadku możesz użyć jednego miejsca i już będziesz używać innego podejścia niż dziedziczenie bezpośrednie.

Juan Mendes
źródło
Potencjalnie ukryty błąd: jeśli mam, var x = new Ctor();a potem mam X tak jak thisi robię var y = Ctor();, to nie działałoby zgodnie z oczekiwaniami.
luiscubal
@luiscubal Nie wiesz, co mówisz „później masz x jako this”, czy możesz opublikować jsfiddle, aby pokazać potencjalny problem?
Juan Mendes
Twój kod jest nieco bardziej niezawodny, niż początkowo myślałem, ale udało mi się wymyślić (nieco skomplikowany, ale wciąż aktualny) przykład: jsfiddle.net/JHNcR/1
luiscubal
@luiscubal Widzę twój punkt widzenia, ale to naprawdę jest zawiłe. Zakładasz, że możesz zadzwonić Ctor.call(ctorInstance, 'value'). Nie widzę prawidłowego scenariusza dla tego, co robisz. Aby zbudować obiekt, użyj var x = new Ctor(val)albo var y=Ctor(val). Nawet jeśli istniał prawidłowy scenariusz, moim twierdzeniem jest, że możesz mieć konstruktory bez użycia new Ctor, nie że możesz mieć konstruktory, które działają przy użyciu Ctor.callZobacz jsfiddle.net/JHNcR/2
Juan Mendes
0

Co powiesz na to?

/* 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' */

EDYCJA: Zapomniałem dodać:

„Jeśli twoja aplikacja jest naprawdę„ ciężka ”, ostatnią rzeczą, jakiej chcesz, jest jakiś przekręcony mechanizm konstrukcyjny, który ją spowolni”

Absolutnie się zgadzam - podczas tworzenia „rzeczy” powyżej bez słowa kluczowego „nowy” jest wolniejszy / cięższy niż przy nim. Błędy są twoim przyjacielem, ponieważ mówią ci, co jest nie tak. Co więcej, mówią swoich kolegów programistów co oni robi źle.

enkoder
źródło
Tworzenie wartości dla brakujących argumentów konstruktora jest podatne na błędy. Jeśli ich brakuje, pozostaw właściwości niezdefiniowane. Jeśli są niezbędne, zgłoś błąd, jeśli ich brakuje. Co jeśli prop1 miałby być bool? Testowanie „nie” na prawdziwość będzie źródłem błędów.
JBRWilkinson