如何在JavaScript中“正确”创建自定义对象?

2022-08-29 23:10:09

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

我看过一些例子,其中使用的人,然后在所有函数中使用,以确保范围始终正确。var self = thisself.

然后,我看到了用于添加属性的示例,而其他人则以内联方式执行此操作。.prototype

有人可以给我一个具有一些属性和方法的JavaScript对象的适当示例吗?


答案 1

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

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

让我们从原型方式开始。这是您可以获得的最基本的JavaScript原生代码:有最小的开销代码,并且 instanceof将适用于此类对象的实例。

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

我们可以将方法添加到通过将它们写入此构造函数的查找来创建的实例中:new Shapeprototype

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

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

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

此示例将起作用,您将在许多教程中看到类似的代码。但是,伙计,这很丑陋:我们正在实例化基类,即使没有实际的形状被创建。它碰巧在这个简单的情况下工作,因为JavaScript是如此草率:它允许零参数传入,在这种情况下,并成为并分配给原型的和。如果构造函数正在执行更复杂的操作,它将在其表面上变得平坦。new Shape()xyundefinedthis.xthis.y

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

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

这会将基类在其原型中的成员转移到不执行任何操作的新构造函数,然后使用该构造函数。现在我们可以简单地写:

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

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

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

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

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

因此,一个常见的扩展是将初始化内容分解为自己的函数,而不是构造函数本身。然后,此函数可以从基继承即可:

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 吐出子类:Function.prototype.subclass

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

...它开始看起来更像其他语言,尽管语法稍微笨拙一些。如果您愿意,可以添加一些额外的功能。也许你想采用并记住一个类名,并提供一个默认值。也许您想让构造函数检测它是否在没有运算符的情况下意外调用(否则通常会导致非常烦人的调试):makeSubclasstoStringnew

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

也许你想传递所有新成员并将它们添加到原型中,以节省您编写这么多内容的时间。许多类系统都这样做,例如:makeSubclassClass.prototype...

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

在对象系统中,您可能会认为有很多潜在的功能是可取的,但没有人真正同意一个特定的公式。


那么,闭包方式。这避免了JavaScript基于原型的继承的问题,根本不使用继承。相反:

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

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

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

[另外,由于此处没有继承,因此运算符将不起作用;如果需要,您必须提供自己的类嗅探机制。虽然你可以用与原型继承类似的方式摆弄原型对象,但这有点棘手,只是为了开始工作并不值得。instanceofinstanceof

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

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

然后方法内部将不会像预期的那样是Circle实例(它实际上是全局对象,导致广泛的调试问题)。在现实中,这通常发生在一个方法被采用并分配给一个,或者一般情况下。thiswindowsetTimeoutonclickEventListener

使用原型方式,您必须为每个此类赋值包含一个闭包:

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

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

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

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

闭包方法也有很多变体。您可能更愿意完全省略,创建一个新的并返回它,而不是使用运算符:thisthatnew

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 当我做强烈的OO工作时,我倾向于为真正的JavaScript继承进行原型设计,并为简单的一次性页面效果提供闭包。

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

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


答案 2

我经常使用这种模式 - 我发现当我需要它时,它给了我相当大的灵活性。在使用中,它与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;
}();

这将使用在创建时调用的匿名函数,并返回新的构造函数。由于匿名函数只调用一次,因此您可以在其中创建私有静态变量(它们位于闭包内,对类的其他成员可见)。构造函数基本上是一个标准的Javascript对象 - 您可以在其中定义私有属性,并且公共属性附加到变量。this

基本上,这种方法将Crockfordian方法与标准Javascript对象相结合,以创建一个更强大的类。

你可以像使用任何其他Javascript对象一样使用它:

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