我們先來思考一下下面代碼的輸出,來引入今天的問題:
const a = new Foo()
console.log(a.id) // 1
const b = Foo()
console.log(b.id) // 2
const c = new Foo()
console.log(c.id) // 3
id
是函數對象Foo
中的成員屬性,每一次對Foo
函數的調用,得到一個Foo
對象的實例,打印id
值,引出兩個問題:
- 每個實例對象的id值每次調用都是遞增的,是怎麼實現的呢?
new Foo()
和Foo()
調用的區別是什麼呢?
本着能動手就先動手實現的原則,後面再分析原理,我先寫代碼實現一波先:
第一版
let tmp = 1
function Foo() {
if (this instanceof Foo) {
this.id = tmp++
} else {
return new Foo()
}
}
我們實現了上圖展示的效果,但是這裏用到了全局變量tmp
,我們都知道全局變量會透着一股不好的味道,會引出意想不到的bug出來,所以纔會出現諸如模塊化機制
來隔離變量作用域等;所以,我們打算第二版引入閉包來優化一下代碼,如下所示:
第二版
let Foo = (function() {
let tmp = 1
return function() {
if (this instanceof Foo) {
this.id = tmp++
} else {
return new Foo()
}
}
})()
這裏我們是通過 立即執行函數 的方式去實現的,tmp
封裝在立即執行函數的內部;現在,在Foo
函數的外部是訪問不到tmp
變量的,並且返回一個函數作爲返回值,裏面的這個內部函數我們就叫做 閉包 了,閉包就可以訪問到外部函數的tmp
變量。
相信讀者讀到這裏,關於第一個問題的解答應該是解釋清楚了:本質就是通過閉包(內部函數)調用一個內部變量遞增實現給id
屬性賦值。
接下來我們來解答一下第二個問題:new
運算符加與不加有何區別?
直接引入MDN上面的解釋吧:
new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。new 關鍵字會進行如下的操作:
1.創建一個空的簡單JavaScript對象(即{});
2.鏈接該對象(即設置該對象的構造函數)到另一個對象 ;
3.將步驟1新創建的對象作爲this的上下文 ;
4.如果該函數沒有返回對象,則返回this。
我們先來看一下下面這一段代碼
function Foo() {
this.id = 1
}
const f = Foo()
console.log(f.id) // TypeError: Cannot read property 'id' of undefined
這裏沒有加 new
運算符, 僅僅是普通的 foo()
函數調用,this
的指向是根據運行時決定的,很明顯 this
目前所在的環境是 全局上下文對象
,在瀏覽器環境下,foo()
的調用就等價於 window.foo()
的調用,因此, this
的指向就是 window
本身啦,this.id = 1
的操作本質上就相當於window.id = 1
console.log(window.id) // 1
說了一大堆,接下來正式進入主題,加上new
運算符之後實際上就是改變了函數對象內部this
的指向了,這時候this
指向了該函數對象創建的實例本身了。
function Foo() {
this.id = 1
}
const f = new Foo()
console.log(f.id) // 1
再返回來看一下剛開始的問題:
const b = Foo()
console.log(b.id) // 2
這個 b
實例看上去只是普通的函數對象調用,反而能夠使用了內部的id屬性,這不是很奇怪嗎?我們再去看裏面的具體函數實現:
let Foo = (function() {
let tmp = 1
return function() {
if (this instanceof Foo) {
this.id = tmp++
} else {
return new Foo()
}
}
})()
原來內部使用了 instanceof
運算符去判斷了一下 當前的 this
是否爲 Foo
創建的實例,否則會再次通過 new
去調用一次自身去獲得一個實例對象,這樣就能正確的使用到它的id
屬性啦。
結論: 在 js
裏面,我們所說的 構造函數 無非其實就是在 普通函數 前面加上一個 new
運算符,以此來獲得函數對象的屬性,本質上還是this
的指向問題。所以大家不要被迷惑了。
關於 new
的小結暫且告一段落,後期想到再做補充~