目錄
- 序言
- 不同返回值的構造函數
- 深入 new 調用函數原理
- 總結
- 參考
1.序言
在 深入理解JS中的對象(一):原型、原型鏈和構造函數 中,我們分析了JS中是否一切皆對象以及對象的原型、原型鏈和構造函數。在談到構造函數時,應該有注意到箭頭函數是不能作為構造函數的,也就是不能使用 new 關鍵字調用箭頭函數,這是為什么呢?我們將在本篇深入討論剖析對象的構造(new)的工作原理。
2.不同返回值的構造函數
先看幾個示例:
(1)沒有 return 的構造函數
function Foo(x) {
this.x = x
}
var foo = new Foo(10)
console.log(foo.x) // 10
(2) return 一個 object 的構造函數
function Foo(x) {
this.x = x
return { y: 20 }
}
var foo = new Foo(10)
console.log(foo) // { y: 20 }
console.log(foo.x) // undifined
console.log(foo.y) // 20
(3) return 一個非 object 的構造函數
function Foo(x) {
this.x = x
return 20
}
var foo = new Foo(10)
console.log(foo.x) // 10
簡單分析一下:
第(1)中情況中,在構造函數中,沒有任何顯式的 return,最終返回的是 this 值。
第(2)種情況中,在構造函數中,似乎this被舍棄掉了,最終返回的是顯式 return 的 object。
第(3)中情況中,在構造函數中,雖然顯式 return 了一個非對象的 number,但似乎被舍棄掉了,最終返回的是 this 值。
從上述情況可以得出,構造函數顯式的返回了對象類型的值,會影響最終創建的對象。要弄明白這是為什么,我們就需要明白 new 調用函數到底做了些什么操作。
3.深入 new 調用函數原理
我們來看看 EcmaScript 5.1標准的規定,了解一下 new 運算符 的規范。
針對有無參數進行執行提供了兩種規范,由於兩者區別很小,這里只選取無參規范分析:
產生式 NewExpression : new NewExpression 按照下面的過程執行 :
- 令 ref 為解釋執行 NewExpression 的結果 .
- 令 constructor 為 GetValue(ref).
- 如果 Type(constructor) is not Object ,拋出一個 TypeError 異常 .
- 如果 constructor 沒有實現 [[Construct]] 內部方法 ,拋出一個 TypeError 異常 .
- 返回調用 constructor 的 [[Construct]] 內部方法的結果 , 按無參數傳入參數列表 ( 就是一個空的參數列表 ).
簡單解析:
第1~3步,主要是從引用類型中得到一個對象真正的值(constructor),並判斷其類型是不是一個對象。
第4步,判斷構造函數是否實現了 [[Construct]] 內部方法,如果沒有則拋出異常。
第5步,調用構造函數的 [[Construct]] 內部方法,並返回其結果。
解答第一個問題:箭頭函數為什么不能作為構造函數?
箭頭函數剛好符合上述第4步中的情況,其沒有實現 [[Construct]]
方法,以下來自ES6中 Arrow functions 規范參考:
An arrow function is different from a normal function in only two ways:
- The following constructs are lexical:
arguments
,super
,this
,new.target
- It can’t be used as a constructor: Normal functions support
new
via the internal method[[Construct]]
and the propertyprototype
. Arrow functions have neither, which is whynew (() => {})
throws an error.
在瀏覽器中測試用 new 調用箭頭函數報錯,如下圖:
解答第二個問題:為什么構造函數顯式的返回了對象類型的值會影響最終創建的對象?
從 new 運算符的規范來看,用 new 調用函數 F,相當於觸發 F 的 [[Construct]] 內部方法,所以我們需要再看看 EcmaScript 5.1標准中的 [[Construct]] 的規范:
當以一個可能的空的參數列表調用函數對象 F 的 [[Construct]] 內部方法,采用以下步驟:
- 令 obj 為新創建的 ECMAScript 原生對象。
- 依照 8.12 設定 obj 的所有內部屬性。
- 設定 obj 的 [[Class]] 內部屬性為 "Object"。
- 設定 obj 的 [[Extensible]] 內部屬性為 true。
- 令 proto 為以參數 "prototype" 調用 F 的 [[Get]] 內部屬性的值。
- 如果 Type(proto) 是 Object,設定 obj 的 [[Prototype]] 內部屬性為 proto。
- 如果 Type(proto) 不是 Object,設定 obj 的 [[Prototype]] 內部屬性為 15.2.4 描述的標准內部的 Object 的 prototype 對象。
- 以 obj 為 this 值, 傳遞給 [[Construct]] 的參數列表為 args,調用 F 的 [[Call]] 內部方法,令 result 為調用結果。
- 如果 Type(result) 是 Object,則返回 result。
- 返回 obj
簡單解析:
第1~7步,主要創建了一個原生對象 obj,並給這個 obj 設定各種屬性(包括 [[Prototype]] 內部屬性,即對象的原型)。
第8步,相當於 result = F.[[Call]].apply(obj, args)
,為了更清楚 [[Call]] 內部方法做了些什么,將在下面從規范層次做出解讀。
第9、10步,就是判斷 result 的類型是不是對象?如果是對象,則返回 result;如果不是,則返回 obj。
EcmaScript 5.1標准中的 [[Call]] 的規范:
當用一個 this 值,一個參數列表調用函數對象 F 的 [[Call]] 內部方法,采用以下步驟:
- 用 F 的 [[FormalParameters]] 內部屬性值,參數列表 args,10.4.3 描述的 this 值來建立 函數代碼 的一個新執行環境,令 funcCtx 為其結果。
- 令 result 為 FunctionBody(也就是 F 的 [[Code]] 內部屬性,即函數 F 自身)解釋執行的結果。如果 F 沒有 [[Code]] 內部屬性或其值是空的 FunctionBody,則 result 是 (normal, undefined, empty)。
- 退出 funcCtx 執行環境,恢復到之前的執行環境。
- 如果 result.type 是 throw 則拋出 result.value。
- 如果 result.type 是 return 則返回 result.value。
- 否則 result.type 必定是 normal。返回 undefined。
簡單解析:首先,創建根據相關參數和屬性創建一個新的執行上下文,然后執行函數 F 的代碼,並令 result 為其調用結果, 然后退出當前執行上下文,最后根據 result.type 返回對應的值。(實質上就是執行了一遍函數,返回其結果)
因此,我們可以對上面所列舉的三個不同返回值的構造函數的示例一個合理的解釋了:
new 調用構造函數,如果構造函數中顯式的 return 了值並且其類型是一個對象,那么這個值將替代創建的原生對象 obj 作為最終返回值,否則最終將返回創建的原生對象 obj。
4.總結
new 調用函數 F:
- 獲取函數 F 引用的真正的值 constructor,如果其不是對象或其沒有實現 [[Construct]] 內部方法,都會拋出異常
- 返回調用 constructor 的 [[Construct]] 內部方法的結果
- 新創建一個 ES 原生對象 obj
- 為 obj 設置各種屬性(包括原型屬性等)
- 令 result =
constructor.[[Call]].apply(obj, args)
,其中 args 是傳遞給 [[Construct]] 的參數列表,[[Call]] 相當於函數 F 自身 - 如果 result 的類型是對象,則返回 result,否則返回 obj
5.參考