JS 对象机制深剖——new 运算符
- 其实关于 new 的讨论,早有众多前辈做了先行。然而作为 JS 对象机制系列的一个重要成员,这一篇不可少,而且按照自己的惯例,我将首先引用语言规范的内容。另外,本篇引用到的规范内容比较多,不过我会做详细的说明,读者朋友可自行选择阅读规范内容。
考察 ECMAScript 语言规范中 new 运算符的定义:
The new Operator
The production NewExpression : new NewExpression is evaluated as follows:
- Evaluate NewExpression.
- Call GetValue(Result(1)).
- If Type(Result(2)) is not Object, throw a TypeError exception.
- If Result(2) does not implement the internal [[Construct]] method, throw a TypeError exception.
- Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).
- 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:
- Evaluate MemberExpression.
- Call GetValue(Result(1)).
- Evaluate Arguments, producing an internal list of argument values (11.2.4).
- If Type(Result(2)) is not Object, throw a TypeError exception.
- If Result(2) does not implement the internal [[Construct]] method, throw a TypeError exception.
- Call the [[Construct]] method on Result(2), providing the list Result(3) as the argument values.
- Return Result(6).
考察 [[Construct]] 内部方法,先给出语言规范的描述:
When the [[Construct]] property for a Function object F is called, the following steps are taken:
- Create a new native ECMAScript object.
- Set the [[Class]] property of Result(1) to “Object”.
- Get the value of the prototype property of the F.
- If Result(3) is an object, set the [[Prototype]] property of Result(1) to Result(3).
- 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.
- 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.
- If Type(Result(6)) is Object then return Result(6).
- 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 的另外一个难点系列,在此便不多谈了。
PROPERTY | DESCRIPTION |
---|---|
[[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. |