JavaScript 面向對象淺析

什麼事類

類(class):類是對擁有一樣屬性(property)和行爲的一系列對象(object)的抽象。 這裏說的「行爲」,在基於類的面向對象的語言中一般叫作類的方法(method)。而在 JavaScript 裏,函數也是「一等公民」,能夠被直接賦值給一個變量或一個對象的屬性,所以在本文後續的討論中,把「行爲」也納入「屬性」的範疇。javascript

JavaScript 對類的實現

JavaScript 規定每個對象均可以有一個原型([[prototype]] 內部屬性)。(在實現 ECMAScript 5.1 規範之前,除了 Object.prototype 之外的對象都必須有一個原型。)每一個對象都「共享」其原型的屬性:在訪問一個對象的屬性時,若是該對象自己沒有這個屬性,則 JavaScript 會繼續試圖訪問其原型的屬性。這樣,就能夠經過指定一些對象的原型來使這些對象都擁有一樣的屬性。從而咱們能夠這樣認爲,在 JavaScript 中,以同一個對象爲原型的對象就是屬於同一個類的對象。java

1. JavaScript 中對象的原型的指定方式

那麼 JavaScript 中的對象與其原型是怎樣被關聯起來的呢?或者說,JavaScript 中的對象的原型是怎樣被指定的呢?數組

1.1 new 操做符

JavaScript 有一個 new 操做符(operator),它基於一個函數來建立對象。這個用 new 操做符建立出來的對象的原型就是 new 操做符後面的函數(稱爲「構造函數」)的 prototype 屬性。例如:閉包

var obj = {"key": 1};
function fun() {}
fun.prototype = obj;
var a = new fun();

此時 fun 對象的原型就是 obj 對象。app

1.2 Object.create 方法

Object.create 方法直接以給定的對象做爲原型建立對象。一個代碼例子:ide

var a = {"aa": 1};
var b = Object.create(a);

此時 b 對象的原型就是 a 對象。函數

1.3 Object.setPrototypeOf 方法

new 操做符和 Object.create 方法都是在建立一個對象的同時就指定其原型。而 Object.setPrototypeOf 方法則是指定一個已被建立的對象的原型。代碼例子:this

var a = {"aa": 1};
var b = Object.create(a);
// 此時 b 的原型是 a
var c = {"cc": 2};
Object.setPrototypeOf(b, c);
// 此時 b 的原型變爲 c 了
1.4 隱式指定

數字、布爾值、字符串、數組和函數在 JavaScript 中也是對象,而它們的原型是被 JavaScript 隱式指定的:prototype

  1. 數字(例如 一、1.一、NaN、Infinity)的原型是 Number.prototype;
  2. 布爾值(true 和 false)的原型爲 Boolean.prototype;
  3. 字符串(例如 ""、"abc")的原型爲 String.prototype;
  4. 函數(例如 function () {}、function (a) { return a + '1'; }) 的原型爲 Function.prototype;
  5. 數組(如 []、[1, '2'])的原型是 Array.prototype;
  6. 用花括號直接定義的對象(如 {}, {"a": 1})的原型是 Object.prototype。
2 JavaScript 中定義類的代碼示例

下面給出定義一個類的一段 JavaScript 代碼的示例。它定義一個名爲 Person 的類,它的構造函數接受一個字符串的名稱,還一個方法 introduceSelf 會輸出本身的名字。code

// ----==== 類定義開始 ====----
function Person(name) {
    this.name = name;
}
Person.prototype.introduceSelf = function () {
    console.log("My name is " + this.name);
};
// ----==== 類定義結束 ====----
// 下面實例化一個 Person 類的對象
var someone = new Person("Tom");
// 此時 someone 的原型爲 Person.prototype
someone.introduceSelf(); // 輸出 My name is Tom

若是轉換爲 ECMAScript 6 引入的類聲明(class declaration)語法,則上述 Person 類的定義等同於:

class Person {
    constructor(name) {
        this.name = name;
    }
    introduceSelf() {
        console.log("My name is " + this.name);
    }
}
3 對「構造函數」的再思考

在上面的例子中,假如咱們不經過 Person.prototype 來定義 introduceSelf 方法,而是在構造函數中給對象指定一個 introduceSelf 屬性:

function Person(name) {
    this.name = name;
    this.introduceSelf = function () {
        console.log("My name is " + this.name);
    };
}
var someone = new Person("Tom");
someone.introduceSelf(); // 也會輸出 My name is Tom

雖然這種方法中,經過 Person 構造函數 new 出來的對象也都有 introduceSelf 屬性,但這裏 introduceSelf 變成了 someone 自身的一個屬性而不是 Person 類的共有的屬性:

function Person1(name) {
    this.name = name;
}
Person1.prototype.introduceSelf = function () {
    console.log("My name is " + this.name);
};
var a = new Person1("Tom");
var b = new Person1("Jerry");
console.log(a.introduceSelf === b.introduceSelf); // 輸出 true
delete a.introduceSelf;
a.introduceSelf(); // 仍然會輸出 My name is Tom,由於 introduceSelf 不是 a 自身的屬性,不會被 delete 刪除
b.introduceSelf = function () {
    console.log("I am a pig");
};
Person1.prototype.introduceSelf.call(b); // 輸出 My name is Jerry
// 即便 b 的 introduceSelf 屬性被覆蓋,咱們仍然能夠經過 `Person1.prototype` 來讓 b 執行 Person1 類規定的行爲。
function Person2(name) {
    this.name = name;
    this.introduceSelf = function () {
        console.log("My name is " + this.name);
    };
}
a = new Person2("Tom");
b = new Person2("Jerry");
console.log(a.introduceSelf === b.introduceSelf); // 輸出 false
// a 的 introduceSelf 屬性與 b 的 introduceSelf 屬性是不一樣的對象,分別佔用不一樣的內存空間。
// 所以這種方法會形成內存空間的浪費。
delete a.introduceSelf;
a.introduceSelf(); // 會拋 TypeError
b.introduceSelf = function () {
    console.log("I am a pig");
};
// 此時 b 的行爲已經與 Person2 類規定的脫節,對象 a 和對象 b 看起來已經不像是同一個類的對象了

可是這種方法也不是一無可取。例如咱們須要利用閉包來實現對 name 屬性的封裝時:

function Person(name) {
    this.introduceSelf = function () {
        console.log("My name is " + name);
    };
}
var someone = new Person("Tom");
someone.name = "Jerry";
someone.introduceSelf(); // 輸出 My name is Tom
// introduceSelf 實際用到的 name 屬性已經被封裝起來,在 Person 構造函數之外的地方沒法訪問
// name 至關於 Person 類的一個私有(private)成員屬性

JavaScript 的類繼承

類的繼承實際上只須要實現:

  1. 子類的對象擁有父類定義的全部成員屬性;
  2. 子類的任何一個構造函數都必須在開頭調用父類的構造函數。

實現第 2 點的方式比較直觀。而怎樣實現第 1 點呢?其實咱們只須要讓子類的構造函數的 prototype 屬性 (子類的實例對象的原型) 的原型是父類的構造函數的 prototype 屬性 (父類的實例對象的原型),簡而言之就是:把父類實例的原型做爲子類實例的原型的原型。這樣在訪問子類的實例對象的屬性時,JavaScript 會沿着原型鏈找到子類規定的成員屬性,再找到父類規定的成員屬性。並且子類可在子類構造函數的 prototype 屬性中重載(override)父類的成員屬性。

1 代碼示例

下面給出一個代碼示例,定義一個 ChinesePerson 類繼承上文中定義的 Person 類:

function ChinesePerson(name) {
    Person.apply(this, name); // 調用父類的構造函數
}
ChinesePerson.prototype.greet = function (other) {
    console.log(other + "你好");
};
Object.setPrototypeOf(ChinesePerson.prototype, Person.prototype); // 將 Person.prototype 設爲 ChinesePerson.prototype 的原型

var someone = new ChinesePerson("張三");
someone.introduceSelf(); // 輸出「My name is 張三」
someone.greet("李四"); // 輸出「李四你好」

上述定義 ChinesePerson 類的代碼改用 ECMAScript 6 的類聲明語法的話,就變成:

class ChinesePerson extends Person {
    constructor(name) {
        super(name);
    }

    greet(other) {
        console.log(other + "你好");
    }
}
2 重載父類成員屬性的代碼示例

你會不會以爲上面代碼示例中,introduceSelf 輸出半英文半中文挺彆扭的?那咱們讓 ChinesePerson 類重載 introduceSelf 方法就行了:

ChinesePerson.prototype.introduceSelf = function () {
    console.log("我叫" + this.name);
};
var someone = new ChinesePerson("張三");
someone.introduceSelf(); // 輸出「我叫張三」

var other = new Person("Ba Wang");
other.introduceSelf(); // 輸出 My name is Ba Wang
// ChinesePerson 的重載並不會影響父類的實例對象
相關文章
相關標籤/搜索