Crockford的对象创建技术中发生了什么?

25

只有3行代码,但我仍然不能完全掌握它:

Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F();
};
newObject = Object.create(oldObject);
(来自原型继承
  1. Object.create() 首先创建了一个名为 F 的空函数。我认为函数是一种对象,那么 F 对象被存储在哪里呢?我猜是全局的。

  2. 接下来,我们传入的 oldObject,也就是 o,成为了函数 F 的原型。函数(即对象)F 现在“继承”于我们的 oldObject,这意味着名称解析将通过它进行路由。很好,但我想知道一个对象的默认原型是什么,是 Object 吗?对于一个函数对象也是这样吗?

  3. 最后,F 被实例化并返回,成为我们的 newObject。这里 new 操作符是否必要?F 已经提供了我们需要的内容,或者说函数对象和非函数对象之间有一个关键区别吗?显然,无法使用此技术来构造构造函数。

下一次调用 Object.create() 会发生什么?全局函数 F 会被覆盖吗?肯定不会被重用,因为这会改变先前配置的对象。如果多个线程调用 Object.create(),是否存在一种同步机制来防止在 F 上发生竞态条件?

4个回答

30

1)Object.create()首先创建一个名为F的空函数。我认为函数是一种对象,那么这个F对象存储在哪里呢?我猜是全局环境中。

不,它存储在Object.create函数的本地作用域中。每次调用Object.create函数时,该函数会重新创建F函数。

您甚至可以通过将F函数存储在闭包中并重用它来创建更节省内存的实现:

if (typeof Object.create !== "function") {
  Object.create = (function () {
    function F() {} // created only once
    return function (o) {
      F.prototype = o; // reused on each invocation
      return new F();
    };
  })();
}

2) 接下来,我们的旧对象(传递为o)成为函数F的原型。函数(即对象)F现在“继承”了我们的旧对象,意味着名称解析将通过它进行路由。很好,但我想知道一个对象的默认原型是什么,Object吗?对于函数对象也是如此吗?

所有对象都有一个内部属性,构建原型链,这个属性称为[[Prototype]],它是一个内部属性,尽管某些实现允许您访问它,例如Mozilla,使用obj.__proto__属性。

当您创建一个新对象时,即var obj = {};,默认的[[Prototype]]Object.prototype

所有函数都有一个prototype属性,当函数用作Constructor并使用new操作符调用时,将使用此属性。

在幕后创建一个新的对象实例,并将该对象的[[Prototype]]设置为其构造函数的prototype属性。

3) 最后,F被实例化和返回,成为我们的新对象。在这里,“new”操作是严格必要的吗?F已经提供了我们所需的东西,还是函数对象和非函数对象之间存在关键区别?显然,使用此技术无法拥有构造函数。

是的,在此方法中,new操作符是至关重要的。

new操作符是设置对象的[[Prototype]]内部属性的唯一标准方式,如果您对其工作原理感到好奇,可以查看[[Construct]]内部操作。

下一次调用Object.create()会发生什么?全局函数F被覆盖了吗?它肯定不会被重用,因为那样会改变先前配置的对象。如果多个线程调用Object.create(),是否有任何同步来防止F上的竞争条件?

下一次调用Object.create时,将仅在方法调用的范围内实例化一个新的本地F函数,您不必担心竞争条件

请注意,此实现很难符合ECMAScript 5th Edition规范中描述的Object.create,在该方法中,您可以传递一个属性描述符来初始化对象。

所有浏览器供应商都正在实现它(已在Firefox 3.7 alpha版、最新的Wekit Nightly Builds和Chrome 5 Beta中提供),因此我建议您至少检查是否存在本地实现,然后再覆盖它。


嗯,对于具有局部作用域的Crockford而言并不存在竞态条件问题,但是你的重用版本呢? - Chris Noe
@Chris,没有竞态条件,JavaScript的本质是单线程的,即使像计时器和其他异步事件(如用户交互)也是在阻塞单线程中按顺序执行的。参考链接:http://ejohn.org/blog/how-javascript-timers-work/ - Christian C. Salvadó
这样的对话会让我不再对线程做出过于草率的假设:https://dev59.com/BHE85IYBdhLWcg3whT9r,http://www.oreillynet.com/cs/user/view/cs_msg/81559 - Chris Noe
你的实现确实只创建了一个 F。但是,它会为每个新对象破坏性地更改 F.prototype。同时,所有新对象都将 F 作为它们的 .constructor。因此,在任何给定时间,它们可能具有许多不同的原型,但它们的 .constructor.prototype 都将是最新的原型塞入 F 中。 - FutureNerd
我和提问者遇到了同样的问题,以下文章真正帮助我理解了javascript函数的原型属性并不是函数的原型,而是将成为新对象原型的对象:http://sporto.github.io/blog/2013/02/22/a-plain-english-guide-to-javascript-prototypes/ - Soferio

7

1) 函数确实是一种对象。每次调用Object.create时,都会创建一个名为F的函数对象,并且只能在该次Object.create执行中使用该标识符访问。因此,每次调用Object.create时,您都会得到一个不同的函数对象F。这个函数对象作为Object.create返回的对象的constructor属性而存在。

2)

现在,F“继承”了我们的oldObject,也就是说,名称解析将通过它进行路由。

这并不完全正确。将一个对象someObject分配给函数的prototype属性只意味着通过将该函数作为构造函数调用来创建的任何未来对象的原型将是someObject

3) new对于这种技术非常重要。只有将函数视为构造函数调用时,才会产生一个新对象,并且该对象的原型(通常无法访问)设置为构造函数的prototype属性。没有其他(标准化的)方法可以设置对象的原型。

最后,浏览器中的JavaScript是单线程的,因此您所描述的竞争条件是不可能发生的。


3
由于F是局部变量而不是全局变量,因此竞态条件不会应用于任何情况。 - Matthew Crumley

2

> 显然,使用这种技术是不可能有构造函数的。

因为该技术已经是一个对象构造器,因为它返回了new F(),但是不能像new man('John', 'Smith')一样设置属性值。然而,如果修改Object.create代码,则可以实例化。例如,可以使用Object.creator构建和实例化下面的sarah对象,并继承getName方法。

var girl = {
   name: '',
   traits: {},
   getName: function(){return this.name}
}

var sarah = Object.creator(girl, 'Sarah', {age:29,weight:90})

sarah对象将包含自身属性{ name:'Sarah', traits:{age:9,weight:49} },并且继承的sarah.getName()将产生'Sarah'。

以下方法依赖于自身属性按创建顺序枚举的“for(prop in o)”方法。虽然ECMA规范没有保证,但这个例子(和一些更复杂的例子)在测试的所有主要浏览器(4个)中均有效,前提是使用了hasOwnProperty(),否则无效。

Object.creator = 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()
}

官方的ECMA Object.create有一个可选的第二个参数propertiesObject,可以实例化属性值,但它是一个对象而不是通常的列表,并且使用起来很笨拙。例如,我相信:
o2 = Object.create({}, { p: { value: 42, writable: true, enumerable: true, configurable: true } });

等价于更简单的旧方式:

o2 = new function(p) { this.p=p }(42)

and

o2 = Object.creator({p:''}, 42)

2
您在这里的主要误解是F具有全局作用域。它在Object.create的方法体中声明,因此仅在该方法块内部范围内。

2
而且,令C++开发者惊讶的是,它在块退出时并没有被销毁。(提示:创建的对象保留有引用,因此即使不直接访问,垃圾回收也不会将其清除。) - Javier

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