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

IT技术 javascript
2021-01-18 03:52:48

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

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

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

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

6个回答

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对象,导致广泛的调试问题)。实际上,这通常发生在采用一个方法并将其分配给 a 时setTimeoutonclick或者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 部分。]

似乎 JavaScript 不是您最喜欢的语言,因为您希望像使用类一样使用它。
2021-03-14 03:52:48
Bob 我认为这是一个很棒的答案 - 我一直在努力解决这两种模式,我认为您编写的代码比 Resig 更简洁,并且比 Crockford 的解释更深入。我想不出更高的赞誉了......
2021-03-18 03:52:48
我当然这样做,每个人也是如此:对于当今程序员面临的大多数常见问题,类和实例模型是更自然的模型。我确实同意,从理论上讲,基于原型的继承可能会提供一种更灵活的工作方式,但 JavaScript 完全没有兑现这一Promise。它笨重的构造函数系统给了我们两全其美的好处,使类继承变得困难,同时又没有原型可以提供的灵活性或简单性。简而言之,就是便便。
2021-03-24 03:52:48
从“类”定义到对象实例化的非常好的渐进式步骤。并且很好地绕过new.
2021-04-01 03:52:48
在我看来,将经典继承范式绘制到像 javascript 这样的原型语言上总是一个方钉和一个圆孔。有时这真的是必要的,或者这只是人们将语言变成他们想要的方式而不是简单地使用语言的一种方式?
2021-04-01 03:52:48

我经常使用这种模式 - 我发现它在我需要时给了我很大的灵活性。在使用中,它与 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
ECMA5 方式在哪里?function Class() {} function SubClass() { Class.apply(this, arguments); } SubClass.prototype = Object.create(Class.prototype);
2021-03-14 03:52:48
@virtualnobi:此模式不会阻止您编写原型方法:constructor.prototype.myMethod = function () { ... }
2021-03-20 03:52:48
这里的问题是每个对象都有自己的所有私有和公共函数的副本。
2021-03-24 03:52:48
这看起来很有趣,因为它非常接近我的“主场”,即 C#。我也想我开始理解为什么 privateStaticVariable 真的是私有的(因为它是在函数范围内定义的,并且只要有对它的引用就保持活动状态?)
2021-04-03 03:52:48

Douglas CrockfordThe Good Parts 中广泛讨论了该主题他建议避免使用new操作符来创建新对象。相反,他建议创建定制的构造函数。例如:

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

在 Javascript 中,函数是一个对象,可用于与new运算符一起构造对象按照惯例,用作构造函数的函数以大写字母开头。你经常看到这样的事情:

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

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

如果您在实例化新对象时忘记使用new运算符,您将得到一个普通的函数调用,并且this绑定到全局对象而不是新对象。

Crockford 是一个脾气暴躁的老人,我在很多方面不同意他的观点,但他至少在提倡对 JavaScript 进行批判性的审视,值得听听他的看法。
2021-03-10 03:52:48
我同意克罗克福德的观点。new 运算符的问题在于 JavaScript 会使“this”的上下文与调用函数时的上下文大不相同。尽管有正确的大小写约定,但在较大的代码库中还是会出现一些问题,因为开发人员忘记使用 new、忘记大写等。为了务实,您可以在没有 new 关键字的情况下完成所有需要做的事情 - 那么为什么要使用它和在代码中引入更多的失败点?JS 是一种原型语言,而不是基于类的语言。那么为什么我们希望它像静态类型语言一样工作呢?我当然不会。
2021-03-16 03:52:48
@meder:不仅仅是你。至少,我认为新运营商没有任何问题。无论如何都有一个隐含newvar that = {};
2021-03-17 03:52:48
@bobince:同意。大约 5 年前,他关于闭包的写作让我看到了很多东西,他鼓励采用深思熟虑的方法。
2021-03-24 03:52:48
是我还是我认为克罗克福德对新运营商的抨击毫无意义?
2021-03-27 03:52:48

继续关闭bobince 的回答

在 es6 中,您现在实际上可以创建一个 class

所以现在你可以这样做:

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

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

所以扩展到一个圆圈(如在另一个答案中)你可以这样做:

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

最终在 es6 中更清晰一点,更容易阅读。


这是一个很好的例子:

你也可以这样做,使用结构:

function createCounter () {
    var count = 0;

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

然后 :

var counter1 = createCounter();
counter1.increaseBy(4);
我不喜欢那样,因为空格很重要。为了跨浏览器兼容,返回后的curl必须在同一行上。
2021-03-23 03:52:48