Menghindari operator baru dalam JavaScript - cara yang lebih baik

16

Peringatan: Ini adalah posting yang panjang.

Mari kita tetap sederhana. Saya ingin menghindari awalan operator baru setiap kali saya memanggil konstruktor dalam JavaScript. Ini karena saya cenderung melupakannya, dan kode saya rusak parah.

Cara sederhana sekitar ini adalah ini ...

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

  // do your stuff...
}

Tapi, saya perlu ini untuk menerima variabel no. argumen, seperti ini ...

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

Solusi langsung pertama tampaknya menjadi metode 'terapkan' seperti ini ...

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

  // do your stuff
}

Namun ini SALAH - objek baru diteruskan ke applymetode dan BUKAN ke konstruktor kami arguments.callee.

Sekarang, saya telah menghasilkan tiga solusi. Pertanyaan sederhana saya adalah: mana yang tampaknya terbaik. Atau, jika Anda memiliki metode yang lebih baik, katakan itu.

Pertama - gunakan eval()untuk secara dinamis membuat kode JavaScript yang memanggil 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...
}

Kedua - Setiap objek memiliki __proto__properti yang merupakan tautan 'rahasia' ke objek prototipe-nya. Untungnya properti ini dapat ditulisi.

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

Ketiga - Ini adalah sesuatu yang mirip dengan solusi kedua.

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 solusi tampaknya kikuk dan dilengkapi dengan semua masalah "eval jahat".

  • __proto__ solusi tidak standar dan "Peramban Hebat mIsERY" tidak menghormatinya.

  • Solusi ketiga tampaknya terlalu rumit.

Tetapi dengan ketiga solusi di atas, kita dapat melakukan sesuatu seperti ini, bahwa kita tidak dapat sebaliknya ...

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

Jadi secara efektif solusi di atas memberi kita konstruktor "benar" yang menerima variabel no. argumen dan tidak perlu new. Apa pendapat Anda tentang ini.

- PEMBARUAN -

Beberapa mengatakan "just throw a error". Tanggapan saya adalah: kami sedang melakukan aplikasi berat dengan 10+ konstruktor dan saya pikir itu akan jauh lebih rumit jika setiap konstruktor dapat "dengan cerdas" menangani kesalahan itu tanpa melemparkan pesan kesalahan pada konsol.

treecoder
sumber
2
atau hanya melempar kesalahan ketika itu buruk memeriksa stacktrace dan Anda dapat memperbaiki kode
ratchet freak
2
Saya pikir pertanyaan ini akan lebih baik ditanyakan pada Stack Overflow atau Code Review . Tampaknya cukup kode-sentris daripada pertanyaan konseptual.
Adam Lear
1
@ Greeng bukannya melempar kesalahan menggunakan jslint. Ini akan memperingatkan Anda jika Anda melakukannya Make()tanpa newkarena Make dikapitalisasi dan dengan demikian ia menganggap itu adalah konstruktor
Raynos
1
Jadi tunggu - apakah Anda mencari cara yang lebih baik untuk mencapai ini, atau Anda hanya mencari seseorang untuk memberi Anda kode sehingga Anda dapat membuat objek argumen-variabel tanpa new? Karena jika yang terakhir, Anda mungkin bertanya di situs yang salah. Jika yang pertama, Anda mungkin tidak ingin mengabaikan saran tentang menggunakan kesalahan baru dan mendeteksi begitu cepat ... Jika aplikasi Anda benar-benar "berat", hal terakhir yang Anda inginkan adalah beberapa mekanisme konstruksi yang terlalu padat untuk memperlambatnya. new, untuk semua kekurangan yang didapat, cukup cepat.
Shog9
5
Ironisnya, mencoba untuk 'pintar' menangani kesalahan programmer itu sendiri bertanggung jawab untuk banyak 'bagian buruk' dari JavaScript.
Daniel Pratt

Jawaban:

19

Pertama arguments.calleetidak lagi digunakan dalam ES5, jadi kami tidak menggunakannya. Solusi sebenarnya agak sederhana.

Kamu tidak menggunakan new sama sekali.

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

Itu rasa sakit yang tepat di pantat kan?

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

Sekarang tentu saja ini membutuhkan ES5, tetapi semua orang menggunakan ES5-shim bukan?

Anda mungkin juga tertarik pola js OO alternatif

Sebagai tambahan Anda dapat mengganti opsi dua dengan

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

  return that;
}

Jika Anda ingin ES5 Object.createshim Anda sendiri maka itu sangat mudah

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
Raynos
sumber
Ya benar - tetapi semua keajaiban di sini adalah karena Object.create. Bagaimana dengan pre ES5? ES5-Shim terdaftar Object.createsebagai DUBIOUS.
treecoder
@reengit jika Anda membacanya lagi, ES5 shim menyatakan argumen kedua ke Object.create adalah DUBIOUS. yang pertama baik-baik saja.
Raynos
1
Ya saya memang membacanya. Dan saya pikir (JIKA) mereka menggunakan __proto__sesuatu di sana, maka kita masih pada titik yang sama. Karena sebelum ES5 tidak ada cara yang lebih mudah untuk bermutasi prototipe. Tapi bagaimanapun, solusi Anda tampaknya paling elegan dan berwawasan ke depan. +1 untuk itu. (batas pemungutan suara saya tercapai)
treecoder
1
Terima kasih @ ps. Dan @Raynos Object.createshim Anda cukup banyak solusi ketiga saya tetapi tidak terlalu rumit dan lebih tampan daripada milik saya tentu saja.
treecoder
37

Mari kita tetap sederhana. Saya ingin menghindari awalan operator baru setiap kali saya memanggil konstruktor dalam JavaScript. Ini karena saya cenderung melupakannya, dan kode saya rusak parah.

Jawaban yang jelas adalah jangan lupa new kata kunci.

Anda mengubah struktur dan makna bahasa.

Yang, menurut pendapat saya, dan demi pengelola kode Anda di masa mendatang, adalah ide yang mengerikan.

CaffGeek
sumber
9
+1 Tampaknya aneh untuk bertengkar bahasa di sekitar kebiasaan pengkodean buruk seseorang. Tentunya beberapa sintaks penyorotan / kebijakan check-in dapat memberlakukan penghindaran pola rawan bug / kemungkinan kesalahan ketik.
Stoive
Jika saya menemukan perpustakaan di mana semua konstruktor tidak peduli apakah Anda menggunakan newatau tidak, saya akan menemukan itu lebih dapat dikelola.
Jack
@ Jack, tetapi itu akan memperkenalkan lebih keras dan lebih halus untuk menemukan bug. Lihat saja semua bug yang disebabkan oleh javascript "tidak peduli" jika Anda menyertakan ;pernyataan akhir. (Penyisipan Titik Koma Otomatis)
CaffGeek
@CaffGeek "Javascript tidak peduli dengan titik koma" tidak akurat - ada kasus di mana menggunakan titik koma dan tidak menggunakannya agak berbeda, dan ada kasus di mana mereka tidak. Itulah masalahnya. Solusi yang disajikan dalam jawaban yang diterima justru sebaliknya - dengan itu, dalam semua kasus menggunakan newatau tidak identik secara semantik . Ada yang tidak ada kasus halus di mana invarian ini rusak. Itu sebabnya bagus dan mengapa Anda ingin menggunakannya.
Jack
14

Solusi termudah adalah dengan hanya mengingat newdan melempar kesalahan agar jelas Anda lupa.

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

Apa pun yang Anda lakukan, jangan gunakan eval. Saya akan menghindar dari menggunakan properti non-standar seperti __proto__khusus karena mereka non-standar dan fungsinya dapat berubah.

Josh K.
sumber
Yang pasti menghindarinya .__proto__adalah iblis
Raynos
3

Saya sebenarnya menulis posting tentang ini. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

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

Dan Anda bahkan dapat menggeneralisasikannya sehingga Anda tidak perlu menambahkannya ke atas setiap konstruktor. Anda dapat melihatnya dengan mengunjungi posting saya

Penafian Saya tidak menggunakan ini dalam kode saya, saya hanya mempostingnya untuk nilai didaktik. Saya menemukan bahwa melupakan newadalah bug yang mudah dikenali. Seperti yang lain, saya tidak berpikir kita benar-benar membutuhkan ini untuk sebagian besar kode. Kecuali jika Anda menulis perpustakaan untuk membuat warisan JS, dalam hal ini Anda bisa menggunakan dari satu tempat dan Anda sudah akan menggunakan pendekatan yang berbeda dari warisan lurus ke depan.

Juan Mendes
sumber
Bug yang berpotensi tersembunyi: jika saya memiliki var x = new Ctor();dan kemudian memiliki x sebagai thisdan lakukan var y = Ctor();, ini tidak akan berperilaku seperti yang diharapkan.
luiscubal
@luiscubal Tidak yakin apa yang Anda katakan "nanti punya x sebagai this", bisakah Anda mengirim jsfiddle untuk menunjukkan masalah potensial?
Juan Mendes
Kode Anda sedikit lebih kuat dari yang saya pikir, tapi aku berhasil datang dengan (agak berbelit-belit tapi masih berlaku) contoh: jsfiddle.net/JHNcR/1
luiscubal
@luiscubal Saya mengerti maksud Anda, tetapi itu benar-benar berbelit-belit. Anda berasumsi tidak apa-apa menelepon Ctor.call(ctorInstance, 'value'). Saya tidak melihat skenario yang valid untuk apa yang Anda lakukan. Untuk membangun objek, Anda dapat menggunakan var x = new Ctor(val)atau var y=Ctor(val). Bahkan jika ada skenario yang valid, klaim saya adalah bahwa Anda dapat memiliki konstruktor tanpa menggunakan new Ctor, bukan bahwa Anda dapat memiliki konstruktor yang berfungsi menggunakan Ctor.callSee jsfiddle.net/JHNcR/2
Juan Mendes
0

Bagaimana dengan ini?

/* 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: Saya lupa menambahkan:

"Jika aplikasi Anda benar-benar 'berat', hal terakhir yang Anda inginkan adalah mekanisme konstruksi yang terlalu padat untuk memperlambatnya"

Saya benar-benar setuju - ketika membuat 'barang' di atas tanpa kata kunci 'baru', itu lebih lambat / lebih berat daripada dengan itu. Kesalahan adalah teman Anda, karena mereka memberi tahu Anda apa yang salah. Selain itu, mereka memberi tahu sesama pengembang Anda apa yang mereka lakukan salah.

pembuat enkode
sumber
Menciptakan nilai untuk argumen konstruktor yang hilang adalah rawan kesalahan. Jika hilang, biarkan properti tidak terdefinisi. Jika itu penting, buat kesalahan jika hilang. Bagaimana jika prop1 seharusnya bool? Menguji "tidak" untuk kebenaran akan menjadi sumber kesalahan.
JBRWilkinson