如何“适当地”;在JavaScript中创建一个自定义对象?

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

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

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

谁能给我一个适当的例子,一个JavaScript对象的一些属性和方法?

152567 次浏览

当一个人在构造函数调用期间使用关闭“this”的技巧时,是为了编写一个函数,可以被其他不希望调用对象上的方法的对象用作回调。这与“使范围正确”无关。

这是一个普通的JavaScript对象:

function MyThing(aParam) {
var myPrivateVariable = "squizzitch";


this.someProperty = aParam;
this.useMeAsACallback = function() {
console.log("Look, I have access to " + myPrivateVariable + "!");
}
}


// Every MyThing will get this method for free:
MyThing.prototype.someMethod = function() {
console.log(this.someProperty);
};

通过阅读道格拉斯Crockford对JavaScript的描述,你可能会得到很多东西。John Resig也很精彩。好运!

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

function createCounter () {
var count = 0;


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

然后:

var counter1 = createCounter();
counter1.increaseBy(4);

道格拉斯Crockford好的部分中广泛讨论了这个主题。他建议避免使用操作符来创建新对象。相反,他建议创建定制的构造函数。例如:

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中,函数是一个对象,可以与操作符一起用于构造对象。按照惯例,打算用作构造函数的函数以大写字母开头。你经常会看到这样的事情:

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


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

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

我经常使用这种模式——我发现当我需要它的时候,它给了我相当大的灵活性。在使用中,它非常类似于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

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

结果是,在混合公司中,您将拥有一堆元类,它们的行为都略有不同。更糟糕的是,大多数JavaScript教程材料都很糟糕,提供了一些介于两者之间的折衷方案,覆盖了所有基础,让您非常困惑。(可能作者也很困惑。JavaScript的对象模型与大多数编程语言非常不同,而且在很多地方设计得很糟糕。)

让我们从原型方式开始。这是您能得到的最原生的javascript:开销代码最少,instanceof将处理这类对象的实例。

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

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

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是如此草率:它允许传入零参数,在这种情况下,xy成为undefined,并被分配给原型的this.xthis.y。如果构造函数要做更复杂的事情,它就会彻底失败。

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

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!

现在我们为每个类都得到了相同的构造函数样板。也许我们可以把它移到它自己的helper函数中,这样我们就不必一直输入它,例如,将它转过来,让基类的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;
};

...它开始看起来有点像其他语言,尽管语法略显笨拙。如果你喜欢,你可以添加一些额外的功能。也许你想要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工作并不值得。]

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

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

那么方法中的this将不是预期的Circle实例(它实际上是全局window对象,导致广泛的调试麻烦)。实际上,当一个方法被赋值给setTimeoutonclickEventListener时,通常会发生这种情况。

使用原型的方式,你必须为每一个这样的赋值包含一个闭包:

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

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

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

如果你的实例是通过闭包的方式完成的,绑定是通过实例变量的闭包免费完成的(通常称为thatself,尽管我个人不建议使用后者,因为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

哪种方式是“合适的”?两者都有。哪个是“最好的”?那要看你的情况了。FWIW:当我在做强面向对象的东西时,我倾向于为真正的JavaScript继承创建原型,而为简单的一次性页面效果创建闭包。

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

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

另一种方法是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"

Closure是通用的。bobince很好地总结了创建对象时的原型vs闭包方法。然而,你可以用函数式编程的方式使用闭包来模拟OOP的某些方面。记住函数是JavaScript中的对象;所以用另一种方式使用function作为对象。

这里有一个闭包的例子:

function outer(outerArg) {
return inner(innerArg) {
return innerArg + outerArg; //the scope chain is composed of innerArg and outerArg from the outer context
}
}

前段时间我看到Mozilla关于Closure的文章。下面是让我眼前一亮的:“闭包让你将一些数据(环境)与对这些数据进行操作的函数相关联。这与面向对象编程有明显的相似之处,在面向对象编程中,对象允许我们将一些数据(对象的属性)与一个或多个方法关联起来”。这是我第一次在没有参考原型的情况下看到闭包和经典OOP之间的相似之处。

如何?

假设您想要计算某些项目的增值税。增值税可能在应用程序的生命周期内保持稳定。在OOP(伪代码)中实现的一种方法是:

public class Calculator {
public property VAT { get; private set; }
public Calculator(int vat) {
this.VAT = vat;
}
public int Calculate(int price) {
return price * this.VAT;
}
}
基本上,你将VAT值传递给你的构造函数,你的calculate方法可以通过关闭对它进行操作。 现在不再使用类/构造函数,而是将VAT作为参数传递到函数中。因为你唯一感兴趣的是计算本身,返回一个新函数,这是计算方法:

function calculator(vat) {
return function(item) {
return item * vat;
}
}
var calculate = calculator(1.10);
var jsBook = 100; //100$
calculate(jsBook); //110

在您的项目中,确定顶级价值是什么是增值税计算的良好候选者。作为一个经验法则,当你传递相同的参数时,有一种方法可以使用闭包来改进它。不需要创建传统的对象。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures

基本上在JS中没有类的概念,所以我们使用函数作为与现有设计模式相关的类构造函数。

//Constructor Pattern
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.doSomething = function(){
alert('I am Happy');
}
}

到目前为止,JS还不知道你想要创建一个对象,所以这里有一个新的关键字。

var person1 = new Person('Arv', 30, 'Software');
person1.name //Arv

参考:专业JS网页开发人员- Nik Z

var Person = function (lastname, age, job){
this.name = name;
this.age = age;
this.job = job;
this.changeName = function(name){
this.lastname = name;
}
}
var myWorker = new Person('Adeola', 23, 'Web Developer');
myWorker.changeName('Timmy');


console.log("New Worker" + myWorker.lastname);

继续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中更干净,更容易阅读。


下面是一个很好的例子:

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

除了2009年接受的答案。如果你可以瞄准现代浏览器,你可以使用Object.defineProperty

Object.defineProperty()方法直接在上定义一个新属性 对象,或修改对象上的现有属性,并返回 对象。 来源:Mozilla < / p >
var Foo = (function () {
function Foo() {
this._bar = false;
}
Object.defineProperty(Foo.prototype, "bar", {
get: function () {
return this._bar;
},
set: function (theBar) {
this._bar = theBar;
},
enumerable: true,
configurable: true
});
Foo.prototype.toTest = function () {
alert("my value is " + this.bar);
};
return Foo;
}());


// test instance
var test = new Foo();
test.bar = true;
test.toTest();

要查看桌面和移动兼容性列表,请参见Mozilla的浏览器兼容性列表。是的,IE9+和Safari手机版一样支持。

你也可以试试这个

    function Person(obj) {
'use strict';
if (typeof obj === "undefined") {
this.name = "Bob";
this.age = 32;
this.company = "Facebook";
} else {
this.name = obj.name;
this.age = obj.age;
this.company = obj.company;
}


}


Person.prototype.print = function () {
'use strict';
console.log("Name: " + this.name + " Age : " + this.age + " Company : " + this.company);
};


var p1 = new Person({name: "Alex", age: 23, company: "Google"});
p1.print();

创建对象

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

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


console.log(test);
console.log(test.f(3));

这对于以结构化的方式存储数据非常有用。

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

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

这允许你创建多个对象共享相同的“蓝图”,类似于你如何使用类在eg。Java。

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

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

在我们的例子中,将方法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));

继承

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

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

这类似于这样做:

B.prototype = new A();

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

在创建B的新实例时,你总是可以选择运行A的构造函数,方法是将它添加到B的构造函数中:

function B(arg1, arg2) {
A(arg1, arg2); // This is optional
}

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

function B() {
A.apply(this, arguments); // This is optional
}

如果你想将另一个对象混合到B的构造函数链中,你可以将Object.createObject.assign结合起来:

B.prototype = Object.assign(Object.create(A.prototype), mixin.prototype);
B.prototype.constructor = B;

演示

function A(name) {
this.name = name;
}


A.prototype = Object.create(Object.prototype);
A.prototype.constructor = A;


function B() {
A.apply(this, arguments);
this.street = "Downing Street 10";
}


B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;


function mixin() {


}


mixin.prototype = Object.create(Object.prototype);
mixin.prototype.constructor = mixin;


mixin.prototype.getProperties = function() {
return {
name: this.name,
address: this.street,
year: this.year
};
};


function C() {
B.apply(this, arguments);
this.year = "2018"
}


C.prototype = Object.assign(Object.create(B.prototype), mixin.prototype);
C.prototype.constructor = C;


var instance = new C("Frank");
console.log(instance);
console.log(instance.getProperties());


请注意

Object.create可以在每个现代浏览器中安全使用,包括IE9+。Object.assign不能在任何版本的IE和一些移动浏览器中工作。如果你想使用它们并且支持没有实现它们的浏览器,建议使用polyfill Object.create和/或Object.assign

你可以在这里找到Object.create 的polyfill Object.assign 在这里.

一个适合我的模式
var Klass = function Klass() {
var thus = this;
var somePublicVariable = x
, somePublicVariable2 = x
;
var somePrivateVariable = x
, somePrivateVariable2 = x
;


var privateMethod = (function p() {...}).bind(this);


function publicMethod() {...}


// export precepts
this.var1 = somePublicVariable;
this.method = publicMethod;


return this;
};

首先,您可以更改为实例添加方法,而不是构造函数的prototype对象的首选项。我几乎总是在构造函数内部声明方法,因为我经常使用构造函数劫持来实现继承&修饰符。

以下是我如何决定哪些声明应该写在哪里:

  • 永远不要直接在上下文对象上声明方法(this)
  • var声明优先于function声明
  • 让原语优先于对象({}[])
  • public声明优先于private声明
  • 优先选择Function.prototype.bind而不是thusselfvmetc
  • 避免在另一个类中声明一个类,除非:
    • 很明显,这两者是不可分割的
    • Inner类实现了命令模式
    • 内部类实现了单例模式
    • 内部类实现了状态模式
    • 内部类实现了另一个保证这一点的设计模式
    • 李< / ul > < / >
    • 总是从闭包空间的词法作用域中返回this

    以下是为什么这些方法有帮助:

    构造函数劫持
    var Super = function Super() {
    ...
    this.inherited = true;
    ...
    };
    var Klass = function Klass() {
    ...
    // export precepts
    Super.apply(this);  // extends this with property `inherited`
    ...
    };
    
    模型设计
    var Model = function Model(options) {
    var options = options || {};
    
    
    this.id = options.id || this.id || -1;
    this.string = options.string || this.string || "";
    // ...
    
    
    return this;
    };
    var model = new Model({...});
    var updated = Model.call(model, { string: 'modified' });
    (model === updated === true);  // > true
    
    设计模式
    var Singleton = new (function Singleton() {
    var INSTANCE = null;
    
    
    return function Klass() {
    ...
    // export precepts
    ...
    
    
    if (!INSTANCE) INSTANCE = this;
    return INSTANCE;
    };
    })();
    var a = new Singleton();
    var b = new Singleton();
    (a === b === true);  // > true
    

    正如你所看到的,我真的不需要thus,因为我Function.prototype.bind2。在Singleton类中,我们甚至不把它命名为thus,因为INSTANCE传达了更多的信息。对于Model,我们返回this,这样我们就可以使用.call调用构造函数来返回传递给它的实例。多余的是,我们将它分配给变量Function.prototype.bind1,尽管它在其他情况下是有用的。

    此外,我更喜欢使用new关键字而不是{括号}来构造对象字面量:

    首选
    var klass = new (function Klass(Base) {
    ...
    // export precepts
    Base.apply(this);  //
    this.override = x;
    ...
    })(Super);
    
    不喜欢
    var klass = Super.apply({
    override: x
    });
    

    如您所见,后者无法覆盖其超类的“override”属性。

    如果我确实向类的prototype对象添加了方法,我更喜欢使用对象文字——无论是否使用new关键字:

    首选
    Klass.prototype = new Super();
    // OR
    Klass.prototype = new (function Base() {
    ...
    // export precepts
    Base.apply(this);
    ...
    })(Super);
    // OR
    Klass.prototype = Super.apply({...});
    // OR
    Klass.prototype = {
    method: function m() {...}
    };
    
    不喜欢
    Klass.prototype.method = function m() {...};
    
我想提一下,我们可以使用一个标题一个字符串来声明一个对象 每种类型都有不同的调用方法。见下文:< / p >

var test = {


useTitle : "Here we use 'a Title' to declare an Object",
'useString': "Here we use 'a String' to declare an Object",
  

onTitle : function() {
return this.useTitle;
},
  

onString : function(type) {
return this[type];
}
  

}


console.log(test.onTitle());
console.log(test.onString('useString'));