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

Mic*_*tum 464 javascript

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

我已经看到了这个人在所有函数中使用var self = this然后使用的示例,self.以确保范围始终正确.

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

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

bob*_*nce 880

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

结果是,在混合公司中,你会有一个混杂的元类,所有表现都略有不同.更糟糕的是,大多数JavaScript教程材料都很糟糕,并提供某种中间折衷以覆盖所有基础,让你非常困惑.(可能作者也很困惑.JavaScript的对象模型与大多数编程语言非常不同,并且在很多地方都是直接设计得很糟糕.)

让我们从原型方式开始吧.这是您可以获得的最多JavaScript本机:有最少的开销代码,instanceof将使用这种对象的实例.

function Shape(x, y) {
    this.x= x;
    this.y= y;
}
Run Code Online (Sandbox Code Playgroud)

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

Shape.prototype.toString= function() {
    return 'Shape at '+this.x+', '+this.y;
};
Run Code Online (Sandbox Code Playgroud)

现在将它子类化,尽可能多地调用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();
Run Code Online (Sandbox Code Playgroud)

在向其添加方法之前:

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

这个例子可以工作,你会在许多教程中看到类似的代码.但是男人,这new Shape()很难看:即使没有创建实际的Shape,我们也会实例化基类.它恰好在这个简单的情况下工作,因为JavaScript是如此草率:它允许传入零参数,在这种情况下x,y变成undefined并分配给原型this.xthis.y.如果构造函数正在做任何更复杂的事情,那么它的表面就会变得平坦.

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

function subclassOf(base) {
    _subclassOf.prototype= base.prototype;
    return new _subclassOf();
}
function _subclassOf() {};
Run Code Online (Sandbox Code Playgroud)

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

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.prototype= subclassOf(Shape);
Run Code Online (Sandbox Code Playgroud)

而不是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);
Run Code Online (Sandbox Code Playgroud)

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

function Point() {
    Shape.apply(this, arguments);
}
Point.subclass(Shape);
Run Code Online (Sandbox Code Playgroud)

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

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!
Run Code Online (Sandbox Code Playgroud)

现在我们为每个类提供了相同的构造函数样板.也许我们可以将它移动到它自己的辅助函数中,这样我们就不必继续输入它,例如代替它,转而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;
};
Run Code Online (Sandbox Code Playgroud)

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

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

也许你想要传递所有新成员并将它们makeSubclass添加到原型中,以节省你必须写得Class.prototype...非常多.很多类系统都这样做,例如:

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

在对象系统中你可能会考虑许多潜在的功能,而且没有人真正同意一个特定的公式.


封闭的方式,然后.这可以避免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();
Run Code Online (Sandbox Code Playgroud)

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

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

[也因为这里没有继承,instanceof操作员不会工作; 如果需要,你必须提供自己的类嗅探机制.虽然你可以用与原型继承类似的方式来调整原型对象,但它有点棘手,并且只是为了instanceof工作而不值得.]

每个实例都有自己的方法的好处是该方法可以绑定到拥有它的特定实例.这很有用,因为JavaScript this在方法调用中使用了奇怪的绑定方式,如果从方法调用中分离出一个方法,那么结果是:

var ts= mycircle.toString;
alert(ts());
Run Code Online (Sandbox Code Playgroud)

然后this在方法内部将不会像预期的那样是Circle实例(它实际上是全局window对象,导致广泛的调试祸害).在现实中,当一个方法被取出并分配给此通常发生setTimeout,onclickEventListener一般.

使用原型方法,您必须为每个此类任务包含一个闭包:

setTimeout(function() {
    mycircle.move(1, 1);
}, 1000);
Run Code Online (Sandbox Code Playgroud)

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

setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
Run Code Online (Sandbox Code Playgroud)

如果你的实例是以闭包的方式完成的,那么绑定是通过实例变量的闭包来完成的(通常称为that或者self,虽然我个人会建议反对后者,因为self在JavaScript中已经有了另一个不同的含义).你没有免费获得1, 1上述代码片段中的参数,所以你仍然需要另一个闭包或者bind()你需要这样做.

闭包方法也有很多变种.您可能更喜欢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
Run Code Online (Sandbox Code Playgroud)

哪种方式"合适"?都.哪个是"最好的"?这取决于你的情况.FWIW当我做强OO的东西时,我倾向于为真正的JavaScript继承进行原型设计,以及简单的一次性页面效果的闭包.

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

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

  • 当然,我也是如此:对于程序员今天面临的大多数常见问题,类和实例模型更为自然.我确实同意,从理论上讲,基于原型的继承可以提供更灵活的工作方式,但JavaScript完全无法实现这一承诺.它笨重的构造函数系统给我们带来了两个世界中最糟糕的情况,使类似类继承变得困难,同时不提供原型所能提供的灵活性或简单性.简而言之,它是便便. (58认同)
  • 从"class"def到对象实例化的非常好的渐进式步骤.绕过"新",很好的接触. (13认同)
  • 似乎JavaScript不是您最喜欢的语言,因为您想要使用它就像它有类一样. (8认同)
  • Bob我认为这是一个很棒的答案 - 我一直在努力解决这两种模式,我认为你编写的内容比Resig更简洁,并且比Crockford更有洞察力.我无法想到...... (4认同)
  • 在我看来,将经典继承范式映射到像javascript这样的原型语言是方形挂钩和圆孔.是否有时候这是真正必要的,或者这只是一种方式让人们把语言变成他们想要的方式,而不是简单地使用语言来实现它的本质? (4认同)
  • @Jean:嗯,他们做了不同的事情.`Object.create()`被设计为一个低级语言特性,可以访问ES5对象的所有功能,而不是制裁一个特定的类/实例实现.你当然可以在`Object.create()`之上构建类/实例系统(事实上,我正在使用的`makeSubclass()`的版本就是这样,ES3的`create()`的shimmed部分实现浏览器). (3认同)
  • @zerkms:MDN不是规范性的. (3认同)
  • 互联网上似乎有很多错误的信息和错误的模式,对于刚接触javascript的人来说,他们说混乱和不良优先.你的答案(特别是最后两种模式)真的让我清醒了.顺便说一句,这些成员函数在调试器中显示为匿名,你怎么能避免这种情况? (2认同)
  • @haridsv:原则上使用命名函数表达式,如`x.prototype.doThing = function doThing(){...};`.不幸的是,IE <9 [搞砸了](http://kangax.github.com/nfe/). (2认同)
  • ^^我也使用Object.create()主要是因为这是在ES5中,但我认为名称是错误的 - 即很难描述它的作用,prototypeClone()会更好 - .Object.create还会删除典型使用模式中目标的构造函数属性,这可以通过不同的签名来避免,并且它的参数应该是一个对象,而不是它的原型,因为大多数(如果不是全部)使用模式都需要无论如何得到原型. (2认同)
  • @zerkms:`constructor`是完全可变的,构造函数上的`prototype`属性也是如此,所以你可以拥有许多具有相同`constructor`但不同原型链的对象.ECMAScript标准并不保证"构造函数"可用于任何实际目的,因此如果你想将它变成有意义的东西,这完全是你自己的选择.它是您可能想要的类/实例系统的一个功能,但它不是最小系统的必要部分. (2认同)

ShZ*_*ShZ 90

我经常使用这种模式 - 我发现它在我需要它时给了我非常大的灵活性.在使用中它与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;
}();
Run Code Online (Sandbox Code Playgroud)

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

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

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

Foo.publicStaticMethod(); //calling a static method
var test = new Foo();     //instantiation
test.publicMethod();      //calling a method
Run Code Online (Sandbox Code Playgroud)

  • 这看起来很有趣,因为它与我的"家庭草坪"C#相当接近.我也认为我开始理解为什么privateStaticVariable真的是私有的(因为它在函数范围内定义并且只要有引用它就会保持活动状态?) (4认同)
  • 这里的问题是每个对象都有自己的所有私有和公共函数的副本. (4认同)
  • @virtualnobi:这种模式不会妨碍你编写protytpe方法:`constructor.prototype.myMethod = function(){...}`. (2认同)

Die*_*ino 25

Douglas Crockford"好的部分"中广泛讨论了这个话题.他建议避免使用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'});
Run Code Online (Sandbox Code Playgroud)

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

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

var person = new Person();
alert("name: " + person.name);**
Run Code Online (Sandbox Code Playgroud)

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

  • 我同意克罗克福德的观点.new运算符的问题在于JavaScript会使"this"的上下文与调用函数时的上下文非常不同.尽管有适当的大小写惯例,但是由于开发人员忘记使用new,忘记大写等等,因此在更大的代码库中会出现问题.为了务实,您可以在没有new关键字的情况下完成所有操作 - 所以为什么要使用它和在代码中引入更多的失败点?JS是一种原型,而不是基于类的语言.那么为什么我们希望它像静态类型语言一样?我当然不会. (20认同)
  • Crockford是一个胡思乱想的老人,我在很多方面不同意他,但他至少在推动对JavaScript的批判性看待,并且值得倾听他所说的话. (17认同)
  • 是我还是我认为Crockford对他的新操作员的抨击毫无意义? (5认同)
  • @meder:不仅仅是你.至少,我认为新运营商没有任何问题.无论如何,`var中有一个隐含的`new` = {};`. (3认同)
  • @bobince:同意.他关于闭合的文章大约在5年前让我看到很多东西,他鼓励采取深思熟虑的方法. (2认同)

Nea*_*eal 13

继续关闭bobince的答案

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

所以现在你可以这样做:

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

    toString() {
        return `Shape at ${this.x}, ${this.y}`;
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,您可以执行以下操作:(如在另一个答案中):

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}`;
    }
}
Run Code Online (Sandbox Code Playgroud)

在es6中结束了一点清洁,更容易阅读.


这是一个很好的例子:

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

let c = new Circle(1, 2, 4);

console.log('' + c, c);
Run Code Online (Sandbox Code Playgroud)


Ein*_*din 6

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

function createCounter () {
    var count = 0;

    return {
        increaseBy: function(nb) {
            count += nb;
        },
        reset: function {
            count = 0;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后 :

var counter1 = createCounter();
counter1.increaseBy(4);
Run Code Online (Sandbox Code Playgroud)

  • 我不喜欢这种方式因为空白很重要.返回后的卷曲必须在同一行,以实现跨浏览器兼容性. (6认同)

Flu*_*nkt 5

另一种方式是http://jsfiddle.net/nnUY4/ (我不知道这种处理对象创建和显示功能是否遵循任何特定模式)

// Build-Reveal

var person={
create:function(_name){ // 'constructor'
                        //  prevents direct instantiation 
                        //  but no inheritance
    return (function() {

        var name=_name||"defaultname";  // private variable

        // [some private functions]

        function getName(){
            return name;
        }

        function setName(_name){
            name=_name;
        }

        return {    // revealed functions
            getName:getName,    
            setName:setName
        }
    })();
   }
  }

  // … no (instantiated) person so far …

  var p=person.create(); // name will be set to 'defaultname'
  p.setName("adam");        // and overwritten
  var p2=person.create("eva"); // or provide 'constructor parameters'
  alert(p.getName()+":"+p2.getName()); // alerts "adam:eva"
Run Code Online (Sandbox Code Playgroud)


Joh*_*ers 5

创建一个对象

在 JavaScript 中创建对象的最简单方法是使用以下语法:

var test = {
  a : 5,
  b : 10,
  f : function(c) {
    return this.a + this.b + c;
  }
}

console.log(test);
console.log(test.f(3));
Run Code Online (Sandbox Code Playgroud)

这非常适合以结构化方式存储数据。

然而,对于更复杂的用例,通常最好创建函数实例:

function Test(a, b) {
  this.a = a;
  this.b = b;
  this.f = function(c) {
return this.a + this.b + c;
  };
}

var test = new Test(5, 10);
console.log(test);
console.log(test.f(3));
Run Code Online (Sandbox Code Playgroud)

这允许您创建共享相同“蓝图”的多个对象,类似于在例如中使用类的方式。爪哇。

然而,通过使用原型,这仍然可以更有效地完成。

只要函数的不同实例共享相同的方法或属性,您就可以将它们移动到该对象的原型。这样,函数的每个实例都可以访问该方法或属性,但不需要为每个实例重复它。

在我们的例子中,将方法移至f原型是有意义的:

function Test(a, b) {
  this.a = a;
  this.b = b;
}

Test.prototype.f = function(c) {
  return this.a + this.b + c;
};

var test = new Test(5, 10);
console.log(test);
console.log(test.f(3));
Run Code Online (Sandbox Code Playgroud)

遗产

在 JavaScript 中进行继承的一种简单而有效的方法是使用以下两行代码:

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
Run Code Online (Sandbox Code Playgroud)

这类似于这样做:

B.prototype = new A();
Run Code Online (Sandbox Code Playgroud)

两者的主要区别在于A使用时不运行的构造函数Object.create,这更直观,更类似于基于类的继承。

A在创建 的新实例时,您始终可以选择运行 的构造函数B,方法是将其添加到 的构造函数中B

function B(arg1, arg2) {
    A(arg1, arg2); // This is optional
}
Run Code Online (Sandbox Code Playgroud)

如果你想传递Bto的所有参数A,你也可以使用Function.prototype.apply()

function B() {
    A.apply(this, arguments); // This is optional
}
Run Code Online (Sandbox Code Playgroud)

如果你想将另一个对象混合到 的构造函数链中B,你可以Object.create与结合使用Object.assign

B.prototype = Object.assign(Object.create(A.prototype), mixin.prototype);
B.prototype.constructor = B;
Run Code Online (Sandbox Code Playgroud)

演示

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
Run Code Online (Sandbox Code Playgroud)


笔记

Object.create可以在所有现代浏览器中安全使用,包括 IE9+。Object.assign不适用于任何版本的 IE 或某些移动浏览器。建议使用polyfill Object.create和/或Object.assign如果您想使用它们并支持未实现它们的浏览器。

Object.create 您可以在此处找到一个 Polyfill ,在Object.assign 此处找到 一个。