###### 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)