iT邦幫忙

0

JavaScript 物件導向(Object-Oriented Programming)、類別(Class)、封裝、繼承、多型

  • 分享至 

  • xImage
  •  

核心概念:是一種程式設計的思維方式,讓你可以把資料和相關的功能包裝在一起。

物件導向 = 一種思維方式。
class = 實現這種思維的語法工具。
封裝、繼承、多型 = 物件導向的「三大特性」,及核心精神。

例如寫一個物件來描述一隻貓,
js

javascript
const cat = {
  name: "小黑",
  age: 5,
  scratching: function() {
    console.log("刮!");
  }
};

cat.scratching(); // 輸出:刮!

scratching有抓/刮的意思。

當你今天想要描述五隻貓,會寫五次對吧!
const cat1 = {name: "小紅", age:5,scratching: function() {console.log("刮!"); }};
const cat2 = {name: "小黃", age:1,scratching: function() {console.log("刮!"); }};
const cat3 = {name: "小藍", age:6,scratching: function() {console.log("刮!"); }};
const cat4 = {name: "小綠", age:3,scratching: function() {console.log("刮!"); }};
const cat5 = {name: "小紫", age:9,scratching: function() {console.log("刮!"); }};

當寫完五次會有產生一個想法,就是累! 雖然能把第一個範例來複製貼上,是還是要改const的變數名稱、name屬性、
age屬性。但其實有更快的更簡短的寫法,就是下面要講的。

類別(Class)

像是製作物件的設計圖。用 class 可以批量生產相似的物件。

class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}

scratching() {
console.log(${this.name} 刮!);
}
}

const Cat1 = new Cat("小紅", 5);
const Cat2 = new Cat("小黃", 1);
const Cat3 = new Cat("小藍", 6);
const Cat4 = new Cat("小綠", 3);
const Cat2 = new Cat("小紫", 9);

當你console.log(Cat1);會得到
Cat {name: '小紅', age: 5,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小黃', age: 1,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小藍', age: 6,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小綠', age: 3,scratching(){console.log(${this.name} 刮!);}}
當你console.log(Cat2);會得到
Cat {name: '小紫', age: 9,scratching(){console.log(${this.name} 刮!);}}

雖然看起來好像前面在寫class的時候,更複雜、更久,但是當你要創立描述100隻貓的物件,這時候你應該就會覺得方便了/images/emoticon/emoticon38.gif

建構子constructor

建構子的特性:
唯一性: 當您定義一個類別時,如果需要初始化新建立的物件實例,只能定義一個constructor的方法。
彈性參數: 不能根據傳入的參數數量或類型不同而定義多個同名函式。

當你使用類別Class創造一個模板(設計圖),只有在 new 新的物件實例時,需要初始化屬性/值,才需要寫 constructor(){}。如果 new 新的物件實例時,並沒有打算初始化屬性/值,就可以不寫constructor(){}。

需要使用constructor的例子:

// 情況 1:創建時需要給屬性/值 → 需要 constructor
class Cat {
  constructor(name, age) {  // 需要!
    this.name = name;
    this.age = age;
  }
}

const cat1 = new Cat("小黑", 5); // 創建時就給值
console.log(cat1.name); // "小黑"
console.log(cat1.age); // 5

在這例子當中,我要創立一個Cat的模板物件(設計圖),並且當使用這個模板物件new一個
新實例物件的時候,能在創立新物件時,把name、age的屬性給新實例物件,這樣新的cat1物件
就有name、age屬性,值分別為new的時候給的參數"小黑"、 5。

這個例子如果不寫this,會發生什麼事情?

// 情況 1:創建時需要給屬性/值 → 需要 constructor
class Cat {
  constructor(name, age) {  // 需要!
    name = name;  // 沒有 this
    age = age;
  }
}

const cat1 = new Cat("小黑", 5); // 創建時就給值
console.log(cat1); // {} 空物件
console.log(cat1.name); // undefined
console.log(cat1.age); // undefined

這個例子中,你會發現,如果不寫this,cat1是{},你說為什麼?不是在new的時候就把name、age
給cat1了嗎?這時候就要看以下new Cat("小黑", 5)時發生什麼事情。

// 1. 創建一個空物件
const cat1 = {};

// 2. 呼叫 constructor,並傳入參數
// 此時 constructor 裡的 name = "小黑", age = 5(區域變數)

// 3. 執行 constructor 內的程式碼
name = name;  // "小黑" = "小黑"(只是把變數指派給自己,沒做任何事)
age = age;    // 5 = 5(同上)

// 注意:這裡完全沒有碰到 cat1 這個物件
// 因為沒有 this.name,所以 cat1 還是 {}

當你看到步驟三。name = name; ,帶進去的「參數 name」指派給「參數 name」自己。
age = age;,帶進去的「參數 age」指派給「參數 age」自己。
這樣的話是一件沒有意義的事情。當你用new創一個實例的時候,你也希望創建物件時就有給模板物件內的屬性跟值,
所以必須要在class的Cat模板物件中的constructor函式裡面,使用this.name = name的方式,才能夠透過模板物件把值跟屬性給到cat1物件。

模板(設計圖)constructo裡的this是指誰?

接下來你或許對this不太熟悉,可能會問
constructor(name, age) {
this.name = name;
this.age = age;
}
裡面的this是什麼意思?
this 就是指「你現在正在創造的這個物件」。每次用 new 創造新物件,this 就指向那個新物件,在以上描述五隻貓的範例中,Cat1的this就是Cat1、Cat2的this就是Cat2、Cat3的this就是Cat3、Cat4的this就是Cat4、Cat5的this就是Cat5。

一定要有 this 嗎?
在 constructor 裡如果你要設定物件的屬性,就一定要用 this。以上面用class描述五隻貓的物件作為例子,如果 把constructor方法裡面的this去掉,會變成name = name; age = age;。其他不變的情況下,這時候const Cat1 = new Cat("小紅", 5);新增一個實例Cat1,當你使用console.log(Cat1.name);會出現undefined

為何要加new?
new 是 JavaScript 的關鍵字,作用是:
創造一個新的空物件
執行 class 的 constructor
把新物件回傳給你
如果不寫 new ,
const cat1 = Cat("小紅", 5); 會出錯(TypeError: Class constructor Cat cannot be invoked without 'new'),TypeError:不能在沒有「new」的情況下呼叫類別建構子 Dog。

封裝(Encapsulation)

是將物件的內部狀態(資料/屬性)隱藏起來,只透過物件提供的公共方法(行為)來存取這些狀態。核心目的是保護資料的完整性,並限制外部程式碼隨意修改內部資料。

方式一:使用 # 私有欄位 (現代、嚴格的封裝)

javascript
class SafeBox {
  #password = "Secret123"; // 私有屬性

  // 類別內部的方法可以存取 #password
  checkPassword(input) {
    return input === this.#password;
  }
}
const myBox = new SafeBox();

// 外部程式碼嘗試存取,這會導致錯誤!
console.log(myBox.#password); // Uncaught SyntaxError: Private field '#password' must be declared in an enclosing class

方式二:沒有 # 私有欄位 (傳統的命名約定/閉包技巧

使用命名約定:
我們使用一個底線 _ 作為屬性名稱開頭,告訴其他工程師:「嘿,這個屬性是私有的,請不要動它。」但 JavaScript
執行環境不會阻止任何人去存取它。本質是開發團隊內部的共識或禮貌。

使用閉包:
利用 JavaScript 的閉包特性來實現真正的私有變數。

繼承(Inheritance)

當程式碼需要重用的時候,不用重複寫。

// 父類別:所有動物共同的特性
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name}在吃東西`);
  }
  
  sleep() {
    console.log(`${this.name}在睡覺`);
  }
}

// 子類別:狗(繼承 Animal)
class Dog extends Animal {
  bark() {
    console.log(`${this.name}說:汪汪!`);
  }
}

// 子類別:貓(繼承 Animal)
class Cat extends Animal {
  meow() {
    console.log(`${this.name}說:喵喵!`);
  }
}

// 子類別:鳥(繼承 Animal)
class Bird extends Animal {
  fly() {
    console.log(`${this.name}在飛`);
  }
}

// 使用
const dog = new Dog("小白");
dog.eat();   // 繼承來的方法
dog.sleep(); // 繼承來的方法
dog.bark();  // 自己的方法

const cat = new Cat("小咪");
cat.eat();   // 繼承來的方法
cat.meow();  // 自己的方法

const bird = new Bird("小鳥");
bird.eat();  // 繼承來的方法
bird.fly();  // 自己的方法

當你有定義多個class模板類別,且全部類別都有一樣的方法時,可以不用每個new的實例物件都寫一次,而是可以先class定義這個有一樣方法的類別,當new一個有相同方法的新實例物件,就可以在new Cat之後寫extends,後面再放裝有重複方法的類別名稱,以上面例子來說就是Animal類別物件,這時候就完成繼承。不必在創建class類別物件時寫重複的方法,就可以使用。

多型(Polymorphism)

不同類別可以有同名的方法,但做不同的事情。

class Dog {
  speak() {
    console.log("汪汪!");
  }
}

class Cat {
  speak() {
    console.log("喵喵!");
  }
}

class Cow {
  speak() {
    console.log("哞~");
  }
}

// 都叫 speak,但行為不同
const animals = [
  new Dog(),
  new Cat(),
  new Cow()
];

animals.forEach(animal => {
  animal.speak();  // 每個動物發出不同的聲音
});
// 輸出:
// 汪汪!
// 喵喵!
// 哞~

雖然每個物件的類別不同,但都有 speak() 方法,就能統一處理,這就是多型的好處。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言