[JS] 物件導向 JavaScript (object-oriented JavaScript)
Object-oriented JavaScript for beginners @ MDN > Learn web development
keywords: object-oriented programming
常用
// 取得某一物件的原型,使用 Object.getPrototypeOf()
Object.getPrototypeOf(obj); // 等同於過去的 obj.__proto__
// 檢驗某一屬性是否是該物件自己本身的,而非原型內的,使用 obj.hasOwnProperty(<property>)
obj.hasOwnProperty('foobar'); // 檢驗 obj 內有無名為 'foobar' 的屬性
// 檢驗某物件是否為某類別的實例 instanceof
obj instanceof Class;
物件導向程式設計(object-oriented programming)
keywords: 封裝(encapsulated)
, 命名空間(namespace)
, 物件實例(object instance)
, 類別(class)
, 建構子函式(constructor function)
, 實例化(instantiation)
物件導向程式設計(OOP)的基本概念是使用物件的方式來表徵,並模擬真實世界中的事物。透過類別(class)的函式建構式(function constructor)可以產生物件實例(object instance),而這個從類別產生物件實例的過程稱作實例化(instantiation)。
不同的類別之間可以有繼承關係,例如 Student 和 Teacher 的類別都可以繼承自 Person 這個類別,如此 Student 和 Teacher 共通的屬性都可以繼承自 Person 中的「姓名」、「年齡」等屬性,都可 以也可以保有該類別底下特有的部分。在多個不同的類別中建置相同的方法,但方法的內容不同,稱作「多型 (Polymorphism)」。
JavaScript 中的物件導向
JavaScript 中使用一種名為**「建構式函式(constructor functions)」**的函式來定義物件和它們的特色,透過函式建構式,將可以根據你的需要用更有效率的方式來產生許多物件,而這些物件都已經包含你所定義好的屬性和方法。
JavaScript 慣例上來說,函式建構式的命名會以大寫的字母開頭,如此可以更容易在程式中辨認出哪個函式是屬於建構式函式。
原型架構的程式語言
keywords: 原型鏈(prototype chain)
, Object.getPrototypeOf(obj)
, __proto__
此外,JavaScript 被稱作是「原型架構(prototype-based)」的程式語言,在每一個 JavaScript 的物件中,都包含了一個原型物件(prototype object)可以當作該物件的模版,用來讓 JavaScript 的物件 可以從原型物件中繼承屬性和方法,而原型物件本身可能又會有屬於它的原型物件,而這就是我們所稱的「原型鏈(prototype chain)」。更精確的說,這些原型物件中的屬性和方法是透過函式建構式定義的,而不是直接定義在物件實例上的。
很重要的是去區分物件的原型物件(prototype object)和定義函式建構的式的原型。某一物件的原型我們可以透過 Object.getPrototypeOf(obj)
或只透過即將被移除的 __proto__
屬性來存取;而函式建構式的原型則是透過該建構式的 prototype
屬性來存取,例如 Foobar.prototype
。
由於在 JavaScript 中是透過原型鏈來實做繼承的方式,在不同物件之間函式共享的行為被稱作 委託(delegation),即特定物件將功能委託至通用物件類型。「委託」其實比繼承更精確一點。因為「所繼承的功能」並不會複製到「進行繼承的物件」之上,卻是保留在通用物件之中。
函式建構式的 constructor 屬性
keywords: instanceObj.constructor
, instanceObj.constructor.name
每一個建構式函式的 prototype
物件都包含一個名為 constructor
的屬性,而這個 constructor
屬性會指稱到建立該實例的建構式函式,因此你甚至可以用某一物件實例來產生該類別的新的物件實例,例如:
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
var aaron = new Person('Aaron', 'Chen');
// aaron.constructor 會指稱回 Person 這個函式建構式,
// 因此可以透過它產生其他的物件實例
var john = new aaron.constructor('John', 'Liang');
var constructorName = aaron.constructor.name; // 取得建構式的名稱
我們不常透過一個物件實例來產生另一個物件實例,但是當你沒辦法參照到原本的 function constructor 時,這麼做非常方便。
修改 prototype
修改 prototype 中的方法(function)
我們可以透過 Foobar.prototype
的方式來修改某個 function constructor 的 prototype
的內容,要留意的是-當你改變了該函式建構式中 prototype
內的方法,那麼所有根據這個建構式所建立的實例(不管是修改 prototype
之前或之後所建立的實例),在使用這個方法時,都會用到更新 prototype
後的方法:
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// 定義一開始的 greet 方法
Person.prototype.greet = function () {
console.log(`Hello, ${this.firstName} ${this.lastName}`);
};
var aaron = new Person('Aaron', 'Chen');
var john = new aaron.constructor('John', 'Liang');
aaron.greet(); // Hello, Aaron Chen
// 修改原本的 greet 方法
Person.prototype.greet = function () {
console.log(`Welcome, ${this.firstName} ${this.lastName}`);
};
aaron.greet(); // Welcome, Aaron Chen
john.greet(); // Welcome, John Liang
當你改變了該函式建構式中
prototype
內的方法,那麼所有根據這個建構式所建立的實例(不管是修改prototype
之前或之後所建立的實例),在使用這個方法時,都會用到更新prototype
後的方法。
修改 prototype 中的屬性
我們很少直接在 prototype
定義屬性,除非它是在所有實例中都不會改變的常數:
Person.prototype.legalAge = 18;
你無法在建構式的 prototype
中透過 this
取得該物件的內容,因為這時候的 this
指稱到的會是全域物件(global object),因此你不能這麼做:
// 這麼做是錯誤的!!
Person.prototype.fullName = this.firstName + ' ' + this.lastName; // undefined undefined
JavaScript 中的繼承
keywords: Object.getOwnPropertyNames(obj)
原型鏈繼承:物件實例會影響到父層屬性
先建立父層類別 Person
和子層類別 Student
:
// Parent Classes
function Person(name, age) {
this.name = name || 'default';
this.age = age || 0;
this.interest = ['reading', 'music'];
}
Person.prototype.greet = function () {
console.log(`My name is ${this.name}. I am ${this.age} years old.`);
};
// Child Classes
function Student(name) {
this.name = name;
this.score = 80;
}
接著透過 Student
的原型鏈(Student.prototype = new Person();
),就可以讓 Student
繼 承 Person
的內容:
// 讓 Student 繼承 Person
Student.prototype = new Person();
// 增加 Student 自己的方法
Student.prototype.say = function () {
console.log(`My name is ${this.name}. I am studying.`);
};
使用子類別 Student
:
var aaron = new Student('Aaron');
console.log(aaron.name); // Aaron --子類覆蓋父類的屬性
console.log(aaron.interest); // [ 'reading', 'music' ] --父類的屬性
console.log(aaron.score); // 80 --子類自己的屬性
aaron.greet(); // My name is ... --繼承自父類的方法
aaron.say(); // I am studying --子類自己的方法
var lucy = new Student('Lucy');
console.log(lucy.interest); // [ 'reading', 'music' ]
這麼做雖然乍看沒有太大問題,但是由於 interest
這個屬性是定義在父層元素,而父層元素是會被子層元素所影響,因此如果這樣寫:
// 因為在 aaron 物件中並沒有 interest 屬性
// 因此會找到的是 aaron.__proto__.interest
// 因此這樣寫會修改到父層的屬性
aaron.interest.push('basketball');
所以,雖然我們是想修改 aaron.interest
卻連帶的影響到的 lucy.interest
console.log(aaron.interest); // [ 'reading', 'music', 'basketball' ]
console.log(lucy.interest); // [ 'reading', 'music', 'basketball' ]
使用
Student.prototype = new Person();
的作法與透過Object.create()
的方法類似,兩者的差別主要在於Object.create()
內的參數不一定要是 function constructor。
建構式函式繼承:物件實例不會影響到父層屬性
為了避免子類別的實例共享父類別屬性的問題,我們可以使用 Person.call(this)
的用法:
// Parent Class
function Person(name, age) {
this.name = name || 'default';
this.age = age || 0;
this.interest = ['reading', 'music'];
}
// Child Class
function Student(name, age, score) {
// 把 Person 裡面的 this 指稱對象改成當前透過 Student 建構式所建立的物件實例
Person.call(this, name, age); // 只需填寫想延伸進 Student 的屬性
this.score = score;
}
在
Person.call()
當中,不一定要填入Person
中所有的屬性,而是只需使用想延伸到Student
的屬性即可。
這時候等於是把 Person
建構式的內容複製了一份到 Student
當中:
function Student(name, age, score) {
// 等同於把原本 Person 的內容複製到 Student 中
this.name = name || 'default';
this.age = age || 0;
this.interest = ['reading', 'music'];
this.score = score;
}
這時候它們就不會共享到父層的屬性了:
aaron = new Student('aaron', 28, 80);
lucy = new Student('lucy', 29, 88);
aaron.interest.push('basketball');
console.log(aaron.interest); // ["reading", "music", "basketball"]
console.log(lucy.interest); // ["reading", "music"]