js通過構造函數結合原型鏈的方式來定義類和實現繼承。

雖然es6新增了類的概念,使得js面向對象編程的語法更加簡潔和清晰。但es6中的類只是一個語法糖,並沒有改變js中「類是特殊函數」和其「原型繼承」的本質。學習es5中定義類和實現繼承的方式,有助於理解這個本質。

本文介紹了es5中類定義和繼承的方式,並簡單對比了es6中的類語法。

  • 對象創建(定義類)
  • 繼承
  • ES6面向對象語法

對象創建(定義類)

組合(構造函數與原型)模式 是目前認同度最高的創建對象的方式。先看代碼:

// 構造函數
function Person(name) {
this.name = name;
}

// 原型
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
}

// 實例化
var person1 = new Person(Jack);

組合模式包含構造函數模式和原型模式兩部分。

構造函數

構造函數與普通函數並無區別,只是使用new關鍵字調用。

使用new調用構造函數時,會經歷4個步驟:

  1. 創建一個對象
  2. 將構造函數的作用域賦給新建的對象(因此this指向了這個新對象)
  3. 執行構造函數中的代碼
  4. 返回新對象

js中的函數一般包含 [[Call]]和[[Construct]] 兩個內部方法。當通過函數調用符調用時(直接調用),運行[[Call]]中的代碼;通過new運算符調用時,調用[[Contruct]]創建一個對象。(實現[[Construct]]方法的對象稱作構造器。)

原型

prototype與[[prototype]]

每個函數都有一個prototype屬性(事實上只有其作為構造函數使用時才有用),用來定義它的實例對象的原型;

每個對象都有一個內部屬性[[prototype]], 指向其原型對象。

當使用new調用函數,創建實例化對象時,函數的prototype對象的引用被複制到實例的[[prototype]]中。

使用「[[]]」包裹的屬性為內部屬性,由底層引擎實現,本不暴露到js層面。但chrome中提供了__proto__屬性來訪問[[prototype]]的值

constructor

函數的原型對象prototype默認帶有兩個屬性: constuctor和__proto__。 constructor指向函數本身,__proto__指向Object

當使用對象字面量定義的對象,只有默認有一個指向Object的__proto__屬性。

因此,在使用字面量方式重寫Person.prototype時,constructor被丟掉了,需要手動添加。

事實上,prototype上默認的constructor屬性是不可枚舉的,準確的寫法應該是:

Person.prototype = {
sayName: function() {
console.log(this.name);
}
}
Object.defineProperty(Person.prototype, constructor, {
enumberable: false,
value: Person
})

其他模式及特點 * (有興趣看) *

工廠模式

「工廠模式」顧名思義,就是把函數當做一個(生產對象的)工廠,把對象當成產品。對象的創建和初始化操作就是一個標準生產線。

// 工廠函數
function personFactory(name, age) {
var person = new Object();

person.name = name;
person.age = age;
person.sayName = function() {
console.log(this.name);
}

return person;
}

// 創建對象
var person1 = personFactory(Jack, 21);

缺點:

  • 無法通過instanceof判斷實例類別。(對象識別問題)
  • 每次創建對象都要重新定義屬性和方法。(無法共享屬性和方法)

構造函數模式

構造函數模式可以解決對象識別問題。

構造函數通過new關鍵字調用。函數內不需手動創建 / 返回對象。通過this給實例添加屬性 / 方法。

// 構造函數
function Person(name, age) {
this.name = name;
this.age = age;

this.sayName = function() {
console.log(this.name);
}
}

// 創建對象
var person1 = new Person(Jack, 20);

// 判斷實例的類型
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true

構造函數可以使用instanceof校驗實例的類型。原理如下:

拿instanceof左側的對象的原型鏈上的節點 依次 與右側函數的prototype屬性做比較。如果發現有兩者相同(指向同一個對象),則停止比較,返回true。遍歷到原型鏈頂部時,仍未找到相同情況,則返回false。參考:instanceof運算符,[[hasInstance]]

缺點:

  • 每次創建對象都要重新定義屬性和方法。(不能共享屬性和方法)

原型模式

通過原型模式可以定義共享的屬性和方法。

function Person() {}
// 定義共享屬性|方法
Person.prototype.sayName = function() {
console.log(this.name);
}

var person1 = new Person();
person1.name = Jack;
var person2 = new Person();
person2.name = Alice;

person1.sayName(); // Jack
person2.sayName(); // Alice
console.log(person1.sayName === person2.sayName); // true

例子中將公用方法sayName定義在Person的prototype。由於所有Person的實例化對象都指向這同一個原型,所以sayName方法被所有實例共享。

然而定義私有屬性name時,就顯得麻煩了。

上面講到的「組合構造函數和原型模式」就是結合構造函數與原型模式的優勢。在構造函數內方便地定義私有屬性,而通過原型模式定義公有屬性和方法。

動態原型模式

獨立地定義構造函數和原型的方式,會讓一些人(尤其是有其他OO語言開發經驗的人)感到困惑。動態原型模式便是為了解決這個問題,它把所有的信息都封裝在構造函數中。即在構造函數中動態地定義原型。

function Person(name, age) {
this.name = name;
this.age = age;
// 動態原型
if(typeof this.sayName != "function") { // 沒找到sayName方法,說明原型還沒有定義
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}

var person1 = new Person(Jack, 19);
person1.sayName(); // Jack

寄生構造函數模式

寄生構造函數模式通常用於改造內建對象。假設我們想創建一個具有額外方法的特殊數組,又不能直接修改Array構造函數。就可以使用這個模式:

function SpecialArray() {
var values = new Array();

values.push.apply(values, arguments);

values.toPipedString = function() {
return this.join("|");
}

return values;
}

var colors = new SpecialArray(red, blue, green);
console.log(colors.toPipedString()); // "red|blue|green"

可以看出SpecialArray函數與工廠函數沒有區別,不過它使用new操作符調用,故SpecialArray實際上是一個構造函數。

構造函數在不返回值的情況下,會默認返回新的對象實例。而如果在構造函數末尾添加一個return語句,可以重寫調用構造函數時的返回值。

需要強調的是,SpecialArray的使用方式與原生的Array並無區別。這也是寄生構造函數模式追求的目標。

繼承

組合繼承是最常用的繼承模式。寄生組合式繼承是引用類型最理想的繼承範式。

組合繼承

// 定義父類構造函數及原型
function SuperType(name) {
this.name = name;
}
SuperType.prototype.sayName = function() {
console.logf(this.name);
}
// 子類構造函數
function SubType(name, age) {
SuperType.call(this, name);// 調用父類構造函數
// 添加子類自有屬性
this.age = age;
}

// 繼承
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

// 添加子類方法
SubType.prototype.sagAge = function() {
console.log(this.age);
}

var sub1 = new SubType(Jack, 18);
sub1.sayName(); // "Jack"

上面的例子,通過將SubType的prototype指向SuperType的實例,從而子類的實例可以通過原型鏈訪問到父類的方法。此外,在子類的構造函數中,使用call方法調用父類的構造函數,來繼承的實例屬性。

所以,js主要通過原型鏈實現繼承,而借用構造函數可以為每個實例定義自己的屬性。

寄生組合式繼承

組合繼承存在兩個問題:

  1. 定義子類是需要調用兩次父類的構造函數。(一次是在子類構造函數內,一次是在創建子類原型時調用。)
  2. 子類的原型中包含父類的所有實例屬性。(這些屬性對子類來說是無用的)

寄生組合式繼承很好地解決了這些問題。

// 實現繼承(寄生式繼承)
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
// 定義超類
function SuperType(name) {
this.name = name;
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
// 子類構造函數
function SubType(name, age) {
SuperType.call(this, name);

this.age = age;
}
// 繼承
inheritPrototype(Subtype, SuperType);

SubType.prototype.sayAge = function() {
console.log(this.age);
}

上面例子中inheritPrototype()接受兩個參數: 子類構造函數subType 和 父類構造函數superType。 執行inheritPrototype後,subType的原型對象的__proto__指向了superType的原型對象。即subType.prototype.__proto__ = superType.prototype,由此實現繼承。(此時,子類的prototype中只有constructor和__proto__兩個屬性,不會有父類的實例屬性。)

Object.create()是es5新增的,用來規範原型式繼承。

Object.create()返回一個新建對象。該方法接受兩個參數,一個參數作為新對象的原型對象;第二個是可選參數,為新對象定義額外的屬性。當只傳入一個參數時,該方法等同於:

function object(o) {
var F = function() {}
F.prototype = o;
return new F();
}

上面這段代碼就是原型式繼承的核心。使用原型式繼承,需要先有一個對象作為繼承的基礎,然後吧這個對象作為參數傳給object(), 得到一個新對象,這個對象新對象會把傳入的基礎對象作為原型。

ES6面向對象語法

es6引入了類的概念,通過class關鍵字可以定義類,通過extends實現繼承。

定義類

es6類語法是一個語法糖。就是說它的絕大部分功能,都可以用es5實現,es6類語法並沒有為js添加新的功能。

class SuperType {
// 構造函數
constructor(name) {
this.name = name;
}
// 原型方法
sayName() { // 同SuperType.prototype.sayName = function ...
console.log(this.name);
}
// 靜態方法
static sayHi() { // 同SuperType.sayHi = function ...
console.log(hi.);
}
}

es6中使用關鍵字class來定義類。類中只有一個保留方法constructor,他的作用和es5中的構造函數相同。

有趣的是,使用typeof測試類的類型時,返回的是"function"。

類內部定義的方法會被添加在它的原型對象上。

static關鍵字用來定義靜態方法,靜態方法不會被添加到原型上。因此也不會被繼承。

繼承

es6主要依靠extends關鍵字和super()方法來實現繼承。

super()方法可以在子類中調用父類的構造函數。與es5中使用call調用同理。

class SubType extends SuperType {
constructor(name, age) {
// 調用父類構造函數
super(name); // 同SuperType.call(this, name)
this.age = age;
}
// 添加子類原型方法
sayAge() {
console.log(this.age);
}
}

var sub1 = new SubType(Jack, 17);
sub1.sayName(); // "Jack"
sub1.sayAge(); // 17

推薦閱讀:

相关文章