javascript - 如何 "properly"在 JavaScript 中创建自定义对象?

标签 javascript

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

我看过这个人使用 var self = this 的例子然后使用 self.在所有功能中以确保范围始终正确。

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

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

最佳答案

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

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

让我们从 开始原型(prototype)方式 .这是您可以获得的最原生的 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属性(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并分配给原型(prototype)的 this.xthis.y .如果构造函数正在做任何更复杂的事情,它就会一败涂地。

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

这会将其原型(prototype)中的基类成员转移到一个新的构造函数,该函数什么也不做,然后使用该构造函数。现在我们可以简单地写:
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.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将它们添加到原型(prototype)中,这样您就不必编写 Class.prototype...相当多。许多类系统都这样做,例如:
Circle= Shape.makeSubclass({
    _init: function(x, y, z) {
        Shape.prototype._init.call(this, x, y);
        this.r= r;
    },
    ...
});

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

关闭方式 , 然后。这通过根本不使用继承来避免 JavaScript 基于原型(prototype)的继承的问题。反而:
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 副本方法(以及我们添加的任何其他方法或其他类成员)。

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

【也是因为这里没有继承,instanceof运算符(operator)将无法工作;如果需要,您必须提供自己的类嗅探机制。虽然您可以使用与原型(prototype)继承类似的方式来处理原型(prototype)对象,但这有点棘手,而且仅仅获得 instanceof 并不值得。在职的。]

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

然后 this方法内部不会是预期的 Circle 实例(它实际上是全局 window 对象,导致广泛的调试问题)。实际上,这通常发生在采用方法并将其分配给 setTimeout 时。 , onclickEventListener一般来说。

使用原型(prototype)方式,您必须为每个此类分配包含一个闭包:
setTimeout(function() {
    mycircle.move(1, 1);
}, 1000);

或者,在 future (或者现在,如果你破解 Function.prototype)你也可以用 function.bind() 来做到这一点。 :
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);

如果您的实例以闭包方式完成,则绑定(bind)由实例变量上的闭包免费完成(通常称为 thatself ,但我个人建议不要使用后者,因为 self 已经有另一个不同的JavaScript 中的意思)。你没有得到参数 1, 1在上面的代码片段中是免费的,所以你仍然需要另一个闭包或 bind()如果你需要这样做。

闭包方法也有很多变体。您可能更愿意省略 this完全,创建一个新的that并返回它而不是使用 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 继承进行原型(prototype)设计,并为简单的一次性页面效果使用闭包。

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

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

关于javascript - 如何 "properly"在 JavaScript 中创建自定义对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1595611/

相关文章:

javascript - 是否可以在文件夹中导入 *.vue 文件?

javascript - 如何转换为字符串插入sql

javascript指针数组或#define缩写

javascript - 局部变量影响全局变量 - Javascript 变量作用域

php - 使用 AJAX 获取响应 - 但页面仍在刷新

javascript - 如何对 emojione 库中的 emoji 进行分类?

javascript - 正则表达式,替换某些组合

javascript - 在 Iphone Web 应用程序上使用 canvas drawImage() 函数

javascript - jQuery 中的屏蔽输入 - 不使用库

javascript - 创建新的继承和隔离范围的两个指令 - 元素获取哪一个