使用"Object.create"代替"new"

391

Javascript 1.9.3 / ECMAScript 5 引入了 Object.create,Douglas Crockford 等人长期以来一直在 倡导。如何使用 Object.create 替换下面代码中的 new

var UserA = function(nameParam) {
    this.id = MY_GLOBAL.nextId();
    this.name = nameParam;
}
UserA.prototype.sayHello = function() {
    console.log('Hello '+ this.name);
}
var bob = new UserA('bob');
bob.sayHello();

(假设存在MY_GLOBAL.nextId)。

我能想到的最好解决办法是:

var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var bob = Object.create(userB);
bob.init('Bob');
bob.sayHello();

看起来没有任何优势,所以我认为我没有理解它。我可能过于新古典主义了。如何使用 Object.create 来创建用户“bob”?


当被接受的答案的投票数少于问题本身时,也许这个被接受的答案并不可接受?@CMS会让你编写一个工厂函数,该函数在内部使用Object.create来获得与new UserA('bob')相同的单步功能。 - Rick Jolly
1
或许这是可接受的答案,因为它获得了6个答案中最多的投票。 - Cory Gross
1
Crockford曾是JS世界中的早期指导智者,但他的全盛时期已经过去。尽管Object.create在其他方面很有用,但他建议完全停止使用new从未得到广泛采纳。 - Andy
@Andy 这取决于你使用的编程范式。如果你在 JS 中使用函数式编程,你更喜欢使用 Object.create 而不是 new 关键字。 - Anastasis
15个回答

267

如果只有一层继承,你的示例可能无法让你看到 Object.create 的真正好处。

此方法允许您轻松实现差异继承,其中对象可以直接从其他对象继承。

在您的userB示例中,如果您在现有对象实例上再次调用此方法,则不认为应该公开或甚至存在init方法,否则idname属性将更改。

Object.create 允许您使用其第二个参数初始化对象属性,例如:

var userB = {
  sayHello: function() {
    console.log('Hello '+ this.name);
  }
};

var bob = Object.create(userB, {
  'id' : {
    value: MY_GLOBAL.nextId(),
    enumerable:true // writable:false, configurable(deletable):false by default
  },
  'name': {
    value: 'Bob',
    enumerable: true
  }
});

正如您所见,可以在Object.create的第二个参数上使用对象字面量来初始化属性,这种语法类似于Object.definePropertiesObject.defineProperty方法所使用的语法。

它允许您设置属性的特性(enumerablewritableconfigurable),这非常有用。


11
  1. 感谢指出差异继承。
  2. 这是不是意味着不再需要构造函数了?我需要记得每次创建用户时将“id”设置为MY_GLOBAL.nextId()吗?
- Graham King
4
@Graham,不用更多的构造函数了,你是正确的。虽然 Firefox 3.7apre5、最新的 WebKit Nightly 构建版本和 Chrome 5 Beta 上当前可用的实现与传统构造函数相比速度都不太快,但希望这种情况在不久的将来会改变。关于对象创建,您可以创建一个工厂函数(例如 function createUser(name) { ... }),并在其中使用 Object.create 创建所需逻辑以创建您的用户对象。 - Christian C. Salvadó
5
关于不再使用构造函数的回复:通常您可以编写一个普通函数作为对象的“工厂”。内部它会使用 Object.create 创建一个空对象,然后根据需要进行修改,最后返回该对象。调用该工厂的函数不必记住前缀 new - Daniel Earwicker
2
@GrahamKing 你可以使用闭包来初始化你的对象:http://jsfiddle.net/Prqdt/ - amiuhle
1
@ryanve 当对象具有一些私有属性时,您可能不希望枚举它们(例如,在 for in 中,您不想列出对象元数据而只是实际数据属性)。请参见 MDN Enumerability - Kamafeather
显示剩余7条评论

56

使用 Object.create(...) 比起 new object 真的没有什么优势。

支持这种方法的人通常陈述相当模糊的优点:“可扩展性” 或者 “更自然地符合JavaScript的特性”等等。

然而,我还没有看到任何具体的例子,证明 Object.create 比使用 new 有任何优势。相反,它存在已知问题。Sam Elsamman描述了在存在嵌套对象并使用 Object.create(...) 时会发生什么

var Animal = {
    traits: {},
}
var lion = Object.create(Animal);
lion.traits.legs = 4;
var bird = Object.create(Animal);
bird.traits.legs = 2;
alert(lion.traits.legs) // shows 2!!!

这是因为Object.create(...)倡导使用数据来创建新对象;在这里,Animal数据成为lionbird原型的一部分,并且由于共享而引起问题。当使用new时,原型继承是明确的:
function Animal() {
    this.traits = {};
}

function Lion() { }
Lion.prototype = new Animal();
function Bird() { }
Bird.prototype = new Animal();

var lion = new Lion();
lion.traits.legs = 4;
var bird = new Bird();
bird.traits.legs = 2;
alert(lion.traits.legs) // now shows 4

关于可选的属性特性,这些特性可以通过使用Object.defineProperties(...)进行添加,其中传入参数为Object.create(...)。请注意,内容中保留了html标签。

26
我不同意。Object.create 并没有像所链接的文章暗示的那样,强制或鼓励将原型作为“默认数据值存储器”使用。适当的数据初始化是由创建特定对象的人(比如 OO 设计中的工厂或建造者)负责的。在 JavaScript 中继承数据而不是行为是可行的,但并不常见。 - Kos
43
只要你理解了Object.create的参数应该是原型对象,这个问题就不会出现。当你使用 new 声明Animal.prototype.traits = {};时,你也会遇到同样的问题。唯一能清楚知道不应这么做的原因是你理解了Javascript原型的工作原理。 - plediii
21
天哪!提供正确答案就被疯狂点踩 :-) 问题是Object.create不允许构造函数参数,因此我们被迫扩展"data"。现在,这个"data"可能包含嵌套对象,这导致了上述问题。另一方面,通过原型继承,我们只有在显式编写Animal.prototype.traits = {};时才会遇到这个问题。一个方法是隐式的,另一个方法是显式的。选择那个不会导致问题的方法。 - Zephyr was a Friend of Mine
6
请参考这篇文章以获取解决“两只腿的狮子”问题的简单方法。这里提供一些示例代码以说明它的运作:http://jsfiddle.net/d131/a6xhu/ - d13
12
我建议阅读Kyle Simpson的这篇文章。三个部分都很有趣,但第三部分非常重要。如果你在阅读后仍然认为“new”比Object.create()更好,那就没有希望了! :) http://davidwalsh.name/javascript-objects-deconstruction - MindJuice
显示剩余6条评论

42

在一些浏览器中,例如IE8、Opera v11.5和Konq4.3等版本,Object.create还没有被标准化。对于这些浏览器,你可以使用Douglas Crockford的版本来代替Object.create,但这个版本不包括CMS答案中使用的第二个“初始化对象”参数。

为了实现跨浏览器代码,可以暂时修改Crockford的Object.create方法以实现对象初始化。以下是一种方法:

Object.build = function(o) {
   var initArgs = Array.prototype.slice.call(arguments,1)
   function F() {
      if((typeof o.init === 'function') && initArgs.length) {
         o.init.apply(this,initArgs)
      }
   }
   F.prototype = o
   return new F()
}

这个方法维护了 Crockford 的原型继承,并且还会检查对象中是否存在任何 init 方法,然后使用参数运行它,例如 new man('John','Smith')。您的代码将变成:

MY_GLOBAL = {i: 1, nextId: function(){return this.i++}}  // For example

var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var bob = Object.build(userB, 'Bob');  // Different from your code
bob.sayHello();

因此,Bob继承了sayHello方法,并具有自己的属性id=1和name='Bob'。当然,这些属性都是可写和可枚举的。如果您不关心可写、可枚举和可配置属性,这也比ECMA Object.create更简单的初始化方式。

对于没有init方法的初始化,可以使用以下Crockford模式:-

Object.gen = function(o) {
   var makeArgs = arguments 
   function F() {
      var prop, i=1, arg, val
      for(prop in o) {
         if(!o.hasOwnProperty(prop)) continue
         val = o[prop]
         arg = makeArgs[i++]
         if(typeof arg === 'undefined') break
         this[prop] = arg
      }
   }
   F.prototype = o
   return new F()
}

这会按照从左到右的顺序使用Object.gen参数填充userB自己的属性,紧随其后的是userB参数。它使用for(prop in o)循环,因此根据ECMA标准,属性枚举的顺序不能保证与属性定义的顺序相同。然而,通过在使用hasOwnProperty过滤器的情况下进行测试,甚至有时即使没有使用该过滤器,几个测试过的主流浏览器的代码示例显示它们是相同的。

MY_GLOBAL = {i: 1, nextId: function(){return this.i++}};  // For example

var userB = {
   name: null,
   id: null,
   sayHello: function() {
      console.log('Hello '+ this.name);
   }
}

var bob = Object.gen(userB, 'Bob', MY_GLOBAL.nextId());

相较于Object.build方法,我认为这种方法更简单,因为userB不需要init方法。同时,userB不是特别的构造函数,它看起来像一个普通的单例对象。因此,使用这种方法你可以从普通的纯对象中构建和初始化对象。


4
ES5 Shim中有一个Object.create的polyfill,用于模拟该方法。GitHub链接为:https://github.com/kriskowal/es5-shim。 - ryanve
很棒的文章!我添加了一个JSFiddle,这样您就可以在一个文件中获取Object.build和Object.gen的工作示例。此外,我添加了一些缺失的分号(仅在JSFiddle中可用)。 - Matt

28

简述:

new Computer() 会调用构造函数Computer(){}一次,而 Object.create(Computer.prototype)则不会。

所有的优势都基于此点。

关于性能的附注:像new Computer()这样的构造函数调用被引擎进行了大量的优化,因此它甚至可能比Object.create更快。


14

您可以让 init 方法返回 this,然后将调用链接在一起,如下所示:

var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
        return this;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};

var bob = Object.create(userB).init('Bob');

可以进一步执行 create = Object.create.bind(Object); log = console.log.bind(console),然后只需编写 create(...)log(...) 即可。请参见此处了解 .bind(...) 的作用。 - Nils Lindemann

10

使用Object.create的另一个可能性是以一种廉价而有效的方式克隆不可变对象。

var anObj = {
    a: "test",
    b: "jest"
};

var bObj = Object.create(anObj);

bObj.b = "gone"; // replace an existing (by masking prototype)
bObj.c = "brand"; // add a new to demonstrate it is actually a new obj

// now bObj is {a: test, b: gone, c: brand}

注意:上述代码片段创建了一个源对象的克隆体(不是引用,如cObj = aObj)。与复制属性的方法相比(参见1)它的优点在于它不会复制对象成员属性。相反,它创建了另一个-目标-对象,并将其原型设置为源对象。此外,在dest对象上修改属性时,它们是“即时创建”的,掩盖了原型(src的)属性。这构成了一种快速有效地克隆不可变对象的方式。

需要注意的是,这仅适用于创建后不应修改的源对象(不可变对象)。如果创建后修改源对象,则所有克隆的未掩盖属性也将被修改。

Fiddle在此处 (http://jsfiddle.net/y5b5q/1/) (需要支持Object.create的浏览器)。


7
称呼这个副本对我来说很困惑(也可能是许多其他人)。对大多数人来说,“克隆”方法意味着对原始内容的更改不会影响副本。 - kybernetikos
明白了,我修改了答案,只考虑了被视为不可变的源对象。 - basos
重复克隆对象的方式会很快让你陷入麻烦,因为你会得到一个原型链非常长的对象,它会在内存中保留所有旧对象。我有时会在测试中使用Object.create来创建模拟对象,覆盖类的某些属性,同时保持其他属性不变。但我很少使用它。 - Andy
我检查了上面的代码,也许Object.create已经发展了,但目前Object.create不会克隆任何东西,代码的结果var bObj = Object.create(anObj); console.log(bObj);{},意味着没有属性被复制。 - dev_khan

7
我认为问题的主要点是理解newObject.create方法之间的差异。根据这个答案这个视频new关键字会执行以下步骤:
  1. 创建新对象。

  2. 将新对象链接到构造函数(prototype)。

  3. 使this变量指向新对象。

  4. 使用新对象执行构造函数,并隐式执行return this

  5. 将构造函数名称赋值给新对象的属性constructor

Object.create只执行第1步和第2步!!!

在问题中提供的代码示例中,这并不是什么大问题,但在下一个示例中,这就很重要了:

var onlineUsers = [];
function SiteMember(name) {
    this.name = name;
    onlineUsers.push(name);
}
SiteMember.prototype.getName = function() {
    return this.name;
}
function Guest(name) {
    SiteMember.call(this, name);
}
Guest.prototype = new SiteMember();

var g = new Guest('James');
console.log(onlineUsers);

作为副作用的结果将会是:
[ undefined, 'James' ]

由于 Guest.prototype = new SiteMember(); 的执行,我们不需要调用父构造函数方法,我们仅需要让方法 getName 在 Guest 中可用。因此,我们需要使用 Object.create。 如果将 Guest.prototype = new SiteMember(); 替换为 Guest.prototype = Object.create(SiteMember.prototype);,结果将为:
[ 'James' ]

这是一个不好的使用Object.create解决组合继承(构造函数偷取+原型链)的例子,主要问题是父构造函数被调用了两次。Object.create使用现有对象作为原型创建新对象,这意味着在原型链中多了一个原型。通过寄生式组合继承(构造函数偷取+混合原型链),可以解决上述问题。我们只需要复制父原型即可,可以像这样实现:var prototype = new Object(SiteMember.prototype); prototype.constructor = Guest; Guest.prototype = prototype; - Dalibor

6
有时你无法使用NEW关键字创建一个对象,但仍然能够调用CREATE方法。
例如:如果您想要定义一个自定义元素,则必须从HTMLElement继承。
proto = new HTMLElement  //fail :(
proto = Object.create( HTMLElement.prototype )  //OK :)
document.registerElement( "custom-element", { prototype: proto } )

4

摘要:

  • Object.create() 是一个JavaScript函数,它接受两个参数并返回一个新对象。
  • 第一个参数是一个对象,将成为新创建对象的原型
  • 第二个参数是一个对象,将成为新创建对象的属性

示例:

const proto = {
  talk : () => console.log('hi')
}

const props = {
  age: {
    writable: true,
    configurable: true,
    value: 26
  }
}


let Person = Object.create(proto, props)

console.log(Person.age);
Person.talk();

实际应用:

  1. 使用这种方法创建对象的主要优点是,可以明确定义原型。在使用对象字面量或new关键字时,您无法控制它们(当然,您可以重写它们)。
  2. 如果我们想要有一个原型,则new关键字会调用构造函数。使用Object.create()不需要调用或声明构造函数
  3. 当您想以非常动态的方式创建对象时,它基本上可以是一个有用的工具。我们可以创建一个对象工厂函数,根据接收到的参数创建具有不同原型的对象。

4
优点在于,Object.create在大多数浏览器上通常比new慢。在这个jsperf示例中,在Chromium浏览器中,newObject.create(obj)30倍,尽管两者都很快。这有点奇怪,因为new做了更多的事情(如调用构造函数),而Object.create应该只是使用传入的对象作为原型创建一个新对象(按照Crockford说法中的秘密链接)。也许各种浏览器在提高Object.create效率方面仍需努力(也许它们在内部基于new实现…甚至在本地代码中)。

2
那个 jsperf 网站好像已经消失了。 - UpTheCreek

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