JavaScript 物件導向式程式設計 (Object-oriented programming, OOP)
本篇來介紹在 JavaScript 中如何實作物件導向 (OOP)!
物件導向程式使用「物件」來做設計,有兩個主要的概念:
- 類別 (class):將一件事物的特性封裝成一個抽象的類別,類別中會包含資料的形式以及操作資料的方法。
- 物件 (object):根據一個類別為模板,建立出來的類別實例。
宣告一個類別 (Defining a Class) - Constructor Function
JavaScript 是基於 Prototype-based 的語言,不像 C++ 或 Java 等 Class-based 的語言有 class 語法來宣告一個類別,JavaScript 使用函數 (function) 做為類別 (class) 的建構子 (constructor) 來定義一個類別。
var Person = function () {
// ...
};
上面我們宣告了一個 Person 類別的建構子 (constructor function)。
定義類別的屬性 (Property) - this
你可以用 this 在建構函數中定義類別的屬性:
var Person = function (nickname) {
this.nickname = nickname;
};
上面我們定義了 Person 類別的 nickName 屬性。
this
用來引用物件本身。定義類別的方法 (Method) - prototype
我們將一個函數加到類別的 prototype object 上,即定義一個類別方法:
Person.prototype.sayHello = function() {
alert('Hello, I\'m ' + this.nickname);
};
上面我們對 Person 類別定義了一個 sayHello 方法。
類別的實例 (Instance),物件 - new
我們用關鍵字 new
一個 constructor function 來建立 (instantiate) 一個新的物件實例 (instance):
var mike = new Person('Mike');
操作物件:
// 用 "." 點號存取物件的屬性
mike.nickname; // Mike
// 或用 [] 的語法,也可以存取物件的屬性
mike['nickname']; // Mike
// 執行物件的方法
mike.sayHello(); // 顯示 Hello, I'm Mike
// 屬性可以讀,也可以寫
mike.nickname = 'Brad';
mike.sayHello(); // 顯示 Hello, I'm Brad
Prototype-based Inheritance (基於原型的繼承)
什麼是繼承 (inheritance)?就是一個類別可以繼承其他類別的屬性和方法,然後再延伸增加自己的。
在 C++ 或 Java 中,分別有 class 和 object,而一個 class 繼承自其它 class,我們稱做 class-based inheritance。而在 JavaScript 的繼承是 prototype-based,意思就是在 JavaScript 中沒有 class,所有的 object 都繼承自其它的 object。
先再來介紹幾個觀念:
constructor
在 JavaScript 每個物件都有一個屬性叫 constructor,constructor 這屬性指向該物件的 constructor function。
例如:
var Person = function (nickname) {
this.nickname = nickname;
};
var mike = new Person('Mike');
// true
mike.constructor === Person;
上面的 mike.constructor 會指向 mike 這物件的建構子 (constructor function),也就是 Person。
prototype
而每個 constructor function 都有一個屬性叫 prototype (原型物件),prototype 物件上面有類別的屬性和方法。
所以在 JavaScript 中,類別更精確來說其實應該是稱原型 (prototype)。
prototype chain
前面提到 JavaScript 的繼承是 prototype-based,那是什麼意思?
JavaScript 的繼承關係,是將不同的 object 藉由 prototype 串連在一起,形成一個 prototype chain (原型鍊)。JavaScript 在尋找一個物件有沒有某個屬性或某個方法,就是沿著 prototype chain 一路往上找,直到找到為止 (或找不到 undefined)。
__proto__
, Object.getPrototypeOf(obj)
每個物件內部都有一個 __proto__
屬性,指向一個物件繼承的原型 (internal prototype)。
你也可以用 Object.getPrototypeOf(obj)
方法取得一個物件的 internal prototype。
例如:
Object.getPrototypeOf(mike) === mike.__proto__
Object.getPrototypeOf(obj)
方法。__proto__
屬性。Prototype-based Inheritance Example (繼承實作範例)
舉個例子來看 JavaScript 怎麼來實作繼承關係:
// 建立一個叫 Shape 的 constructor function
var Shape = function () {
};
// 建立一個物件 p
var p = {
a: function () {
alert('aaa');
}
};
// 將 p 指定給 Shape (constructor function) 的 prototype
Shape.prototype = p;
// 建立一個新物件 circle (Shape 的實例)
var circle = new Shape();
// 顯示 aaa
circle.a();
// true
Shape.prototype === circle.__proto__;
// 在 circle 物件上增加一個 a 方法
circle.a = function() {
alert('bbb');
};
// 顯示 bbb
circle.a();
+-------------------+ | Shape.prototype | +-------------------+ ↑ circle.__proto__ +-------------------+ | circle | +-------------------+
上面的例子中,物件的繼承關係 (prototype chain) 就像上圖這樣,所有的屬性和方法會從最下面的 circle 開始找,一路找到 prototype chain 結束。所以第一個 circle.a() 會顯示 aaa 因為在 circle 物件中找不到,繼續往上找到在 Shape.prototype 中有 a() 這個方法;第二個 circle.a(),因為在 circle 有定義了 a() 這個方法,所以就會執行 circle 的 a()。
null
!再來看另一個繼承的例子:
// 叫 Animal 的 constructor function
function Animal(name) {
this.name = name;
}
// Animal 的 prototype
Animal.prototype = {
canWalk: true,
sit: function() {
this.canWalk = false;
alert(this.name + ' sits down.');
}
};
// 叫 Kangaroo 的 constructor function
function Kangaroo(name) {
this.name = name;
}
// 使 Kangaroo 繼承 Animal
Kangaroo.prototype = inherit(Animal.prototype);
// Kangaroo 的 prototype 方法
Kangaroo.prototype.jump = function() {
this.canWalk = true;
alert(this.name + ' jumps!')
};
// 建立 Kangaroo 的物件實例
var kango = new Kangaroo('kango');
// 顯示 kango sits down.
kango.sit();
// 顯示 kango jumps!
kango.jump();
// 繼承的 helper function
function inherit(proto) {
function F() {};
F.prototype = proto;
return new F();
}
你可能會在其他地方看到另一種常見的繼承寫法,像是這樣:
Kangaroo.prototype = new Animal();
那跟我這邊使用一個 inherit helper function 的方法差在哪呢?
用 new Animal 的方法基本上也沒錯,因為實例化的物件原本就會繼承 Animal.prototype,但是用 new Animal 的方法有幾個缺點:
- 你需要事先知道 Animal 這 constructor function 的參數有哪些、怎麼用,像我們這邊沒傳參數 name 進去,就可能會不小心發生意外錯誤!
- 另外本質上,我們只是想繼承 Animal 的 prototype 啊,不是要建立一個 Animal 物件實例。
再來看看 inherit helper function 的作法就乾淨多了:
- 它先建立一個空的、乾淨的 F constructor function
- 再將 F.prototype 指向傳進來的 proto
- 最後將一個繼承 proto 的空物件傳回去
在這個例子中,物件的繼承關係 (prototype chain) 會像是這樣:
+----------------------+ | Animal.prototype | +----------------------+ ↑ Kangaroo.prototype.__proto__ +----------------------+ | Kangaroo.prototype | +----------------------+ ↑ kango.__proto__ +----------------------+ | kango | +----------------------+
這就是 JavaScript OOP 繼承的實作方法啦!
Polymorphism (多型)
OOP 中的多型 (polymorphism) 在 JavaScript 中的實作也很簡單,就是在子類別中重寫覆蓋 (override) 掉父類別中的方法或屬性。
例如:
Kangaroo.prototype.sit = function() {
alert(this.name + ' sits as a Kangaroo!');
}
當執行 kango.sit() 時,就會優先執行 Kangaroo 中定義的 sit,因為 prototype chain 原理!
Private/Protected Properties/Methods - Encapsulation (封裝)
在 JavaScript 中沒有 private/protected 的屬性,只能用 naming convention 的方式,像是大家約定好不要直接存取底線開頭 (underscore) 的屬性。
例如:
function Animal(name) {
this.name = name;
}
// protected
Animal.prototype._walk = function() {
alert(this.name + ' walks.');
};
// public
Animal.prototype.walk = function() {
this._walk();
}
var doggy = new Animal('doggy');
// 大家默契約定好只可以執行公開的 walk() 方法,禁止直接執行 _walk()
doggy.walk();
Static Methods/Properties
OOP 中的靜態屬性或方法在 JavaScript 中的實作方式,是直接將方法或屬性加在 constructor function 上。
例子:
function Animal() {
Animal.count++;
}
// 靜態屬性
Animal.count = 0;
// 靜態方法
Animal.getCount = function() {
alert(Animal.count);
};
new Animal();
new Animal();
// 顯示 2
Animal.getCount();
instanceof
instanceof 運算子用來檢查一個物件 (object) 是否建立或繼承 (prototype chain) 自某個 constructor。
語法:
object instanceof constructor
instanceof 比較的邏輯是:
- 取出
object.__proto__
- 比較
object.__proto__
和constructor.prototype
是否相等 - 如果不相等,將 object 重設為
object = object.__proto__
,再重複步驟 2,直到條件符合或 prototype chain 結束找不到為止
例子:
function Kangaroo() {}
var kango = new Kangaroo;
// true
kango instanceof Kangaroo;
function C() {}
function D() {}
var o = new C();
// true
o instanceof C;
// false
o instanceof D;
// true
o instanceof Object;
// true
C.prototype instanceof Object;
C.prototype = {};
var o2 = new C();
// true
o2 instanceof C;
// false
o instanceof C;
// 繼承
D.prototype = new C();
var o3 = new D();
// true
o3 instanceof D;
// true
o3 instanceof C;