小编典典

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

all

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

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

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

有人可以给我一个带有一些属性和方法的 JavaScript 对象的正确示例吗?


阅读 111

收藏
2022-03-14

共1个答案

小编典典

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

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

让我们从 原型方式 开始。这是您可以获得的最原生的 JavaScript:有最少的开销代码,并且 instanceof 将与这种对象的实例一起工作。

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

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

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

这个例子可以工作,你会在很多教程中看到类似的代码。但是,这new Shape()很丑陋:我们正在实例化基类,即使没有要创建实际的
Shape。它恰好在这种简单的情况下工作,因为 JavaScript
太草率了:它允许传入零个参数,在这种情况下x,并y成为undefined和分配给原型的this.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.prototype.subclass,把它转过来,让基类的
Function 吐出子类:

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操作符的情况下被意外调用(否则通常会导致非常烦人的调试):

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

也许您想传入所有新成员并将它们makeSubclass添加到原型中,以节省您编写Class.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();

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

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

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

每个实例都有自己的方法的好处是该方法可以绑定到拥有它的特定实例。这很有用,因为
JavaScriptthis在方法调用中的绑定方式很奇怪,如果你从它的所有者中分离一个方法:

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

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

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

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

或者,将来(或者现在如果你破解 Function.prototype)你也可以这样做function.bind()

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

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

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

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
继承进行原型设计,并为简单的一次性页面效果使用闭包。

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

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

2022-03-14