使用Object.create(null)和{}相比,是否是一种不好的实践?

37

我更喜欢使用Object.create(null),这样我可以将对象用作简单的字典来存储基本信息。如果我需要对象功能,那么我会创建一个普通对象,例如var obj = {x:3, etc};

这是因为{}不是真正的空对象,而是链接到Object原型,所以在循环中必须做hasOwnProperty来防止它在原型链上跳动,并在每个循环中执行此检查会影响性能,尽管影响微乎其微。

是否使用Object.create(null)而不是{}并在每个循环中写hasOwnProperty是不好的实践?

编辑:可能存在重复,但我不是在问它们之间的差异如何或为什么不同,而是想知道其他人是否使用Object.create(null)?在Javascript领域中是标准操作程序吗?

编辑2:我还喜欢进行一行检查,例如if(obj[someKey])... ht '致敬APJ Kalam先生'。


1
@Julius Davies - 我在发布问题之前已经阅读过了; 我的问题不是关于差异,而是询问群体意见,哪种方法更好。 - Data
群体智慧在这个问题上似乎存在分歧。我认为应该去做! - Julius Musseau
1
我了解你在使用for..in时遇到的问题,但是还有Object.keys(...).forEach(...)和其他可以直接应用于自身属性数组的Array方法。另一方面,也可以使用if (o.hasOwnProperty(s)而不是if (s in o)。我想这归结于看待特定情况,因为将一个具有null[[Prototype]]对象视为普通对象可能会导致奇怪的结果(例如没有valueOftoStringconstructor属性)。var o = Object.create(null);console.log(o+'')会抛出错误。 - RobG
@PaulS.——在我看来,如果您在完全控制的环境中使用它,那么就没有问题,因为您知道已添加和未添加的属性。否则,总是可以使用Object.prototype.hasOwnProperty.call(obj, prop)。;-) 我理解OP的观点,即对象只会像if (prop in obj)这样使用,在这种情况下是有意义的。 - RobG
显示剩余8条评论
7个回答

37

通过使用Object.create(null)而不是从Object.prototype继承,您的对象不再具有对象原型上的方法。那么显而易见的问题是,该原型上有哪些方法,您是否需要它们?

这些方法包括:

  • hasOwnProperty
  • isPrototypeOf
  • propertyIsEnumerable
  • toString/toLocaleString
  • valueOf

hasOwnProperty似乎有点无用,因为如果没有原型,创建的对象上的所有属性都是自己的属性。但是,某些库或其他代码可能正在使用for...in遍历对象的属性,并使用if (o.hasOwnProperty(key))惯用语。现在这样的代码将失败。或者,您可能正在创建一个子对象,并将创建的对象作为原型,想知道一个属性是子对象的自有属性还是来自原型。

isPrototypeOf 可能有用,如果您计划将创建的对象用作其他对象的原型,并且由于某种原因想要检查创建的对象是否在这些其他对象的原型链中。

propertyIsEnumerable 不会被过多使用,但我们无法预测系统中的其他代码会做什么。如果它尝试在创建的对象上调用此方法,或者任何以它作为原型创建的对象,则会失败。

toString 同样如此。谁知道谁可能会尝试在您创建的对象上调用它?例如,一些库可能会尝试通过执行 toString 并查看结果是否为 "[object Object]" 来测试值的类型。希望他们进行安全的 Object.prototype.toString.call(o),但是......

valueOf 很少被显式使用,但如果引擎需要将对象强制转换为原始值,则会调用它。如果缺少它,代码可能会出错:

> o2 = Object.create(null)
> o2 + 1
Uncaught TypeError: Cannot convert object to primitive value

当然,也有可能有人向Object原型添加了方法,在这种情况下,Object.create({})将会捕获它们。也许你不想要或不需要它们。另一方面,它们很可能是无害的。
考虑到所有这些,似乎应该将Object.create(null)限制在特定情况下,可以证明上述问题都不会发生。例如,对象从未传递到其本地上下文之外,或永远不会用作另一个对象的原型。即使在这种情况下,性能优势也非常小,甚至为零。

我想知道其他人是否使用Object.create(null)?在JavaScript领域中,这是标准操作吗?

我认为它不能被称为标准操作。

你可能想要更正一个语法错误:s/Is is/Is it/ - Vidul
当一个对象被创建为临时实例对象以执行某些转换,但该对象是其他函数调用的受让人时,人们的看法是什么,例如:var tempObj = Object.create(null);for (var i=0; i<n; i++) { tempObj = JSON.parse(...); }上下文现在已经改变,因为从JSON.parse返回的对象及其创建方式将取决于原型。即var tempObj = Object.create(null); tempObj instanceof Object === falsetempObj = JSON.parse(...); tempObj instanceof Object === true - stephenwil

27
据我所知,
{} == Object.create(Object.prototype);

同样地,

对我个人而言,Object.create(null)更能清晰地告诉你这个对象不会继承任何东西,即它是一个纯空的映射


同样地,

在两种情况下,您都可以将对象用作字典。


同样地,

取决于您应该如何处理您的对象。


当,

  1. {} == Object.create(Object.prototype);// 您继承了所有对象属性

  2. o = Object.create(null); // 您不会继承任何东西。 这是完全空白的对象。

如果您使用对象作为映射,并使用上述方法创建对象,则在查找映射中的内容时必须特别小心。因为从对象继承的属性和方法,您的代码可能会遇到一些您从未插入的键。

例如,如果您在toString上进行查找,您将找到一个函数,即使您从未将该值放在那里。您可以像这样解决:

if (Object.prototype.hasOwnProperty.call(object, 'toString')) {
    // we actually inserted a 'toString' key into object
}

请注意,您不能仅使用 object.hasOwnProperty('toString') 进行检查,因为您可能已将键“hasOwnProperty”插入到对象中,因此我们强制其使用 Object 中的实现。

另一方面,如果您使用上面的方法 2,则无需担心来自 Object 的事物出现在映射中。

您不能像这样简单地使用 if 检查属性是否存在:

// Unreliable:
if (object[someKey]) {
    // ...
}

所以,

大部分人使用Object.create(null)将对象作为Map使用,因为这种方法没有继承属性的开销。

在其他情况下,你只需要使用var myObject = {}

MDN - Object.create(英文)

MSDN - Object.create(英文)


这正是我所做的,就像你所说的那样进行简单的一行检查:if (object[someKey]) 这只有通过 Object.create(null) 才能实现。感谢您澄清了我的方法的完整性。 - Data
1
如果你的意思是语义上等价,那么{} === Object.create(Object.prototype)是成立的。但如果你的意思是严格相等,那么就不成立。 - user663031
@Pilot {} == Object.create(Object.prototype) 返回 false。这两个方法({}Object.create(Object.prototype)) 执行的是相同的操作,但它们的返回值不是 相等 的(我的意思是,不是 == 也不是 ===Object.is())。我认为你应该使用一些英文单词来取代 == - DMaster

8

我想参考Mozilla关于对象和映射比较的文档,在这里给Map一个机会作为现今更好的实践。

ObjectsMaps类似,两者都可以将键设置为值,检索这些值,删除键,并检测某些内容是否存储在键中。由于这个原因(以及没有内置的替代方案),Objects历史上被用作Maps;然而,有一些重要的差异使得在某些情况下使用Map更加可取

  • Object的键是字符串和符号,而Map的键可以是任何值,包括函数、对象和任何基本类型。
  • 您可以轻松地通过size属性获取Map的大小,而必须手动确定Object中的属性数。
  • Map是可迭代的,因此可以直接迭代,而迭代Object需要以某种方式获取其键并对其进行迭代。
  • Object具有原型,因此地图中有默认键,如果不小心会与您的键发生冲突。从ES5开始,可以通过使用map = Object.create(null)来绕过此问题,但很少这样做
  • Map在涉及频繁添加和删除键值对的情况下可能表现更好。

1
对于现代的代码,这绝对是正确的方式。使用 Map 存储任意键/值对,而 Object.create(null) 的合法用例则几乎消失不见了。但这并不意味着你永远不会遇到它们:旧代码仍然经常使用它,所以要做好处理它们的准备。 - hraban
提到一些缺点:1)Map需要使用new。像Douglas Crockford这样的人认为new是一个危险因素,并强烈反对在任何地方使用它。2)因为Map是对象,所以可以使用["key"] = value语法误用它们。 - Oliver Schimmer

7

来自《你不知道的JS:this和对象原型》一书:

Object.create(null)类似于{},但没有委托给Object.prototype,因此它比只有{}“更空”。

Object.create(null)是一种良好的实践方式,以防需要一个DMZ(显式绑定的空对象:apply & callbind)。


但是你也可以使用null0进行bind绑定。 - user663031
@torazaburo 错了!你不想要null或者undefined,因为涉及到的函数可能期望对象上下文,甚至更糟糕的是-对此没有期望,并且会改变全局对象。你需要像ø这样的DMO。 - Vidul
如果函数不需要对象上下文(例如Math.max),那么传递0null是完全没有问题的,事实上这是常见做法。我不理解你提到全局对象突变的参考。 - user663031
@torazaburo 请看下面的例子:function global_this() { this.name = 'global property changed'; } global_this.apply(null); console.log(window.name);; - Vidul
1
很遗憾,你错了。经验丰富的开发人员广泛使用它来处理this无关紧要的情况,作为JS没有不需要初始上下文参数的bindcall版本的解决方法。请注意,当要求Babel转译foo(...args)时,它将精确地生成foo.apply(null, args) - user663031
显示剩余2条评论

5
你说你正在使用 Object.create(null) 来创建一个不从 Object.prototype 继承的 Object
这正是使用带有 null 作为第一个参数的 Object.create 方法的用例。
使用 {} 等效于 Object.create(Object.prototype)。大多数情况下我们想要做到这一点,这也是字面量所做的,但在你的情况下,你对继承的控制更加清晰。
如果您需要来自 Object.prototype 的任何方法,则可以在您的 Object 上使用 .call.apply 调用。
var o = Object.create(null);
Object.prototype.hasOwnProperty.call(o, 'foo'); // some key foo

他还明确表示他需要对象功能。 - Omar Himada
@Adosi - 我想你误读了我的问题;当我使用null时,我不需要功能,它只是一个哈希映射/字典,用于存储简单的数据,例如 var size = Object.create(null); size.height = 3 等。 - Data
不,你误解了我的评论。Paul S的建议是使用null代替Object.create(null){}。这将删除任何对象功能。如果var t = null,则不能将t视为对象。 - Omar Himada
不是的。它是一种原始类型。请参阅https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null - Omar Himada
1
@Adosi Object.create 是一个方法。我将编辑答案中的“this”,以使其更加明确。 - Paul S.
显示剩余6条评论

3

其他答案没有提到的一点是,如果你正在使用 {} 作为字典,而且你的键是不可预测的(例如来自用户输入),那么你应该选择 Object.create(null)Map,因为通过使用 = 操作符(即对象的 setter)给某个键赋值可能会默默地失败,因为它试图修改原型链中的只读属性,例如在 __proto__ 属性上:

let dictObject = {};
let dictNull = Object.create(null);

let key = '__proto__';
dictObject[key] = 'value';
dictNull[key] = 'value';

console.log(dictObject[key] === value);  // false
console.log(dictNull[key] === value);  // true

为了在一个对象上定义__proto__属性,你需要在对象本身上定义属性/值:
Object.assign(dictObject, '__proto__', {value:'value'});

所以在这种情况下的解决方案是使用 Object.create(null)Map

-1

在我看来,如果可能的话,你应该使用Map

如果不行,你应该尝试设计你的算法/代码,以避免查找继承原型键。除非你正在编写一些较低级别的实用程序库,在99%的情况下,你不需要或不想担心一个属性是来自对象本身还是从原型链继承而来。

如果不行,那么你可以使用Object.create(null),或者如果涉及到继承的原型键的情况不多,可以使用hasOwnProperty()保护。

因此,Object.create(null) 应该是最后的选择。


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