1. 程式人生 > >深入理解JS中的物件(二):new 的工作原理

深入理解JS中的物件(二):new 的工作原理

**目錄** - 序言 - 不同返回值的建構函式 - 深入 new 呼叫函式原理 - 總結 - 參考
**1.序言** 在 [深入理解JS中的物件(一):原型、原型鏈和建構函式](https://www.cnblogs.com/forcheng/p/12866827.html) 中,我們分析了JS中是否一切皆物件以及物件的原型、原型鏈和建構函式。在談到建構函式時,應該有注意到箭頭函式是不能作為建構函式的,也就是不能使用 new 關鍵字呼叫箭頭函式,這是為什麼呢?我們將在本篇深入討論剖析物件的構造(new)的工作原理。
**2.不同返回值的建構函式** 先看幾個示例: (1)沒有 return 的建構函式 ```js function Foo(x) { this.x = x } var foo = new Foo(10) console.log(foo.x) // 10 ```
(2) return 一個 object 的建構函式 ```js 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 的建構函式 ```js 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 運算子](https://yanhaijing.com/es5/#163) 的規範。 針對有無引數進行執行提供了兩種規範,由於兩者區別很小,這裡只選取無參規範分析: > 產生式 NewExpression : new NewExpression 按照下面的過程執行 : > > 1. 令 ref 為解釋執行 NewExpression 的結果 . > 2. 令 constructor 為 GetValue(ref). > 3. 如果 Type(constructor) is not Object ,丟擲一個 TypeError 異常 . > 4. 如果 constructor 沒有實現 [[Construct]] 內部方法 ,丟擲一個 TypeError 異常 . > 5. 返回呼叫 constructor 的 [[Construct]] 內部方法的結果 , 按無引數傳入引數列表 ( 就是一個空的引數列表 ). 簡單解析: 第1~3步,主要是從引用型別中得到一個物件真正的值(constructor),並判斷其型別是不是一個物件。 第4步,判斷建構函式是否實現了 [[Construct]] 內部方法,如果沒有則丟擲異常。 第5步,呼叫建構函式的 [[Construct]] 內部方法,並返回其結果。
**解答第一個問題:箭頭函式為什麼不能作為建構函式?** 箭頭函式剛好符合上述第4步中的情況,其沒有實現 `[[Construct]]`方法,以下來自ES6中 [Arrow functions](https://exploringjs.com/es6/ch_arrow-functions.html) 規範參考: > 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 property `prototype`. Arrow functions have neither, which is why `new (() => {})` throws an error. 在瀏覽器中測試用 new 呼叫箭頭函式報錯,如下圖: ![new 呼叫箭頭函式報錯](https://img2020.cnblogs.com/blog/898684/202005/898684-20200513175835591-749369788.png)
**解答第二個問題:為什麼建構函式顯式的返回了物件型別的值會影響最終建立的物件?** 從 new 運算子的規範來看,用 new 呼叫函式 F,相當於觸發 F 的 [[Construct]] 內部方法,所以我們需要再看看 EcmaScript 5.1標準中的 [[Construct]] 的[規範](https://yanhaijing.com/es5/#241): > 當以一個可能的空的引數列表呼叫函式物件 F 的 [[Construct]] 內部方法,採用以下步驟: > > 1. 令 obj 為新建立的 ECMAScript 原生物件。 > 2. 依照 8.12 設定 obj 的所有內部屬性。 > 3. 設定 obj 的 [[Class]] 內部屬性為 "Object"。 > 4. 設定 obj 的 [[Extensible]] 內部屬性為 true。 > 5. 令 proto 為以引數 "prototype" 呼叫 F 的 [[Get]] 內部屬性的值。 > 6. 如果 Type(proto) 是 Object,設定 obj 的 [[Prototype]] 內部屬性為 proto。 > 7. 如果 Type(proto) 不是 Object,設定 obj 的 [[Prototype]] 內部屬性為 15.2.4 描述的標準內部的 Object 的 prototype 物件。 > 8. 以 obj 為 this 值, 傳遞給 [[Construct]] 的引數列表為 args,呼叫 F 的 [[Call]] 內部方法,令 result 為呼叫結果。 > 9. 如果 Type(result) 是 Object,則返回 result。 > 10. 返回 obj 簡單解析: 第1~7步,主要建立了一個原生物件 obj,並給這個 obj 設定各種屬性(包括 [[Prototype]] 內部屬性,即物件的原型)。 第8步,相當於 result = `F.[[Call]].apply(obj, args)`,為了更清楚 [[Call]] 內部方法做了些什麼,將在下面從規範層次做出解讀。 第9、10步,就是判斷 result 的型別是不是物件?如果是物件,則返回 result;如果不是,則返回 obj。
EcmaScript 5.1標準中的 [[Call]] 的[規範](https://yanhaijing.com/es5/#240): > 當用一個 this 值,一個引數列表呼叫函式物件 F 的 [[Call]] 內部方法,採用以下步驟: > > 1. 用 F 的 [[FormalParameters]] 內部屬性值,引數列表 args,10.4.3 描述的 this 值來建立 函式程式碼 的一個新執行環境,令 funcCtx 為其結果。 > 2. 令 result 為 FunctionBody(也就是 F 的 [[Code]] 內部屬性,即函式 F 自身)解釋執行的結果。如果 F 沒有 [[Code]] 內部屬性或其值是空的 FunctionBody,則 result 是 (normal, undefined, empty)。 > 3. 退出 funcCtx 執行環境,恢復到之前的執行環境。 > 4. 如果 result.type 是 throw 則丟擲 result.value。 > 5. 如果 result.type 是 return 則返回 result.value。 > 6. 否則 result.type 必定是 normal。返回 undefined。 簡單解析:首先,建立根據相關引數和屬性建立一個新的執行上下文,然後執行函式 F 的程式碼,並令 result 為其呼叫結果, 然後退出當前執行上下文,最後根據 result.type 返回對應的值。(實質上就是執行了一遍函式,返回其結果)
因此,我們可以對上面所列舉的三個不同返回值的建構函式的示例一個合理的解釋了: new 呼叫建構函式,如果建構函式中顯式的 return 了值並且其型別是一個物件,那麼這個值將替代建立的原生物件 obj 作為最終返回值,否則最終將返回建立的原生物件 obj。
**4.總結** new 呼叫函式 F: 1. 獲取函式 F 引用的真正的值 constructor,如果其不是物件或其沒有實現 [[Construct]] 內部方法,都會丟擲異常 2. 返回呼叫 constructor 的 [[Construct]] 內部方法的結果 1. 新建立一個 ES 原生物件 obj 2. 為 obj 設定各種屬性(包括原型屬性等) 3. 令 result = `constructor.[[Call]].apply(obj, args) `,其中 args 是傳遞給 [[Construct]] 的引數列表,[[Call]] 相當於函式 F 自身 4. 如果 result 的型別是物件,則返回 result,否則返回 obj
**5.參考** [深入理解JavaScript系列(18):面向物件程式設計之ECMAScript實現(推薦)](https://www.cnblogs.com/TomXu/archive/2012/02/06/2330609.html) [詳解 JS 中 new 呼叫函式原理](https://juejin.im/post/5b397b526fb9a00e5d7999a4) [ECMAScript5.1中文版](https://yanhaijing.com/es5) [ES6 - Arrow functions](https://exploringjs.com/es6/ch_arrow-function