ES6+ Class 前置知識(shí)
1. 前言
在早期的 JavaScript 中是沒(méi)類的概念的,如果想要實(shí)現(xiàn)類的功能需要通過(guò)構(gòu)造函數(shù)來(lái)創(chuàng)建,使用 prototype 來(lái)實(shí)現(xiàn)類的繼承。對(duì)于一些高級(jí)語(yǔ)言如 C++、Java、python 等,都是有類的概念的,而且在這些語(yǔ)言中類是非常重要的。而 JavaScript 由于歷史原因在設(shè)計(jì)最初就沒(méi)有想要引入類的概念,隨著 JavaScript 越來(lái)越多地應(yīng)用到大型項(xiàng)目中,JavaScript 的短板就顯現(xiàn)了。雖然,可以使用原型等方式來(lái)解決,但是還是存在各種各樣的問(wèn)題。
要學(xué)習(xí) ES6 中的 class 首先要了解 ES5 中的構(gòu)造函數(shù),主要了解在構(gòu)造函數(shù)中是如何實(shí)現(xiàn)類和繼承的,了解了這些知識(shí)帶你有助于后面我們更深入的理解 ES6 中的 class。
2. 構(gòu)造函數(shù)
2.1 基本用法
我們知道在 ES5 中如果想創(chuàng)建一個(gè)實(shí)例,是通過(guò)構(gòu)造函來(lái)實(shí)現(xiàn)的。下面我們創(chuàng)建一個(gè)動(dòng)物的類:
function Animal(type) {
this.type = type || '鳥(niǎo)類';
}
Animal.prototype.eat = function() {
console.log('鳥(niǎo)類吃蟲(chóng)子!')
};
var animal = new Animal();
上面的代碼就是使用構(gòu)造函數(shù)來(lái)創(chuàng)建一個(gè)類,這里的構(gòu)造函數(shù)首字母需要大寫(xiě),這是約定俗成的,不需要解釋記住就行。然后使用 new 的方式來(lái)實(shí)例化一個(gè)實(shí)例。
了解構(gòu)造函數(shù)后,我們要明確地知道創(chuàng)建的實(shí)例有兩種屬性,一種是自己的,一種是公用的?針對(duì)上面的代碼中 type 和 eat 哪個(gè)是自己的那個(gè)是公用的呢?一般來(lái)說(shuō)綁定在 this 上的是自有屬性,因?yàn)樵趯?shí)例化一個(gè)對(duì)象后 this 是指向這個(gè)實(shí)例的;而公共屬性一般認(rèn)為是 prototype 上的。另外,我們可以使用 hasOwnProperty 來(lái)判斷是否是自身的屬性。
console.log(animal.hasOwnProperty('type')); // true
console.log(animal.hasOwnProperty('eat')); // false
為什么要知道屬性是否是自己的呢?如果能想明白這個(gè)那么就會(huì)對(duì)類的繼承有個(gè)深入的理解。下面我們來(lái)看兩段代碼:
var animal1 = new Animal();
var animal2 = new Animal();
console.log(animal1.type); // 鳥(niǎo)類
console.log(animal2.type); // 鳥(niǎo)類
animal1.type = '家禽';
console.log(animal1.type); // 家禽
console.log(animal2.type); // 鳥(niǎo)類
console.log(animal1.eat()); // 鳥(niǎo)類吃蟲(chóng)子!
console.log(animal2.eat()); // 鳥(niǎo)類吃蟲(chóng)子!
animal1.__proto__.eat = function() {
console.log('家禽吃糧食!')
}
console.log(animal1.eat()); // 家禽吃糧食!
console.log(animal2.eat()); // 家禽吃糧食!
上面的代碼中我們可以看出當(dāng)我們對(duì) animal1 屬性 type 修改后不會(huì)影響 animal2 的 type 屬性,但是我們可以通過(guò) animal1 的原型鏈對(duì)原型上的 eat 方法進(jìn)行修改后,這時(shí) animal2 上的 eat 方法也被修改了。這說(shuō)明在實(shí)例上修改自有屬性不會(huì)影響其他實(shí)例上的屬性,但是,對(duì)非自有屬性進(jìn)行修改時(shí)就會(huì)影響其他屬性的方法。主要這樣會(huì)存在一個(gè)隱患,實(shí)例可以修改類的方法,從而影響到其他繼承這個(gè)類的實(shí)例。在這樣的情況下我們要想實(shí)現(xiàn)一個(gè)完美的繼承就需要考慮很多的東西了。
2.2 __proto__
、 prototype
、 constructor
在說(shuō)構(gòu)造函數(shù)繼承之前我們需要明確幾個(gè)概念: __proto__
、 prototype
、 constructor
這三個(gè)都是構(gòu)造函數(shù)中的概念,中文的意思可以理解為 __proto__
(原型鏈) 、 prototype
(原型) 、 constructor
(構(gòu)造方法)。它們?cè)?class 上也是存在的。想要了解它們之間的關(guān)系,我們先看下面的幾段代碼:
var animal = new Animal();
animal.__proto__ === Animal.prototype; // true
animal.__proto__.hasOwnProperty('eat'); // true
animal.constructor === animal.__proto__.constructor; // true
通過(guò)上面的關(guān)系對(duì)比可以使用示意圖的方式更容易理解。
通過(guò)上面的代碼和示意圖我們知道,原型是構(gòu)造函數(shù)上的屬性,實(shí)例可以通過(guò)自身的原型鏈查找到,并且可以修改屬性。
2.3 繼承
了解了 __proto__
、 prototype
、 constructor
三者的關(guān)系那么我們就要來(lái)學(xué)習(xí)一下構(gòu)造函數(shù)的繼承了,上面我們定義了一個(gè)動(dòng)物的構(gòu)造函數(shù),但是我們不能直接去 new 一個(gè)實(shí)例,因?yàn)?new 出來(lái)的實(shí)例沒(méi)有任何意義,是一個(gè)動(dòng)物實(shí)例,沒(méi)有具體指向。這時(shí)我們需要?jiǎng)?chuàng)建一個(gè)子類來(lái)繼承它。這時(shí)可以對(duì) Animal 類做個(gè)限制:
function Animal(type) {
if (new.target === Animal) {
throw new Error('Animal 類不能被 new,只能被繼承!')
}
this.type = type || '鳥(niǎo)類';
}
Animal.prototype.eat = function() {
console.log('鳥(niǎo)類吃蟲(chóng)子!')
};
var animal = new Animal();
//VM260:3 Uncaught Error: Animal 類不能被 new,只能被繼承!
既然不能被 new 那要怎么去繼承呢?雖然不能被 new 但是我們可以去執(zhí)行這個(gè)構(gòu)造函數(shù)啊,比較它本質(zhì)還是一個(gè)函數(shù)。執(zhí)行構(gòu)造函數(shù)時(shí) this 的指向就不是當(dāng)前的實(shí)例了,所以還需要對(duì) this 進(jìn)行綁定。我們定義一個(gè)子類:Owl(貓頭鷹)
function Owl() {
Animal.call(this);
}
var owl = new Owl();
通過(guò)使用 call 方法在 Owl 內(nèi)部綁定 this,這樣實(shí)例就繼承了 Animal 上 this 的屬性了。但是在 Animal 的原型中還有關(guān)于 Animal 類的方法,這些方法怎么繼承呢?
首先要明確的是不能使用 Owl.prototype = Animal.prototype
這樣的方式去繼承,上面也說(shuō)了這會(huì)使我們對(duì)子類原型修改的方法會(huì)作用到其他子類中去。那么怎么可以實(shí)現(xiàn)這一繼承呢?這時(shí)就需要原型鏈出場(chǎng)了,我們可以使用 Owl 原型上的原型鏈指向 Animal 的原型,實(shí)例 owl 根據(jù)鏈的查找方式是可以繼承 Animal 的原型上的方法的。
function Owl() {
Animal.call(this);
}
Owl.prototype.__proto__ = Animal.prototype;
var owl = new Owl();
owl.eat(); // 鳥(niǎo)類吃蟲(chóng)子!
通過(guò)原型鏈的方式還是比較麻煩的,也不優(yōu)雅,ES6 提供了 setPrototypeOf()
方法可以實(shí)現(xiàn)相同的效果:
// Owl.prototype.__proto__ = Animal.prototype;
Owl.setPrototypeOf(Owl.prototype, Animal.prototype);
這樣在子類 Owl 的原型上增加方法不會(huì)影響父類,這樣也算是比較好的方式解決了子類的繼承。
3. 小結(jié)
本節(jié)沒(méi)有去學(xué)習(xí) class 的使用,而是復(fù)習(xí)了在 ES5 中是怎么定義類的存在的,使用的是構(gòu)造函數(shù)的方式來(lái)定義一個(gè)類。在類的實(shí)際應(yīng)用中繼承是最為關(guān)鍵的,通過(guò)對(duì)如何實(shí)現(xiàn)構(gòu)造函數(shù)中的繼承,復(fù)習(xí)了原型、原型鏈和構(gòu)造方法。在構(gòu)造函數(shù)的繼承中,子類不能直接去 new 一個(gè)父類,因?yàn)檫@樣沒(méi)有意義。所以我們通過(guò)在子類中執(zhí)行構(gòu)造函數(shù)并綁定子類的this繼承了父類的屬性,再通過(guò)子類原型的原型鏈繼承了父類原型上的屬性。通過(guò)本節(jié)的學(xué)習(xí)我們更加深刻地理解構(gòu)造函數(shù)在 JavaScript 中扮演什么樣的角色,繼而 ES6 提出了 “真正“ 意義上的類,其實(shí)本質(zhì)還是通過(guò)原型的方式,下一節(jié)我們將具體學(xué)習(xí) ES6 的 class。