?了解構(gòu)造函數(shù)和原型鏈之前爸黄,我們先復(fù)習(xí)一下關(guān)于引用類型的知識搁凸。
# 引用類型
? 在OO語言中這種數(shù)據(jù)結(jié)構(gòu)通常稱作“類”萌朱,在JS中這么稱呼不太準(zhǔn)確屠阻。是一種用于將數(shù)據(jù)和功能組織在一起的數(shù)據(jù)結(jié)構(gòu)红省。引用類型的最大特征就是,使用一個指針來指向數(shù)據(jù)真正存儲的位置国觉,而指針與數(shù)據(jù)之間并沒有強(qiáng)關(guān)聯(lián)關(guān)系吧恃。常說的對象,是Object引用類型
(以下簡稱Object類型)的實(shí)例麻诀。
? JavaScript中引用類型主要有以下幾種:關(guān)于數(shù)組和對象更詳細(xì)請閱讀引用類型之「對象/數(shù)組」
(1)Object類型:使用鍵值對來存放數(shù)據(jù)的一種數(shù)據(jù)結(jié)構(gòu)痕寓,通常判斷方法有:實(shí)例.constructor === Object
傲醉,實(shí)例 instanceof Object
返回true
,而typeof 實(shí)例 === 'object'
(2)Array類型:數(shù)組中的每一項(xiàng)可以存儲不同類型的數(shù)據(jù)呻率。使用下標(biāo)來取硬毕。實(shí)例.constructor === Array
,實(shí)例 instanceof Array
返回true
礼仗, 而typeof 實(shí)例 === 'array'
(3)Date類型:日期類型吐咳,通常使用UTC時間格式。new Date()
接收毫秒?yún)?shù)元践。當(dāng)直接傳入字符串如2020, 4, 10
時韭脊,其實(shí)是在Date()內(nèi)模擬了Date.UTC()
(Date.parse()
的字符串參數(shù)格式是'月/日/年')進(jìn)行了解析,變成毫秒數(shù)后再傳遞給Date()構(gòu)造函數(shù)的卢厂。注意UTC()
方法中的月份從0開始乾蓬,因此該例子是2020年5月10日(parse()無此問題)。
(4)RegExp類型:正則類型慎恒,語法:var exp = /pattern/flag
任内。pattern定義使用^
符號開始,$
符號結(jié)束融柬。flag中g
表示全查詢死嗦,i
表示不區(qū)分大小寫,m
表示多行查詢粒氧。
(5)Function類型:函數(shù)類型越除。JS中:函數(shù)是對象,函數(shù)名是指針外盯。復(fù)制函數(shù)名只是新增了一個指向函數(shù)對象的指針摘盆。當(dāng)我們把函數(shù)當(dāng)做值在別的函數(shù)中進(jìn)行傳遞時,使用的是函數(shù)名傳遞饱苟,也就是說只是把函數(shù)對象的地址告訴了宿主調(diào)用函數(shù)孩擂。但是如果在傳遞時我們給參數(shù)函數(shù)名增加了括號,此時表示將函數(shù)對象進(jìn)行執(zhí)行后箱熬,把返回值傳遞給宿主調(diào)用函數(shù)类垦。此外,函數(shù)中內(nèi)置有arguments
對象和this
對象城须,前者存放函數(shù)的參數(shù)對象以及callee(指向該函數(shù)名)和caller(指向調(diào)用該函數(shù)的宿主函數(shù))等信息蚤认,而后者存放的是運(yùn)行該函數(shù)時函數(shù)所在的執(zhí)行環(huán)境。當(dāng)我們需要修改或指定函數(shù)執(zhí)行環(huán)境時糕伐,可以使用call()
和apply()
方法砰琢。前者接收多個參數(shù),后者接受兩個參數(shù),第一個參數(shù)均表示要調(diào)用該函數(shù)的執(zhí)行環(huán)境如window氯析、某個私有域
等亏较,該作用域近在本次函數(shù)運(yùn)行時有效。bind()
方法用于將該函數(shù)的執(zhí)行環(huán)境長期綁定到某個環(huán)境對象中掩缓,注意bind()
不改變當(dāng)前函數(shù)的運(yùn)行環(huán)境雪情,而是返回?fù)碛薪壎ōh(huán)境對象的新函數(shù)。
(6)基本包裝類型:String類型你辣,Number類型巡通,Boolean類型。它們的特點(diǎn)是在執(zhí)行過程中自動創(chuàng)建舍哄,且生命周期僅存在于代碼執(zhí)行的那一瞬間宴凉,運(yùn)行結(jié)束即銷毀。這也是為什么作為值類型的String卻擁有slice()
表悬、substring()
弥锄、substr()
、indexOf()
蟆沫、lastIndexOf()
籽暇、trim()
等屬性方法的原因,這些方法都繼承于String類型的原型方法饭庞。
(7)單體內(nèi)置對象:Global對象戒悠、Math對象。Global對象是JS中的“兜底兒對象”舟山,其實(shí)并沒有什么全局變量全局方法绸狐,它們只不過都是Global對象的屬性和方法罷了,在各大Web瀏覽器中累盗,將Global對象作為window對象的一部分加以實(shí)現(xiàn)(window還有別的任務(wù))寒矿,所以通常使用window
來調(diào)用或直接省略window
。比較經(jīng)典的幾個方法是encodeURI
若债、isNaN()
劫窒、parseInt()
、eval()
拆座、構(gòu)造函數(shù)Object
、構(gòu)造函數(shù)Function
冠息、構(gòu)造函數(shù)SybtaxError
挪凑、構(gòu)造函數(shù)TypeError
,而屬性也有undefined
逛艰、NaN
等躏碳。而Math對象中保存了數(shù)學(xué)計(jì)算中可能會用到的一些特殊值和常用方法,如Math.E(自然對數(shù)底數(shù))
散怖、Math.PI
菇绵、Math.SQRT(平方根)
等肄渗、min()
、max()
咬最、ceil()向上舍入
翎嫡、floor()向下舍入
、round()四舍五入
永乌、random()大于0小于1的隨機(jī)數(shù)
惑申、abs()絕對值
、pow(n, p)n的p次冪
翅雏、cos()余弦值
等圈驼。
# 構(gòu)造函數(shù)
? 一般用來創(chuàng)建特定類型的對象。構(gòu)造函數(shù)分為兩種:原生構(gòu)造函數(shù)和自定義構(gòu)造函數(shù)望几。在實(shí)例中绩脆,一般使用 實(shí)例.constructor
來獲取構(gòu)造函數(shù)名。注意JS慣例規(guī)定構(gòu)造函數(shù)首字母要大寫橄抹。構(gòu)造函數(shù)是JavaScript被定義為 OO (Object Oriented)語言的重要因素之一靴迫,也是JS實(shí)現(xiàn)繼承的重要手段
原生構(gòu)造函數(shù)
最常見原生構(gòu)造函數(shù)是Object、Array害碾、Date矢劲,當(dāng)然還有RegExp、Function等等慌随。
(1)Object: 用于創(chuàng)建Object類型的實(shí)例芬沉,即對象
。用法:var obj = new Object()
(2)Array: 用于創(chuàng)建Array類型的實(shí)例阁猜,即數(shù)組
丸逸。用法var arr = new Array()
(3)Date: 用于創(chuàng)建Date類型的實(shí)例,即日期
剃袍。用法var date = new Date()
自定義構(gòu)造函數(shù)
(1)一個自定義構(gòu)造函數(shù)實(shí)踐
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name)
}
}
var person1 = new Person('Nic', '22', 'softEngineer')
var person2 = new Person('Xud', '16', 'teacher')
? new操作符主要執(zhí)行的邏輯包括:1)創(chuàng)建了一個新對象黄刚;2)將構(gòu)造函數(shù)的作用域(this
)賦給新對象;3)執(zhí)行構(gòu)造函數(shù)中的邏輯民效;4)返回新對象腹纳。
(2)構(gòu)造函數(shù)和函數(shù)
? 主要的區(qū)別就是調(diào)用方式不同啃勉。構(gòu)造函數(shù)其實(shí)也是函數(shù)。任何函數(shù)使用new
操作符來調(diào)用,就可以作為構(gòu)造函數(shù)來使用象缀,任何構(gòu)造函數(shù)直接函數(shù)名調(diào)用薪捍,那它和普通函數(shù)也就沒有區(qū)別藐鹤。構(gòu)造函數(shù)同樣也有call()
和apply()
方法鳍置,可以根據(jù)需要改變它的執(zhí)行環(huán)境。
(3)構(gòu)造函數(shù)和工廠函數(shù)
? 以對象為例,工廠函數(shù)旨在在函數(shù)內(nèi)部new
一個Object
實(shí)例章鲤,根據(jù)人參處理對象屬性并return
出該對象摊灭,運(yùn)行工廠函數(shù)后實(shí)實(shí)在在的從無到有的創(chuàng)建了一個新的對象并返回。
? 而自定義構(gòu)造函數(shù)則是根據(jù)需要的數(shù)據(jù)結(jié)構(gòu)先定義好一個構(gòu)造模板败徊,在需要的地方直接使用new
來實(shí)例化這個構(gòu)造函數(shù)而非實(shí)時實(shí)例化Object帚呼,且這個過程不需要手動return,因?yàn)樵趫?zhí)行實(shí)例化的時候?qū)嵗詣泳蛽碛辛藰?gòu)造函數(shù)所擁有的實(shí)例屬性和原型屬性集嵌。
(4)B芗贰!根欧!使用new操作符來實(shí)例化構(gòu)造函數(shù)時怜珍,每個屬性和方法都要重新創(chuàng)建一遍,無論是值類型還是引用類型凤粗。從而保證了每次創(chuàng)建出來的實(shí)例都是互相獨(dú)立的酥泛。但我們知道,對一個相同的方法進(jìn)行多次定義是不必要的嫌拣,因此通常建議不在構(gòu)造函數(shù)中定義方法柔袁,而是定義在原型對象中。
# 原型對象异逐、原型
? 原型對象共享于所有實(shí)例中捶索,這為實(shí)現(xiàn)繼承創(chuàng)造了天然的條件。下文將原型對象灰瞻,原型指針腥例,實(shí)例原型指針等概念區(qū)分的比較精確,實(shí)際上在稱呼時常有混淆酝润,需要注意觀察稱呼時表示的是誰的屬性燎竖,就能理解是什么意思。
(1)只要創(chuàng)建了一個新函數(shù)要销,就會根據(jù)一組特定規(guī)則為該函數(shù)創(chuàng)建一個prototype(原型)
屬性构回,它是一個指針,指向函數(shù)的原型對象疏咐。默認(rèn)的原型對象會自動獲取一個constructor(構(gòu)造函數(shù))
屬性纤掸,該屬性指向prototype所在函數(shù)對象的指針(即函數(shù)名)。
? 拿上面的構(gòu)造函數(shù)來舉例:Person.prototype.constructor
指向了Person
浑塞。
(2)除constructor
屬性是自動擁有外借跪,我們還能夠?yàn)槠涮砑幼远x的原型屬性,這些原型屬性被所有的實(shí)例共享缩举,假設(shè)構(gòu)造函數(shù)Person 有兩個實(shí)例 person1 和 person2,它們的指向關(guān)系如下:
1)理解一點(diǎn):Person.prototype
是一個指針托猩,而圖中的 Person Prototype是 Person的原型對象
,在JS中辽慕,正是用Person.prototype
來訪問和設(shè)置該原型對象的京腥。原型對象上的屬性被稱為構(gòu)造函數(shù)或?qū)嵗?em>原型屬性。而構(gòu)造函數(shù)和實(shí)例自身的屬性稱為實(shí)例屬性溅蛉。
2)圖中實(shí)例person1和實(shí)例person2中的 [[Prototype]]
是在實(shí)例化構(gòu)造函數(shù)時公浪,每個實(shí)例都會自動獲取的一個內(nèi)部指針屬性,ECMA-262第5版將他表示為[[Prototype]]
船侧,在各大Web瀏覽器中欠气,將這個內(nèi)部屬性表示為__proto__
,可以稱為實(shí)例原型指針镜撩。在JS中预柒,執(zhí)行hasOwnProperty()
查找對象某個屬性時,當(dāng)實(shí)例屬性里沒有需要繼續(xù)查找原型屬性時袁梗,就是通過該屬性找到的原型對象并訪問其屬性的宜鸯。
3)[[Prototype]]
即 __proto__
,是實(shí)例與原型對象之間的關(guān)系遮怜,它與構(gòu)造函數(shù)并沒有直接關(guān)系淋袖。
4)圖中可以看出,實(shí)例.constructor
獲取到構(gòu)造函數(shù)名的原因锯梁,正式因?yàn)橥ㄟ^__proto__
這個指針找到了共享在原型對象中的constructor
屬性即碗,而該屬性的值正是該原型對象的構(gòu)造函數(shù)名。
5)原型對象自身是一個數(shù)據(jù)結(jié)構(gòu)存放在內(nèi)存中涝桅,構(gòu)造函數(shù)通過prototype
指針屬性來訪問拜姿,而實(shí)例通過__proto__
指針屬性來訪問。這形成了一個三角關(guān)系
Person.prototype === person1.__proto__ === person2.__proto__
(3)新創(chuàng)建一個函數(shù)時默認(rèn)會生成它的原型對象冯遂,我們可以通過構(gòu)造函數(shù)的prototype
屬性或任意一個實(shí)例的__proto__
(通常用前者)來給原型對象中添加和刪除原型屬性蕊肥,這些屬性的最大特征就是在所有的實(shí)例中共享。但某個屬性是值類型時蛤肌,如果不想共享可以在實(shí)例定義來屏蔽原型屬性壁却;當(dāng)某個原型屬性是引用類型時,某個實(shí)例對該屬性的修改裸准,將會實(shí)時影響其他實(shí)例展东,畢竟這些屬性只是保存了引用類型的地址。為了規(guī)避這個問題炒俱,可以將所有除方法外的引用類型屬性定義在夠造函數(shù)中即可盐肃。
? 通常設(shè)置原型屬性會使用Person.prototype
來一個個的添加爪膊,這樣不會重寫protorype
指針的指向。當(dāng)使用字面量法操作原型對象時砸王,即如下:
function Person() {}
var person = new Person();
// 使用字面量法修改原型對象上的屬性
Person.prototype = {
constructor: Person, // 見下方解釋
name: 'Nic',
age: '22',
job: 'Soft Engineer',
sayName: function() {
alert(this.name)
}
}
alert(person.name); // 'Nic'
M剖ⅰ!谦铃!需要警惕的是耘成,這種方式雖然很方便很實(shí)用,但實(shí)質(zhì)上是給Person.prototype
重新賦值給新定義的對象驹闰,由于指針的指向發(fā)生了改變瘪菌,該原型指針已經(jīng)切斷了和創(chuàng)建函數(shù)時默認(rèn)的原型對象的關(guān)系,也就是說嘹朗,重寫了原型對象师妙。它將無法正確的獲取默認(rèn)原型對象上constructor
指向值,而變成什么骡显?
? 把等號后面的內(nèi)容單獨(dú)看疆栏,其實(shí)就是使用字面量法創(chuàng)建的一個Object類型的實(shí)例,那么它的構(gòu)造器應(yīng)該是 Object惫谤,即Person.prototype.constructor === Object
!!!
? 為了修正為Person.prototype.constructor === Person
壁顶,我們可以重寫字面量定義里的constructor
屬性為 Person。如上代碼所示溜歪。
? 另外若专,從該案例中可以發(fā)現(xiàn),雖然先執(zhí)行了實(shí)例化蝴猪,在重寫Person的原型调衰,實(shí)例依然能正確獲取原型屬性,這歸功于原型具有動態(tài)性自阱,也就是prototype
指針適用于引用類型的特性嚎莉。
# 原型鏈、繼承
? 上一個例子中沛豌,在沒有重寫constructor
屬性之前趋箩,細(xì)心的你可能發(fā)現(xiàn)這個結(jié)構(gòu)其實(shí)就是把 Person 的protorype
指針指向了Object類型的一個實(shí)例上,而由字面量定義的對象實(shí)例加派,也有prototype
指針屬性和原型對象叫确,那么就可以得到如下信息:
person.__proto__.__proto__.constructor === Object
這里所出現(xiàn)的兩個__proto__
指針屬性,形成了原型對象上的鏈?zhǔn)秸{(diào)用芍锦,我們稱之為原型鏈竹勉。
實(shí)現(xiàn)繼承的主要方法是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法,其主要思想是重寫一個引用類型的原型對象娄琉,使其等于另一個類型的實(shí)例次乓。
(1)一個實(shí)現(xiàn)繼承的實(shí)踐
function Super() {
this.sex = 'male'
}
Super.prototype.getSex = function() {
return this.sex
}
function Sub() {
this.hobby= 'runing'
}
// 重寫Sub構(gòu)造函數(shù)的prototype指針吓歇,利用原型鏈實(shí)現(xiàn)繼承
Sub.prototype = new Super()
Sub.prototype.getName = function() {
return this.name
}
var person = new Sub()
alert(person.getSex()) // male
? 這段代碼中,person
是構(gòu)造函數(shù)Sub
的實(shí)例票腰,但Sub構(gòu)造函數(shù)
中并沒有getSex()
這個實(shí)例方法照瘾,因此會去查找Sub
的原型屬性,而案例中把Sub
的默認(rèn)原型對象重寫成了Super的實(shí)例
丧慈,而Super
的實(shí)例可以通過__proto__
指針取到Super
原型對象中的getSex()
方法,因此主卫,輸出了male
? 我們把上例中實(shí)現(xiàn)繼承的關(guān)鍵代碼拆開逃默,可得到如下描述
var superObj = new Super()
alert(superObj.sex); // 'male'
alert(superObj.getSex()); // 'male'
Sub.prototype = superObj
alert(Sub.sex); // 'male'
alert(Sub.getSex()); // 'male'
var person = new Sub()
alert(person.__proto__.__proto__ === Super.prototype); // true
alert(person.__proto__.__proto__.constructor === Super); // true
?
(2)默認(rèn)的原型
? 需要了解的是,所有的函數(shù)的默認(rèn)原型都是Object的實(shí)例簇搅,也就是說完域,所有的自定義構(gòu)造函數(shù)都默認(rèn)繼承了原生的Object類型,而這個繼承也是通過原型鏈實(shí)現(xiàn)的瘩将。因此默認(rèn)原型都會包含一個內(nèi)部指針指向Object.prototype
吟税,這也是為什么所有的自定義類型都有toString()
、valueOf()
等默認(rèn)的方法的根本原因姿现。
? 因此針對上述實(shí)踐肠仪,我們可以再加上默認(rèn)的原型,就有了如下關(guān)系
alert(person.__proto__.__proto__.__proto__ === Object.prototype) // true
alert(person.__proto__.__proto__.__proto__.constructor === Object) // true
# 確定實(shí)例和原型的關(guān)系
(1) instanceof
? instance
即:“實(shí)例”备典。instanceof
操作符用來表示操作符左側(cè)的實(shí)例是否是右側(cè)構(gòu)造函數(shù)實(shí)例化來的异旧,這個構(gòu)造函數(shù)可以是實(shí)例原型鏈上constructor中的一員。借用上面的例子有:
person instanceof Sub // true
person instanceof Super // true
person instanceof Object // true
? 在真實(shí)業(yè)務(wù)環(huán)境中提佣,通常判斷的是直接由原生構(gòu)造函數(shù)Object或Array創(chuàng)建的實(shí)例吮蛹,原型鏈上只有一環(huán),就是如上第三個表達(dá)式拌屏。
? 以對象為例來看instanceof
操作符的執(zhí)行原理潮针,其實(shí)就是操作符右側(cè)的構(gòu)造函數(shù)的原型指針,和操作符左側(cè)的原型指針是否指向了同一個原型對象倚喂。
person instanceof Object
每篷!等價于判斷
person.__proto__=== Object.prototype
!注意不是以下判斷务唐,可以用字面量發(fā)重定義原型再修改原型構(gòu)造函數(shù)的constructor來驗(yàn)證
person.__proto__.constructor===Object
(2) isPrototypeOf()
? isPrototypeOf()
方法是原型對象的一個方法屬性雳攘,表示只要是原型鏈中出現(xiàn)過的原型,都是該原型鏈所派生的實(shí)例的原型枫笛《置穑或者理解為:某個構(gòu)造函數(shù)的原型對象,是否是某個實(shí)例的原型鏈上的一員刑巧。上面的例子用該方法描述即:
Object.prototype.isPrototypeOf(person) // true
Super.prototype.isPrototypeOf(person) // true
Sub.prototype.isPrototypeOf(person) // true