Tránh toán tử mới trong JavaScript - cách tốt hơn


16

Cảnh báo: Đây là một bài viết dài.

Hãy giữ nó đơn giản. Tôi muốn tránh phải tiền tố toán tử mới mỗi lần tôi gọi hàm tạo trong JavaScript. Điều này là do tôi có xu hướng quên nó, và mã của tôi rất tệ.

Cách đơn giản xung quanh đây là ...

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

  // do your stuff...
}

Nhưng, tôi cần điều này để chấp nhận biến không. tranh luận, như thế này ...

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

Giải pháp tức thời đầu tiên dường như là phương pháp 'áp dụng' như thế này ...

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

  // do your stuff
}

Tuy nhiên, đây là SAI - đối tượng mới được truyền cho applyphương thức và KHÔNG cho hàm tạo của chúng ta arguments.callee.

Bây giờ, tôi đã đưa ra ba giải pháp. Câu hỏi đơn giản của tôi là: cái nào có vẻ tốt nhất. Hoặc, nếu bạn có một phương pháp tốt hơn, hãy nói với nó.

Đầu tiên - sử dụng eval()để tự động tạo mã JavaScript gọi hàm tạo.

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

Thứ hai - Mọi đối tượng đều có thuộc __proto__tính là liên kết 'bí mật' với đối tượng nguyên mẫu. May mắn là tài sản này là có thể ghi.

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

Thứ ba - Đây là một cái gì đó tương tự như giải pháp thứ hai.

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 Giải pháp có vẻ vụng về và đi kèm với tất cả các vấn đề của "tệ nạn xấu xa".

  • __proto__ giải pháp là không chuẩn và "Trình duyệt tuyệt vời của mIsERY" không tôn trọng nó.

  • Giải pháp thứ ba có vẻ quá phức tạp.

Nhưng với tất cả ba giải pháp trên, chúng ta có thể làm một cái gì đó như thế này, mà chúng ta không thể ...

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

Vì vậy, hiệu quả của các giải pháp trên cung cấp cho chúng tôi các hàm tạo "thực" chấp nhận biến số không. tranh luận và không yêu cầu new. Những gì bạn có về điều này.

- CẬP NHẬT -

Một số người đã nói "chỉ cần ném một lỗi". Câu trả lời của tôi là: chúng tôi đang thực hiện một ứng dụng nặng với hơn 10 nhà xây dựng và tôi nghĩ sẽ hiệu quả hơn nhiều nếu mọi nhà xây dựng có thể "xử lý" một cách thông minh lỗi đó mà không ném thông báo lỗi trên bảng điều khiển.


2
hoặc chỉ đưa ra một lỗi khi kiểm tra lỗi stacktrace và bạn có thể sửa mã
ratchet freak

2
Tôi nghĩ rằng câu hỏi này sẽ được hỏi tốt hơn về Stack Overflow hoặc Code Review . Nó có vẻ là mã trung tâm khá hơn là một câu hỏi khái niệm.
Adam Lear

1
@greengit thay vì ném lỗi sử dụng jslint. Nó sẽ cảnh báo bạn nếu bạn đã làm Make()mà không có newchữ viết hoa và do đó, nó cho rằng đó là một công cụ xây dựng
Raynos

1
Vì vậy, chờ đợi - bạn đang tìm kiếm một cách tốt hơn để thực hiện điều này, hoặc bạn chỉ đang tìm ai đó để cung cấp cho bạn mã để bạn có thể tạo đối tượng biến đối số mà không cần new? Bởi vì nếu đó là cái sau, có lẽ bạn đang hỏi sai trang web. Nếu đó là trước đây, bạn có thể không muốn loại bỏ các đề xuất liên quan đến việc sử dụng mới và phát hiện lỗi quá nhanh ... Nếu ứng dụng của bạn thực sự "nặng", điều cuối cùng bạn muốn là một số cơ chế xây dựng quá mức để làm chậm nó. new, đối với tất cả các flack nó nhận được, là khá nhanh chóng.
Shog9

5
Trớ trêu thay, cố gắng 'thông minh' xử lý các lỗi lập trình viên lại tự chịu trách nhiệm cho nhiều 'phần xấu' của JavaScript.
Daniel Pratt

Câu trả lời:


19

Đầu tiên arguments.calleelà không được chấp nhận trong ES5 vì vậy chúng tôi không sử dụng nó. Giải pháp thực sự khá đơn giản.

Bạn hoàn toàn không sử dụng new.

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

Đó là một cơn đau đúng ở mông phải không?

Thử 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);
  }
}

Bây giờ tất nhiên điều này đòi hỏi ES5, nhưng mọi người đều sử dụng ES5-shim phải không?

Bạn cũng có thể quan tâm đến các mẫu OO js thay thế

Khi sang một bên, bạn có thể thay thế tùy chọn hai bằng

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

  return that;
}

Trong trường hợp bạn muốn Object.createshim ES5 của riêng mình thì thật dễ dàng

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

Đúng vậy - nhưng tất cả các phép thuật ở đây là vì Object.create. Còn ES5 trước thì sao? ES5-Shim liệt kê Object.createlà DUBIOUS.
treecoder

@greengit nếu bạn đọc lại, ES5 shim nêu đối số thứ hai cho Object.create là DUBIOUS. cái đầu tiên là tốt
Raynos

1
Có tôi đã đọc nó. Và tôi nghĩ (NẾU) họ đang sử dụng một số __proto__thứ ở đó, sau đó chúng ta vẫn ở cùng một điểm. Bởi vì trước ES5 KHÔNG có cách nào dễ dàng hơn để đột biến nguyên mẫu. Nhưng dù sao, giải pháp của bạn có vẻ thanh lịch và hướng tới tương lai. +1 cho điều đó. (đạt đến giới hạn bỏ phiếu của tôi)
treecoder

1
Cảm ơn @psr. Và @Raynos Object.createshim của bạn là khá nhiều giải pháp thứ ba của tôi nhưng ít phức tạp hơn và tìm kiếm tốt hơn so với tôi tất nhiên.
treecoder

36

Hãy giữ nó đơn giản. Tôi muốn tránh phải tiền tố toán tử mới mỗi lần tôi gọi hàm tạo trong JavaScript. Điều này là do tôi có xu hướng quên nó, và mã của tôi rất tệ.

Câu trả lời rõ ràng là đừng quên newtừ khóa.

Bạn đang thay đổi cấu trúc và ý nghĩa của ngôn ngữ.

Mà theo tôi, và vì lợi ích của những người duy trì tương lai của mã của bạn, là một ý tưởng khủng khiếp.


9
+1 Có vẻ kỳ quặc khi đánh vần một ngôn ngữ xung quanh thói quen mã hóa xấu của một người. Chắc chắn một số chính sách tô sáng / kiểm tra cú pháp có thể thực thi việc tránh các mẫu dễ bị lỗi / lỗi chính tả có thể xảy ra.
Stoive

Nếu tôi tìm thấy một thư viện nơi tất cả các nhà xây dựng không quan tâm bạn có sử dụng newhay không, tôi sẽ thấy nó dễ bảo trì hơn .
Jack

@Jack, nhưng nó sẽ giới thiệu khó hơn và tinh vi hơn để tìm lỗi. Chỉ cần nhìn xung quanh tất cả các lỗi gây ra bởi javascript "không quan tâm" nếu bạn bao gồm các ;câu lệnh kết thúc. (Chèn dấu chấm phẩy tự động)
CaffGeek

@CaffGeek "Javascript không quan tâm đến dấu chấm phẩy" không chính xác - có những trường hợp sử dụng dấu chấm phẩy và không sử dụng nó khác nhau một cách tinh tế và có những trường hợp chúng không có. Đó chính là vấn đề. Giải pháp được trình bày trong câu trả lời được chấp nhận hoàn toàn ngược lại - với nó, trong mọi trường hợp sử dụng newhay không giống nhau về mặt ngữ nghĩa . Có không có trường hợp tinh tế nơi bất biến này bị phá vỡ. Đó là lý do tại sao nó tốt và tại sao bạn muốn sử dụng nó.
Jack

14

Giải pháp dễ nhất là chỉ cần nhớ newvà đưa ra một lỗi để làm cho nó rõ ràng bạn đã quên.

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

Dù bạn làm gì, đừng sử dụng eval. Tôi sẽ ngại sử dụng các thuộc tính không chuẩn như __proto__cụ thể vì chúng không chuẩn và chức năng của chúng có thể thay đổi.


Chắc chắn tránh .__proto__nó là ác quỷ
Raynos

3

Tôi thực sự đã viết một bài về điều này. http://js-bits.blogspot.com/2010/08/constructor-without-USE-new.html

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

Và bạn thậm chí có thể khái quát hóa nó để bạn không phải thêm nó vào đầu mỗi nhà xây dựng. Bạn có thể thấy điều đó bằng cách truy cập bài viết của tôi

Tuyên bố miễn trừ trách nhiệm Tôi không sử dụng mã này trong mã của mình, tôi chỉ đăng nó với giá trị mô phạm. Tôi thấy rằng quên một newlà một lỗi dễ dàng để phát hiện. Giống như những người khác, tôi không nghĩ rằng chúng ta thực sự cần điều này cho hầu hết các mã. Trừ khi bạn đang viết thư viện để tạo kế thừa JS, trong trường hợp đó bạn có thể sử dụng từ một nơi duy nhất và bạn sẽ sử dụng một cách tiếp cận khác với kế thừa thẳng.


Lỗi tiềm ẩn: nếu tôi có var x = new Ctor();và sau đó có x như thisvà làm var y = Ctor();, điều này sẽ không hoạt động như mong đợi.
luiscubal

@luiscubal Không chắc chắn những gì bạn đang nói "sau này có x là this", bạn có thể đăng một jsfiddle để hiển thị vấn đề tiềm ẩn không?
Juan Mendes

Mã của bạn mạnh hơn một chút so với tôi nghĩ ban đầu, nhưng tôi đã cố gắng đưa ra một ví dụ (hơi phức tạp nhưng vẫn hợp lệ): jsfiddle.net/JHNcR/1
luiscubal

@luiscubal Tôi thấy quan điểm của bạn nhưng nó thực sự là một điểm khó hiểu. Bạn đang cho rằng nó ổn để gọi Ctor.call(ctorInstance, 'value'). Tôi không thấy một kịch bản hợp lệ cho những gì bạn đang làm. Để xây dựng một đối tượng, bạn có thể sử dụng var x = new Ctor(val)hoặc var y=Ctor(val). Ngay cả khi có một kịch bản hợp lệ, yêu cầu của tôi là bạn có thể có các hàm tạo mà không cần sử dụng new Ctor, không phải là bạn có thể có các hàm tạo hoạt động bằng Ctor.callSee jsfiddle.net/JHNcR/2
Juan Mendes

0

Còn cái này thì sao?

/* 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: Tôi quên thêm:

"Nếu ứng dụng của bạn thực sự 'nặng', điều cuối cùng bạn muốn là một số cơ chế xây dựng quá mức để làm chậm nó"

Tôi hoàn toàn đồng ý - khi tạo 'điều' ở trên mà không có từ khóa 'mới', nó chậm / nặng hơn so với từ khóa. Lỗi là bạn của bạn, vì họ nói với bạn những gì sai. Hơn nữa, họ nói với các nhà phát triển đồng nghiệp của bạn những gì họ đang làm sai.


Phát minh giá trị cho các đối số hàm tạo bị thiếu là dễ bị lỗi. Nếu chúng bị thiếu, hãy để các thuộc tính không xác định. Nếu chúng cần thiết, hãy đưa ra lỗi nếu chúng bị thiếu. Điều gì xảy ra nếu prop1 được coi là bool? Kiểm tra "không" cho sự trung thực sẽ là một nguồn lỗi.
JBRWilkinson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.