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]] 的內部方法(其實這種對象就是構造器),否則會拋出異常,比如:

  1. var Str = "test";
  2. var aStr = new Str;
  3. // FF 顯示“Str is not a constructor”
  4. // IE 顯示“對象不支持此操作”
  5. var Num = new Number(999);
  6. var aNum = new Num;
  7. // 結果同上

如果符合以上條件,那麼引擎將調用其 [[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]] 方法來模擬此流程(其實已有衆多前輩做過此工作):

  1. function MyObject(age) {
  2. this.age = age;
  3. }
  4. MyObject.construct = function() {
  5. var o = {}, Constructor = MyObject;
  6. o.__proto__ = Constructor.prototype;
  7. // FF 支持用戶引用內部屬性 [[Prototype]]
  8. Constructor.apply(o, arguments);
  9. return o;
  10. };
  11. var obj1 = new MyObject(10);
  12. var obj2 = MyObject.construct(10);
  13. alert(obj2 instanceof MyObject);
  14. // true

到此,new 運算的過程已經描述得足夠清楚了,然而,如果你還想繼續瞭解內部方法 [[Call]] 的詳情,不好意思,那就要牽涉到 JS 的函數閉包、作用域鏈,甚至深入到引擎對函數體的解析等內容了,這些又是 JS 的另外一個難點系列,在此便不多談了。

本文轉載自:JS 對象機制深剖——new 運算符

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章