iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 19
0
Modern Web

JavaScript 忍者的修練--從下忍進階到中忍系列 第 19

Day 19: getter & setter

JavaScript 的自由度讓我們能隨意的新增及更改物件屬性值,但是有些時候我們想要有些限制,以保持我們資料的正確性。比如說某個屬性的資料類別是數字,我們希望在設值之前能做檢查,拒絕不是數字的新值。

當然可以另外建立一個檢查的函式,不過 JavaScript 物件提供了 getter 和 setter 二種工具,能讓我們用物件屬性方式呼叫方法,使得程式碼更簡潔,不必再另外建立函式。

const ash = {
	pokemons: ["Pikachu", "Bulbasaur", "Charmander", "Squirtle", "Butterfree", "Pidgey"],
	get firstPokemon() {
		return this.pokemons[0];
	},
	set firstPokemon(pokemon) {
		this.pokemons[0] = pokemon;
	}
};

console.log(ash.firstPokemon === "Pikachu"); // true
console.log(ash.firstPokemon = "Eevee");
console.log(ash.firstPokemon === "Eevee"); // true
console.log(ash.pokemons[0] === "Eevee"); // true

Ash 的夢想是成為世界上最偉大的寶可夢訓練大師,所以帶著寶可夢各處去挑戰,但是一個人最多只能帶六隻寶可夢。

Ash 出發時帶著pokemons: ["Pikachu", "Bulbasaur", "Charmander", "Squirtle", "Butterfree", "Pidgey"]這六隻寶可夢,他可以將其中一隻放在第一位,寶可夢就會待在他的身旁。

這裡我們用了firstPokemon屬性名稱,在前面用getset關鍵字分別寫了二個函式,get firstPokemon會回傳pokemons陣列第一個的值,set firstPokemon會將陣列第一個的值取代為參數的值。

使用上就和平常呼叫物件屬性一樣,想知道 Ash 帶了哪一隻寶可夢在身邊,用ash.firstPokemon,今天 Ash 得到了新寶可夢 Eevee,喜新厭舊要換掉 Pikachu,用ash.firstPokemon = "Eevee"

這樣的語法是不是很簡單呢?如果我們採用的是類別的寫法,也只要在類別方法名稱前面加上getset。上面的範例可以改寫成如下。

class Trainer {
	constructor() {
		this.pokemons = ["Pikachu", "Bulbasaur", "Charmander", "Squirtle", "Butterfree", "Pidgey"];
	}
	get firstPokemon() {
		return this.pokemons[0];
	}
	set firstPokemon(pokemon) {
		this.pokemons[0] = pokemon;
	}
}

const ash = new Trainer();

在物件屬性上設立getset有個缺點,就是它無法運用函式的閉包特性,物件屬性還是可以被外界存取ash.pokemons。幸好我們還有另一個方法,用前幾天提到過的Object.defineProperty

Object.defineProperty能讓我們建立物件屬性並且設定屬性的性質,它可以代入三個參數,第一個是要建立屬性的物件,第二個是屬性名稱,第三個參數是一個 descriptor 物件,裡面可以帶有getset屬性,分別會在物件屬性取用及設定時呼叫。

// 建立寶可夢訓練師的 constructor function
function Trainer() {
  let _inventorySize = 10;
  let _inventory = ["Pokeball", "Pokeball", "Pokeball", "Potion", "Potion", "Razz Berry"];

  Object.defineProperty(this, "inventorySize", {
    get: () => _inventorySize,
    set: value => {
      if(!Number.isInteger(value)) {
        console.log("Inventory size should be a number!");
        return;
      }
      _inventorySize = value;
    }
  });

  Object.defineProperty(this, "inventory", {
    get: () => _inventory
  });

  Object.defineProperty(this, "getItem", {
    set: items => {
      if(!Array.isArray(items)) {
        console.log("Items should be an array!");
        return;
      } else if(items.length + _inventory.length > _inventorySize) {
        console.log("Your inventory is out of space, buy a larger bag!");
        return;
      }
      _inventory = _inventory.concat(items);
    }
  });

  Object.defineProperty(this, "useItem", {
    set: item => {
      let i = _inventory.indexOf(item);
      if(i < 0) {
        console.log("You don't have this item in your inventory!");
        return;
      }
      _inventory.splice(i, 1);
    }
  });
}

// Ash 的夢想是成為寶可夢訓練大師
const ash = new Trainer();

// Ash 使用攜帶的物品
console.log(ash.inventory);
// ["Pokeball", "Pokeball", "Pokeball", "Potion", "Potion", "Razz Berry"]
ash.useItem = "Potion";
console.log(ash.inventory);
// ["Pokeball", "Pokeball", "Pokeball", "Potion", "Razz Berry"]
ash.getItem = ["Great Ball", "Sun Stone"];
// ["Pokeball", "Pokeball", "Pokeball", "Potion", "Razz Berry", "Great Ball", "Sun Stone"]

// Ash 想要擴充他的背包容量
console.log(ash.inventorySize);
// 10
ash.inventorySize = "Unlimited";
// "Inventory size should be a number!"
console.log(ash.inventorySize);
// 10
ash.inventorySize = 15;
console.log(ash.inventorySize);
// 15

Ash 從真新鎮出發時,大木博士給他一個可以裝十件物品的背包,和一些基本的用品。_inventorySize_inventory都是私有變數,只有透過我的設計的函式才能更改。

Ash 可以用ash.inventorySize得到他的背包容量,當他買了新背包可以擴充容量,容量的值我們設定是整數,避免玩家用金手指破壞遊戲規則。

在物品使用上我們建了二個屬性,當他得到新物品時會用ash.getItem先檢查新物品數量能不能放進背包,不行的話會顯示錯誤訊息。夠放的話就會把新物品收進_inventory私有變數內。當使用物品ash.useItem時,會試著在_inventory裡找第一個符合物品名稱的 index,沒找到會顯示錯誤訊息,有找到會將該物品從陣列中移除。

不過gettersetter的能力不只有這樣,我們能用它產生自動化的屬性,這個屬性會根據既有屬性的值,在每次取值的時候依照設定的計算過程產生。

const ash = {
  firstName: "Ash",
  lastName: "Ketchum",
  age: 10,
  hometown: "Pallet Town",
  gender: "male",
  dream: "become the best pokemon trainer in the world",
  get biography() {
    const refer = (function(gender, age){
      if(gender === "male"){
        return age < 20 ? "boy" : "man";
      } else {
        return age < 20 ? "girl" : "woman";
      }
    })(this.gender, this.age);
    
    return `${this.firstName} ${this.lastName}, a ${refer} from ${this.hometown}, has a dream to ${this.dream}!`;
  }
};

// 我們能依照 Ash 填寫的寶可夢訓練師基本資料,自動生成他的自我介紹!
console.log(ash.biography);
// Ash Ketchum, a boy from Pallet Town, has a dream to become the best pokemon trainer in the world! 

這樣的屬性叫做 computed property,如果你用過 Vue.js 的話應該對這種屬性不陌生,事實上 Vue 就是利用getter來做到的。


上一篇
Day 18: Class
下一篇
Day 20: proxy
系列文
JavaScript 忍者的修練--從下忍進階到中忍30

尚未有邦友留言

立即登入留言