你真的瞭解JS裡的"new"嗎?
我們常常喜歡用new
關鍵字去建立一些物件如new Vue()
,但是這個關鍵字的背後究竟做了什麼其實沒太多人去關注。
想象我們是蘋果公司,要生產30部iPod,規定:
- 每臺iPod都會有自己的ID
- 每臺iPod都是一樣的製造商:Apple
- 每臺iPod的功能都是一樣的(函式一樣)
letiPod
={
// 製造商不會變
manufacturer
:'Apple',
// 播放音樂
play:
function
()
{
},
// 暫停
pause:
function
()
{
},
// 繼續播放
resume:
function
()
{
}
}
好了,現在我們開始生產吧。
簡單生產
要生產那麼多iPod,那就迴圈30次吧。每次迴圈都建立一個物件,將這個物件加入到數組裡就行了。
letbox
=[]
let iPod
for(let
i
=0;
i
<30;
i
++){
iPod
={
// 每次都改變 ID
id
:i
,
// 製造商不會變
manufacturer
:'Apple',
// 播放音樂
play:
function
()
{},
// 暫停
pause:
function
()
{},
// 繼續播放
resume:
function
()
{}
}
box
.push(iPod
)
}
Manufacturer
.deliver(box
)
但是,這裡有一個問題:每次都會新建立play()
,pause()
,resume()
這些函式,manufacturer
使用原型改進
使用原理鏈,我們可以將上面說到的共有函式,屬性放在一個共有物件裡,然後用iPod.__proto__
指向這個iPodCommon
不就好了嗎?所以現在程式碼可以改寫成這樣:
letbox
=[]
let iPod
letiPodCommon
={
// 製造商不會變
manufacturer
:'Apple',
// 播放音樂
play:
function
()
{},
// 暫停
pause:
function
()
{},
// 繼續播放
resume:
function
()
{}
}
for(let
i
=0;
i
<30;
i
++){
iPod
={
// 每次都改變 ID
id
:i
,
}
iPod
.__proto__
=iPodCommon
box
.push(iPod
)
}
Manufacturer
.deliver(box
)
這樣就好多了,省了很多空間。但是這個iPod物件的程式碼有點太分散了,跟for迴圈耦合在一起了。學習重構時聽得最多的一句就是重複程式碼最好用函式包起來,所以我們可以試著傳入要改變的屬性(ID)用函式來返回iPod物件。,
函式返回物件
我們可以用一個函式返回iPod物件,這樣就不用每次都在for迴圈裡去定義物件了。
functioniPod(id)
{
let
tempObj
={
}
// 自有屬性
tempObj
.id
=id
// 共有屬性,函式
tempObj
.__proto__
=iPod
.common
return
tempObj
}
iPod
.common
={
// 製造商不會變
manufacturer
:'Apple',
// 播放音樂
play:
function
()
{},
// 暫停
pause:
function
()
{},
// 繼續播放
resume:
function
()
{}
}
// 儲存為 iPod.js 檔案
然後在建立時候引入這個檔案,再去生成iPod。
letbox
=[]
for(let
i
=0;
i
<30;
i
++){
box
.push(iPod(i
))
}
Manufacturer
.deliver(box
)
有沒有感覺這樣清爽了很多?我們將所有有關iPod的邏輯都放在一個檔案裡,這樣就和主檔案完全解耦了。
new
上面是很清爽,但是每次都要寫建立一個臨時物件好麻煩。這時候JS的new就上場了,它的作用如下:
- 幫你建立臨時物件
tempObj
,函式裡的this
繫結為這個tempObj
- 統一共有屬性所在物件的名字叫
prototype
而不是comon
- 幫你完成原型的繫結
- 幫你返回臨時物件
tempObj
現在iPod.js檔案可以寫成這樣
functioniPod(id)
{
this.
id
=id
}
// 共有屬性
iPod
.prototype
={
// 製造商不會變
manufacturer
:'Apple',
// 播放音樂
play:
function
()
{},
// 暫停
pause:
function
()
{},
// 繼續播放
resume:
function
()
{}
}
// 儲存為 iPod.js 檔案
使用new
再次生產iPod
letbox
=[]
for(let
i
=0;
i
<30;
i
++){
box
.push(newiPod(
i
))
}
Manufacturer
.deliver(box
)
這就是new
的由來,不過是一種語法糖,和Java裡面的new
是完全不一樣的東西,希望大家不要混為一談。當然了,最後的這個iPod
函式也就成了我們所說的建構函式。
Js中萬物皆物件。
js裡的物件,屬性等說法,其實就是指標,它們指向自己的例項空間(除了基本型別)
先看一個簡單的function變數
function fun1(name) {
this.name = name;
}
console.log("fun1", fun1)
從結果可以看到定義一個function,它裡邊所含有的內容這六個屬性是每個function所必有的,直接看第五個prototype(注意prototype是一個物件)就是傳說中的原型(本文只稱它為prototype),第六個屬性是灰色的並且用尖括號括起來,它這麼不顯眼是因為js就不想讓程式設計師去用它,在以前版本的瀏覽器它有另外一個名字叫__proto__(為了方便區分,下文就以__proto__來稱呼它,而且在以前的版本里名字是__proto__,以下劃線開頭還是兩個說明是js堅決拒絕程式設計師去修改它的,但是下面為了剖析其內部原理我會對其做一些粗暴的改變,大家注意在工程中儘量避免)。
如果大家去實驗一下就會發現,每個物件都會有__proto__這個屬性,但一般情況下只有宣告function的變數(例如上圖中的fun1)才會有(自動生成)prototype這個屬性,而function通過在它的名字前加new 可以創建出屬於它的例項,因此我認為js裡的function有三個角色:函式,物件和類(類似於Java裡的物件可以通過類產生,有人說js和java沒有半毛錢關係,我想說它們都是面向物件的語言,都有物件這個概念,那麼伴隨的也就有類,function宣告的變數就是類)。而prototype這個就體現了function類的概念
可以看到在 prototype裡有兩個屬性constructor和__proto__,在前面我們說過prototype是一個物件和每個物件都會有__proto__這個屬性,因此prototype也是有__proto__這個屬性;constructor(構造方法)這個屬性是在生成prototype時自動生成的屬性,其指向函式本身(在申明函式時,js自動建立該函式的peototype屬性)。
function fun1(name) {
this.name = name;
}
console.log("fun1", fun1)
console.log(fun1.prototype.constructor === fun1)
一個物件__proto__指向產生它的類的prototype(就是指向new 是後邊所跟的那個東西,嚴格來講是它作為左值時等號右邊的東西,function fun1(){}等價於var fun1 = function(){},fun1的__proto__指向Function的prototype)。
function fun1(name) {
this.name = name;
}
var temp = new fun1("");
var obj = new Object;
console.log(temp.__proto__ === fun1.prototype)
console.log(fun1.__proto__ === Function.prototype)
console.log(obj.__proto__ === Object.prototype)
總結一下:
所有物件都有__proto__屬性,是用來通過__proto__找到它的原型即prototype,function宣告的變數的__proto__指向Function的prototype,其它物件的__proto__指向Object的prototype
function宣告的變數、Function和Object都有prototype, 有prototype的東西可以產生例項(即可以作為new 後邊的值)。
prototype它是一個物件(在宣告函式變數是在函式內部由js自動建立),因此它也有__proto__,並且指向Object的prototype。
Function和Object非常特殊,我們來具體分析
首先看一下Function裡的東西
console.log("Function", Function);
console.log("Function.__proto__", Function.__proto__)
從控制檯的列印可以明顯的看出Function的__proto__指向了它自己的prototype
接下里看Object(Object裡有很多其它的東西,為了簡潔我直接列印Object的prototype)
console.log("Object.prototype", Object.prototype)
console.log("Object.__proto__", Object.__proto__)
Object的prototype和Function的prototype的__proro__指向是相同的如下圖:
綜上可以看出Object的__proto__指向Function的prototype,而Object的prototype並沒有灰色的<prototype>即__proto__,即它是一切之源。
console.log("Object.prototype.__proto__", Object.prototype.__proto__)
我將Object的prototype稱為源型,下面我給出我個人對這些現象的解釋:
源型是js的內建的一段程式碼,所有所有通過這個源型創造出的都是object,第一步先創造出Function的prototype,因此這個prototype的__proto__指向源型,然後再通過這個prototype造出Function,因此Function的__proto__指向它自己的prototype,然後用Function造出Object,因此Object的__proto__指向Function的prototype,然後js直接將Object的prototype替換為源型。
並且我認為js裡判斷繼承(即A instanceof B)是沿著__proto__往下走,首先要求B必須有prototype屬性且必須是一個物件(否則會瀏覽器會報 'prototype' property of B is not an object),判斷時先判斷A的__proto__是否和B的prototype指向是否相同(即===結果為true),若相同則返回true,若不相同則判斷A的__proto__指向屬性裡是否還有__proto__屬性,若有則進行再次進行判斷指向是否相同,直到找到源型,它的__protot__為null,返回false
為了證明只要A的__protot__和B的prototype指向相同就返回true,給瞭如下測試:
var obj = new Object;
function fun () {};
console.log(obj instanceof fun);
var temp = new Object
fun.prototype = temp
obj.__proto__ = temp;
console.log(obj instanceof fun)
有點顛覆三觀不過習慣就好。
下面用我的結論來解釋下邊四個現象:
console.log(Function instanceof Function)
console.log(Function instanceof Object)
console.log(Object instanceof Function)
console.log(Object instanceof Object)
Function的__proto__和Function的prototype指向相同,因此返回true,
Function的__proto__和Function的prototype指向指向相同,Function的prototype的__protot__和Object的prototype指向相同,因此返回true。
Object的__proto__和Function的prototype指向相同(因為Object就是以Function為模板創造的),因此返回true。
Object的__proto__指向Function的prototype,Function的prototype的__proto__指向Object的prototype,這個prototype是屬於Object(饒了一圈),因此返回true。
只要高清內部原理,理解instanceof就非常簡單
下面再來一個小測試:
var obj = new Object;
obj.__proto__ = Function.prototype;
console.log(obj instanceof Function)
總結:peototype是原型,__proto__所指向的以及其後的所有peototype稱為原型鏈。“js裡一切皆物件”倒不如所是js裡的所有物件都是由“源型”生成。
簡單(較優雅的方式)實現繼承
在上篇博文裡講過function宣告的物件(比如fun)有三個角色:函式、物件和類,作為類js會自動給它新增prototype屬性,prototype是一個物件,它裡面第一個屬性就是constructor(建構函式),按道理說通過它(fun)建立的例項時有三步(這和java的new的過程很像)
在記憶體裡分配好所需的空間
執行建構函式,完成對成員的初始化
將空間的首地址賦值給等號有邊的變數
但問題是在第二步的時候,執行的建構函式是prototype裡的constructor嗎
function animal(name) {
this.name = name;
}
animal.prototype.constructor = null;
console.log("animal", animal)
var a = new animal("狗子")
console.log("a", a)
我將animal的prototype的constructor屬性賦值為null,但是a依然被正確的構造出來了,並且a的<prototype>也就是__proto__中的constructor也是null,我認為在構造例項的時候js呼叫建構函式時是直接呼叫animal本身的程式碼,而不是通過animal的prototype訪問其constructor。那麼constructor是不是就沒有用了呢?在這裡我覺得prototype裡的constructor屬性只是給一個物件提供找到它的夠造函式來用的,給它一個追根溯源的機會(可能我見識淺薄暫時看不到它的用處)。
什麼是繼承:繼承要實現的目的就是子類的物件可以呼叫父類提供的一些公共的屬性和方法,子類物件是通過__proto__來尋找的。當子類物件要訪問某一個方法或屬性,js會先在子類物件裡尋找是否有該屬性若沒有則沿著__proto__指向的prototype空間找,找到了則返回,子類物件只能訪問而不能修改,若要試圖給父類的屬性賦值,則js只會在該物件建立一個名稱一樣的變數而不會修改父類的prototype空間裡的值。
那麼如上文所說直接修改類的prototype的__protot__屬性,讓它指向另一個類的prototype不就可以實現繼承嗎,但我們堅決拒絕直接修改__proto__。先講一下思路:首先我們需要讓一個類的prototype的__proto__指向另一個類的prototype,但是不能直接修改prototype的__proto__屬性,但我們可以修改該類的prototype屬性,也就是我們可以建立一個物件,讓該物件的__proto__指向父類的prototype並且讓這個物件作為子類的prototype。
具體做法:先宣告一個function變數,讓其的prototype指向父類的prototype,並new 它,產生的物件的__proto__就指向了父類的prototype,然後將它作為子類的prototype就完成了繼承(那種直接new 父類並將產生的物件作為子類的方法是不可取的,因為在new父類時會呼叫父類的建構函式生成很多垃圾屬性,這些屬性不應該存在在prototype中)
function ExtendClass(base, klass) {
if (base == undefined || base == null || klass == undefined || klass == null || !base instanceof Object || !klass instanceof Function) {
return;
}
function fun() {};
fun.prototype = base.prototype;
klass.prototype = new fun();
klass.prototype.constructor = klass;
}
function Anaimal(name) {
this.name = name;
}
function Dog(name, type) {
Anaimal.call(this, name)
this.type = type;
}
ExtendClass(Anaimal, Dog);
var dog = new Dog("小白", "京巴");
console.log("Anaimal", Anaimal)
console.log("Dog", Dog)
console.log("dog", dog)
我編寫了一個繼承的函式,繼承函式必須在類變數聲明後在prototype修改前呼叫,否則以前的修改無效。
從上圖可以得知我們成功讓Dog繼承Animal,即讓Dog的prototype的__proto__指向Animal的prototype屬性。這時dog就可以呼叫它的原型鏈裡面的所有方法和屬性。
一般在new的過程中我們是先呼叫父類的構造方法,然後依次執行,所以我們在new Dog時要在內部先呼叫Animal的構造方法(因為哪個類繼承與哪個類是程式設計師所知道的,因此可以直接指定呼叫哪個方法作為父類構造方法),但是如果你在Dog程式碼塊內部直接呼叫Animal(“”)方法它的預設執行物件是window,即最終的結果不是給dog裡增加一個name屬性,而是用Animal類建立了一個匿名物件,因此使用call函式,它的第一個引數是執行的物件,之後是形參,我將this傳進去就相當於我將執行物件變為dog,這樣dog就成功執行了父類的構造方法。
————————————————
版權宣告:本文為CSDN博主「Backee」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結及本宣告。