其實關於 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 的另外一個難點系列,在此便不多談了。
本文轉載自:JS 對象機制深剖——new 運算符