iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Modern Web

JavaScript學習日記系列 第 14

JavaScript學習日記 : Day14 - 原型與繼承(一)

在寫程式時,我們經常會想要拓展一些東西。

例如我們有一個user object,他有自己的屬性跟函數,我們希望將admin與guest基於user稍作修改,重用user中的內容,並不是複製,只是在user上建構一個新的對象。

原型繼承(Prototypal inheritance)這個語言特性能幫助我們實現這個需求。

1. [[prototype]]

在JavaScript中,對象有一個特殊的隱藏屬性,不是null就是對另一個Object的引用,該對像稱為原型

當我們從object讀取一個不存在的屬性時,JavaScript會自動從原型中找尋獲取該屬性,這就是原型繼承。[[prototype]]是隱藏的,但有另一個屬性可以獲取到原型 :

let animal = {
  eats: true
};
let cat = {
  jumps: true
};

cat.__proto__ = animal; // 設置 rabbit.[[Prototype]] = animal

// 現在這兩個属性我们都能在 cat 中找到:
console.log( cat.eats ); // true 
console.log( cat.jumps ); // true

如果animal中有個函數,他一樣可以在cat中取用 :

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let cat = {
  jumps: true,
  __proto__: animal
};

// walk 方法是从原型中獲得/繼承的
rabbit.walk(); // Animal walk

原型鏈可以很長 :

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let cat = {
  jumps: true,
  __proto__: animal
};

let foldEar = {
  earLength: 2,
  __proto__: cat
};

// walk 一樣透過原型鍊取得
longEar.walk(); // Animal walk
alert(foldEar.jumps); // true (繼承cat)

這裡有兩個限制

  1. 引用不能形成閉循環,JavaScript會報錯
  2. __prototype__的值可以是object,也可以是null,其他類型都會被忽略(但還是可以設置)。

當然只能有一個[[prototype]]。

__proto__是[[prototype]]的歷史原因而留下來的getter/setter
這兩個是不一樣的,__prototype__是[[prototype]]的getter/setter。
__proto__屬性有點過時了,他的出現是因為歷史原因。現代編程建議我們使用函數Object.getPrototypeof/Object.setPrototypeOf來取代__prototype__去get/set原型。

2. 原型鍊查找規則

原型只用於讀取屬性,如果要寫入或刪除屬性直接在object中操作。

在下面的例子我們將cat分配自己的walk :

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let cat = {
  __proto__: animal
};

cat.walk = function() {
  console.log("cat walk!");
};

cat.walk(); // cat walk!

訪問器(accessor)是一個例外,因為assignment操作是由setter函數處裡的。因此,寫入此類型屬性時實際上跟調用函數相同。

參考以下代碼 :

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith 

// setter 作用
admin.fullName = "Alice Cooper"; // 在admin object中新增name與surname

alert(admin.fullName); // Alice Cooper,admin 的内容被修改了
alert(user.fullName);  // John Smith,user 的内容被保护了

3. for...in迴圈

for..in迴圈也會把繼承的屬性算入 :

let animal = {
  eats: true
};

let cat = {
  jumps: true,
  __proto__: animal
};

// Object.keys 只返回自己的屬性
console.log(Object.keys(cat)); // jumps

// for..in 會遍歷自己以及繼承的屬性
for(let prop in cat) console.log(prop); // jumps,eats

如果只是要object本身的屬性,並不想要繼承的屬性,那可以使用內建方法obj.hasOwnProperty(key) :

let animal = {
  eats: true
};

let cat = {
  jumps: true,
  __proto__: animal
};

for(let prop in cat) {
  let isOwn = cat.hasOwnProperty(prop);

  if (isOwn) {
    console.log(`Our: ${prop}`); // Our: jumps
  } else {
    console.log(`Inherited: ${prop}`); // Inherited: eats
  }
}

cat從animal中繼承,animal從Object.prototype中繼承(默認繼承),然後像上是null。

那cat.hasOwnProperty方法從哪來的? 圖中可以看到是Object.prototype.hasOwnProperty提供的,換句話說是繼承的,那為什麼for..in迴圈並沒有像eat和jumps那樣出現在迴圈中呢?

Ans: 因為它是不可枚舉的。就像object.prototype的其他屬性,hasOwnProperty有enumerable:false標誌。並且for...in只會列出可枚舉的屬性。


上一篇
JavaScript學習日記 : Day13 - 閉包(Closure)
下一篇
JavaScript學習日記 : Day15 - 原型與繼承(二)
系列文
JavaScript學習日記30

尚未有邦友留言

立即登入留言