JavaScript Object-oriented programming




Prototype-based Inheritance (基於原型的繼承)

JavaScript 的繼承是 prototype-based,意思就是在 JavaScript 中沒有 class,所有的 object 都繼承自其它的 object

以下為繼承的實作範例,先建立父層類別 Person 和子層類別 Student:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Person 的 constructor function
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

// Person 的 prototype
Person.prototype.hi= function () {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}

// Student 的 constructor function
function Student(name) {
this.name = name;
this.score = 100;
}

// Student 繼承 Person
Student.prototype = new Person()

// Student的 prototype 方法
Student.prototype.say = function() {
console.log(`Say, ${this.name} ,My score is ${this.score}.`);
};

// 建立物件實例
var roman = new Student('roman');
var hera = new Student('hera');

// Hi, roman ,My age is 0.
roman.hi();

// Say, roman ,My score is 100.
roman.say();

roman.skill.push('JavaScript')

console.log(roman.skill) // ["HTML","CSS","JavaScript"]
console.log(hera.skill) // ["HTML","CSS","JavaScript"]

//物件的繼承關係 (prototype chain)
console.log(roman.__proto__ === Student.prototype)//true
console.log(Student.prototype.__proto__ === Person.prototype)//true

由於 skill 這個屬性是定義在父層元素,而父層元素是會被子層元素所影響;因為 roman 物件中並沒有 skill 屬性,因此會透過原型練存取到 Person 的 skill(roman.__proto__.skill),造成修改 roman.skill 卻連帶的影響到 hera.skill 。

1
2
3
roman.skill.push('JavaScript')
console.log(roman.skill) // ["HTML","CSS","JavaScript"]
console.log(hera.skill) // ["HTML","CSS","JavaScript"]

物件實例不會影響到父層屬性

為了避免子類別實例影響父類別屬性的問題,我們可以使用 Person.call(this) ,把 Person 裡面的 this 指稱對象改成當前透過 Student 建構式所建立的物件實例:

1
2
3
4
5
function Student(name) {
this.name = name;
this.score = 100;
Person.call(this,name);
}

等同於把原本 Person 的內容複製到 Student 中:

1
2
3
4
5
6
7
8
function Student(name) {
//父層屬性
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
//子層屬性
this.score = 100;
}

這時候物件實例就不會共享到父層的屬性了:

1
2
3
4
5
6
7
// 建立物件實例
var roman = new Student('roman');
var hera = new Student('hera');

roman.skill.push('JavaScript')
console.log(roman.skill) // ["HTML","CSS","JavaScript"]
console.log(hera.skill) // ["HTML","CSS"]

Object.create() and new operator 差異

也可以使用此方法來實現繼承:

1
Student.prototype = Object.create(Person.prototype);

為了避免瀏覽器太舊不支援 Object.create() ,可自行 polyfill 來達成一樣的效果:

1
2
3
4
5
6
7
8
9
10
11
12
// 同 Student.prototype = Object.create(Person.prototype)
Student.prototype = inherit(Person.prototype);

// Object.create()
function inherit(proto) {
//先建立一個空的 F constructor function
function F() {};
//將 F.prototype 指向傳進來的 proto
F.prototype = proto;
//用函式建構式的方式回傳
return new F();
}

兩種方法差異在於 new Person() 會執行建構式中的程式碼,而 object.create() 並不會,以下為第一個範例的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Person 的 constructor function
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

// Person 的 prototype
Person.prototype.hi= function () {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}

// Student 的 constructor function
function Student(name) {
this.name = name;
this.score = 100;
}

// Student 繼承 Person
Student.prototype = Object.create(Person.prototype)

// Student的 prototype 方法
Student.prototype.say = function() {
console.log(`Say, ${this.name} ,My score is         ${this.score}.`);
};

// 建立物件實例
var roman = new Student('roman');
var hera = new Student('hera');

// Hi, roman ,My age is undefined.
roman.hi();
// Say, roman ,My score is 100.
roman.say();

//Uncaught TypeError: Cannot read property 'push' of undefined
roman.skill.push('JavaScript')

因為沒有執行 Person 函式建構式的內容,當呼叫 roman.hi(); 中的 this.age 為 undefined;沒有 skill 屬性所以也無法使用 push 方法。

以下為使用兩種方式建立 roman 物件:

Student.prototype = new Person()



Student.prototype = Object.create(Person.prototype)

Polymorphism (多型)

透過在子類別中重寫覆蓋 (override) 掉父類別中的方法或屬性來完成多型。

1
2
3
Student.prototype.hi = function() {
console.log(`Hi, ${this.name} ,My score is ${this.score}.`);
};

因為原型鏈的關係,當執行 roman.hi() 時,就會優先執行 Student 中定義的 hi。

Encapsulation (封裝)

無法直接存取底線開頭 (underscore) 的屬性或方法,藉由公開的 hi() 來呼叫 __hi() 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

// protected
Person.prototype.__hi= function () {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}

// public
Person.prototype.hi= function () {
this.__hi();
}

var roman = new Person('roman','18');


// Hi, roman ,My age is 18.
roman.hi();

靜態屬性或方法在 JavaScript 中的實作方式,是直接將方法或屬性加在 constructor function 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name,age) {
Person.count++;
}

// 靜態屬性
Person.count = 0;


// 靜態方法
Person.getCount = function() {
console.log(`${Person.count}.`);
};

new Person();
new Person();

// 顯示 2
Person.getCount();

參考文獻

  1. https://www.fooish.com/javascript/oop-object-oriented-programming.html

  2. https://ithelp.ithome.com.tw/articles/10196763

  3. https://pjchender.github.io/2018/08/01/js-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-javascript-object-oriented-javascript/

  4. https://github.com/noahlam/articles/blob/master/JS%E4%B8%AD%E7%9A%84%E7%BB%A7%E6%89%BF(%E4%B8%8A).md