Web前端入門第 67 問:JavaScript 中的面向?qū)ο缶幊?/span>
此 對象 非彼對象啊,不要理解錯了哦~~
面向?qū)ο缶幊?/code> 這個概念在 Java 編程語言中用得比較多,JS 同時支持 面向?qū)ο缶幊?/code> 和 函數(shù)式編程。
像大名鼎鼎的 React 和 Vue 他們都有兩種開發(fā)風(fēng)格,比如:
Vue 中的 組合式API 和 選項式API 也是兩種編程模式的代表。
React 中的 函數(shù)式組件 和 類組件 就是兩種編程模式的代表。
原型鏈
JS 中的每個對象(null 除外)都有一個隱式原型,可以通過 __proto__ 或者 Object.getPrototypeOf() 訪問。
null 雖然用 typeof 檢測會獲得 Object 類型,但 null 有一點特殊,表示空,什么都沒有的意思。
比如:
const a = 'str'
console.log(a.__proto__) // 獲得 String.prototype
console.log(Object.getPrototypeOf(a)) // 獲得 String.prototype
console.log(a.__proto__ === String.prototype) // true
console.log(Object.getPrototypeOf(a) === String.prototype) // true
這么多年的搬磚經(jīng)驗來看,__proto__ 這個屬性能派上用場的場景真的少見~~
構(gòu)造函數(shù)
function 申明的函數(shù)都擁有一個顯式原型 prototype 屬性,如果用 new 關(guān)鍵字調(diào)用這個函數(shù),那么此時這個函數(shù)就稱之為 構(gòu)造函數(shù)。
實例化構(gòu)造函數(shù)的時候,實例化對象的 __proto__ 就指向構(gòu)造函數(shù)的 prototype 屬性。
function Person() {}
Person.prototype.name = '前端路引'
const person = new Person()
console.log(person.__proto__ === Person.prototype) // true
console.log(person.name) // 輸出:前端路引
編程實踐推薦:構(gòu)造函數(shù)聲明時,首字母一般大寫,而函數(shù)聲明時首字母一般小寫。
繼承
繼承 這玩意兒可以算作面向?qū)ο缶幊痰暮诵乃枷耄绻幊陶Z言不支持 繼承,那面向?qū)ο缶褪且痪淇赵挕?/p>
JS 中的繼承玩法多種多樣,掌握一種就可以獨步武林~~ 但面試官可是全能高手,一般都會問知道有幾種繼承方式,他們怎么實現(xiàn)這些問題。
原型鏈繼承
子類通過 prototype 指向父類實例,就是原型鏈繼承,但此種方式繼承有一個大缺陷,會共享父類中的引用類型(比如數(shù)組、對象)。
function Parent() {
// 父類中申明的屬性
this.arr = ['公眾號', '前端路引'];
}
// 父類中申明的方法
Parent.prototype.getName = function () {
console.log('前端路引');
}
function Child() {}
// 使用原型鏈繼承父類
Child.prototype = new Parent();
const child1 = new Child();
// 修改 child1 的實例屬性
child1.arr.push(1);
child1.getName();
const child2 = new Child();
child2.getName();
console.log(child2.arr); // 輸出:['公眾號', '前端路引', 1]
可以看到子類都可以調(diào)用父類的 getName 方法,但是在 child1 實例修改 arr 屬性后,child2 也會受影響,這邊是原型鏈繼承中的弊端。
構(gòu)造函數(shù)繼承
此繼承方式的特點是利用函數(shù)的 call 或者 apply 方法,再傳入子類的 this 指針實現(xiàn)繼承,缺點是無法繼承父類上的原型方法。
function Parent(name) {
this.name = name;
this.arr = ['公眾號', '前端路引'];
this.test = function () {
console.log('調(diào)用父類 test 方法');
}
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child(name) {
Parent.call(this, name);
}
const child1 = new Child('前端路引');
// 修改 child1 的實例屬性
child1.arr.push(1);
console.log(child1.arr); // ['公眾號', '前端路引', 1]
child1.test(); // 輸出:調(diào)用父類 test 方法
const child2 = new Child('前端路引');
console.log(child2.arr); // ['公眾號', '前端路引']
child2.getName(); // 報錯 TypeError: child2.getName is not a function
此繼承方式修復(fù)了 原型鏈繼承 中共享 引用類型 問題,但卻存在無法繼承父類原型鏈方法的弊端。
組合繼承
此繼承方式結(jié)合了原型鏈繼承和構(gòu)造函數(shù)繼承而衍生出的另一種繼承方式,同時解決了兩種繼承方式的弊端。
function Parent(name) {
this.name = name;
this.arr = ['公眾號', '前端路引'];
this.test = function () {
console.log('調(diào)用父類 test 方法');
}
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child(name) {
Parent.call(this, name); // 繼承屬性(第二次調(diào)用)
}
Child.prototype = new Parent(); // 繼承方法(第一次調(diào)用)
const child1 = new Child('前端路引');
// 修改 child1 的實例屬性
child1.arr.push(1);
console.log(child1.arr); // ['公眾號', '前端路引', 1]
child1.test(); // 輸出:調(diào)用父類 test 方法
const child2 = new Child('前端路引');
console.log(child2.arr); // ['公眾號', '前端路引']
child2.getName(); // 輸出:前端路引
組合繼承可以擁有父類上的 getName,同時還不會共享父類上的引用類型,但父類構(gòu)造函數(shù)卻被調(diào)用了兩次,存在性能優(yōu)化上的空間,這也是此種繼承方式的弊端。
寄生組合繼承
此繼承方式通過 Object.create 方法復(fù)制父類的原型鏈,優(yōu)化父類會被調(diào)用兩次問題,算是比較完美的一種繼承方式,不存在性能浪費。
function Parent(name) {
this.name = name;
this.arr = ['公眾號', '前端路引'];
this.test = function () {
console.log('調(diào)用父類 test 方法');
}
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child(name) {
Parent.call(this, name); // 繼承屬性(第二次調(diào)用)
}
Child.prototype = Object.create(Parent.prototype); // 繼承原型
Child.prototype.constructor = Child; // 修復(fù)子類的 constructor 引用
const child1 = new Child('前端路引');
// 修改 child1 的實例屬性
child1.arr.push(1);
console.log(child1.arr); // ['公眾號', '前端路引', 1]
child1.test(); // 輸出:調(diào)用父類 test 方法
const child2 = new Child('前端路引');
console.log(child2.arr); // ['公眾號', '前端路引']
child2.getName(); // 輸出:前端路引
寄生組合繼承重點就是兩句代碼:
Child.prototype = Object.create(Parent.prototype); // 繼承原型
Child.prototype.constructor = Child; // 修復(fù)子類的 constructor 引用
由于 Object.create 會復(fù)制父類的 constructor 屬性,導(dǎo)致子類的 constructor 屬性被重寫了,所以需要手動修復(fù)。
在 ES6 出現(xiàn)之前,這種繼承已經(jīng)是 JS 面向?qū)ο缶幊讨械?strong>最優(yōu)解了。
ES6 class 繼承
ES6 出現(xiàn)了 class 類關(guān)鍵字,也多了 extends 繼承關(guān)鍵字,可以很方便的實現(xiàn)繼承。
但其底層實現(xiàn)邏輯還是 寄生組合繼承,相當(dāng)于是提供了一種語法糖,簡化了寄生組合繼承中的代碼。
class Parent {
constructor(name) {
this.name = name;
this.arr = ['公眾號', '前端路引'];
}
test () {
console.log('調(diào)用父類 test 方法');
}
getName () {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name) {
super(name); // 必須有此行
}
}
const child1 = new Child('前端路引');
// 修改 child1 的實例屬性
child1.arr.push(1);
console.log(child1.arr); // ['公眾號', '前端路引', 1]
child1.test(); // 輸出:調(diào)用父類 test 方法
const child2 = new Child('前端路引');
console.log(child2.arr); // ['公眾號', '前端路引']
child2.getName(); // 輸出:前端路引
如果沒有 super() 這行代碼,JS 解析器會報錯:
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
意思就是在父類訪問 this 之前,子類中必須調(diào)用 super() 方法。
原型鏈查找規(guī)則
有了父子兩層繼承關(guān)系,那肯定就有更多層次的繼承關(guān)系,比如:
class A {}
class B extends A {}
class C extends B {}
在這種多層級的繼承關(guān)系中,JS 的原型鏈查找規(guī)則永遠(yuǎn)都是一層一層往上找,終點是找到 Object.prototype 為止,如果還找不到就報錯。
比如:
class A {}
class B extends A {}
class C extends B {}
const child = new C();
console.log(child.toString())
console.log(child.test()) // 報錯 TypeError: child.test is not a function
以上代碼中 A、B、C 都沒有 toString 方法,但是實例 child 卻可以調(diào)用,原因就是 child 的原型鏈最終找到了 Object.prototype.toString 方法。
而 test 直到 Object.prototype 為止都沒找到,所以最終報錯。
可以理解其查找規(guī)則是這樣的:
實例 (obj) --> 構(gòu)造函數(shù).prototype --> 父構(gòu)造函數(shù).prototype --> ... --> Object.prototype --> null
寫在最后
雖然個人更喜歡 函數(shù)式編程 方式,但面向?qū)ο筮@種寫法也必須要掌握,要不然看到面向?qū)ο蟮拇a,就玩完了~~
文章首發(fā)于微信公眾號【前端路引】,歡迎 微信掃一掃 查看更多文章。
本文來自博客園,作者:前端路引,轉(zhuǎn)載請注明原文鏈接:http://www.rzrgm.cn/linx/p/18937897

浙公網(wǎng)安備 33010602011771號