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

485

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

我看过一些例子,其中的人使用了 var self = this,然后在所有函数中使用 self. 确保作用域始终正确。

接着,我又看到有人使用 .prototype 添加属性,而其他人则在代码中直接添加属性。

能否给我一个适当的JavaScript对象示例,其中包含一些属性和方法?


15
没有一种“最好”的方式。 - Kenan Banks
“self”不是保留字吗?如果不是,它应该是;因为“self”是一个预定义变量,指向当前窗口。“self === window”。 - Shaz
2
@Shaz:它并不比浏览器对象模型中的其他 window 属性如 documentframes 更具保留意义;你肯定可以将其作为变量名重新使用该标识符。尽管如此,从风格上讲,我更喜欢使用 var that= this 以避免任何可能的混淆。尽管 window.self 最终是毫无意义的,所以很少有理由去碰它。 - bobince
7
当JS代码被压缩时,将this赋值给一个本地变量(例如self)可以减小文件大小。 - Patrick Fisher
Classjs新链接:https://github.com/divio/classjs - Nikola
15个回答

897
在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()真的很丑:我们实例化了基类,即使没有实际的形状要创建。在这种简单的情况下它能够工作,因为JavaScript非常松散:它允许传入零个参数,在这种情况下xy变成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。请注意保留HTML标记。
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());

如果方法被赋值给setTimeout, onclick或一般的EventListener,那么方法内部的this将不会是预期的Circle实例(实际上它将是全局的window对象,导致广泛的调试问题)。使用原型方式时,你需要为每个这样的赋值包含一个闭包:

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

或者,在未来(或者现在如果你hack了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部分。]

13
非常好的逐步介绍从“类”定义到对象实例化。以及巧妙地避开new的处理方式。 - Crescent Fresh
8
似乎 JavaScript 不是你最喜欢的语言,因为你想要像使用有类的语言一样使用它。 - Jonathan Feinberg
61
当然,每个人都这样认为:对于当今大多数程序员面临的常见问题,类和实例模型更自然。我确实同意从理论上讲,基于原型的继承可能提供一种更灵活的工作方式,但JavaScript完全没有兑现这一承诺。它笨拙的构造函数系统给我们带来了两个世界中最糟糕的东西,使得类似继承变得困难,同时没有提供原型可以提供的任何灵活性或简单性。简而言之,它很糟糕。 - bobince
4
Bob,我认为这是一个很棒的回答 - 我已经困扰了一段时间这两个模式,我认为你编写的代码比Resig更简洁,解释也比Crockford更深刻。我想不出更高的赞美了... - James Westgate
4
在我看来,将经典继承范例套用到像JavaScript这样的原型语言中似乎是个不切实际的想法。这是否真正必要?还是说这只是人们为了让语言符合他们的意愿而强行使用它,而不是简单地按照语言的本意使用它呢? - slf
显示剩余7条评论

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

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

基本上,这种方法结合了Crockfordian方法和标准的JavaScript对象,以创建一个更强大的类。

你可以像使用其他JavaScript对象一样使用它:

Foo.publicStaticMethod(); //calling a static method
var test = new Foo();     //instantiation
test.publicMethod();      //calling a method

4
看起来很有趣,因为它与我熟悉的C#非常接近。我也认为我开始明白privateStaticVariable为什么真正是私有的了(因为它在函数范围内定义,并在存在对它的引用时保持活动状态?) - Michael Stum
4
问题在于每个对象都会拥有所有私有和公共函数的副本。 - virtualnobi
2
@virtualnobi:这种模式并不会阻止您编写原型方法:constructor.prototype.myMethod = function () { ... } - Nicolas Le Thierry d'Ennequin

25

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将绑定到全局对象而不是新对象。


5
是我太蠢还是 Crockford 在批评 new 操作符时完全没有道理? - meder omuraliev
3
@meder:不仅是你。至少我认为新操作符没有问题。而且在“var that = {};”中已经有一个隐式的“new”。 - Tim Down
17
Crockford 是一个脾气古怪的老人,我在很多方面都不同意他的看法,但他至少提倡对 JavaScript 进行批判性审视,值得倾听他的观点。 - bobince
2
@bobince:同意。他关于闭包的写作在5年前启发了我很多东西,而且他鼓励一种深思熟虑的方法。 - Tim Down
1
我同意你们的看法,Crockford 对 JavaScript 提供了深入的洞察,但他的一些技巧/建议有时并不值得付出代价。特别是在这个话题上,我仍然喜欢使用 new 来创建新对象。 - Diego Pino
20
我同意Crockford的观点。使用new操作符的问题在于,与直接调用函数时相比,JavaScript会使“this”的上下文非常不同。尽管有适当的大小写约定,但在较大的代码库中,开发人员忘记使用new、忘记大写等问题仍然会出现。从实用角度考虑,你可以在不使用new关键字的情况下完成所有需要做的事情-那么为什么要使用它并在代码中引入更多故障点呢?JS是一种原型化语言,而不是基于类的语言。那么我们为什么要让它像静态类型的语言一样运行呢?我肯定不希望这样。 - Joshua Ramirez

18

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


6
您也可以使用结构体这种方式来实现,具体如下:
function createCounter () {
    var count = 0;

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

那么:
var counter1 = createCounter();
counter1.increaseBy(4);

6
我不喜欢那种方式,因为空格很重要。为了跨浏览器兼容性,return后的花括号必须在同一行。 - geowa4

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

这种方法非常适用于以结构化方式存储数据。

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

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

这使您能够创建多个共享相同“蓝图”的对象,类似于在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的 polyfill。


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"

4
当一个人在构造函数调用时使用“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);
};

你可能会从阅读Douglas Crockford关于JavaScript的观点中受益。John Resig也很出色。祝你好运!


1
嗯,将this包裹起来与“使范围正确”有着千丝万缕的联系。 - Roatin Marth
3
乔纳森是正确的。JS函数的作用域取决于你如何设计它。将self=this技巧应用到特定实例上可以确保在其他上下文中调用时不会改变,但有时候你可能希望出现这种情况,这要根据具体情况而定。 - Marco
我认为你们都说的其实是同一件事情。self=this虽然不能强制this持久化,但可以通过闭包轻松实现“正确”的作用域限定。 - Crescent Fresh
2
你这样做的原因是为了让嵌套函数能够访问构造函数中存在的this作用域。当嵌套函数位于构造函数内部时,它们的“this”作用域会恢复到全局作用域。 - Joshua Ramirez

4

闭包非常灵活。 bobince很好地总结了创建对象时的原型与闭包方法。但是,您可以使用函数式编程中的闭包来模仿OOP的某些方面。请记住,在JavaScript中,函数是对象;因此可以以一种不同的方式将函数用作对象。

以下是一个闭包示例:

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之间的平行关系,没有涉及原型的参考。
如何实现呢?
假设你想计算一些物品的增值税(VAT)。VAT可能在应用程序的生命周期内保持稳定。在面向对象编程中可以这样实现(伪代码):
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值传递到构造函数中,并且可以通过闭包对其进行操作。 现在,不要使用类/构造函数,而是将您的VAT作为参数传递给函数。因为您只关心计算本身,所以返回一个新函数,即计算方法:

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

在您的项目中,识别出适合计算VAT的顶级值。通常情况下,每当您不断传递相同的参数时,使用闭包可以改进它。无需创建传统对象。 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Closures

1
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
  • 避免在另一个类中声明类,除非:
    • 明显两者不可分割
    • 内部类实现命令模式
    • 内部类实现单例模式
    • 内部类实现状态模式
    • 内部类实现其他需要的设计模式
  • 始终从闭包空间的词法作用域内返回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.bind(或 .call.apply)而不是 thus。在我们的 Singleton 类中,我们甚至没有将其命名为 thus,因为 INSTANCE 传达了更多信息。对于 Model,我们返回 this,以便我们可以使用 .call 调用构造函数,从而返回我们传递给它的实例。虽然在其他情况下它很有用,但我们将其赋值给变量 updated 是多余的。

同时,我更喜欢使用 new 关键字构建对象字面量而不是 {括号}:

var klass = new (function Klass(Base) {
    ...
    // export precepts
    Base.apply(this);  //
    this.override = x;
    ...
})(Super);

var klass = Super.apply({
    override: x
});

如您所见,后者没有覆盖其超类的“override”属性的能力。
如果我向类的原型对象添加方法,我更喜欢使用对象字面量 - 无论是否使用“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() {...};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接