本文深入淺出地討論了 JAVAScript 構(gòu)造函數(shù)、原型、類(lèi)、繼承的特性和用法,以及它們之間的關(guān)系。希望看完本文,能幫助大家對(duì)它們有更加清晰通透的認(rèn)識(shí)和掌握!
01、前言
眾所周知,JavaScript 是一門(mén)面向?qū)ο蟮恼Z(yǔ)言,而構(gòu)造函數(shù)、原型、類(lèi)、繼承都是與對(duì)象密不可分的概念。在我們?nèi)粘G岸藰I(yè)務(wù)開(kāi)發(fā)中,系統(tǒng)和第三方庫(kù)已經(jīng)為我們提供了大部分需要的類(lèi),我們的關(guān)注點(diǎn)更多是在對(duì)象的使用和數(shù)據(jù)處理上,而比較少需要去自定義構(gòu)造函數(shù)和類(lèi),對(duì)原型的直接接觸就更少了。
然而,能深度理解并掌握好構(gòu)造函數(shù)、原型、類(lèi)與繼承,對(duì)我們的代碼設(shè)計(jì)大有裨益,也是作為一名高級(jí)前端工程師必不可少的基本功。
本文旨在用最通俗易懂的解釋和簡(jiǎn)單生動(dòng)的代碼示例,來(lái)徹底捋清對(duì)象、構(gòu)造函數(shù)、原型、類(lèi)與繼承。我們會(huì)以問(wèn)答對(duì)話的形式,層層遞進(jìn),從構(gòu)造函數(shù)談起,再引出原型與原型鏈,分析類(lèi)為什么是語(yǔ)法糖,最后再推理出 JS 的幾種繼承方式。
在進(jìn)入正式篇章之前,我們可以先嘗試思考以下幾個(gè)問(wèn)題:
1.new Date().__proto__ == Date.prototype?
2.new Date().constructor == Date?
3.Date.__proto__ == Function.prototype?
4.Function.__proto__ == Function.prototype?
5.Function.prototype.__proto__== Object.prototype?
6.Object.prototype.__proto__ == null?
—— 思考分割線 ——
沒(méi)錯(cuò),它們都是 true !為啥?聽(tīng)我娓娓道來(lái)~
02、構(gòu)造函數(shù)
某IT公司前端研發(fā)部,新人小Q和職場(chǎng)混跡多年的老白聊起著構(gòu)造函數(shù)、原型與類(lèi)的話題。
小Q:構(gòu)造函數(shù)我知道呀,平時(shí) new Date(),new Promise() 經(jīng)常用, Date,Promise 不就是構(gòu)造函數(shù),我們通過(guò) new 一個(gè)構(gòu)造函數(shù)去創(chuàng)建并返回一個(gè)新對(duì)象。
老白:沒(méi)錯(cuò),這些是系統(tǒng)自帶的一些構(gòu)造函數(shù),那你可以自己寫(xiě)個(gè)構(gòu)造函數(shù)嗎?
小Q:雖然平時(shí)用的不多,但也難不倒我~
// 定義個(gè)構(gòu)造函數(shù)
function Person(name) {
this.name = name;
}
// new構(gòu)造函數(shù),創(chuàng)建對(duì)象
let person = new Person("張三");
小Q:看吧 person 就是對(duì)象,Person 就是構(gòu)造函數(shù),清晰明了!
老白:那我要是單純寫(xiě)這個(gè)方法算不算構(gòu)造函數(shù)?
function add(a, b) {
return a + b;
}
小Q:這不是吧,這明顯就是個(gè)普通函數(shù)???
老白:可是它也可以 new 對(duì)象哦!
function add(a, b) {
return a + b;
}
let a = new add(1, 2);
// add {}
console.log(a);
// true
console.log(a instanceof add);
// object
console.log(typeof a);
小Q:誒?
老白:其實(shí)所謂構(gòu)造函數(shù),就是普通函數(shù),關(guān)鍵看你要不要 new 它,但是 new 是在使用的時(shí)候,在定義的時(shí)候咋知道它后面會(huì)不會(huì)被 new 呢,所以構(gòu)造函數(shù)只不過(guò)是當(dāng)被用來(lái)new 時(shí)的稱(chēng)呼。就像你上面的 Person 函數(shù),不要 new 直接運(yùn)行也是可以的嘛。
function Person(name) {
this.name = name;
}
Person("張三");
小Q:哦,我懂了,所有函數(shù)都可以被 new,都可以作為構(gòu)造函數(shù)咯,所謂構(gòu)造函數(shù)只是一種使用場(chǎng)景。
老白:嗯嗯,總結(jié)得很好,但也不全對(duì),比如箭頭函數(shù)就不能被 new,因?yàn)樗鼪](méi)有自己的 this 指向,所以不能作為構(gòu)造函數(shù)。比如下面這樣就會(huì)報(bào)錯(cuò)。
let Person = (name) => {
this.name = name;
};
// Uncaught TypeError: Person is not a constuctor
let person = new Person("張三");
小Q:原來(lái)如此,那你剛剛 Person("張三"); ,既然沒(méi)有創(chuàng)建新對(duì)象,那里面的 this 又指向誰(shuí)了?
老白:這就涉及到函數(shù)內(nèi) this 指向問(wèn)題了,可以簡(jiǎn)單總結(jié)以下 5 種場(chǎng)景。
1. 通過(guò) new 調(diào)用,this 指向創(chuàng)建的新對(duì)象;
2. 直接當(dāng)做函數(shù)調(diào)用,this 指向 window(嚴(yán)格模式下為 undefined);
function Person(name) {
this.name = name;
}
// this 指向 window
Person("張三");
// 張三
console.log(window.name);
(看吧,不注意的話,不小心把 window 對(duì)象改了都不知道)
3.作為對(duì)象的方法調(diào)用,this 指向該對(duì)象;
function Person(name) {
this.name = name;
}
let obj = {
Person,
};
// this 指向 obj
obj.Person("張三");
// { "name": "張三", Person: f }
console.log(obj);
4.通過(guò) Apply,call,bind 方法,顯式指定 this;
function Person(name) {
this.name = name;
}
// this 指向 call 的第一個(gè)參數(shù)
Person.call(Math, "張三");
// 張三
console.log(Math.name);
5.箭頭函數(shù)中沒(méi)有自己的 this 指向,取決于上下文:
function Person(name) {
this.name = name;
// 普通函數(shù),this 取決于調(diào)用者,即上述的 4 種情況
setTimeout(function() {
console.log(this);
}, 0)
// 箭頭函數(shù),this 取決于上下文,我們可以忽略箭頭函數(shù)的存在
// 即同上面 this.name = name 中的 this 指向一樣
setTimeout(() => {
console.log(this);
}, 0)
}
小Q:原來(lái) this 指向都有這么多種情況,好的,小本本記下了,等下就去試驗(yàn)下。
小Q:等下,我重新看了你的 new add(1, 2),那 a + b = 3 還被 return 了呢,這 3 return 到哪去了?
function add(a, b) {
return a + b;
}
let a = new add(1, 2);
老白:沒(méi)錯(cuò),你注意到了,構(gòu)造函數(shù)是不需要 return 的,函數(shù)中的 this 就是創(chuàng)建并返回的新對(duì)象了。
但當(dāng) new 一個(gè)有 return 的構(gòu)造函數(shù)時(shí),如果 return 的是基本類(lèi)型,則 return 的數(shù)據(jù)直接被拋棄。
如果 return 一個(gè)對(duì)象,則最終返回的新對(duì)象就是 return 的這個(gè)對(duì)象,這時(shí)原本 this 指向的對(duì)象就會(huì)被拋棄。
function Person(name) {
this.name = name;
// 返回的是對(duì)象類(lèi)型
return new Date();
}
let person = new Person("張三");
// 返回的是 Date 對(duì)象
// Sat Jul 29 2023 16:13:01 GMT+0800 (中國(guó)標(biāo)準(zhǔn)時(shí)間)
console.log(person);
老白:當(dāng)然如果要把一個(gè)函數(shù)的使用用途作為構(gòu)造函數(shù)的話,像我剛剛起名 add() 肯定是不規(guī)范的, 一般首字母要大寫(xiě),并且最好用名詞,像你起的 Person 就不錯(cuò)。
小Q:新知識(shí)get√
要點(diǎn)歸納 |
1. 除箭頭函數(shù)外的所有函數(shù)都可以作為構(gòu)造函數(shù)被new |
2. 函數(shù)內(nèi)this指向問(wèn)題 |
3. 構(gòu)造函數(shù)return問(wèn)題 |
4. 構(gòu)造函數(shù)命名規(guī)范 |
03、原型
小Q:都說(shuō)原型原型,可看了這么久,這代碼里也沒(méi)出現(xiàn)原型呀?
老白:沒(méi)錯(cuò),原型是個(gè)隱藏的家伙,我們可以通過(guò)對(duì)象或者構(gòu)造函數(shù)去拿到它。
// 構(gòu)造函數(shù)
function Person(name) {
this.name = name;
}
// 對(duì)象
let person = new Person("張三");
// 通過(guò)對(duì)象拿到原型(2種方法)
let proto1 = Object.getPrototypeOf(person);
let proto2 = person.__proto__;
// 通過(guò)構(gòu)造函數(shù)拿到原型
let proto3 = Person.prototype;
// 驗(yàn)證一下
// true
console.log(proto1 == proto2);
// true
console.log(proto1 == proto3);
小Q:可是這個(gè)原型是哪來(lái)的呀,我代碼里也沒(méi)創(chuàng)建它呀?
老白:當(dāng)你聲明一個(gè)函數(shù)時(shí),系統(tǒng)就自動(dòng)幫你生成了一個(gè)關(guān)聯(lián)的原型啦,當(dāng)然它也是一個(gè)普通對(duì)象,包含 constructor 字段指向構(gòu)造函數(shù),并且構(gòu)造函數(shù)的 prototype 屬性也會(huì)指向這個(gè)原型。
當(dāng)你用構(gòu)造函數(shù)創(chuàng)建對(duì)象時(shí),系統(tǒng)又幫你把對(duì)象的 __proto__ 屬性指向原型。
// 構(gòu)造函數(shù)
function Person(name) {
this.name = name;
}
// 可以理解為:聲明函數(shù)時(shí),系統(tǒng)自動(dòng)執(zhí)行了下面代碼
Person.prototype = {
// 指向構(gòu)造函數(shù)
constructor: Person
}
// 對(duì)象
let person = new Person("張三");
// 可以理解為:創(chuàng)建對(duì)象時(shí),系統(tǒng)自動(dòng)執(zhí)行了下面代碼
person.__proto__ == Person.prototype;
小Q:它們的引用關(guān)系,稍微有點(diǎn)繞啊~
老白:沒(méi)事,我畫(huà)兩個(gè)圖來(lái)表示,更加清晰點(diǎn)。
(備注:proto 只是單純用來(lái)表示原型的一個(gè)代名而已,代碼中并不存在)
圖片
圖片
小Q:懂了!
老白:那你說(shuō)說(shuō) {}.__proto__ 和 {}.consrtuctor 分別是什么?
小Q:讓我分析下,{} 其實(shí)就是 new Object() 的一種字面量寫(xiě)法,本質(zhì)上就是 Object 對(duì)象,那 {}.__proto__ 就是原型 Object.prototype,{}.constructor 就是構(gòu)造函數(shù) Object,對(duì)吧?
老白:沒(méi)錯(cuò),只要能熟練掌握上面這個(gè)圖,構(gòu)造函數(shù),原型和對(duì)象這三者的引用關(guān)系基本很清晰了。一開(kāi)始提的1、2 題基本也迎刃而解了!
- new Date().__proto__ == Date.prototype ?
- new Date().constructor == Date ?
小Q:那這個(gè)原型有什么用呢?
老白:一句話總結(jié):當(dāng)訪問(wèn)對(duì)象的屬性不存在時(shí),就會(huì)去訪問(wèn)原型的屬性。
圖片
圖3
老白:我們可以通過(guò)代碼驗(yàn)證下,person 對(duì)象是沒(méi)有 age 屬性的,所以 person.age 返回的其實(shí)是原型的 age 屬性值,當(dāng)原型的 age 屬性改變時(shí),person.age 也會(huì)跟著改變。
function Person(name) {
this.name = name;
}
// 給原型增加age屬性
Person.prototype.age = 18;
// 對(duì)象
let person = new Person("張三");
// 18
console.log(person.age);
// 修改原型的age屬性
Person.prototype.age++;
// 19
console.log(person.age);
小Q:那如果我直接 person.age++ 呢,改的是 person 還是原型?
老白:這樣的話就相當(dāng)于 person.age = person.age + 1 啦,等號(hào)右邊的 person.age 因?yàn)?nbsp; 對(duì)象目前還沒(méi) age 屬性,所以拿到的是原型的 age 屬性,即18,然后 18 + 1 = 19 將賦值給 person 對(duì)象。
后續(xù)當(dāng)你再訪問(wèn) person.age 時(shí),因?yàn)?nbsp;person 對(duì)象已經(jīng)存在 age 屬性了,就不會(huì)再檢索到原型上了。
這種行為我們一般稱(chēng)為重寫(xiě),在這個(gè)例子里也描述為:person 對(duì)象重寫(xiě)了原型上的 age 屬性。
圖片
圖4
小Q:那這樣的話使用起來(lái)豈不是很亂,我還得很小心的分析 person.age 到底是 person 對(duì)象的還是原型的?
老白:沒(méi)錯(cuò),如果你不想出現(xiàn)這種無(wú)意識(shí)的重寫(xiě),將原型上的屬性設(shè)為對(duì)象類(lèi)型不失為一種辦法。
function Person(name) {
this.name = name;
}
// 原型的info屬性是對(duì)象
Person.prototype.info = {
age: 18,
};
let person = new Person("張三");
person.info.age++;
小Q:我懂了,改變的是 info 對(duì)象的 age 屬性, person 并沒(méi)有重寫(xiě) info 屬 性,所以 person 對(duì)象本身依然沒(méi)有 info 屬性,person.info 依然指向原型。
老白:沒(méi)錯(cuò)!不過(guò)這樣也有個(gè)壞處,每一個(gè) Person 對(duì)象都可以共享原型的 info ,當(dāng) info 中的屬性被某個(gè)對(duì)象改變了,也會(huì)對(duì)其他對(duì)象造成影響。
function Person(name) {
this.name = name;
}
Person.prototype.info = {
age: 18,
};
let person1 = new Person("張三");
let person2 = new Person("李四");
// person1修改info
person1.info.age = 19;
// person2也會(huì)被影響,打?。?9
console.log(person2.info.age);
老白:這對(duì)我們代碼的設(shè)計(jì)并不好,所以我們一般不在原型上定義數(shù)據(jù),而是定義函數(shù),這樣對(duì)象就可以直接使用掛載在原型上的這些函數(shù)了。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("hello");
}
let person = new Person("張三");
// hello
person.sayHello();
小Q:我理解了,數(shù)據(jù)確實(shí)不應(yīng)該被共享,每個(gè)對(duì)象都應(yīng)該有自己的數(shù)據(jù)好點(diǎn),但是函數(shù)無(wú)所謂,多個(gè)對(duì)象可以共享同一個(gè)原型函數(shù)。
老白:所以你知道為啥 {} 這個(gè)對(duì)象本身沒(méi)有任何屬性,卻可以執(zhí)行 toString() 方法嗎?
小Q:【恍然大悟】來(lái)自它的原型 Object.prototype !
老白:不僅如此,很多系統(tǒng)自帶的構(gòu)造函數(shù)產(chǎn)生的對(duì)象,其方法都是掛載在原型上的。比如我們經(jīng)常用的數(shù)組方法,你以為是數(shù)組對(duì)象自己的方法嗎?不,是數(shù)組原型 Array.prototype 的方法,我們可以驗(yàn)證下。
let array = [];
// array對(duì)象的push和原型上的push是同一個(gè)
// 打印:true
console.log(array.push == Array.prototype.push);
// array對(duì)象本身沒(méi)有自己的push屬性
// 打印:false
console.log(array.hasOwnProperty("push"));
圖片
小Q:【若有所思】
老白:再比如,你隨便定義一個(gè)函數(shù) function fn() {},為啥它就能 fn.call() 這樣執(zhí)行呢,它的 call 屬性是哪來(lái)的?
小Q:來(lái)自它的原型?函數(shù)其實(shí)是 Function 的對(duì)象,那它的原型就是 Function.prototype,試驗(yàn)一下。
function fn() {}
// true
console.log(fn.constructor == Function);
// true
console.log(fn.call == Function.prototype.call);
老白:回答正確。在實(shí)際開(kāi)發(fā)中,我們也可以通過(guò)修改原型上的函數(shù),來(lái)改變對(duì)象的函數(shù)執(zhí)行。比如說(shuō)我們修改數(shù)組原型的 push 方法,加個(gè)監(jiān)聽(tīng),這樣所有數(shù)組對(duì)象執(zhí)行 push 方法時(shí)就能被監(jiān)聽(tīng)到了。
Array.prototype.push = (function (push) {
// 閉包,push是原始的那個(gè)push方法
return function (...items) {
// 執(zhí)行push要指定this
push.call(this, ...items);
console.log("監(jiān)聽(tīng)push完成,執(zhí)行一些操作");
};
})(Array.prototype.push);
let array = [];
// 打?。罕O(jiān)聽(tīng)push完成,執(zhí)行一些操作
array.push(1, 2);
// 打?。篬1, 2]
console.log(array);
老白:不只修改,也可以新增,比如說(shuō)某些舊版瀏覽器數(shù)組不支持 includes 方法,那我們就可以在原型上新增一個(gè) includes 屬性,保證代碼中數(shù)組對(duì)象使用 includes() 不會(huì)報(bào)錯(cuò)(這也是 Polyfill.js 的目的)。
// 沒(méi)有includes
if(!Array.prototype.includes) {
Array.prototype.includes = function() {
// 自己實(shí)現(xiàn)includes
}
}
小Q:又又漲知識(shí)了~
老白:原型相關(guān)的也說(shuō)的差不多了,結(jié)合剛剛討論的構(gòu)造函數(shù),考你一個(gè):手寫(xiě)一個(gè) new 函數(shù)。
小Q:啊啊,提示一下?
老白:好,我們簡(jiǎn)單分析一下 new 都做了什么
- 創(chuàng)建一個(gè)對(duì)象,綁定原型;
- 以這個(gè)對(duì)象為 this 指向執(zhí)行構(gòu)造函數(shù)。
小Q:我試試~
function myNew(Fn, ...args) {
var obj = {
__proto__: Fn.prototype,
};
Fn.apply(obj, args);
return obj;
}
小Q:試驗(yàn)通過(guò)!
// 構(gòu)造函數(shù)
function Person(name) {
this.name = name;
}
// 原型
Person.prototype.age = 18;
// 創(chuàng)建對(duì)象
let person = myNew(Person, "張三");
// Person {name: "張三"}
console.log(person);
// 18
console.log(person.age);
老白:不錯(cuò)不錯(cuò),讓我?guī)湍阍偕晕⑼晟埔幌潞俸賬
function myNew(Fn, ...args) {
// 通過(guò)Object.create指定原型,更加符合規(guī)范
var obj = Object.create(Fn.prototype);
// 指定this為obj對(duì)象,執(zhí)行構(gòu)造函數(shù)
let result = Fn.apply(obj, args);
// 判斷構(gòu)造函數(shù)的返回值是否是對(duì)象
return result instanceof Object ? result : obj;
}
要點(diǎn)歸納 |
1. 對(duì)象,構(gòu)造函數(shù),原型三者的引用關(guān)系 |
2. 原型的定義,特性及用法 |
3. 手寫(xiě)new函數(shù) |
04、原型鏈
老白:剛剛我們說(shuō)當(dāng)訪問(wèn)對(duì)象的屬性不存在時(shí),就會(huì)去訪問(wèn)原型的屬性,那假如原型上的屬性也不存在呢?
小Q:返回 undefined?
老白:不對(duì)哦,原型本身也是一個(gè)對(duì)象,它也有它自己的原型。所以當(dāng)訪問(wèn)一個(gè)對(duì)象的屬性不存在時(shí),就會(huì)檢索它的原型,檢索不到就繼續(xù)往上檢索原型的原型,一直檢索到根原型 Object.prototype,如果還沒(méi)有,才會(huì)返回 undefined,這也稱(chēng)為原型鏈。
圖片
小Q:原來(lái)如此,所以說(shuō)所有的對(duì)象都可以使用根原型 Object.prototype 上定義的方法咯。
老白:沒(méi)錯(cuò),不過(guò)有一些原型會(huì)重寫(xiě)根原型上的方法,就比如 toString(),在 Date.prototype,Array.prototype 中都會(huì)有它們自己的定義。
// [object Object]
console.log({}.toString())
// 1,2,3
console.log([1,2,3].toString())
// Tue Aug 01 2023 17:58:05 GMT+0800 (中國(guó)標(biāo)準(zhǔn)時(shí)間)
console.log(new Date().toString())
小Q:理解了原型鏈,看回開(kāi)始的3~6題,好像也不難了。
Date、Function 的原型是 Function.prototype,第 3、4 題就解了。
Function.prototype 的原型是 Object.prototype,第 5 題也解了。
Object.prototype 是根原型,所以它的 __proto__ 屬性就為 null,第 6 題也解了。
- Date.__proto__ == Function.prototype ?
- Function.__proto__ == Function.prototype ?
- Function.prototype.__proto__== Object.prototype ?
- Object.prototype.__proto__ == null ?
老白:完全正確。最后再考你一道和原型鏈相關(guān)的題,手寫(xiě) instanceOf 函數(shù)。提示一下,instanceOf 的原理是判斷構(gòu)造函數(shù)的 prototype 屬性是否在對(duì)象的原型鏈上。
// array的原型鏈:Array.prototype → Object.prototype
let array = [];
// true
console.log(array instanceof Array);
// true
console.log(array instanceof Object);
// false
console.log(array instanceof Function);
小Q:好了嘞~
function myInstanceof(obj, Fn) {
while (true) {
obj = obj.__proto__;
// 匹配上了
if (obj == Fn.prototype) {
return true;
}
// 到達(dá)原型鏈的盡頭了
if (obj == null) {
return false;
}
}
}
檢測(cè)一下:
let array = [];
// true
console.log(myInstanceof(array, Array));
// true
console.log(myInstanceof(array, Object));
// false
console.log(myInstanceof(array, Function));
老白:Good!
要點(diǎn)歸納 |
1. 原型鏈 |
2. 手寫(xiě) instanceOf函數(shù) |
05、類(lèi)
小Q:好不容易把構(gòu)造函數(shù)和原型都弄懂,怎么 ES6 又推出類(lèi)呀,學(xué)不動(dòng)了 T_T。
老白:不慌,類(lèi)其實(shí)只是種語(yǔ)法糖,本質(zhì)上還是”構(gòu)造函數(shù)+原型“。
我們先看一下類(lèi)的語(yǔ)法,類(lèi)中可以包含有以下4種寫(xiě)法不同的元素。
- 對(duì)象屬性:key = xx
- 原型屬性:key() {}
- 靜態(tài)屬性:static key = x 或 static key() {}
- 構(gòu)造器:constructor() {}
class Person {
// 對(duì)象屬性
a = "a";
b = function () {
console.log("b");
};
// 原型屬性
c() {
console.log("c");
}
// 構(gòu)造器
constructor() {
// 修改對(duì)象屬性
this.a = "A";
// 新增對(duì)象屬性
this.d = "d";
}
// 靜態(tài)屬性
static e = "e";
static f() {
console.log("f");
}
}
我們?cè)賹⑦@種 class 語(yǔ)法糖寫(xiě)法還原成構(gòu)造函數(shù)寫(xiě)法。
function Person() {
// 對(duì)象屬性
this.a = "a";
this.b = function () {
console.log("b");
};
// 構(gòu)造器
this.a = "A";
this.d = "d";
}
// 原型屬性
Person.prototype.c = function () {
console.log("c");
};
// 靜態(tài)屬性
Person.e = "e";
Person.f = function () {
console.log("f");
};
通過(guò)下面一些方法檢測(cè),上面的2種寫(xiě)法會(huì)得到同樣的結(jié)果。
// Person類(lèi)本質(zhì)是個(gè)構(gòu)造函數(shù),打?。篺unction
console.log(typeof Person);
// Person的靜態(tài)屬性,打?。篹
console.log(Person.e);
// 可以看到原型屬性c,打印:{constructor: ƒ, c: ƒ}
console.log(Person.prototype);
let person = new Person();
// 可以看到對(duì)象屬性a b d,打印:Person {a: 'A', d: 'd', b: ƒ}
console.log(person);
// 對(duì)象的構(gòu)造函數(shù)就是Person,打印:true
console.log(person.constructor == Person);
小Q:所以類(lèi)只不過(guò)是將本來(lái)比較繁瑣的構(gòu)造函數(shù)的寫(xiě)法給簡(jiǎn)化了而已,這語(yǔ)法糖果然甜~
小Q:不過(guò)我發(fā)現(xiàn)一個(gè)問(wèn)題,在 class 寫(xiě)法中的原型屬性只能是函數(shù),不能是數(shù)據(jù)?
老白:沒(méi)錯(cuò),這也呼應(yīng)了前面說(shuō)的,原型上只推薦定義函數(shù),不推薦定義數(shù)據(jù),避免不同對(duì)象共享同一個(gè)數(shù)據(jù)。
要點(diǎn)歸納 |
1. 類(lèi)的語(yǔ)法 |
2. 類(lèi)還原成構(gòu)造函數(shù)寫(xiě)法 |
06、繼承
小Q:我又又發(fā)現(xiàn)了一個(gè)問(wèn)題,ES6 的 class 還可以 extends 另一個(gè)類(lèi)呢,這也是語(yǔ)法糖?
老白:沒(méi)錯(cuò),這就是繼承,但是要弄懂 ES6 的這套繼承是怎么來(lái)的,還得從最開(kāi)始的繼承方式說(shuō)起。所謂繼承,就是我們是希望子類(lèi)可以擁有父類(lèi)的屬性方法,這和上面談到的原型的特性有點(diǎn)不謀而合。
我們用一個(gè)例子來(lái)思考思考,有這么 2 個(gè)類(lèi),如何讓 Cat 繼承 Animal,使得 Cat 的對(duì)象也有 type 屬性呢?
// 父類(lèi)
function Animal() {
this.type = "動(dòng)物";
}
// 子類(lèi)
function Cat() {
this.name = "貓";
}
小Q:讓 Animal 對(duì)象充當(dāng) Cat 的原型!
function Animal() {
this.type = "動(dòng)物";
}
function Cat() {
this.name = "貓";
}
// 指定Cat的原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
let cat = new Cat();
// Cat對(duì)象擁有了Animal的屬性
console.log(cat.type);
老白:沒(méi)錯(cuò),這是我們學(xué)完原型之后,最直觀的一種繼承實(shí)現(xiàn)方式,這種繼承又叫原型鏈?zhǔn)嚼^承。但是這種繼承方式存在 2 個(gè)缺點(diǎn):
- 父類(lèi)對(duì)象作為原型,其屬性會(huì)被所有子類(lèi)對(duì)象共享;
- 創(chuàng)建子類(lèi)對(duì)象時(shí)無(wú)法向父類(lèi)構(gòu)造函數(shù)傳參。
function Animal(type) {
this.type = type;
}
function Cat(type) {
this.name = "貓";
}
// 在這里就已經(jīng)創(chuàng)建了Animal對(duì)象
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 創(chuàng)建子類(lèi)對(duì)象時(shí)無(wú)法向父類(lèi)構(gòu)造函數(shù)傳參
let cat = new Cat("哺乳動(dòng)物");
// type屬性來(lái)自原型,被所有Cat對(duì)象共享,打?。簎ndefined
console.log(cat.type);
小Q:我想到個(gè)辦法,可以一舉解決上面2個(gè)缺點(diǎn)。
在子類(lèi)構(gòu)造函數(shù)中執(zhí)行父類(lèi)構(gòu)造函數(shù),并且指定執(zhí)行父類(lèi)構(gòu)造函數(shù)中的 this 是子類(lèi)對(duì)象,這樣屬性就都是屬于子類(lèi)對(duì)象本身了,不存在共享。同時(shí)在創(chuàng)建子類(lèi)對(duì)象時(shí),也可以給父類(lèi)構(gòu)造函數(shù)傳參了,一舉兩得。
function Animal(type) {
this.type = type;
}
function Cat(type) {
// 執(zhí)行父類(lèi),顯式指定this就是子類(lèi)的對(duì)象
Animal.call(this, type);
this.name = "貓";
}
let cat = new Cat("哺乳動(dòng)物");
// Cat {type: '哺乳動(dòng)物', name: '貓'}
console.log(cat);
老白:這種繼承方式叫 構(gòu)造函數(shù)式繼承,確實(shí)解決了 原型鏈?zhǔn)嚼^承 帶來(lái)的問(wèn)題,不過(guò)這種繼承方式因?yàn)闆](méi)有用到原型,又有產(chǎn)生了2個(gè)新的問(wèn)題:
- 沒(méi)有繼承父類(lèi)原型的屬性方法;
- 子類(lèi)對(duì)象不是父類(lèi)的實(shí)例。
function Animal(type) {
this.type = type;
}
// 父類(lèi)的原型方法
Animal.prototype.eat = function () {
console.log("吃");
};
function Cat(type) {
Animal.call(this, type);
this.name = "貓";
}
let cat = new Cat("哺乳動(dòng)物");
// 沒(méi)有繼承父類(lèi)原型的屬性方法,打?。簎ndefined
console.log(cat.eat);
// 子類(lèi)對(duì)象不是父類(lèi)的實(shí)例,打印:false
console.log(cat instanceof Animal);
小Q:看來(lái)還要再改進(jìn),不如我把 原型鏈?zhǔn)?nbsp;和 構(gòu)造函數(shù)式 這 2 種繼承方式都用上,讓它們互補(bǔ)。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log("吃");
};
// 子類(lèi)構(gòu)造函數(shù)
function Cat(type) {
Animal.call(this, type);
this.name = "貓";
}
// 父類(lèi)對(duì)象充當(dāng)子類(lèi)原型
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
試驗(yàn)一下,果然所有問(wèn)題都解決了。
// 可以給父類(lèi)構(gòu)造函數(shù)傳參
let cat = new Cat("哺乳動(dòng)物");
// 子類(lèi)對(duì)象擁用自己屬性,而非來(lái)自原型,避免數(shù)據(jù)共享
// 打?。篊at {type: '哺乳動(dòng)物', name: '貓'}
console.log(cat);
// 子類(lèi)對(duì)象可以繼承到父類(lèi)原型的方法,打?。撼?cat.eat();
// 子類(lèi)對(duì)象屬于父類(lèi)的實(shí)例,打?。簍rue
console.log(cat instanceof Animal);
老白:非常聰明,你又道出了第三種繼承方式,組合式繼承。即 原型鏈?zhǔn)?+ 構(gòu)造函數(shù)式 = 組合式。問(wèn)題確實(shí)都解決了,但是有沒(méi)有發(fā)現(xiàn),這種方式執(zhí)行了 2 遍父類(lèi)構(gòu)造函數(shù)。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log("吃");
};
function Cat(type) {
// 第二次執(zhí)行父類(lèi)構(gòu)造函數(shù)
Animal.call(this, type);
this.name = "貓";
}
// 第一次執(zhí)行父類(lèi)構(gòu)造函數(shù)
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
小Q:多執(zhí)行了一遍,確實(shí)不夠完美,這怎么搞?
老白:其實(shí)關(guān)鍵在 Cat.prototype = new Animal(),你只不過(guò)想讓子類(lèi)對(duì)象也能繼承到父類(lèi)的原型,而這里創(chuàng)建了一個(gè)父類(lèi)對(duì)象,為啥?說(shuō)到底還是利用原型鏈: 子類(lèi)對(duì)象 → 父類(lèi)對(duì)象 → 父類(lèi)原型。
如果我們不要中間那個(gè)"父類(lèi)對(duì)象",而是用一個(gè)“空對(duì)象x”替換,讓原型鏈變成:子類(lèi)對(duì)象 → 空對(duì)象x → 父類(lèi)原型,這樣也能達(dá)到目的,就不用執(zhí)行那遍沒(méi)必要的父類(lèi)構(gòu)造函數(shù)了。
// 組合式繼承:創(chuàng)建父類(lèi)對(duì)象做子類(lèi)原型
let animal = new Animal();
Cat.prototype = animal;
// 改進(jìn):創(chuàng)建一個(gè)空對(duì)象做子類(lèi)原型,并且這個(gè)空對(duì)象的原型是父類(lèi)原型
let x = Object.create(Animal.prototype);
Cat.prototype = x;
小Q:妙啊,這回完美了。
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function () {
console.log("吃");
};
function Cat(type) {
Animal.call(this, type);
this.name = "貓";
}
// 寄生組合式,改進(jìn)了組合式,少執(zhí)行了一遍沒(méi)必要的父類(lèi)構(gòu)造函數(shù)
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
老白:這種繼承方式又叫 寄生組合式繼承,相當(dāng)于在組合式繼承的基礎(chǔ)上進(jìn)一步優(yōu)化?;仡櫳厦娴膸追N繼承方式的演變過(guò)程,原型鏈?zhǔn)?→ 構(gòu)造函數(shù)式 → 組合式 → 寄生組合式, 其實(shí)就是不斷優(yōu)化的過(guò)程,最終我們才推理出比較完美的繼承方式。
小Q:那 ES6 class 的 extends 繼承 又是怎樣呢?
老白:說(shuō)到底就是 寄生組合式繼承 的語(yǔ)法糖。我們先看看它的語(yǔ)法。
class Animal {
eat() {
console.log("吃");
}
constructor(type) {
this.type = type;
}
}
// Cat繼承Animal
class Cat extends Animal {
constructor(type) {
// 執(zhí)行父類(lèi)構(gòu)造函數(shù),相當(dāng)于 Animal.call(this, type);
super(type);
// 執(zhí)行完super(),子類(lèi)對(duì)象就有父類(lèi)屬性了,打?。翰溉閯?dòng)物
console.log(this.type);
this.name = "貓";
}
}
創(chuàng)建對(duì)象試驗(yàn)一下:
let cat = new Cat("哺乳動(dòng)物");
// 子類(lèi)原型的原型就是父類(lèi)原型,打印:true
console.log(Cat.prototype.__proto__ == Animal.prototype);
// 子類(lèi)本身?yè)碛懈割?lèi)的屬性,打?。篊at {type: '哺乳動(dòng)物', name: '貓'}
console.log(cat);
打印的結(jié)果展示的特性和 寄生組合式 是一樣的:
- 子類(lèi)原型的原型就是父類(lèi)原型;
- 子類(lèi)本身?yè)碛懈割?lèi)的屬性。
特性1 可以理解為 extends 背地里執(zhí)行了:
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
特性2 在于 super(),它相當(dāng)于 Animal.call(this),執(zhí)行 super() 就是執(zhí)行父類(lèi)構(gòu)造函數(shù),將原本父類(lèi)中的屬性都賦值給子類(lèi)對(duì)象。
在 ES6 的語(yǔ)法中還要求 super() 必須在 this 的使用前調(diào)用,也是為了保證父類(lèi)構(gòu)造函數(shù)先執(zhí)行,避免在子類(lèi)構(gòu)造器中設(shè)置的 this 屬性被父類(lèi)構(gòu)造函數(shù)覆蓋。
class Animal {
constructor() {
// 假如不報(bào)錯(cuò),this.name = "貓" 就被 this.name= "狗" 覆蓋了
this.name = "狗";
}
}
class Cat extends Animal {
constructor(type) {
this.name = "貓";
// 沒(méi)有在this使用前調(diào)用,報(bào)錯(cuò)
super();
}
}
小Q:看懂 寄生組合式繼承, extends 繼承 就是小菜一碟呀~
老白:最后再補(bǔ)充一下 super 的語(yǔ)法,可以子類(lèi)的靜態(tài)屬性方法中通過(guò) super.xx 訪問(wèn)父類(lèi)靜態(tài)屬性方法。
class Animal {
constructor() {}
static num = 1;
static say() {
console.log("hello");
}
}
class Cat extends Animal {
constructor() {
super();
}
// super.num 相當(dāng)于 Animal.num
static count = super.num + 1;
static talk() {
// super.say() 相當(dāng)于 Animal.say()
super.say();
}
}
// 2
console.log(Cat.count);
// hello
Cat.talk();
super 是一個(gè)語(yǔ)法糖的特殊關(guān)鍵詞,特殊用法,并不指向某個(gè)對(duì)象,不能單獨(dú)使用,以下情況都是不允許的。
class Animal {}
class Cat extends Animal {
constructor() {
// 報(bào)錯(cuò)
let _super = super;
// 報(bào)錯(cuò)
console.log(super);
}
static talk() {
// 報(bào)錯(cuò)
console.log(super);
}
}
要點(diǎn)歸納
- 原型鏈?zhǔn)嚼^承
- 構(gòu)造函數(shù)式繼承
- 組合式繼承
- 寄生組合式繼承
- extends 繼承
07、總結(jié)
本文深入淺出地討論了 JavaScript 構(gòu)造函數(shù)、原型、類(lèi)、繼承的特性和用法,以及它們之間的關(guān)系。希望看完本文,能幫助大家對(duì)它們有更加清晰通透的認(rèn)識(shí)和掌握!