###### tags: `PHP` `Javascript`
# 物件導向基礎概念
> OOP,Object-Oriented Programming,是一種程式設計的方式,中國翻作「面向對象」
這篇筆記主要以 php 為主,JS 為輔。
## 為什麼需要物件導向
在一般寫程式的時候,我們經常會使用到各種函式(function),要用的時候才會再呼叫這個函式,函式遍布各處比較零散,因此在程式的可讀性較低。
而使用物件導向的方式,就像是用一個箱子把所有箱子需要用到的都放進去,會相對好維護。
在 php 當中物件導向的相關用法非常完整,但在 JS 中並不是以物件導向為基礎的語言,即便 ES6 後有相應的函式,但那也只是語法糖。而 ES5 之前也都是透過 JS 原生語法達成相同的效果。
## 名詞解釋
### Class 跟 Instance
Class,中文翻成「類別」,可以想像成是一個設計圖,而 Instance,中文翻成「實體」,就像是一個實際的商品。
Class (類別)會透過 new 的方式建構出一個 Instance(實體),這個 Instance 也是一個物件(Object)。
要特別注意的是一個設計圖(Class)可以產生出多個商品(Instance)。
一般 class 命名以大駝峰式:開頭大寫、後面單字字首也大寫,例如:MyName。
而 Instance 則是小駝峰式:開頭小寫、後面單字字首大寫,例如:myName。
### Method
被包在 Class 中的函式會被稱為 Method,中文翻成「方法」,如下面範例的 `hello()`。
* php 透過 `->hello()` 存取。
* JS 透過 `.hello()` 存取。
### Property
中文翻成「屬性」,是物件的特徵,可以理解成是物件裡需要的變數。
### Public 跟 Private
使用權限。JS 沒有這個概念。
在 Class 當中宣告函式、變數時,都需要再前面加上 `public` 或是 `private`,如果是 `public` 代表在 Class 外也可呼叫。反之如果是 `private`,則只能在 Class 內呼叫。
```php=
<?php
Class Dog { // 這個類別名稱是 dog
public function hello() { //公開的方法,名稱是 hello
echo 'hello, I am a dog.';
}
}
$realDog = new Dog(); // 建構出實體,實體名稱是 dog
$realDog->hello(); // 得出的結果是 hello, I am a dog. 成功使用方法
?>
```
今天如果 function 前面加的是 `private`,那上面的範例就會讀取不到。
那什麼時候會用到 `private` 呢?這時候我們就會討論到物件導向的特性之一,封裝。
## 特性
物件導向有三大特性,分別是封裝、繼承跟多型。
### 封裝
封裝,Encapsulation。簡單來說就是把複雜的程式細節鎖在 Class 裡,並在 Class 中設定幾個 public method 讓 Class 外的程式方便存取。
#### 為什麼要鎖在 Class 裡
有些 property 不希望容易被更改到、或者是更改時必須符合一些條件,這些條件就會被鎖在 Class 中。
舉例來說,今天有個 `$name` 的 property,那 class 可能就會透過 public method 來規範,如果想要更改 `$name` 的話,條件是不能是髒話、又或者是對 `$name` 的長度進行限制。
這樣的 public method 被稱為 **setter**,供設定。
跟 setter 相反的是 **getter**,getter 相對簡單,供取值。
```php=
<?php
Class Dog {
private $name = 'default';
public function hello() {
echo $this->getHelloSentence(); //呼叫了只能在內部呼叫的 getHelloSentence() 方法
}
private function getHelloSentence() { // 私人的方法只供 class 內部呼叫
return 'hello, I am '. $this->name . '<br>'; //詳見下方 $this-> 補充
}
// setter
public function setName($name) {
//加入條件約束取名,如果是髒話 'fuck'或是 $name 長度超過 15 就無法設定成功
if ($name === 'fuck' || strlen($name) > 15) return; //詳見下方 strlen() 的補充
$this->name = $name;
}
// getter
public function getName() {
return $this->name;
}
}
$realDog = new Dog();
$realDog->setName('Leo'); // 利用 setter 設定名字
$realDog->hello(); // hello, I am Leo
$realDogName = $realDog->getName(); // 利用 getter 拿到名字後存入變數中
echo $realDogName; //Leo
//同樣的設計圖(class)可以產出第二個產品(instance)也就是 $truedog
$trueDog = new Dog();
$trueDog->setName('AmyAmyAmyAmyAmyAmy');
$trueDog->hello();
// hello, I am default(因為 AmyAmyAmyAmyAmyAmy 超過 15 個字不符合條件)
?>
```
* `strlen()` 其實計算的是 bytes 數量,剛好英文字母一個字母會是 1 個 bytes,所以可以用此來計算長度。但中文字一個字有 3 個 bytes,用 `strlen()` 會跟想像中的結果不同,這時候我們可以使用 `mb_strlen()`。
* `mb_strlen(string, "string encoding")`:第一個參數放入要測量的字串,第二個參數放入編碼,像萬國碼就會是「utf-8」。
* `this->` :指的是 new 出來的實體,在上面的例子當中,第一個 new 出來的實體是 realdog,所以 realdog 呼叫 `setName()` 方法時,方法中的最後一行`$this->name = $name; ` 也可以解釋為 `realdog->name = $name; `,所以後面也自然可以得到 `$name`。
* 在 JS 當中,`this` 有兩種意思,一種是物件導向的用法 `this.`,另一種之後才會提到。
#### constructor 建構子
其實就是提供初始化的參數,讓 class 被 new 出一個 instance 時,就會呼叫這個 constructor 的 method,使用時更簡單。
比方說剛剛要設定名字必須用上 `setName('名字')` ,如果我們透過建構子的方式,建構出一個簡單的 method,傳名字進去括號內就可以,不須再呼叫一次`setName('名字')`。
```php=
<?php
Class Dog {
private $name = 'default';
// 初始化
public function __construct($name) { // 建構子,一定會是 public
if ($name === 'fuck' || strlen($name) > 15) return;
$this->name = $name; // 讓帶入的參數就是 name
$this->hello(); // 並且呼叫 hello() 這個方法
}
private function hello() {
echo $this->getHelloSentence();
}
private function getHelloSentence() {
return 'hello, I am '. $this->name . '<br>';
}
}
$realDog = new Dog('Leo'); // 只要 new 出 instance 時把參數帶進去
// hello, I am Leo
$trueDog = new Dog('Dublin Florenskosky');
// 名字過長,啥都不會跑出來
?>
```
在 JS ES6 當中是用 `constructor()` 來呼叫,在 php 則是使用 `__construct()` 。
其實除了 constructor 外還有 modifier 修飾子 跟 destructor 解構子,目前知道有這些就可以了。
#### JS
剛都示範 php 的語法,現在來試試看 JS 的。前面有提到在 ES6 之前其實 JS 是沒有完整的 OOP 的,會透過其他 function 來達成相同的效果。接下來我們就來看看 ES5 的 OOP 及 ES6 的 OOP。
```javascript=
// ES6
Class Calculator {
constructor(name) { // 建構子
this.name = name;
this.text = ''; // 用 this. 來指 new 出來的實體
}
input(str) {
this.text += str;
}
getResult() {
return eval(this.text);
}
}
// ES5
function Calculator(name) { // 用 function 來取代 class 名稱跟建構子
this.name = name;
this.text = '';
}
// 透過在剛的 Calculator function 名稱後加上 .prototype 加上.function 名稱
Calculator.prototype.input = function(str) {
this.text += str;
}
Calculator.prototype.getResult = function() {
return eval(this.text);
}
// ES5 及 ES6 呼叫方式相同
let calculator = new Calculator('name');
calculator.input(1);
calculator.input('+');
calculator.input(3);
const result = calculator.getResult();
console.log(result); // 4
```
### 繼承
Inheritance,個人理解是類似子類別。在做物件導向的時候,有時候會有類似的需求,class A 跟 class B 可能只有一兩個方法或是屬性不同,這時候我們可以透過繼承來減少撰寫重複的程式,並達成相同的效果。
#### extends
```php=
// 透過 extends 繼承
class A extends B { // class A 繼承 class B
[...不同於 class B 的內容...]
}
```
#### 在子類別使用父類別方法
* JS:直接呼叫 / `super.方法名稱()`
* php: `parent::方法名稱()`
來進行呼叫。要特別注意 JS 在子類別的建構子中使用 `this` 前,記得要先呼叫 `super(arguments)` :
```javascript=
// javascript
class Dog {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(this.name, 'Hi');
}
}
class TaiwansDog extends Dog {
constructor(name) { // 只需要一個參數 name
super(name); // 記得要先 super() 並把參數放進來
this.sayHello(); // 就可以開始使用 this
}
sayHello() {
this.sayHi(); // 可直接透過 this. 呼叫父類別方法,也可透過 super. 呼叫
}
}
const black = new TaiwansDog('black'); // 在建構的當下就會印出 Black Hi
```
以下來看 PHP 的程式碼:
```php=
<?php
// 延續前面的 class Dog{ ... }
Class BabyDog extends Dog { // Class BabyDog 繼承 class Dog
public function newBorn() {
echo $this->name . " was born." . '<br>';
}
public function hello() {
echo "Hello! My name is " . $this->name . '<br>';
}
public function sayYoAndHello() {
echo 'Yo! ';
parent::hello(); // 執行父類別 Dog 的 hello 方法
}
}
$realDog = new Dog('Jack'); // Hello, I am Jack 表示有成功呼叫
$realBabyDog = new BabyDog('Sara'); // Undefined property: BabyDog::$name
$realBabyDog->newBorn(); // Undefined property: BabyDog::$name
$realBabyDog->sayYoAndHello(); // Undefined property: BabyDog::$name
?>
```
只有 `$realDog` 這個 instance 執行成功。其他都出現 `Undefined property: BabyDog::$name` 錯誤訊息。
為什麼呢?那是因為前面的 `class Dog` 中 ` privat $name = 'default';` ,`private` 只供自己類別使用,所以即便 `class BabyDog` 繼承了 `class Dog` 還是無法使用。
#### protected
因此 `protected` 就誕生了,讓繼承的子類別可以存取,但其他外部的不能存取。改完之後,完整的程式如下:
```php=
<?php
Class Dog {
protected $name = 'default'; // 使用 protected 取代 private
function __construct($name) {
if ($name === 'fuck' || strlen($name) > 15) return;
$this->name = $name;
$this->hello();
}
public function hello() {
echo $this->getHelloSentence();
}
private function getHelloSentence() {
return 'Hello, I am '. $this->name . '<br>';
}
}
Class BabyDog extends Dog { // 繼承 class Dog
public function newBorn() {
echo $this->name . " was born." . '<br>';
}
public function hello() {
echo "Hello! My name is " . $this->name . '<br>';
}
public function sayYoAndHello() {
echo 'Yo! ';
parent::hello(); // 執行父類別 Dog 的 hello 方法
}
}
$realDog = new Dog('Jack'); // Hello, I am Jack
$realBabyDog = new BabyDog('Sara'); // Hello! My name is Sara
// ↑ 子類別的 hello() 覆寫了父類別 BabyDog 的 hello()
$realBabyDog->newBorn(); // Sara was born.
$realBabyDog->sayYoAndHello(); // Yo! Hello, I am Sara
// ↑ 成功使用父類別的 hello() 而非 BabyDog 的 hello()
?>
```
#### override
仔細看`$realBabyDog = new BabyDog('Sara');` echo 出來的內容跟 `$realDog = new Dog('Jack');` 除了名字不同外,整個句子也不同了。
那是因為在 `class BabyDog` 中也有 `hello()` 方法,所以當我們在子類別的 instance 中呼叫 `hello()` 方法時,子類別的 `hello()` 方法就會覆寫父類別 `hello()` 方法。這樣的狀態就被稱為 **override**。
#### static
中文翻成靜態方法,標註為 `static` 的 method 或是 property 是給 class 用的,不是給 new 出來的 instance 用的。
為什麼需要用 `static` 呢?
因為一般如果使用 `public` 或是 `protected`,在套用繼承時,被定義 `public` / `protected` 的方法或 property 也會同樣的被繼承,因為這樣才可以 new 出不同的 instance。
不過有些資料是不需要 new 出不同的 instance 的,當這些資料也需要被所有 instance 跟子類別共享,這時候就可以套用 `static`。
常用於:
* 被父子類別共用且不變的 property / method
* 每個 instance 都需要共享的 property / method
* 透過 `static method` 檢查要 new 出 instance 時所加入的參數是否合理
```php=
<?php
class Name {
public static $school = "w3c";
// 注意!如果要讓下方的 Name::$school 直接存取的到的話,這邊一定要用 public
protected $name;
public static function mySchool() {
echo "My school is " . self::$school . "<br/>";
} // self:: 指向這個 class 的 $school,詳見下方
protected function getName(){
return $this->name = "Mike";
}
public function getNameByThis(){
return $this->getName();
}
public function getNameBySelf(){
return self::getName(); // self:: 指向這個 class 的 getName()
}
}
class Name2 extends Name{
public function getName(){
return $this->name = "Jason";
}
}
echo Name::$school . "<br/>"; // w3c。靜態存取 class 內的 $school
Name::mySchool(); // My school is w3c
$newName = new Name2();
echo $newName->getNameByThis() . "<br/>"; // Jason
// ↑ Name2 沒有這個方法,所以回去找父類別,父類別內的 getNameByThis() 指向 getName()
// Name2 有 getName(),所以覆寫了父類別的 getName() 所以會是 Jason
echo $newName->getNameBySelf() . "<br/>"; // Mike
// ↑ Name2 沒有這個方法,一樣回去找父類別,self:: 指這個父類別這個 class 的 getName()
// 所以會是 Mike
?>
```
語法:
* `class 名稱::$property 名稱` :呼叫 class 中的 `static` property,記得前面要加上 $ 字號
* `class 名稱::method 名稱` :呼叫 class 中的 `static` method
* `self::$property 名稱` :呼叫這個 class 的 property,記得前面要加上 $ 字號
* `self::method 名稱` :呼叫 class 中的 method
### 多型
待補
## 待解問題
* private method / property 會被繼承嗎?
## 參考資料
* [Yakim 大的筆記](https://yakimhsu.com/project/project_w9_OOP.html)
## 推薦閱讀
* [物件導向基本概念](https://expect7.pixnet.net/blog/post/38682120)
* [該來理解 JS 的原型練 - Huli](https://blog.techbridge.cc/2017/04/22/javascript-prototype/)
* [JS 的繼承與原型鏈 - MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)
* [物件導向基礎 - 保哥](https://blog.miniasp.com/post/2009/08/27/OOP-Basis-What-is-class-and-object)
* [在 JS 建立物件藍圖 - 保哥](https://blog.miniasp.com/post/2020/06/19/JavaScript-Object-in-depth)