JavaScript 中經常使用建構函式建立物件(通過 new
操作符呼叫一個函式),那在使用 new
呼叫一個函式的時候到底發生了什麼?先看幾個例子,再解釋背後發生了什麼。
1)看三個例子
1.1 無 return 語句
建構函式最後沒有 return
語句,這也是使用建構函式時預設情況,最後會返回一個新物件,如下:
function Foo(age) {
this.age = age;
}
var o = new Foo(111);
console.log(o);
複製程式碼
這是常見的使用建構函式建立物件的過程,列印出來的是 {age: 111}
。
1.2 return 物件型別資料
建構函式最後 return
物件型別資料:
function Foo(age) {
this.age = age;
return { type: "我是顯式返回的" };
}
var o = new Foo(222);
console.log(o);
複製程式碼
列印出來的是 {type: '我是顯式返回的'}
,也就是說,return
之前的工作都白做了,最後返回 return
後面的物件。
1.3 return 基本型別資料
那是不是隻要建構函式體內最後有 return
,返回都是 return
後面的資料呢?
我們看下返回基本型別資料的情況:
function Foo(age) {
this.age = age;
return 1;
}
var o = new Foo(333);
console.log(o);
複製程式碼
列印出來的是 {age: 333}
,和沒有 return
時效果一樣。跟預期不一樣,背後你原理看下面分析。
2)背後原理
2.1 非箭頭函式的情況
當使用 new
操作符建立物件是,ES5 官方文件在 函式定義 一節中做了如下定義 13.2.2 [[Construct]]:
When the [[Construct]]
internal method for a Function
object F
is called with a possibly empty list of arguments, the following steps are taken:
- Let obj be a newly created native ECMAScript object.
- Set all the internal methods of obj as specified in 8.12.
- Set the [[Class]] internal property of obj to Object.
- Set the [[Extensible]] internal property of obj to true.
- Let proto be the value of calling the [[Get]] internal property of F with argument "prototype".
- If Type(proto) is Object, set the [[Prototype]] internal property of obj to proto.
- If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4.
- Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args.
- If Type(result) is Object then return result.
- Return obj.
看第 8、9 步:
8)呼叫函式
F
,將其返回值賦給result
;其中,F
執行時的實參為傳遞給[[Construct]]
(即F
本身) 的引數,F
內部this
指向obj
; 9)如果result
是Object
型別,返回result
;
這也就解釋瞭如果建構函式顯式返回物件型別,則直接返回這個物件,而不是返回最開始建立的物件。
最後在看第 10 步:
10)如果
F
返回的不是物件型別(第 9 步不成立),則返回建立的物件obj
。
如果建構函式沒有顯式返回物件型別(顯式返回基本資料型別或者直接不返回),則返回最開始建立的物件。
2.2 箭頭函式的情況
那如果建構函式是箭頭函式怎麼辦?
箭頭函式中沒有 [[Construct]]
方法,不能使用 new
呼叫,會報錯。
NOTICE:其中 [[Construct]]
就是指建構函式本身。
相關規範在 ES6 的官方文件 中有提,但自從 ES6 以來的官方文件巨難懂,在此不做表述。
3)new 呼叫函式完整過程
3.1 中文描述及相關程式碼分析
除了箭頭函式之外的任何函式,都可以使用 new
進行呼叫,背後發生了什麼,上節英文講述的很清楚了,再用中文描述如下:
1)建立 ECMAScript 原生物件 obj
;
2)給 obj
設定原生物件的內部屬性;(和原型屬性不同,內部屬性表示為 [[PropertyName]]
,兩個方括號包裹屬性名,並且屬性名大寫,比如常見 [[Prototype]]
、[[Constructor]]
)
3)設定 obj
的內部屬性 [[Class]]
為 Object
;
4)設定 obj
的內部屬性 [[Extensible]]
為 true
;
5)將 proto
的值設定為 F
的 prototype
屬性值;
6)如果 proto
是物件型別,則設定 obj
的內部屬性 [[Prototype]]
值為 proto
;(進行原型鏈關聯,實現繼承的關鍵)
7)如果 proto
是不物件型別,則設定 obj
的內部屬性 [[Prototype]]
值為內建建構函式 Object 的 prototype
值;(函式 prototype
屬性可以被改寫,如果改成非物件型別,obj
的 [[Prototype]]
就指向 Object 的原型物件)
8)9)10)見上節分析。(決定返回什麼)
對於第 7 步的情況,見下面程式碼:
function Foo(name) {
this.name = name;
}
var o1 = new Foo("xiaoming");
console.log(o1.__proto__ === Foo.prototype); // true
// 重寫建構函式原型屬性為非物件型別,例項內部 [[Prototype]] 屬性指向 Object 原型物件
// 因為例項是一個物件型別的資料,預設會繼承內建物件的原型,
// 如果建構函式的原型不滿足形成原型鏈的要求,那就跳過直接和內建物件原型關聯
Foo.prototype = 1;
var o2 = new Foo("xiaohong");
console.log(o2.__proto__ === Foo.prototype); // false
console.log(o2.__proto__ === Object.prototype); // true
複製程式碼
3.2 更簡潔的語言描述
若執行 new Foo()
,過程如下:
1)建立新物件 o
;
2)給新物件的內部屬性賦值,關鍵是給[[Prototype]]
屬性賦值,構造原型鏈(如果建構函式的原型是 Object 型別,則指向建構函式的原型;不然指向 Object 物件的原型);
3)執行函式 Foo
,執行過程中內部 this
指向新建立的物件 o
;
4)如果 Foo
內部顯式返回物件型別資料,則,返回該資料,執行結束;不然返回新建立的物件 o
。
4)幾點說明
4.1 判斷是否是 Object 型別
關於一個資料是否是 Object
型別,可以通過 instanceof
操作符進行判斷:如果 x instanceof Object
返回 true
,則 x
為 Object
型別。
由上可知,null instanceof Object
返回 false
,所以 null
不是 Object
型別,儘管typeof null
返回 "Object"。
4.2 instanceof 原理
instanceof
的工作原理是:在表示式 x instanceof Foo
中,如果 Foo
的原型(即 Foo.prototype
)出現在 x
的原型鏈中,則返回 true
,不然,返回 false
。
因為函式的原型可以被改寫,所以會出現在 x
通過 Foo
new 出來之後完全改寫 Foo
的原型 x instanceof Foo
返回 false
的情況。因為例項建立之後重寫建構函式原型,例項指向的原型已經不是建構函式的新的原型了,見下面程式碼:
const Foo = function() {};
const o = new Foo();
o instanceof Foo; // true
// 重寫 Foo 原型
Foo.prototype = {};
o instanceof Foo; // false
複製程式碼
參考資料
What values can a constructor return to avoid returning this?
[[Construct]] internal method