物件導向程式設計

2023-03-06 09:00:53

在編寫軟體時,你所做的大部分工作就是建立和連線多個值和方法,讓他們一起工作,以便提供應用程式的功能。物件導向程式設計可以幫助你更容易地,並且是宣告式地實現這些功能。

在這篇文章中,你將瞭解到在JavaScript中開始使用類和物件導向程式設計方法所需要的一切。

前置知識

在閱讀本篇文章之前,你需要掌握JavaScript的基礎知識。

物件導向程式設計

物件導向程式設計(OOP)是一種程式設計正規化,在大型、複雜和積極維護的專案中,OOP每天都在被使用。

OOP使你更容易建立和管理你應用程式的許多部分,並在不使它們相互依賴的情況下將它們連線起來。接下來,讓我們看看OOP的四個主要概念。

抽象

OOP中的抽象是指只向用戶公開必要的功能,同時隱藏複雜的內部工作,使程式更容易使用和理解。

舉例來說,當你在手機上傳送訊息時,所有將你的資訊傳遞給對方的函數和邏輯都是隱藏的,因為你不需要知道它們是如何工作的。

同樣地,在程式設計中,如果你正在構建一個幫助金融app驗證使用者身份和銀行資訊的API,使用你API的開發者不需要知道你使用的是哪種資料庫系統,也不需要知道你如何呼叫你的資料庫。他們唯一需要知道的是要呼叫的函數,以及他們需要提供的引數。

抽象有助於降低複雜性,增加可用性,並使應用程式的變化不那麼具有破壞性。

封裝

封裝是將相關程式碼捆綁在一個獨立單元中的過程。封裝使程式碼的其他部分無法改變應用程式捆綁部分的工作方式,除非你顯式地進入該單元並改變它們。

舉例來說,如果你正在建立一個航班預訂的API,把搜尋航班的程式碼和預訂航班的程式碼分開是有意義的。這樣一來,兩個不同的開發者就可以無縫地在每個部分工作而不發生衝突,因為每個開發者都沒有理由直接操作對方的程式碼。

封裝有助你降低複雜性,增加程式碼可用性。

繼承

OOP中的繼承降低了程式碼重複性,使你能夠通過繼承應用程式部分的屬性和方法,在其他地方構建你的應用程式。

OOP中繼承的一個重要優勢是降低了程式碼重複性。

多型

在程式設計中,多型是一個術語,用來描述一個程式碼或程式,它可以通過根據給定的資料返回一個響應或結果來處理多個型別的資料。

舉例來說,你有一個用來向產品目錄新增產品的表單,並有三種不同型別的產品。通過多型,你可以建立一個單一類方法來格式化所有種類的產品,然後再將它們新增到資料庫中。

多型可以幫助你消除複雜性和不必要的ifswitch語句,因為在編寫複雜的程式時,這些語句會變得冗長。

接下來我們來看看JavaScript物件。

JavaScript物件

JavaScript中的物件是一個無序的鍵值對集合,也就是屬性和值。物件的鍵可以是字串,值可以是任何型別。

接下來,我們來看看如何在JavaScript中建立物件。

建立物件

在JavaScript中建立一個物件相當容易:

const car = {
    name: 'Ford',
    year: 2015,
    color: 'red',
    description: function () {
    return `${this.name} - ${this.year} - ${this.color}`;
    }
}

上述程式碼宣告了一個car物件,物件屬性包括nameyearcolor以及description函數。

存取物件屬性

在JavaScript中有兩種方式存取物件屬性。我們接著往下看:

使用點符號

下面的例子展示瞭如何使用點符合來存取物件屬性。

const country = {
    name: 'Spain',
    population: 4000000,
    description: function () {
    return `${this.name} - ${this.population}`;
    }
}

如果你有一個像上面所示的物件,你可以使用objectName.keyName的格式,它應該返回給定鍵的值:

console.log(country.name); // returns 'Spain'

使用陣列符號

下面的例子展示瞭如何使用陣列符號來存取物件屬性。

const job = {
  role: "Software Engineer",
  'salary': 200000,
  applicationLink: "meta.com/careers/SWE-role/apply",
  isRemote: true,
};

如果你有一個像上面所示的物件,你可以使用objectName[keyName]的格式,它應該返回給定鍵的值:

console.log(job[role]); // returns 'Software Engineer'

此外,你只能用陣列符號來存取salary屬性。試圖用點符號來獲取它將返回一個錯誤:

console.log(job.'salary'); // SyntaxError: Unexpected string

接下來,我們來看看如何修改物件屬性。

修改物件屬性

你可以在JavaScript動態新增、編輯和刪除物件屬性。

編輯屬性

你可以使用賦值=操作符來修改物件的值。這裡一個例子:

const person = {
  name: "John",
  age: 30,
  job: "Software Developer",
  country: "Nigeria",
  car: "Ford",
  description: function () {
    return `${this.name} - ${this.age} - ${this.job.role} - ${this.country.name} - ${this.car.name}`;
  },
};

你還可以更改上述物件中name的值:

person.name = "Smith Doe";
console.log(person.name); // returns "Smith Doe"

新增新屬性

其他語言中的物件與JavaScript中的物件之間的一個顯著區別是,在建立後可以向物件新增新的屬性。

為了向物件中新增新屬性,你可以使用點符號:

// adding a new `race` property
person.race = "Asian";
console.log(person.race); // returns "Asian"

刪除物件屬性

JavaScript允許你通過使用delete關鍵字從一個物件中刪除屬性:

delete person.race;
console.log(person.race); // returns 'undefined'

注意:你只能刪除現有的物件屬性。

檢查屬性

在向一個物件新增或刪除屬性之前,確定該物件上是否存在該屬性是一個很好的主意。這個看似簡單的檢查將為你節省幾個小時的時間來偵錯一個由重複值引起的錯誤。

要確定一個屬性是否存在於一個物件上,你可以使用in關鍵字:

console.log('name' in person) // returns true
console.log('race' in person) // returns false

上面的程式碼對於name的檢查返回true,因為name存在,而對於被刪除的race屬性返回false

現在你知道了什麼是物件以及如何使用它們,讓我們通過學習類來邁出JavaScript的OOP的下一步。

在程式設計中,類是由程式設計師定義的一種結構,然後用來建立同一型別的多個物件。例如,如果你正在建立一個處理各種汽車的應用程式,你可以建立一個Car類,它具有適用於所有車輛的基本功能和屬性。然後,你可以用這個類來製作其他的汽車,併為它們新增每種汽車所特有的屬性和方法。

為了擴充套件你在前面的例子中看到的物件,如果你想建立另一個job物件,你需要建立這樣的東西:

const job2 = {
  role: "Head of Design",
  salary: 175000,
  applicationLink: "amazon.com/careers/hod-role",
  isRemote: false,
};

然而,正如你所看到的,上面建立多個物件的方式容易出錯,並且不可延伸。因為你不可能在每次需要建立一個job時都寫出這些,從而產生100個job物件。這時類就派上用場了。

建立類

你可以建立一個Job類來簡化建立多個job

class Job {
  constructor(role, salary, applicationLink, isRemote) {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
}

上述程式碼建立了一個Job類,具有rolesalaryapplicationLink以及isRemote屬性。現在你可以使用new關鍵字建立不同的job

let job1 = new Job(
  "Software Engineer",
  200000,
  "meta.com/careers/SWE-role/apply",
  true
);

let job2 = new Job(
  "Head of Design",
  175000,
  "amazon.com/careers/hod-role",
  false
);

上面的程式碼建立了兩個不同的job,包含所有的必填欄位。讓我們通過在控制檯中列印出兩個job來看看這是否奏效:

console.log(job1);
console.log(job2);

列印結果為:

圖中顯示了兩個job和它們在控制檯中的所有屬性。

this關鍵字

this關鍵字被認為是最令人迷惑的關鍵字,因為它包含有不同的含義,這取決於在程式碼中什麼位置出現。

在上面的例子中,this關鍵詞指的是用Job類建立的任何物件。因此,constructor方法內部的this.role = role; 意味著,將你正在建立的這個物件的role屬性定義為給該建構函式的任何值。

另外請注意,在建立一個新的Job物件時,你給出的初始值必須是按順序的。例如,你像這樣建立一個job3物件:

let job3 = new Job(
  "netflix.com/careers/HOE-role",
  true,
  "Head of Engineering"
);

console.log(job3)

上述程式碼建立了一個新的job3物件,該物件有著錯誤的屬性順序,並缺失了isRemote屬性。你會在控制檯中得到以下結果:

上面的圖片顯示了job3物件在控制檯中列印出來的樣子。請注意,isRemoteundefined

接下來,我們來看看如何給類新增方法。

類方法

當建立類時,你可以新增多個屬性。為了在類內部新增方法,你可以在constructor函數後面新增:

class Vehicle {
  constructor(type, color, brand, year) {
    this.type = type;
    this.color = color;
    this.brand = brand;
    this.year = year;
  }
  start() {
    return "Vroom! Vroom!! Vehicle started";
  }
  stop() {
    return "Vehicle stopped";
  }
  pickUpPassengers() {
    return "Passengers picked up";
  }
  dropOffPassengers() {
    return "Passengers dropped off";
  }
}

上述程式碼定義了一個具有typecolorbrandyear屬性的Vehicle類,同時具有startstoppickUpPassengersdropOffPassengers方法。

為了執行物件中的方法,可以使用點符號:

const vehicle1 = new Vehicle("car", "red", "Ford", 2015);
const vehicle2 = new Vehicle("motorbike", "blue", "Honda", 2018);

console.log(vehicle1.start()); // returns 'Vroom! Vroom!! Vehicle started'
console.log(vehicle2.pickUpPassengers()); // returns "Passengers picked up"

接下來,讓我們看看計算屬性。

計算屬性

程式設計在很大程度上取決於值的變化,類似於你不想寫死類屬性的大部分值,你可能會有一些基於某些值而變化的動態屬性名稱。你可以使用計算屬性來做到這一點;讓我們看看是如何做到的。

例如,在建立工作列表API時,你可能希望開發者能夠將applyThrough函數名稱改為另一個詞,如applyThroughLinkedInapplyThroughIndeed,這取決於他們的平臺。要使用計算屬性,你需要用方括號把屬性名稱包起來:

let applyThrough = "applyThroughIndeed";

class Job {
  constructor(role, salary, applicationLink, isRemote) {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
  [applyThrough]() {
    if (applyThrough === "applyThroughLinkedin") {
      return `Apply through Linkedin`;
    } else if (applyThrough === "applyThroughIndeed") {
      return `Apply through Indeed`;
    }
  }
}

上面的程式碼宣告了applyThrough變數的字串值為 "applyThroughIndeed",以及一個可以呼叫job1.applyThroughIndeed()的計算方法[applyThrough]

計算屬性使其他開發者更容易客製化他們的程式碼。

Getters and Setters

在團隊中編寫程式碼時,你想限制誰可以改變程式碼庫的某些部分,以避免出現bug。建議在OOP中,某些變數和屬性在必要時應該被隱藏。

接下來,讓我們學習getset關鍵字是如何工作的。

Getters

當構建熱衷於確保使用者隱私的應用程式時,例如,法律問題管理應用程式,你要控制誰可以存取使用者的資料,如姓名、電子郵件和地址。get關鍵字可以幫助你實現這一目標。你可以限制誰可以存取資訊:

class Client{
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }
  get name() {
    if (userType === "Lawyer") {
      return this._name;
    } else {
      return "You are not authorized to view this information";
    }
  }
}

上面的程式碼宣告了一個Client類,其屬性是nameage,還有一個getter,只在使用者是律師時返回姓名。如果你試圖以助理的身份存取名字,你會得到一個錯誤:

let userType = "Assistant";
const person = new Client("Benjamin Adeleye", 24);
console.log(person.name); // returns 'You are not authorized to view this information'

注意:將this.name改為this._name以避免命名衝突。

Setters

set關鍵字是get關鍵字的反面。get關鍵字用於控制誰可以存取屬性,而set關鍵字控制誰可以改變屬性的值。

為了瞭解set關鍵字如何工作,讓我們通過新增一個setter來擴充套件前面的例子:

set name(newName) {
  if (userType === "Lawyer" && verifiedData === true) {
    this._name = newName;
  } else {
    console.log("You are not authorized to change this information");
  }
}

上面的程式碼宣告了一個set方法,只有在使用者是律師並且檔案已經被驗證的情況下,才允許對名字進行更改:

let userType = "Lawyer";
let verifiedData = false;
let client = new Client("Benjamin Adeleye", 30);
client.name = "Adeleye Benjamin";
console.log(client.name); // returns 'You are not authorized to change this information'

注意:以getset方法為字首的方法分別稱為gettersetter函數。

接下來,讓我們看看靜態屬性和方法。

靜態值

有時你想建立繫結到類而不是類的範例的屬性和方法。例如,你可能想要一個計算資料庫中客戶數量的屬性,但你不希望這個值繫結到類的範例上。

靜態屬性

為了追蹤資料庫客戶數量,你可以使用static關鍵字:

static clientCount = 0;

上述程式碼宣告了一個靜態clientCount屬性,其值為0。你可以像這樣來存取靜態屬性:

let cl = new Client("Benjamin Adeleye", 30);
console.log(Client.clientCount); // returns 0

注意:試圖使用console.log(cl.clientCount);存取靜態屬性會返回undefined,因為靜態屬性被繫結到類而不是範例上。

接下來,讓我們看看靜態方法。

靜態方法

建立靜態方法跟建立靜態屬性十分類似,因為你只需要在方法名稱前加上static關鍵字:

static increaseClientCount() {
  this.clientCount++;
}

上面的程式碼宣告了一個靜態的increateClientCount方法,該方法每次被呼叫時都會增加clientCount

靜態方法和屬性使得建立可以直接用於類而不是範例的輔助函數變得容易。

私有值

ECMAScript2022的更新支援JavaScript類中的私有值。

私有欄位和方法將類的封裝提升到了一個新的水平,因為你現在可以建立只能在類宣告的大括號內使用的屬性和方法,而在這些大括號外的任何程式碼都無法存取它們。

接下來讓我們看看私有屬性。

私有屬性

你可以通過在變數前加上#號來宣告類中的私有屬性。讓我們通過為每個客戶新增一個唯一的ID來改進Client類的部分:

class Client {
  #client_unique_id = "";
  constructor(name, age, id) {
    this._name = name;
    this._age = age;
    this.#client_unique_id = id;
  // same as Client class...
  }

上面的程式碼宣告了一個私有的#client_unique_id變數,只能在類宣告中使用和存取。試圖在類外存取它將返回一個錯誤:

let cl = new Client("Benjamin Adeleye", 30, "##34505833404494");
console.log(cl.name);
console.log(cl.#client_unique_id); // returns Uncaught SyntaxError: Private field '#client_unique_id' must be declared in an enclosing class

私有方法

如前所述,私有方法只能在類宣告中存取。為了學習私有方法的工作原理,我們將新增一個私有方法,從資料庫中獲取客戶的案件檔案檔案:

#fetchClientDocs(id) {
   return "Fetching client with id: " + id;
}

上述程式碼現在可以在一個公共函數中使用,使用者將呼叫該函數來獲取使用者端的檔案。私有函數的本質是將所有的底層認證和對資料庫的呼叫從使用該程式碼的使用者或開發人員那裡隱藏起來。

注意:你也可以建立私有靜態、gettersetter函數。

接下來,我們來了解如何鏈式類方法。

方法鏈

作為一個開發者,你可能最喜歡做的事情之一就是用盡可能少的程式碼實現一個功能。你可以在JavaScript中通過鏈式方法來實現這一點。例如,當一個使用者登入到你的應用程式時,你想把使用者的狀態改為"線上",當他們退出時,你再把它改回"離線":

class Twita {
  constructor(username, offlineStatus) {
    this.username = username;
    this.offlineStatus = offlineStatus;
  }
  login() {
    console.log(`${this.username} is logged in`);
    return this;
  }
  setOnlineStatus() {
    // set the online status to true
    this.offlineStatus = false;
    console.log(`${this.username} is online`);
    return this;
  }
  setOfflineStatus() {
    // set the offline status to true
    this.offlineStatus = true;
    console.log(`${this.username} is offline`);
    return this;
  }
  logout() {
    console.log(`${this.username} is logged out`);
    return this;
  }
}

上述程式碼宣告了一個Twita類,具有usernameofflineStatus屬性,並具有loginlogoutsetOnlineStatussetOfflineStatus方法。為了鏈式使用這些方法,你可以使用點符號:

const user = new Twita("Adeleye", true);
user.login().setOnlineStatus().logout().setOfflineStatus();

上述程式碼將在user物件上依次執行所有的函數並返回一個響應:

注意:你需要在每個函數的末尾通過返回this來返回當前物件。

接下來,我們來看看如何通過繼承來建立在現有的類上。

類繼承

在處理物件時,你很可能會遇到這樣的情況:你需要建立一個與你程式碼中已經存在的物件非常相似的物件,但你知道它們不可能是一樣的。例如,當建立一個電子商務應用程式時,你會有一個Product類,它有namepricedescription以及image等屬性,以及formatPriceaddToCart等方法。

然而,如果你有多種不同規格的產品,怎麼辦?

  • 帶有作者、重量和頁面詳情的書籍。
  • 傢俱的長度、寬度、高度和木材型別的詳細資訊。
  • 電影光碟的尺寸、時間和內容詳情。

在這種情況下,為每個產品建立一個類將導致大量的程式碼重複,這是OOP和一般程式設計的主要規則之一:don't repeat yourself(DRY)。

類繼承允許你在其他物件的基礎上建立物件。例如,你可以通過建立一個Product類來解決上面提到的問題:

class Product {
  constructor(name, price, description, image) {
    this.name = name;
    this.price = price;
    this.description = description;
    this.image = image;
  }
  formatprice() {
    return `${this.price}$`;
  }
  addToCart() {
    cart.add(this);
  }
}

接著,使用extends關鍵字為每個產品型別建立一個子類:

class Book extends Product {
  constructor(name, price, description, image, weight, pages, author) {
    super(name, price, description, image);
    this.author = author;
    this.weight = weight;
    this.pages = pages;
  }
}

class Movie extends Product {
  constructor(
    name,
    price,
    description,
    image,
    size,
    contentDescription,
    duration
  ) {
    super(name, price, description, image);
    this.size = size;
    this.contentDescription = contentDescription;
    this.duration = duration;
  }
}

class Furniture extends Product {
  constructor(
    name,
    price,
    description,
    image,
    woodType,
    length,
    height,
    width
  ) {
    super(name, price, description, image);
    this.woodType = woodType;
    this.length = length;
    this.height = height;
    this.width = width;
  }
}

上述程式碼通過擴充套件Product類來宣告BookMovieFurniture產品型別。

在上面的程式碼中,有兩個新的關鍵字:extendssuper。接下來,讓我們來看一下它們。

extends關鍵字

extends關鍵字是不言自明的;它被用來擴充套件另一個類的能力。在我們的例子中,我們用它通過擴充套件Product類來建立BookMovieFurniture類。

super關鍵字

super關鍵字消除了你需要為每個新的子類重複進行的多次宣告。例如,上述程式碼中呼叫的super函數取代了以下程式碼:

this.name = name;
this.price = price;
this.description = description;
this.image = image;

還記得DRY嗎?這樣做的原因是為了不重複上述程式碼,因為它已經寫在了Product類裡面。

如果子類不需要建構函式,super函數可以被忽略:

class Animal {
  constructor(name, species, color) {
    this.name = name;
    this.species = species;
    this.color = color;
  }
  makeSound() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Bird extends Animal {
  fly() {
    console.log(`${this.name} flies.`);
  }
}

上述程式碼宣告了一個父類別Animal和一個子類Bird,該類不需要建構函式來執行,因為它沒有在建構函式中宣告任何新變數。因此,下面的程式碼應該可以執行:

const bird = new Bird('Chloe', 'Parrot', 'Green'); 
console.log(`${bird.name} is a ${bird.color} ${bird.species}`);

上述程式碼可以執行,即使Bird類裡面沒有namecolor或者species

如果你只需要給子類新增方法的話,你不需要呼叫super或者重複相同的建構函式。

總結

在本教學中,你瞭解了JavaScript中的物件導向程式設計和類,以及它如何幫助你保持程式碼的清潔、乾燥和可重用。我們涵蓋了物件導向程式設計的四個核心概念,包括抽象、封裝、繼承和多型。

你還了解到,在你的程式碼中使用物件導向的程式設計正規化和類有很多優點,從改善應用結構和程式碼的可重用性到降低程式碼的複雜性。

以上就是本文的全部內容,如果對你有所幫助,歡迎收藏、點贊、轉發~