JavaScript裏的類和繼承

JavaScript與大部分客戶端語言有幾點明顯的不一樣:html

JS是 動態解釋性語言,沒有編譯過程,它在程序運行過程當中被逐行解釋執行
JS是 弱類型語言,它的變量沒有嚴格類型限制
JS是面嚮對象語言,但 沒有明確的類的概念(雖然有class關鍵字,然而目前並無什麼卵用)
JS雖然沒有類,但能夠經過一些方法來模擬類以及實現類的繼承。
一切皆對象,還先從對象提及。函數

 

一、對象(Object)


ECMA-262對對象的定義是:無序屬性的集合,其屬性能夠包含基本值、對象或者函數。
直觀點描述,就是由多個鍵值對組成的散列表。工具

JS建立對象的方法和其它語言大同小異:this

// 經過構造函數建立
var zhangsan = new Object();
zhangsan.name = "張三";
zhangsan.age = 20;
zhangsan.sayHi = function() {
    alert("Hi, I'm " + this.name);
};

// 經過對象字面量建立
var lisi = {
    name: "李四",
    age: 21,
    sayHi: function() {
        alert("Hi, I'm " + this.name);
    }
};

當須要大量建立相同結構的對象時,可使用 對象工廠(Object Factory)spa

// 對象工廠
function createPerson(name, age) {
    return {
        name: name,
        age: age,
        sayHi: function() {
            alert("Hi, I'm " + this.name);
        }
    };
}

var zhangsan = createPerson("張三", 20);
var lisi = createPerson("李四", 21);

但經過這種方式建立出來的實例,不能解決類型識別問題,只知道它是一個對象,但具體什麼?沒法判斷:prototype

zhangsan instanceof ?
lisi.constructor = ?

這時,「類」就登場了。指針

 

二、類(Class)


2.一、構造函數模式

事實上,JS中每一個函數(function)自己就是一個構造函數(constructor),就是一個類:code

// 構造函數模式
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHi = function() {
        alert("Hi, I'm " + this.name);
    };
}

var zhangsan = new Person("張三", 20);
var lisi = new Person("李四", 21);
alert(zhangsan instanceof Person); // true
alert(lisi.constructor === Person); // true

這裏面其實有個問題:htm

alert(zhangsan.sayHi === lisi.sayHi); // false

多個實例中的同名方法並不相等,也就是說存在多個副本。而這些行爲是相同的,應該指向同一個引用纔對。對象

爲了解決這個問題,JS爲每一個函數分配了一個 prototype(原型)屬性,該屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。

2.二、原型模式

原型(Prototype):指向一個對象,做爲全部實例的基引用(base reference)。

// 構造函數+原型組合模式
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayHi = function() {
    alert("Hi, I'm " + this.name);
};

var zhangsan = new Person("張三", 20);
var lisi = new Person("李四", 21);
alert(zhangsan.sayHi === lisi.sayHi); // true

在Person中,sayHi 是 原型成員(prototype),name 和 age 是 特權成員(privileged),它們都是 公共成員(public)

注:「特權」是道格拉斯提出的名詞。道格拉斯·克羅克福德(Douglas Crockford),Web界人稱道爺,JSON創立者,《JavaScript語言精粹》做者,JSLint、JSMin、ADsafe開發者。

類的原型帶有一個 constructor 屬性,指向該類的構造函數(若是從新分配了原型指針,須要手動添加 constructor 屬性);類的實例上會自動生成一個屬性指向該類原型(在Chrome上能夠經過「__proto__」訪問到該對象,而IE上該屬性則是不可見的)。
Person、Person的原型、Person的實例間的關係以下:

須要注意的是,原型成員保存引用類型值時需謹慎:

Person.prototype.friends = [];
zhangsan.friends.push("王五");
alert(lisi.friends); // ["王五"]

張三的基友莫名其妙就變成李四的基友了,因此 friends 應該添加爲特權成員,而不是原型成員。

2.三、類的結構

綜上所述,JS中的類的結構大體以下:

  • 類由構造函數和原型組成
  • 構造函數中能夠聲明私有成員和添加特權成員
  • 原型中能夠添加原型成員
  • 私有成員能夠被特權成員訪問而對原型成員不可見
  • 特權成員和原型成員都是公共成員

 

三、繼承(Inherit)


在JS中繼承是如何實現的呢?

3.一、拷貝繼承

最簡單直接的方式莫過於 屬性拷貝

// 拷貝繼承
function extend(destination, source) {
    for (var property in source) {
        destination[property] = source[property];
    }
}
extend(SubClass.prototype, SuperClass.prototype);

這種方式雖然實現了原型屬性的繼承,但有一個很是明顯的缺陷:子類實例沒法經過父類的 instanceof 驗證,換句話說,子類的實例不是父類的實例。

3.二、原型繼承

在 Chrome 的控制檯中查看 HTMLElement 的原型,大體以下:

能夠清晰看到,HTMLElement 的原型是 Element 的實例,而 Element 的原型又是 Node 的實例,從而造成了一條 原型鏈(Prototype-chain),JS的原生對象就是經過原型鏈來實現繼承。

這裏順道說下解釋器對實例屬性的查找過程:

  1. 在特權屬性中查找
  2. 特權屬性中沒找到,再到原型屬性中查找
  3. 原型屬性中沒找到,再到原型的原型屬性中查找
  4. 直到根原型還沒找到,返回 undefined

這就說明爲何咱們自定義的類明明沒有聲明 toString() 方法,但仍然能夠訪問到,由於全部對象都繼承自 Object。

所以,咱們也能夠經過原型鏈來實現繼承:

// 原型鏈繼承
function User(name, age, password) {
    // 繼承特權成員
    Person.call(this, name, age);
    this.password = password;
}
// 繼承原型
User.prototype = new Person();
// 修改了原型指針,需從新設置 constructor 屬性
User.prototype.constructor = User;

var zhangsan = new User("張三", 20, "123456");
zhangsan.sayHi(); // Hi, I'm 張三

運行正常,貌似沒什麼問題,但其實裏面仍是有些坑:

父類的構造函數被執行了 2 次:繼承特權成員時 1 次,繼承原型時又 1 次。
父類初始化兩次,這有時會致使一些問題,舉個例子,父類構造函數中有個alert,那麼建立子類實例時,會發現有兩次彈框。
不只如此,還致使了下面的問題。從控制檯中查看子類的實例,結構以下:

能夠看到子類的原型中也包含了父類的特權成員(直接建立了一個父類實例,固然會有特權成員),只不過由於解釋器的屬性查找機制,被子類的特權成員所覆蓋,只要子類的特權成員被刪除,原型中相應的成員就會暴露出來:

delete zhangsan.name;
alert(zhangsan.name); // 此時訪問到的就是原型中的name

那怎麼辦呢?對此道爺提供了一個很實用的解決方案—— 原型式寄生組合繼承

3.三、原型式寄生組合繼承

咱們的目的是子類原型只繼承父類的原型,而不要特權成員,原理其實很簡單:建立一個臨時的類,讓其原型指向父類原型,而後將子類原型指向該臨時類的實例便可。實現以下:

function inheritPrototype(subClass, superClass) {
    function Temp() {}
    Temp.prototype = superClass.prototype;
    subClass.prototype = new Temp();
    subClass.prototype.constructor = subClass;
}
inheritPrototype(User, Person);

由於臨時類的構造函數是空實現,子類在繼承原型時天然不會執行到父類的初始化操做,也不會繼承到一堆亂七八糟的特權成員。

再看下 zhangsan 的結構:

此時,子類實例的原型鏈大體以下:

 

總結


修改後的代碼整理以下:

// 用於子類繼承父類原型的工具函數
function inheritPrototype(subClass, superClass) {
    function Temp() {}
    Temp.prototype = superClass.prototype;
    subClass.prototype = new Temp();
    subClass.prototype.constructor = subClass;
}

// 父類
function Person(name, age) {
    // 特權成員(每一個實例都有一份副本)
    this.name = name;
    this.age = age;
}
// 原型成員(全部實例共享)
Person.prototype.sayHi = function() {
    alert("Hi, I'm " + this.name);
};

// 子類
function User(name, age, password) {
    // 繼承父類特權成員(在子類中執行父類的初始化操做)
    Person.call(this, name, age);
    // 添加新的特權成員
    this.password = password;
}
// 繼承父類原型
inheritPrototype(User, Person);
// 重寫父類原型方法
User.prototype.sayHi = function() {
    alert("Hi, my name is " + this.name);
};
// 擴展子類原型
User.prototype.getPassword = function() {
    return this.password;
};

到此爲止,咱們已經比較完美地實現了類和類的繼承。

但每一個類、每一個子類、每一個子類的子類,都要這麼分幾步寫,也是很蛋疼的。對象有對象工廠,類固然也能夠搞個 類工廠(Class Factory),江湖上已有很多現成的類工廠,讓咱們能夠從統一規範的入口來生成自定義類。(見JavaScript幾種類工廠實現原理剖析

相關文章
相關標籤/搜索