Fork me on GitHub

JS 对象机制深剖——new 运算符

  • 其实关于 new 的讨论,早有众多前辈做了先行。然而作为 JS 对象机制系列的一个重要成员,这一篇不可少,而且按照自己的惯例,我将首先引用语言规范的内容。另外,本篇引用到的规范内容比较多,不过我会做详细的说明,读者朋友可自行选择阅读规范内容。

考察 ECMAScript 语言规范中 new 运算符的定义:

The new Operator

The production NewExpression : new NewExpression is evaluated as follows:
  1. Evaluate NewExpression.
  2. Call GetValue(Result(1)).
  3. If Type(Result(2)) is not Object, throw a TypeError exception.
  4. If Result(2) does not implement the internal [[Construct]] method, throw a TypeError exception.
  5. Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).
  6. Return Result(5).
  • 注意到,这段定义中的 new 后的表达式不带参数,即是说,这段内容针对的是诸如 obj = new Object; 这样的用法——带参数的用法如 str = new String(“test”); 将在下面给出。两者有区别且区别不大。

回到上述定义,其大意是,new 后必须跟一个对象并且此对象必须有一个名为 [[Construct]] 的内部方法(其实这种对象就是构造器),否则会抛出异常,比如:

var Str = "test";
var aStr = new Str;
// FF 显示“Str is not a constructor”
// IE 显示“对象不支持此操作”

var Num = new Number(999);
var aNum = new Num;
// 结果同上

如果符合以上条件,那么引擎将调用其 [[Construct]] 内部方法,并不提供入口参数。接下来便要考察此内部方法。

另外,下面一段是 new 运算符的带参用法,由于和无参用法区别不大,读者朋友可直接略过。

The production MemberExpression : new MemberExpression Arguments is evaluated as follows:
  1. Evaluate MemberExpression.
  2. Call GetValue(Result(1)).
  3. Evaluate Arguments, producing an internal list of argument values (11.2.4).
  4. If Type(Result(2)) is not Object, throw a TypeError exception.
  5. If Result(2) does not implement the internal [[Construct]] method, throw a TypeError exception.
  6. Call the [[Construct]] method on Result(2), providing the list Result(3) as the argument values.
  7. Return Result(6).

考察 [[Construct]] 内部方法,先给出语言规范的描述:

When the [[Construct]] property for a Function object F is called, the following steps are taken:
  1. Create a new native ECMAScript object.
  2. Set the [[Class]] property of Result(1) to “Object”.
  3. Get the value of the prototype property of the F.
  4. If Result(3) is an object, set the [[Prototype]] property of Result(1) to Result(3).
  5. If Result(3) is not an object, set the [[Prototype]] property of Result(1) to the original Object prototype object as described in 15.2.3.1.
  6. Invoke the [[Call]] property of F, providing Result(1) as the this value and providing the argument list passed into [[Construct]] as the argument values.
  7. If Type(Result(6)) is Object then return Result(6).
  8. Return Result(1).
  • 引擎将先创建一个语言原生对象,即“{}”或“new Object”,在此我们称之为 O,然后设置其内部属性标识 [[Class]] 为“Object”。接下来,得到构造器 F 的 prototype(根据后文的意思,它可能不是一个对象)。如果 F.prototype 是对象,那么将 O 的内部 [[Prototype]] 属性指向 F.prototype。
  • 请注意,诸如 [[Prototype]] 等为引擎内部标识符,对我们并不可见。[[Prototype]] 正是用于给内部维护原型链,虽然在我们看来,一个对象实例无法直接回溯到其原型(然而引擎内部可以),必须通过构造器中转,即 obj.constructor.prototype。
  • 接着,如果 F.prototype 不是 object,那么将 O 的内部 [[Prototype]] 属性指向“the Object prototype object”(你可以参考这里)。等到 O 的 [[Prototype]] 有了自己的归属以后,引擎调用构造器 F 的 [[Call]] 内部方法,以 O 作为 this 对象,并将传入 [[Construct]] 的参数作为入口参数——如果有的话(即诸如“new Object()”最后括号内的参数)传递过去。最后,如果 [[Call]] 的返回值是对象,那么创建成功并返回此对象,否则回头重来。

根据这些内容,我们完全可以构造一个伪 [[Construct]] 方法来模拟此流程(其实已有众多前辈做过此工作):

function MyObject(age) {
    this.age = age;
}

MyObject.construct = function() {
    var o = {}, Constructor = MyObject;
    o.__proto__ = Constructor.prototype;
    // FF 支持用户引用内部属性 [[Prototype]]

    Constructor.apply(o, arguments);
    return o;
};

var obj1 = new MyObject(10);
var obj2 = MyObject.construct(10);
alert(obj2 instanceof MyObject);
// true

  • 到此,new 运算的过程已经描述得足够清楚了,然而,如果你还想继续了解内部方法 [[Call]] 的详情,不好意思,那就要牵涉到 JS 的函数闭包、作用域链,甚至深入到引擎对函数体的解析等内容了,这些又是 JS 的另外一个难点系列,在此便不多谈了。
附录
PROPERTYDESCRIPTION
[[CONSTRUCT]] Constructs an object. Invoked via the new operator. Objects that implement this internal method are called constructors.
[[CLASS]] A string value indicating the kind of this object.
[[CALL]] Executes code associated with the object. Invoked via a function call expression. Objects that implement this internal method are called functions.
[[PROTOTYPE]] The prototype of this object.
posted on 2012-07-04 07:42 【艾伦】 阅读(...) 评论(...) 编辑 收藏