如何“正确"在 JavaScript 中创建自定义对象? [英] How to "properly" create a custom object in JavaScript?

查看:32
本文介绍了如何“正确"在 JavaScript 中创建自定义对象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道创建具有属性和方法的 JavaScript 对象的最佳方法是什么.

I wonder about what the best way is to create an JavaScript object that has properties and methods.

我见过这样的例子,该人使用 var self = this 然后在所有函数中使用 self. 以确保作用域始终正确.

I have seen examples where the person used var self = this and then uses self. in all functions to make sure the scope is always correct.

然后我看到了使用 .prototype 添加属性的例子,而其他人则是内联的.

Then I have seen examples of using .prototype to add properties, while others do it inline.

谁能给我一个带有一些属性和方法的 JavaScript 对象的正确示例?

Can someone give me a proper example of a JavaScript object with some properties and methods?

推荐答案

在 JavaScript 中有两种实现类和实例的模型:原型方式和闭包方式.两者都有优点和缺点,并且有很多扩展的变化.许多程序员和库使用不同的方法和类处理实用函数来掩盖该语言的一些丑陋部分.

There are two models for implementing classes and instances in JavaScript: the prototyping way, and the closure way. Both have advantages and drawbacks, and there are plenty of extended variations. Many programmers and libraries have different approaches and class-handling utility functions to paper over some of the uglier parts of the language.

结果是,在混合公司中,您将混杂元类,所有元类的行为都略有不同.更糟糕的是,大多数 JavaScript 教程材料都很糟糕,并且提供了某种介于两者之间的妥协以涵盖所有基础,让您感到非常困惑.(可能作者也很困惑.JavaScript 的对象模型与大多数编程语言非常不同,并且在许多地方直接设计得很糟糕.)

The result is that in mixed company you will have a mishmash of metaclasses, all behaving slightly differently. What's worse, most JavaScript tutorial material is terrible and serves up some kind of in-between compromise to cover all bases, leaving you very confused. (Probably the author is also confused. JavaScript's object model is very different to most programming languages, and in many places straight-up badly designed.)

让我们从原型方式开始.这是您可以获得的最原生的 JavaScript:代码开销最少,并且 instanceof 将处理此类对象的实例.

Let's start with the prototype way. This is the most JavaScript-native you can get: there is a minimum of overhead code and instanceof will work with instances of this kind of object.

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

我们可以将方法添加到由 new Shape 创建的实例中,方法是将它们写入此构造函数的 prototype 查找:

We can add methods to the instance created by new Shape by writing them to the prototype lookup of this constructor function:

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

现在对它进行子类化,尽可能多地调用 JavaScript 的子类化.我们通过完全替换那个奇怪的魔法 prototype 属性来做到这一点:

Now to subclass it, in as much as you can call what JavaScript does subclassing. We do that by completely replacing that weird magic prototype property:

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

在向其添加方法之前:

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

这个例子会起作用,你会在许多教程中看到类似的代码.但是伙计,那个 new Shape() 是丑陋的:即使没有实际的 Shape 被创建,我们也在实例化基类.它恰好适用于这种简单的情况,因为 JavaScript 是如此草率:它允许传入零个参数,在这种情况下 xy 变为 undefined 并分配给原型的 this.xthis.y.如果构造函数做任何更复杂的事情,它就会一败涂地.

This example will work and you will see code like it in many tutorials. But man, that new Shape() is ugly: we're instantiating the base class even though no actual Shape is to be created. It happens to work in this simple case because JavaScript is so sloppy: it allows zero arguments to be passed in, in which case x and y become undefined and are assigned to the prototype's this.x and this.y. If the constructor function were doing anything more complicated, it would fall flat on its face.

所以我们需要做的是找到一种方法来创建一个原型对象,该对象包含类级别我们想要的方法和其他成员,而无需调用基类的构造函数.为此,我们将不得不开始编写辅助代码.这是我所知道的最简单的方法:

So what we need to do is find a way to create a prototype object which contains the methods and other members we want at a class level, without calling the base class's constructor function. To do this we are going to have to start writing helper code. This is the simplest approach I know of:

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

这会将其原型中的基类成员转移到一个新的构造函数,该函数什么也不做,然后使用该构造函数.现在我们可以简单地写:

This transfers the base class's members in its prototype to a new constructor function which does nothing, then uses that constructor. Now we can write simply:

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

而不是 new Shape() 错误.我们现在有一组可接受的原语来构建类.

instead of the new Shape() wrongness. We now have an acceptable set of primitives to built classes.

在此模型下,我们可以考虑进行一些改进和扩展.例如这里是一个语法糖版本:

There are a few refinements and extensions we can consider under this model. For example here is a syntactical-sugar version:

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

任何一个版本都有不能继承构造函数的缺点,因为它在许多语言中都是如此.因此,即使您的子类在构造过程中没有添加任何内容,它也必须记住使用基类想要的任何参数调用基类构造函数.这可以使用 apply 稍微自动化,但您仍然必须写出:

Either version has the drawback that the constructor function cannot be inherited, as it is in many languages. So even if your subclass adds nothing to the construction process, it must remember to call the base constructor with whatever arguments the base wanted. This can be slightly automated using apply, but still you have to write out:

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

所以一个常见的扩展是将初始化内容分解为它自己的函数而不是构造函数本身.这个函数可以从基类继承就好了:

So a common extension is to break out the initialisation stuff into its own function rather than the constructor itself. This function can then inherit from the base just fine:

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!

现在我们已经为每个类获得了相同的构造函数样板.也许我们可以把它移到它自己的辅助函数中,这样我们就不必继续输入它,例如代替 Function.prototype.subclass,把它转过来让基类的 Function 吐出来子类:

Now we've just got the same constructor function boilerplate for each class. Maybe we can move that out into its own helper function so we don't have to keep typing it, for example instead of Function.prototype.subclass, turning it round and letting the base class's Function spit out subclasses:

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

...它开始看起来更像其他语言,尽管语法略显笨拙.如果您愿意,您可以添加一些额外的功能.也许您希望 makeSubclass 获取并记住一个类名,并使用它提供一个默认的 toString.也许您想让构造函数检测到它何时在没有 new 运算符的情况下被意外调用(否则通常会导致非常烦人的调试):

...which is starting to look a bit more like other languages, albeit with slightly clumsier syntax. You can sprinkle in a few extra features if you like. Maybe you want makeSubclass to take and remember a class name and provide a default toString using it. Maybe you want to make the constructor detect when it has accidentally been called without the new operator (which would otherwise often result in very annoying debugging):

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

也许您想传入所有新成员并让 makeSubclass 将它们添加到原型中,这样您就不必编写 Class.prototype...很多.许多类系统都这样做,例如:

Maybe you want to pass in all the new members and have makeSubclass add them to the prototype, to save you having to write Class.prototype... quite so much. A lot of class systems do that, eg:

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

您可能认为对象系统中有很多潜在的特性是理想的,但没有人真正同意一个特定的公式.

There are a lot of potential features you might consider desirable in an object system and no-one really agrees on one particular formula.

关闭方式,然后.这通过根本不使用继承来避免 JavaScript 基于原型的继承的问题.相反:

The closure way, then. This avoids the problems of JavaScript's prototype-based inheritance, by not using inheritance at all. Instead:

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

现在 Shape 的每个实例都将拥有自己的 toString 方法副本(以及我们添加的任何其他方法或其他类成员).

Now every single instance of Shape will have its own copy of the toString method (and any other methods or other class members we add).

每个实例都有自己的每个类成员的副本的坏处是它的效率较低.如果您正在处理大量子类实例,原型继承可能会更好地为您服务.调用基类的方法也有点烦人,如您所见:我们必须记住在子类构造函数覆盖它之前该方法是什么,否则它会丢失.

The bad thing about every instance having its own copy of each class member is that it's less efficient. If you are dealing with large numbers of subclassed instances, prototypical inheritance may serve you better. Also calling a method of the base class is slightly annoying as you can see: we have to remember what the method was before the subclass constructor overwrote it, or it gets lost.

[也因为这里没有继承,instanceof操作符就不起作用;如果需要,您必须提供自己的类嗅探机制.虽然您可以以与原型继承类似的方式来处理原型对象,但这有点棘手,并且仅仅为了让 instanceof 工作并不值得.]

[Also because there is no inheritance here, the instanceof operator won't work; you would have to provide your own mechanism for class-sniffing if you need it. Whilst you could fiddle the prototype objects in a similar way as with prototype inheritance, it's a bit tricky and not really worth it just to get instanceof working.]

每个实例都有自己的方法的好处是,该方法可以绑定到拥有它的特定实例.这很有用,因为 JavaScript 在方法调用中绑定 this 的奇怪方式,其结果是如果您将方法与其所有者分离:

The good thing about every instance having its own method is that the method may then be bound to the specific instance that owns it. This is useful because of JavaScript's weird way of binding this in method calls, which has the upshot that if you detach a method from its owner:

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

那么方法中的this不会是预期的Circle实例(它实际上是全局的window对象,导致广泛的调试问题).实际上,这通常发生在一个方法被采用并分配给 setTimeoutonclickEventListener 时.

then this inside the method won't be the Circle instance as expected (it'll actually be the global window object, causing widespread debugging woe). In reality this typically happens when a method is taken and assigned to a setTimeout, onclick or EventListener in general.

使用原型方式,你必须为每个这样的赋值包含一个闭包:

With the prototype way, you have to include a closure for every such assignment:

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

或者,在未来(或者现在,如果你破解 Function.prototype)你也可以用 function.bind() 来做到:

or, in the future (or now if you hack Function.prototype) you can also do it with function.bind():

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

如果您的实例以闭包方式完成,则绑定由实例变量上的闭包免费完成(通常称为 thatself,尽管我个人认为建议不要使用后者,因为 self 在 JavaScript 中已经有另一个不同的含义).但是,您不会免费获得上述代码段中的参数 1, 1,因此如果您需要这样做,您仍然需要另一个闭包或 bind().

if your instances are done the closure way, the binding is done for free by the closure over the instance variable (usually called that or self, though personally I would advise against the latter as self already has another, different meaning in JavaScript). You don't get the arguments 1, 1 in the above snippet for free though, so you would still need another closure or a bind() if you need to do that.

闭包方法也有很多变体.您可能更喜欢完全省略 this,创建一个新的 that 并返回它而不是使用 new 运算符:

There are lots of variants on the closure method too. You may prefer to omit this completely, creating a new that and returning it instead of using the new operator:

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

哪种方式是正确的"?两个都.哪个是最好的"?那要看你的情况了.FWIW,当我在做强烈的面向对象的事情时,我倾向于为真正的 JavaScript 继承进行原型设计,并为简单的一次性页面效果使用闭包.

Which way is "proper"? Both. Which is "best"? That depends on your situation. FWIW I tend towards prototyping for real JavaScript inheritance when I'm doing strongly OO stuff, and closures for simple throwaway page effects.

但是对于大多数程序员来说,这两种方式都非常违反直觉.两者都有许多潜在的混乱变化.如果您使用其他人的代码/库,您将同时遇到两者(以及许多介于两者之间和通常被破坏的方案).没有一个普遍接受的答案.欢迎来到 JavaScript 对象的奇妙世界.

But both ways are quite counter-intuitive to most programmers. Both have many potential messy variations. You will meet both (as well as many in-between and generally broken schemes) if you use other people's code/libraries. There is no one generally-accepted answer. Welcome to the wonderful world of JavaScript objects.

[这是为什么 JavaScript 不是我最喜欢的编程语言的第 94 部分.]

[This has been part 94 of Why JavaScript Is Not My Favourite Programming Language.]

这篇关于如何“正确"在 JavaScript 中创建自定义对象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆