Evitando um novo operador em JavaScript - a melhor maneira


16

Aviso: Este é um post longo.

Vamos simplificar. Quero evitar ter que prefixar o novo operador toda vez que chamo um construtor em JavaScript. Isso ocorre porque costumo esquecê-lo e meu código estraga tudo.

A maneira simples de contornar isso é isso ...

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

  // do your stuff...
}

Mas, eu preciso disso para aceitar a variável no. de argumentos, assim ...

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

A primeira solução imediata parece ser o método 'apply' como este ...

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

  // do your stuff
}

No entanto, isso está errado - o novo objeto é passado para o applymétodo e NÃO para o nosso construtor arguments.callee.

Agora, eu vim com três soluções. Minha pergunta simples é: qual parece melhor. Ou, se você tiver um método melhor, informe.

Primeiro - use eval()para criar dinamicamente o código JavaScript que chama o construtor.

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

Segundo - Todo objeto tem __proto__propriedade que é um link 'secreto' para o seu objeto protótipo. Felizmente, essa propriedade é gravável.

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

Terceiro - isso é algo semelhante à segunda solução.

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 solução parece desajeitada e vem com todos os problemas de "avaliação ruim".

  • __proto__ solução não é padrão e o "Grande Navegador da Misericórdia" não a honra.

  • A terceira solução parece excessivamente complicada.

Mas com todas as três soluções acima, podemos fazer algo assim, que não podemos de outra forma ...

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

Tão efetivamente as soluções acima nos dão construtores "verdadeiros" que aceitam a variável no. de argumentos e não exige new. Qual é a sua opinião sobre isso.

- ATUALIZAÇÃO -

Alguns disseram "apenas jogue um erro". Minha resposta é: estamos fazendo um aplicativo pesado com mais de 10 construtores e acho que seria muito mais difícil se todos os construtores pudessem "inteligentemente" lidar com esse erro sem lançar mensagens de erro no console.


2
ou apenas lançar um erro quando é ruim examinar o stacktrace e você pode corrigir o código
aberração catraca

2
Eu acho que essa pergunta seria melhor feita no Stack Overflow ou Code Review . Parece ser bastante centrado no código, e não uma questão conceitual.
Adam Lear

1
@greengit em vez de gerar um erro, use um jslint. Ele irá avisá-lo se você fez Make()sem newdevido Make é capitalizado e, portanto, assume que é um construtor
Raynos

1
Então espere - você está procurando uma maneira melhor de fazer isso, ou está apenas procurando alguém para fornecer código para que você possa ter a criação de objeto de argumento variável sem new? Porque se for o último, você provavelmente está perguntando no site errado. Se for o primeiro, convém não ignorar sugestões sobre o uso de novos e detectar erros tão rapidamente ... Se seu aplicativo é realmente "pesado", a última coisa que você quer é um mecanismo de construção exagerado para abrandá-lo. new, por todo o flack que obtém, é bem rápido.
Shog9

5
Ironicamente, tentar lidar de maneira inteligente com os erros do programador é responsável por muitas das "partes ruins" do JavaScript.
Daniel Pratt

Respostas:


19

Em primeiro lugar, arguments.calleeé obsoleto no ES5 estrito, portanto não o usamos. A solução real é bastante simples.

Você não usa newnada.

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

Isso é um pé no saco certo?

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

Agora é claro que isso requer o ES5, mas todo mundo usa o calço ES5, certo?

Você também pode estar interessado em padrões alternativos de js OO

Como um aparte, você pode substituir a opção dois por

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

  return that;
}

Caso você queira seu próprio Object.createcalço ES5 , é realmente fácil

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};

Sim verdade - mas toda a magia aqui é por causa disso Object.create. E o pré ES5? ES5-Shim lista Object.createcomo DUBIOUS.
treecoder

@greengit Se você ler novamente, o ES5 shim indica o segundo argumento para Object.create é DUBIOUS. o primeiro está bem.
9119 Raynos

1
Sim, eu li isso. E eu acho que (se) eles estão usando algum tipo de __proto__coisa lá, então ainda estamos no mesmo ponto. Como o pré ES5 não existe uma maneira mais fácil de alterar o protótipo. De qualquer forma, sua solução parece mais elegante e prospectiva. +1 para isso. (meu limite de votação é atingido)
treecoder

1
Obrigado @psr. E @Raynos, seu Object.createcalço é praticamente minha terceira solução, mas menos complicada e com melhor aparência do que a minha, é claro.
treecoder

37

Vamos simplificar. Quero evitar ter que prefixar o novo operador toda vez que chamo um construtor em JavaScript. Isso ocorre porque costumo esquecê-lo e meu código estraga tudo.

A resposta óbvia seria não esquecer a newpalavra - chave.

Você está mudando a estrutura e o significado da linguagem.

O que, na minha opinião, e pelo bem dos futuros mantenedores do seu código, é uma ideia horrível.


9
+1 Parece estranho entender um idioma em torno dos maus hábitos de codificação. Certamente, algumas políticas de destaque / check-in de sintaxe podem impor a evitação de padrões propensos a erros / prováveis ​​erros de digitação.
Stoive

Se eu encontrasse uma biblioteca em que todos os construtores não se importassem se você usava newou não, consideraria isso mais sustentável.
22415 Jack

@ Jack, mas vai apresentar muito mais difícil e sutil para encontrar bugs. Basta olhar em volta para ver todos os erros causados ​​pelo javascript "não se importando" se você incluir as ;instruções to end. (Inserção automática de ponto e vírgula)
CaffGeek

@CaffGeek "O Javascript não se importa com ponto e vírgula" não é exato - há casos em que usar um ponto e vírgula e não usá-lo é sutilmente diferente, e há casos em que não são. Esse é o problema. A solução apresentada na resposta aceita é precisamente o oposto - com ela, em todos os casos, usar newou não é semanticamente idêntico . Não são nenhum caso sutis onde este invariante é quebrado. É por isso que é bom e por que você deseja usá-lo.
23315 Jack

14

A solução mais fácil é apenas lembrar newe lançar um erro para tornar óbvio que você esqueceu.

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

Faça o que fizer, não use eval. Eu evitaria usar propriedades não padrão, como __proto__especificamente porque elas não são padrão e sua funcionalidade pode mudar.


Desafiadoramente evitar .__proto__é o diabo
Raynos

3

Na verdade, eu escrevi um post sobre isso. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

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

E você pode até generalizá-lo para não precisar adicioná-lo ao topo de todo construtor. Você pode ver isso visitando meu post

Isenção de responsabilidade Eu não uso isso no meu código, apenas o publiquei pelo valor didático. Descobri que esquecer um newé um bug fácil de detectar. Como outros, acho que não precisamos disso para a maioria dos códigos. A menos que você esteja escrevendo uma biblioteca para criar herança JS, nesse caso, você poderia usar de um único local e já estaria usando uma abordagem diferente da herança direta.


Bug potencialmente oculto: se eu tiver var x = new Ctor();e depois tiver x como thise var y = Ctor();, isso não se comportaria como esperado.
precisa saber é o seguinte

@luiscubal Não sabe ao certo o que está dizendo "depois, x como this", você pode postar um jsfiddle para mostrar o problema em potencial?
Juan Mendes

Seu código é um pouco mais robusto do que eu pensava inicialmente, mas consegui criar um exemplo (um tanto complicado, mas ainda válido): jsfiddle.net/JHNcR/1
luiscubal 29/13/13 /

@luiscubal Entendo o seu ponto, mas é realmente complicado. Você está assumindo que não há problema em ligar Ctor.call(ctorInstance, 'value'). Não vejo um cenário válido para o que você está fazendo. Para construir um objeto, use var x = new Ctor(val)ou var y=Ctor(val). Mesmo que houvesse um cenário válido, minha afirmação é de que você pode ter construtores sem usar new Ctor, não que possa ter construtores que trabalhem usando Ctor.callVeja jsfiddle.net/JHNcR/2
Juan Mendes

0

Que tal agora?

/* 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: esqueci de adicionar:

"Se seu aplicativo é realmente 'pesado', a última coisa que você deseja é um mecanismo de construção exagerado para abrandá-lo"

Eu concordo absolutamente - ao criar 'coisa' acima sem a palavra-chave 'nova', ela é mais lenta / pesada do que com ela. Os erros são seus amigos, porque eles dizem o que há de errado. Além disso, eles dizem aos seus colegas desenvolvedores o que estão fazendo de errado.


Inventar valores para argumentos de construtores ausentes é suscetível a erros. Se estiverem ausentes, deixe as propriedades indefinidas. Se forem essenciais, gere um erro se estiverem faltando. E se prop1 fosse booleano? Testar "não" à veracidade será uma fonte de erros.
JBRWilkinson
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.