Làm thế nào để có thể đúng cách mà người dùng tạo ra một đối tượng tùy chỉnh trong JavaScript?


471

Tôi tự hỏi về cách tốt nhất để tạo một đối tượng JavaScript có các thuộc tính và phương thức.

Tôi đã thấy các ví dụ nơi người được sử dụng var self = thisvà sau đó sử dụng self.trong tất cả các chức năng để đảm bảo phạm vi luôn chính xác.

Sau đó, tôi đã thấy các ví dụ về việc sử dụng .prototypeđể thêm thuộc tính, trong khi những người khác thực hiện nội tuyến.

Ai đó có thể cho tôi một ví dụ thích hợp về một đối tượng JavaScript với một số thuộc tính và phương thức không?


13
Không có cách "tốt nhất".
Triptych

Không phải là selfmột từ dành riêng? Nếu không, nó nên được; vì selflà một biến được xác định trước đề cập đến cửa sổ hiện tại. self === window
Shaz

2
@Shaz: đó không phải là một từ dành riêng nhiều hơn các thuộc tính khác windowtrong Mô hình Đối tượng Trình duyệt như documenthoặc frames; bạn chắc chắn có thể sử dụng lại mã định danh làm tên biến. Mặc dù, yeah, theo phong cách tôi thích var that= thisđể tránh bất kỳ sự nhầm lẫn có thể. Mặc dù window.selfcuối cùng là vô nghĩa vì vậy hiếm khi có bất kỳ lý do để chạm vào nó.
bobince

7
Khi JS được thu nhỏ, việc gán thischo một biến cục bộ (ví dụ self) làm giảm kích thước tệp.
Patrick Fisher

Classjs liên kết mới: github.com/divio/ classjs
Nikola

Câu trả lời:


889

Có hai mô hình để triển khai các lớp và các thể hiện trong JavaScript: cách tạo mẫu và cách đóng. Cả hai đều có ưu điểm và nhược điểm, và có rất nhiều biến thể mở rộng. Nhiều lập trình viên và thư viện có các cách tiếp cận khác nhau và các hàm tiện ích xử lý lớp để viết lên một số phần xấu nhất của ngôn ngữ.

Kết quả là trong công ty hỗn hợp, bạn sẽ có một mớ hỗn độn của metaclass, tất cả đều cư xử hơi khác nhau. Tệ hơn nữa, hầu hết các tài liệu hướng dẫn JavaScript là khủng khiếp và phục vụ một số loại thỏa hiệp giữa để bao quát tất cả các cơ sở, khiến bạn rất bối rối. (Có lẽ tác giả cũng nhầm lẫn. Mô hình đối tượng của JavaScript rất khác với hầu hết các ngôn ngữ lập trình và ở nhiều nơi được thiết kế tồi.)

Hãy bắt đầu với cách nguyên mẫu . Đây là bản gốc JavaScript nhất mà bạn có thể nhận được: có tối thiểu mã trên không và instanceof sẽ hoạt động với các phiên bản của loại đối tượng này.

function Shape(x, y) {
    this.x= x;
    this.y= y;
}

Chúng ta có thể thêm các phương thức vào thể hiện được tạo bằng new Shapecách viết chúng vào phần prototypetra cứu của hàm tạo này:

Shape.prototype.toString= function() {
    return 'Shape at '+this.x+', '+this.y;
};

Bây giờ để phân lớp nó, nhiều như bạn có thể gọi JavaScript là phân lớp nào. Chúng tôi làm điều đó bằng cách thay thế hoàn toàn prototypetài sản ma thuật kỳ lạ đó :

function Circle(x, y, r) {
    Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
    this.r= r;
}
Circle.prototype= new Shape();

trước khi thêm các phương thức vào nó:

Circle.prototype.toString= function() {
    return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}

Ví dụ này sẽ hoạt động và bạn sẽ thấy mã giống như nó trong nhiều hướng dẫn. Nhưng bạn ơi, điều đó new Shape()thật xấu xí: chúng ta đang tạo ra lớp cơ sở mặc dù không có hình dạng thực tế nào được tạo ra. Nó xảy ra để hoạt động trong trường hợp đơn giản này vì JavaScript rất cẩu thả: nó cho phép không có đối số nào được truyền vào, trong trường hợp này xytrở thành undefinedvà được gán cho nguyên mẫu this.xthis.y. Nếu hàm constructor đang làm bất cứ điều gì phức tạp hơn, nó sẽ nằm thẳng trên mặt của nó.

Vì vậy, những gì chúng ta cần làm là tìm cách tạo một đối tượng nguyên mẫu có chứa các phương thức và các thành viên khác mà chúng ta muốn ở cấp độ lớp, mà không gọi hàm xây dựng của lớp cơ sở. Để làm điều này, chúng ta sẽ phải bắt đầu viết mã trợ giúp. Đây là cách tiếp cận đơn giản nhất mà tôi biết:

function subclassOf(base) {
    _subclassOf.prototype= base.prototype;
    return new _subclassOf();
}
function _subclassOf() {};

Điều này chuyển các thành viên của lớp cơ sở trong nguyên mẫu của nó sang một hàm xây dựng mới mà không làm gì cả, sau đó sử dụng hàm tạo đó. Bây giờ chúng ta có thể viết đơn giản:

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.prototype= subclassOf(Shape);

thay vì new Shape()sai Bây giờ chúng ta có một tập hợp nguyên thủy chấp nhận được cho các lớp được xây dựng.

Có một vài tinh chỉnh và tiện ích mở rộng mà chúng ta có thể xem xét theo mô hình này. Ví dụ ở đây là một phiên bản đường cú pháp:

Function.prototype.subclass= function(base) {
    var c= Function.prototype.subclass.nonconstructor;
    c.prototype= base.prototype;
    this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};

...

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.subclass(Shape);

Cả hai phiên bản đều có nhược điểm là chức năng của hàm tạo không thể được kế thừa, vì nó có trong nhiều ngôn ngữ. Vì vậy, ngay cả khi lớp con của bạn không thêm gì vào quá trình xây dựng, nó vẫn phải nhớ gọi hàm tạo cơ sở với bất kỳ đối số nào mà cơ sở muốn. Điều này có thể được tự động hóa một chút bằng cách sử dụng apply, nhưng bạn vẫn phải viết ra:

function Point() {
    Shape.apply(this, arguments);
}
Point.subclass(Shape);

Vì vậy, một phần mở rộng phổ biến là phá vỡ các công cụ khởi tạo thành chức năng của chính nó chứ không phải là chính hàm tạo. Hàm này sau đó có thể kế thừa từ cơ sở tốt:

function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};

function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!

Bây giờ chúng ta vừa có cùng một hàm khởi tạo hàm cho mỗi lớp. Có lẽ chúng ta có thể chuyển nó ra chức năng của trình trợ giúp riêng của mình để chúng ta không phải tiếp tục gõ nó, ví dụ thay vì Function.prototype.subclassxoay tròn và để Hàm của lớp cơ sở nhổ ra các lớp con:

Function.prototype.makeSubclass= function() {
    function Class() {
        if ('_init' in this)
            this._init.apply(this, arguments);
    }
    Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
    Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
    return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};

...

Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};

Point= Shape.makeSubclass();

Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
    Shape.prototype._init.call(this, x, y);
    this.r= r;
};

... bắt đầu trông hơi giống các ngôn ngữ khác, mặc dù với cú pháp hơi vụng về. Bạn có thể rắc thêm một vài tính năng nếu bạn thích. Có thể bạn muốn makeSubclasslấy và nhớ một tên lớp và cung cấp một mặc định toStringbằng cách sử dụng nó. Có thể bạn muốn làm cho hàm tạo phát hiện khi nó vô tình được gọi mà không có newtoán tử (điều này thường sẽ dẫn đến việc gỡ lỗi rất khó chịu):

Function.prototype.makeSubclass= function() {
    function Class() {
        if (!(this instanceof Class))
            throw('Constructor called without "new"');
        ...

Có lẽ bạn muốn vượt qua tất cả các thành viên mới và đã makeSubclassthêm họ vào nguyên mẫu, để tiết kiệm bạn phải viết Class.prototype...khá nhiều. Rất nhiều hệ thống lớp làm điều đó, ví dụ:

Circle= Shape.makeSubclass({
    _init: function(x, y, z) {
        Shape.prototype._init.call(this, x, y);
        this.r= r;
    },
    ...
});

Có rất nhiều tính năng tiềm năng mà bạn có thể xem là mong muốn trong một hệ thống đối tượng và không ai thực sự đồng ý với một công thức cụ thể.


Cách đóng cửa , sau đó. Điều này tránh được các vấn đề về thừa kế dựa trên nguyên mẫu của JavaScript, bằng cách không sử dụng tính kế thừa nào cả. Thay thế:

function Shape(x, y) {
    var that= this;

    this.x= x;
    this.y= y;

    this.toString= function() {
        return 'Shape at '+that.x+', '+that.y;
    };
}

function Circle(x, y, r) {
    var that= this;

    Shape.call(this, x, y);
    this.r= r;

    var _baseToString= this.toString;
    this.toString= function() {
        return 'Circular '+_baseToString(that)+' with radius '+that.r;
    };
};

var mycircle= new Circle();

Bây giờ mỗi phiên bản đơn lẻ Shapesẽ có bản sao riêng của toStringphương thức (và bất kỳ phương thức nào khác hoặc các thành viên lớp khác mà chúng tôi thêm vào).

Điều tệ hại về mọi trường hợp có bản sao riêng của từng thành viên trong lớp là nó kém hiệu quả hơn. Nếu bạn đang xử lý một số lượng lớn các thể hiện được phân lớp, kế thừa nguyên mẫu có thể phục vụ bạn tốt hơn. Ngoài ra, việc gọi một phương thức của lớp cơ sở là hơi khó chịu như bạn có thể thấy: chúng ta phải nhớ phương thức đó là gì trước khi hàm tạo của lớp con ghi đè lên nó, hoặc nó bị mất.

[Ngoài ra vì không có quyền thừa kế ở đây, instanceofnhà điều hành sẽ không làm việc; bạn sẽ phải cung cấp cơ chế riêng để đánh hơi lớp nếu bạn cần. Mặc dù bạn có thể đánh lừa các đối tượng nguyên mẫu theo cách tương tự như với kế thừa nguyên mẫu, nhưng nó hơi khó và không thực sự đáng để instanceoflàm việc.]

Điểm hay của mọi cá thể có phương thức riêng là phương thức đó có thể bị ràng buộc với thể hiện cụ thể sở hữu nó. Điều này hữu ích vì cách ràng buộc kỳ lạ của JavaScript thistrong các cuộc gọi phương thức, có kết quả là nếu bạn tách một phương thức khỏi chủ sở hữu của nó:

var ts= mycircle.toString;
alert(ts());

sau đó thisbên trong phương thức sẽ không phải là đối tượng Circle như mong đợi (nó thực sự sẽ là windowđối tượng toàn cầu , gây ra sự cố gỡ lỗi rộng rãi). Trong thực tế này thường xảy ra khi một phương pháp được thực hiện và giao cho một setTimeout, onclickhoặc EventListenernói chung.

Với cách thức nguyên mẫu, bạn phải bao gồm một bao đóng cho mỗi lần gán như vậy:

setTimeout(function() {
    mycircle.move(1, 1);
}, 1000);

hoặc, trong tương lai (hoặc bây giờ nếu bạn hack Function.prototype), bạn cũng có thể làm điều đó với function.bind():

setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);

nếu các thể hiện của bạn được thực hiện theo cách đóng, ràng buộc được thực hiện miễn phí bằng cách đóng trên biến đối tượng (thường được gọi thathoặc self, mặc dù cá nhân tôi sẽ khuyên chống lại cái sau vì selfđã có nghĩa khác, trong JavaScript). 1, 1Mặc dù vậy, bạn không nhận được các đối số trong đoạn trích trên miễn phí, vì vậy bạn vẫn sẽ cần một lần đóng cửa khác hoặc bind()nếu bạn cần làm điều đó.

Có rất nhiều biến thể về phương pháp đóng cửa quá. Bạn có thể muốn bỏ qua thishoàn toàn, tạo một cái mới thatvà trả lại nó thay vì sử dụng newtoán tử:

function Shape(x, y) {
    var that= {};

    that.x= x;
    that.y= y;

    that.toString= function() {
        return 'Shape at '+that.x+', '+that.y;
    };

    return that;
}

function Circle(x, y, r) {
    var that= Shape(x, y);

    that.r= r;

    var _baseToString= that.toString;
    that.toString= function() {
        return 'Circular '+_baseToString(that)+' with radius '+r;
    };

    return that;
};

var mycircle= Circle(); // you can include `new` if you want but it won't do anything

Cách nào là đúng cách? Cả hai. Tốt nhất"? Điều đó phụ thuộc vào tình hình của bạn. FWIW Tôi có xu hướng tạo nguyên mẫu cho kế thừa JavaScript thực sự khi tôi đang thực hiện công cụ OO mạnh mẽ và đóng cửa cho các hiệu ứng trang bỏ đi đơn giản.

Nhưng cả hai cách đều khá phản trực quan đối với hầu hết các lập trình viên. Cả hai đều có nhiều biến thể lộn xộn tiềm năng. Bạn sẽ gặp cả hai (cũng như nhiều sơ đồ ở giữa và thường bị hỏng) nếu bạn sử dụng mã / thư viện của người khác. Không có ai trả lời chung chung. Chào mừng bạn đến với thế giới tuyệt vời của các đối tượng JavaScript.

[Đây là phần 94 của Tại sao JavaScript không phải là ngôn ngữ lập trình yêu thích của tôi.]


13
Bước dần dần rất đẹp từ "lớp" def đến đối tượng khởi tạo. Và liên lạc tốt đẹp về bỏ qua new.
Lưỡi liềm tươi

8
Có vẻ như JavaScript không phải là ngôn ngữ yêu thích của bạn vì bạn muốn sử dụng nó như thể nó có các lớp.
Jonathan Feinberg

59
Tất nhiên tôi cũng vậy, tất cả mọi người cũng vậy: mô hình lớp và cá thể là mô hình tự nhiên hơn đối với phần lớn các vấn đề phổ biến mà các lập trình viên gặp phải hiện nay. Tôi đồng ý rằng, trên cơ sở lý thuyết, kế thừa dựa trên nguyên mẫu có thể có khả năng cung cấp một cách làm việc linh hoạt hơn, nhưng JavaScript hoàn toàn không thực hiện được lời hứa đó. Hệ thống chức năng xây dựng cồng kềnh của nó mang đến cho chúng ta điều tồi tệ nhất của cả hai thế giới, khiến cho việc thừa kế giống như giai cấp trở nên khó khăn trong khi không cung cấp bất kỳ nguyên mẫu linh hoạt hay đơn giản nào. Nói tóm lại, đó là poo.
bobince

4
Bob Tôi nghĩ rằng đây là một câu trả lời tuyệt vời - Tôi đã vật lộn với hai mẫu này trong một thời gian và tôi nghĩ rằng bạn đã mã hóa một cái gì đó chính xác hơn một Resig và giải thích với cái nhìn sâu sắc hơn Crockford. Tôi không thể nghĩ đến lời khen nào cao hơn ....
James Westgate

4
Dường như đối với tôi, việc lập mô hình kế thừa cổ điển trên các ngôn ngữ nguyên mẫu như javascript là một chốt vuông và lỗ tròn. Có những lúc điều này thực sự cần thiết hay đây chỉ là một cách để mọi người ham nắm bắt ngôn ngữ theo cách họ muốn chứ không chỉ đơn giản là sử dụng ngôn ngữ cho nó là gì?
slf

90

Tôi sử dụng mô hình này khá thường xuyên - Tôi thấy rằng nó mang lại cho tôi một sự linh hoạt khá lớn khi tôi cần nó. Trong sử dụng, nó khá giống với các lớp kiểu Java.

var Foo = function()
{

    var privateStaticMethod = function() {};
    var privateStaticVariable = "foo";

    var constructor = function Foo(foo, bar)
    {
        var privateMethod = function() {};
        this.publicMethod = function() {};
    };

    constructor.publicStaticMethod = function() {};

    return constructor;
}();

Điều này sử dụng một hàm ẩn danh được gọi khi tạo, trả về một hàm xây dựng mới. Vì hàm ẩn danh chỉ được gọi một lần, nên bạn có thể tạo các biến tĩnh riêng trong nó (chúng nằm trong bao đóng, hiển thị cho các thành viên khác của lớp). Hàm constructor về cơ bản là một đối tượng Javascript tiêu chuẩn - bạn xác định các thuộc tính riêng bên trong nó và các thuộc tính công khai được gắn vào thisbiến.

Về cơ bản, cách tiếp cận này kết hợp cách tiếp cận Crockfordian với các đối tượng Javascript tiêu chuẩn để tạo ra một lớp mạnh hơn.

Bạn có thể sử dụng nó giống như bất kỳ đối tượng Javascript nào khác:

Foo.publicStaticMethod(); //calling a static method
var test = new Foo();     //instantiation
test.publicMethod();      //calling a method

4
Điều đó có vẻ thú vị, bởi vì nó khá gần với "sân nhà" của tôi, đó là C #. Tôi cũng nghĩ rằng tôi bắt đầu hiểu tại sao privateStaticVariable thực sự riêng tư (vì nó được định nghĩa trong phạm vi của một hàm và được duy trì miễn là có tham chiếu đến nó?)
Michael Stum

Vì nó không sử dụng thisnên nó vẫn cần được khởi tạo với new?
Jordan Parmer

Trên thực tế, this không được sử dụng trong constructorhàm, mà trở thành Footrong ví dụ.
ShZ

4
Vấn đề ở đây là mọi đối tượng đều có bản sao riêng của tất cả các chức năng riêng tư và công cộng.
virtualnobi

2
@virtualnobi: Mẫu này không ngăn bạn viết các phương thức protytpe : constructor.prototype.myMethod = function () { ... }.
Nicolas Le Thierry d'Enơcanh

25

Douglas Crockford thảo luận rộng rãi về chủ đề đó trong Phần tốt . Ông khuyến nghị nên tránh toán tử mới để tạo các đối tượng mới. Thay vào đó, ông đề xuất để tạo ra các nhà xây dựng tùy chỉnh. Ví dụ:

var mammal = function (spec) {     
   var that = {}; 
   that.get_name = function (  ) { 
      return spec.name; 
   }; 
   that.says = function (  ) { 
      return spec.saying || ''; 
   }; 
   return that; 
}; 

var myMammal = mammal({name: 'Herb'});

Trong Javascript, một hàm là một đối tượng và có thể được sử dụng để xây dựng các đối tượng cùng với toán tử mới . Theo quy ước, các hàm dự định sẽ được sử dụng như các hàm tạo bắt đầu bằng chữ in hoa. Bạn thường thấy những thứ như:

function Person() {
   this.name = "John";
   return this;
}

var person = new Person();
alert("name: " + person.name);**

Trong trường hợp bạn quên sử dụng toán tử mới trong khi khởi tạo một đối tượng mới, những gì bạn nhận được là một cuộc gọi chức năng thông thường và điều này được ràng buộc với đối tượng toàn cầu thay vì đối tượng mới.


5
Là tôi hay tôi nghĩ Crockford hoàn toàn vô nghĩa với việc anh ta bash nhà điều hành mới?
trung gian ometiciev

3
@meder: Không chỉ bạn. Ít nhất, tôi nghĩ không có gì sai với nhà điều hành mới. Và có một tiềm ẩn newtrong var that = {};anyway.
Tim Down

17
Crockford là một ông già cáu kỉnh và tôi không đồng ý với anh ta nhiều, nhưng ít nhất anh ta đang thúc đẩy việc xem xét nghiêm túc về JavaScript và thật đáng để nghe những gì anh ta nói.
bobince

2
@bobince: Đồng ý. Bài viết của ông về việc đóng cửa đã mở mắt cho tôi rất nhiều thứ khoảng 5 năm trước, và ông khuyến khích một cách tiếp cận chu đáo.
Tim xuống

20
Tôi đồng ý với Crockford. Vấn đề với toán tử mới là JavaScript sẽ làm cho bối cảnh của "cái này" rất khác so với khi gọi hàm khác. Mặc dù quy ước trường hợp phù hợp, có những vấn đề phát sinh trên cơ sở mã lớn hơn khi các nhà phát triển quên sử dụng mới, quên viết hoa, v.v. Để thực dụng, bạn có thể làm mọi thứ bạn cần làm mà không cần từ khóa mới - vậy tại sao nên sử dụng nó và giới thiệu nhiều điểm thất bại trong mã? JS là một nguyên mẫu, không dựa trên lớp, ngôn ngữ. Vậy tại sao chúng ta muốn nó hoạt động như một ngôn ngữ gõ tĩnh? Tôi chắc chắn không.
Joshua Ramirez

13

Tiếp tục câu trả lời của bobince

Trong es6 bây giờ bạn thực sự có thể tạo một class

Vì vậy, bây giờ bạn có thể làm:

class Shape {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    toString() {
        return `Shape at ${this.x}, ${this.y}`;
    }
}

Vì vậy, mở rộng thành một vòng tròn (như trong câu trả lời khác) bạn có thể làm:

class Circle extends Shape {
    constructor(x, y, r) {
        super(x, y);
        this.r = r;
    }

    toString() {
        let shapeString = super.toString();
        return `Circular ${shapeString} with radius ${this.r}`;
    }
}

Kết thúc sạch hơn một chút trong es6 và dễ đọc hơn một chút.


Đây là một ví dụ tốt về nó trong hành động:


6

Bạn cũng có thể làm theo cách này, sử dụng các cấu trúc:

function createCounter () {
    var count = 0;

    return {
        increaseBy: function(nb) {
            count += nb;
        },
        reset: function {
            count = 0;
        }
    }
}

Sau đó :

var counter1 = createCounter();
counter1.increaseBy(4);

6
Tôi không thích cách đó vì khoảng trắng rất quan trọng. Kiểu xoăn sau khi trả về phải nằm trên cùng một dòng để tương thích với nhiều trình duyệt.
geowa4

5

Một cách khác sẽ là http://jsfiddle.net/nnUY4/ (tôi không biết nếu loại xử lý này tạo và hiển thị các hàm theo bất kỳ mẫu cụ thể nào)

// Build-Reveal

var person={
create:function(_name){ // 'constructor'
                        //  prevents direct instantiation 
                        //  but no inheritance
    return (function() {

        var name=_name||"defaultname";  // private variable

        // [some private functions]

        function getName(){
            return name;
        }

        function setName(_name){
            name=_name;
        }

        return {    // revealed functions
            getName:getName,    
            setName:setName
        }
    })();
   }
  }

  // … no (instantiated) person so far …

  var p=person.create(); // name will be set to 'defaultname'
  p.setName("adam");        // and overwritten
  var p2=person.create("eva"); // or provide 'constructor parameters'
  alert(p.getName()+":"+p2.getName()); // alerts "adam:eva"

4

Khi một người sử dụng thủ thuật đóng "cái này" trong khi gọi hàm tạo, nó sẽ viết một hàm có thể được sử dụng như một cuộc gọi lại bởi một đối tượng khác không muốn gọi một phương thức trên một đối tượng. Nó không liên quan đến "làm cho phạm vi chính xác".

Đây là một đối tượng JavaScript vanilla:

function MyThing(aParam) {
    var myPrivateVariable = "squizzitch";

    this.someProperty = aParam;
    this.useMeAsACallback = function() {
        console.log("Look, I have access to " + myPrivateVariable + "!");
    }
}

// Every MyThing will get this method for free:
MyThing.prototype.someMethod = function() {
    console.log(this.someProperty);
};

Bạn có thể nhận được rất nhiều từ việc đọc những gì Douglas Crockford nói về JavaScript. John Resig cũng rực rỡ. Chúc may mắn!


1
Uh, đóng xung quanh thiscó mọi thứ để làm với "làm cho phạm vi chính xác".
Roatin Marth

3
Jonathan nói đúng. Phạm vi của chức năng js là bất cứ điều gì bạn thiết kế. Tự = thủ thuật này là một cách để buộc nó vào trường hợp cụ thể để nó không thay đổi khi được gọi trong ngữ cảnh khác. Nhưng đôi khi đó là những gì bạn thực sự muốn. Phụ thuộc vào bối cảnh.
Marco

Tôi nghĩ rằng tất cả các bạn đang nói điều tương tự thực sự. self=thismặc dù không bắt buộc thisphải kiên trì, dễ dàng cho phép phạm vi "chính xác" thông qua việc đóng cửa.
Lưỡi liềm tươi

2
Lý do bạn làm điều đó = điều này là để cấp cho các hàm lồng nhau quyền truy cập vào phạm vi của điều này vì nó tồn tại trong hàm tạo. Khi các hàm lồng nhau nằm bên trong các hàm tạo, phạm vi "này" của chúng sẽ trở lại phạm vi toàn cục.
Joshua Ramirez

4

Closurelà đa năng. bobince đã tóm tắt tốt nguyên mẫu so với các phương pháp đóng khi tạo đối tượng. Tuy nhiên, bạn có thể bắt chước một số khía cạnh của OOPviệc sử dụng bao đóng theo cách lập trình chức năng. Các hàm nhớ là các đối tượng trong JavaScript ; vì vậy sử dụng chức năng như đối tượng theo một cách khác.

Đây là một ví dụ về việc đóng cửa:

function outer(outerArg) {
    return inner(innerArg) {
        return innerArg + outerArg; //the scope chain is composed of innerArg and outerArg from the outer context 
    }
}

Cách đây một thời gian, tôi đã xem qua bài viết của Mozilla về Đóng cửa. Đây là điều khiến tôi chú ý: "Việc đóng cửa cho phép bạn liên kết một số dữ liệu (môi trường) với một chức năng hoạt động trên dữ liệu đó. Điều này tương đồng với lập trình hướng đối tượng, trong đó các đối tượng cho phép chúng ta liên kết một số dữ liệu (thuộc tính của đối tượng ) với một hoặc nhiều phương thức ". Đó là lần đầu tiên tôi đọc song song giữa đóng cửa và OOP cổ điển mà không liên quan đến nguyên mẫu.

Làm sao?

Giả sử bạn muốn tính thuế VAT của một số mặt hàng. Thuế VAT có khả năng duy trì ổn định trong suốt thời gian sử dụng ứng dụng. Một cách để làm điều đó trong OOP (mã giả):

public class Calculator {
    public property VAT { get; private set; }
    public Calculator(int vat) {
        this.VAT = vat;
    }
    public int Calculate(int price) {
        return price * this.VAT;
    }
}

Về cơ bản, bạn chuyển một giá trị VAT vào hàm tạo của bạn và phương thức tính toán của bạn có thể hoạt động dựa trên giá trị đó thông qua việc đóng . Bây giờ thay vì sử dụng một lớp / hàm tạo, hãy chuyển VAT của bạn làm đối số vào một hàm. Bởi vì điều duy nhất bạn quan tâm là chính phép tính, trả về một hàm mới, là phương thức tính toán:

function calculator(vat) {
    return function(item) {
        return item * vat;
    }
}
var calculate = calculator(1.10);
var jsBook = 100; //100$
calculate(jsBook); //110

Trong dự án của bạn, hãy xác định các giá trị cấp cao là ứng cử viên tốt để tính thuế VAT. Như một quy tắc tự nhiên bất cứ khi nào bạn vượt qua các đối số tương tự trên và trên, có một cách để cải thiện nó bằng cách sử dụng bao đóng. Không cần tạo đối tượng truyền thống.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures


3

Tạo một đối tượng

Cách dễ nhất để tạo một đối tượng trong JavaScript là sử dụng cú pháp sau:

var test = {
  a : 5,
  b : 10,
  f : function(c) {
    return this.a + this.b + c;
  }
}

console.log(test);
console.log(test.f(3));

Điều này hoạt động tuyệt vời để lưu trữ dữ liệu theo cách có cấu trúc.

Tuy nhiên, đối với các trường hợp sử dụng phức tạp hơn, tốt hơn là tạo các thể hiện của hàm:

function Test(a, b) {
  this.a = a;
  this.b = b;
  this.f = function(c) {
return this.a + this.b + c;
  };
}

var test = new Test(5, 10);
console.log(test);
console.log(test.f(3));

Điều này cho phép bạn tạo nhiều đối tượng có chung "bản thiết kế", tương tự như cách bạn sử dụng các lớp trong ví dụ. Java.

Điều này vẫn có thể được thực hiện hiệu quả hơn, tuy nhiên, bằng cách sử dụng một nguyên mẫu.

Bất cứ khi nào các phiên bản khác nhau của hàm chia sẻ cùng một phương thức hoặc thuộc tính, bạn có thể di chuyển chúng đến nguyên mẫu của đối tượng đó. Theo cách đó, mọi phiên bản của hàm đều có quyền truy cập vào phương thức hoặc thuộc tính đó, nhưng nó không cần phải được sao chép cho mọi trường hợp.

Trong trường hợp của chúng tôi, việc chuyển phương thức fsang nguyên mẫu là hợp lý:

function Test(a, b) {
  this.a = a;
  this.b = b;
}

Test.prototype.f = function(c) {
  return this.a + this.b + c;
};

var test = new Test(5, 10);
console.log(test);
console.log(test.f(3));

Di sản

Một cách đơn giản nhưng hiệu quả để thực hiện kế thừa trong JavaScript, là sử dụng hai lớp lót sau:

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

Điều đó tương tự như làm điều này:

B.prototype = new A();

Sự khác biệt chính giữa cả hai là hàm tạo của Akhông chạy khi sử dụng Object.create, nó trực quan hơn và giống với kế thừa dựa trên lớp hơn.

Bạn luôn có thể chọn chạy tùy chọn hàm Atạo khi tạo phiên bản mới Bbằng cách thêm nó vào hàm tạo của B:

function B(arg1, arg2) {
    A(arg1, arg2); // This is optional
}

Nếu bạn muốn chuyển tất cả các đối số của Bsang A, bạn cũng có thể sử dụng Function.prototype.apply():

function B() {
    A.apply(this, arguments); // This is optional
}

Nếu bạn muốn trộn một đối tượng khác vào chuỗi hàm tạo B, bạn có thể kết hợp Object.createvới Object.assign:

B.prototype = Object.assign(Object.create(A.prototype), mixin.prototype);
B.prototype.constructor = B;

Bản giới thiệu

function A(name) {
  this.name = name;
}

A.prototype = Object.create(Object.prototype);
A.prototype.constructor = A;

function B() {
  A.apply(this, arguments);
  this.street = "Downing Street 10";
}

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function mixin() {

}

mixin.prototype = Object.create(Object.prototype);
mixin.prototype.constructor = mixin;

mixin.prototype.getProperties = function() {
  return {
    name: this.name,
    address: this.street,
    year: this.year
  };
};

function C() {
  B.apply(this, arguments);
  this.year = "2018"
}

C.prototype = Object.assign(Object.create(B.prototype), mixin.prototype);
C.prototype.constructor = C;

var instance = new C("Frank");
console.log(instance);
console.log(instance.getProperties());


Ghi chú

Object.createcó thể được sử dụng an toàn trong mọi trình duyệt hiện đại, bao gồm IE9 +. Object.assignkhông hoạt động trong bất kỳ phiên bản IE cũng như một số trình duyệt di động. Bạn nên sử dụng polyfill Object.create và / hoặc Object.assignnếu bạn muốn sử dụng chúng và hỗ trợ các trình duyệt không triển khai chúng.

Bạn có thể tìm thấy một polyfill cho Object.create ở đây và một cho Object.assign ở đây .


0
var Person = function (lastname, age, job){
this.name = name;
this.age = age;
this.job = job;
this.changeName = function(name){
this.lastname = name;
}
}
var myWorker = new Person('Adeola', 23, 'Web Developer');
myWorker.changeName('Timmy');

console.log("New Worker" + myWorker.lastname);

4
Điều gì thêm vào nhiều câu trả lời mở rộng đã được cung cấp?
blm

Tôi thích câu trả lời này vì nó ngắn gọn và hiển thị ba phần của việc thực hiện: 1) Xác định đối tượng, 2) Khởi tạo một thể hiện của đối tượng, 3) Sử dụng ví dụ - nó hiển thị tất cả trong nháy mắt thay vì phân tích cú pháp thông qua tất cả các câu trả lời dài dòng ở trên (tất nhiên, tất cả đều là những câu trả lời cực kỳ tốt với tất cả các chi tiết thích hợp mà người ta muốn) - loại tóm tắt đơn giản ở đây
G-Man

0

Ngoài câu trả lời được chấp nhận từ năm 2009. Nếu bạn có thể nhắm mục tiêu các trình duyệt hiện đại, người ta có thể sử dụng Object.defineProperty .

Phương thức Object.defineProperty () định nghĩa một thuộc tính mới trực tiếp trên một đối tượng hoặc sửa đổi một thuộc tính hiện có trên một đối tượng và trả về đối tượng. Nguồn: Mozilla

var Foo = (function () {
    function Foo() {
        this._bar = false;
    }
    Object.defineProperty(Foo.prototype, "bar", {
        get: function () {
            return this._bar;
        },
        set: function (theBar) {
            this._bar = theBar;
        },
        enumerable: true,
        configurable: true
    });
    Foo.prototype.toTest = function () {
        alert("my value is " + this.bar);
    };
    return Foo;
}());

// test instance
var test = new Foo();
test.bar = true;
test.toTest();

Để xem danh sách tương thích trên máy tính để bàn và thiết bị di động, hãy xem danh sách Tương thích trình duyệt của Mozilla . Có, IE9 + hỗ trợ nó cũng như Safari di động.


0

Bạn cũng có thể thử điều này

    function Person(obj) {
    'use strict';
    if (typeof obj === "undefined") {
        this.name = "Bob";
        this.age = 32;
        this.company = "Facebook";
    } else {
        this.name = obj.name;
        this.age = obj.age;
        this.company = obj.company;
    }

}

Person.prototype.print = function () {
    'use strict';
    console.log("Name: " + this.name + " Age : " + this.age + " Company : " + this.company);
};

var p1 = new Person({name: "Alex", age: 23, company: "Google"});
p1.print();

0
Một mô hình phục vụ tôi tốt
var Klass = function Klass() {
    var thus = this;
    var somePublicVariable = x
      , somePublicVariable2 = x
      ;
    var somePrivateVariable = x
      , somePrivateVariable2 = x
      ;

    var privateMethod = (function p() {...}).bind(this);

    function publicMethod() {...}

    // export precepts
    this.var1 = somePublicVariable;
    this.method = publicMethod;

    return this;
};

Trước tiên, bạn có thể thay đổi tùy chọn thêm phương thức vào thể hiện thay vì prototypeđối tượng của hàm tạo . Tôi hầu như luôn luôn khai báo các phương thức bên trong hàm tạo bởi vì tôi sử dụng Chiếm quyền xây dựng rất thường xuyên cho các mục đích liên quan đến Kế thừa & Trang trí.

Đây là cách tôi quyết định nơi mà tuyên bố được viết:

  • Không bao giờ khai báo một phương thức trực tiếp trên đối tượng bối cảnh (this )
  • Hãy để các vartuyên bố được ưu tiên hơnfunction tuyên bố
  • Hãy để người nguyên thủy được ưu tiên hơn các đối tượng ( {}[] )
  • Hãy để các publictuyên bố được ưu tiên hơnprivate tuyên bố
  • Thích Function.prototype.bindqua thus, self,vm ,etc
  • Tránh khai báo một Class trong một Class khác, trừ khi:
    • Rõ ràng là hai không thể tách rời
    • Lớp bên trong thực hiện Mẫu lệnh
    • Lớp bên trong thực hiện Mô hình Singleton
    • Lớp bên trong thực hiện Mẫu trạng thái
    • Lớp bên trong triển khai một mẫu thiết kế khác bảo đảm điều này
  • Luôn quay trở lại thistừ trong Phạm vi Từ điển của Không gian Đóng.

Đây là lý do tại sao những trợ giúp này:

Xây dựng cướp
var Super = function Super() {
    ...
    this.inherited = true;
    ...
};
var Klass = function Klass() {
    ...
    // export precepts
    Super.apply(this);  // extends this with property `inherited`
    ...
};
Thiết kế mô hình
var Model = function Model(options) {
    var options = options || {};

    this.id = options.id || this.id || -1;
    this.string = options.string || this.string || "";
    // ...

    return this;
};
var model = new Model({...});
var updated = Model.call(model, { string: 'modified' });
(model === updated === true);  // > true
Mẫu thiết kế
var Singleton = new (function Singleton() {
    var INSTANCE = null;

    return function Klass() {
        ...
        // export precepts
        ...

        if (!INSTANCE) INSTANCE = this;
        return INSTANCE;
    };
})();
var a = new Singleton();
var b = new Singleton();
(a === b === true);  // > true

Như bạn có thể thấy, tôi thực sự không có nhu cầu thusvì tôi thích Function.prototype.bind( .callhoặc .apply) hơnthus . Trong Singletonlớp học của chúng tôi , chúng tôi thậm chí không đặt tên cho nó thusINSTANCEtruyền tải nhiều thông tin hơn. Đối với Model, chúng tôi trả lại thisđể chúng tôi có thể gọi Trình xây dựng bằng cách sử dụng .callđể trả về thể hiện mà chúng tôi đã truyền vào nó. Dự phòng, chúng tôi đã gán nó cho biến updated, mặc dù nó hữu ích trong các tình huống khác.

Ngoài ra, tôi thích xây dựng các đối tượng bằng chữ bằng cách sử dụng newtừ khóa trên {ngoặc}:

Ưu tiên
var klass = new (function Klass(Base) {
    ...
    // export precepts
    Base.apply(this);  //
    this.override = x;
    ...
})(Super);
Không ưa thích
var klass = Super.apply({
    override: x
});

Như bạn có thể thấy, cái sau không có khả năng ghi đè lên thuộc tính "ghi đè" của Superclass.

Nếu tôi thêm các phương thức vào prototypeđối tượng Class , tôi thích một đối tượng bằng chữ - có hoặc không sử dụng newtừ khóa:

Ưu tiên
Klass.prototype = new Super();
// OR
Klass.prototype = new (function Base() {
    ...
    // export precepts
    Base.apply(this);
    ...
})(Super);
// OR
Klass.prototype = Super.apply({...});
// OR
Klass.prototype = {
    method: function m() {...}
};
Không ưa thích
Klass.prototype.method = function m() {...};

0

Tôi muốn đề cập rằng chúng ta có thể sử dụng Tiêu đề hoặc Chuỗi để khai báo Đối tượng.
Có nhiều cách khác nhau để gọi từng loại trong số họ. Xem bên dưới:

var test = {

  useTitle : "Here we use 'a Title' to declare an Object",
  'useString': "Here we use 'a String' to declare an Object",
  
  onTitle : function() {
    return this.useTitle;
  },
  
  onString : function(type) {
    return this[type];
  }
  
}

console.log(test.onTitle());
console.log(test.onString('useString'));


-1

Về cơ bản không có khái niệm về lớp trong JS, vì vậy chúng tôi sử dụng hàm như một hàm tạo của lớp có liên quan đến các mẫu thiết kế hiện có.

//Constructor Pattern
function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.doSomething = function(){
    alert('I am Happy');
}
}

Cho đến bây giờ, JS không có manh mối nào về việc bạn muốn tạo một đối tượng nên ở đây có từ khóa mới.

var person1 = new Person('Arv', 30, 'Software');
person1.name //Arv

Tham chiếu: JS chuyên nghiệp cho nhà phát triển web - Nik Z


Downvote được chấp nhận: với một lý do hợp lệ sẽ có nhiều thông tin hơn và sẽ phục vụ một cơ hội để cải thiện.
Airwind711

khái niệm về một classtrong JS, như bạn đề cập trong tiêu đề của bạn bằng cách sử dụng functiontừ khóa. Đó không phải là Mẫu thiết kế mà là một tính năng có chủ ý của ngôn ngữ. Tôi đã không đánh giá thấp bạn về điều này, nhưng có vẻ như người khác đã làm vì sự căng thẳng và gần như không liên quan đến câu hỏi. Hy vọng thông tin phản hồi này sẽ giúp.
Cody
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.