DEV Community

SGsSY
SGsSY

Posted on

The Most Familiar Stranger - JavaScript - 原型

分享記錄檔 YouTube

原型( Prototype )

每個 JavaScript 物件都有一個原型物件,它作為該物件繼承屬性和方法的基礎。物件的原型可以通過 __proto__ 屬性取用 ( ES6 開始使用 Object.getPrototypeof()、Object.setPrototypeof() 這兩個 accessors 取用 ),原型物件也是一個物件,有自己的原型。當一個物件呼叫一個方法或取用一個屬性時,它會先查找自身是否有該屬性或方法,如果沒有,則會透過原型鏈查找。透過原型,物件可以繼承來自其原型的屬性和方法。

原型鏈(Prototype Chain)

原型鏈是由原型物件組成的一個鏈,用於查找物件的屬性和方法。當一個物件取用一個屬性或方法時,它會先查找自身是否有該屬性或方法,如果沒有,則會查找其原型物件是否有該屬性或方法,如果還沒找到,則會繼續查找其原型的原型,直到找到為止。

以下是一個簡單的例子,說明嘗試存取屬性時會發生的事:

// 利用含有 a 與 b 屬性的 f 函式,建立一個 o 物件:
let f = function () {
   this.a = 1;
   this.b = 2;
}
let o = new f(); // {a: 1, b: 2}

// 接著針對 f 函式的原型添加屬性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要寫 f.prototype = {b:3,c:4}; 因為它會破壞原型鏈
// o.[[Prototype]] 有 b 與 c 的屬性:{b: 3, c: 4}
// o.[[Prototype]].[[Prototype]] 是 Object.prototype
// 最後 o.[[Prototype]].[[Prototype]].[[Prototype]] 成了 null
// 這是原型鏈的結末,因為 null 按照定義並沒有 [[Prototype]]。
// 因此,整個原型鏈看起來就像:
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

console.log(o.a); // 1
// o 有屬性「a」嗎?有,該數值為 1。

console.log(o.b); // 2
// o 有屬性「b」嗎?有,該數值為 2。
// o 還有個原型屬性「b」,但這裡沒有被訪問到。
// 這稱作「property shadowing」。

console.log(o.c); // 4
// o 有屬性「c」嗎?沒有,那就找 o 的原型看看。
// o 在「o.[[Prototype]]」有屬性「c」嗎?有,該數值為 4。

console.log(o.d); // undefined
// o 有屬性「d」嗎?沒有,那就找 o 的原型看看。
// o 在「o.[[Prototype]]」有屬性「d」嗎?沒有,那就找 o.[[Prototype]] 的原型看看。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype,預設並沒有屬性「d」,那再找他的原型看看。
// o 在「o.[[Prototype]].[[Prototype]].[[Prototype]]」是 null,停止搜尋。
// 找不到任何屬性,回傳 undefined。
Enter fullscreen mode Exit fullscreen mode

函式建構子( Constructor )

函式建構子是一種用來創建物件的函式,它使用 new 關鍵字創建一個新物件,並將該物件的原型設置為函式的 prototype 屬性。函式建構子也可以添加屬性和方法,透過原型,這些屬性和方法可以被所有由該函式建構的物件所共享。

因此當我們這樣寫時:

let o = new Foo();
Enter fullscreen mode Exit fullscreen mode

JavaScript 其實會做:

let o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);
Enter fullscreen mode Exit fullscreen mode

物件屬性特徵(Object Property Descriptor)

JavaScript 中的每個屬性都有一個物件,稱為屬性描述符(Property Descriptor),它描述了該屬性的特徵。物件屬性特徵描述了一個屬性的值 (value)、可枚舉性 (enumerable)、可寫性 (writable)、可配置性 (configurable) 等屬性。

  • value:該屬性的值。預設值為 undefined
  • writable:如果為 true,則該屬性的值可以被修改。如果為 false,則該屬性的值無法被修改。預設值為 true
  • enumerable:如果為 true,則該屬性可以被 for...in 迴圈枚舉。如果為 false,則該屬性無法被枚舉。預設值為 true
  • configurable:如果為 true,則該屬性的特徵可以被修改,並且該屬性可以被刪除。如果為 false,則該屬性的特徵無法被修改,且該屬性無法被刪除。預設值為 true

以下是一個修改屬性特徵的範例:

let obj = {name: 'John'};

let descriptor = Object.getOwnPropertyDescriptor(obj, 'name');

console.log(descriptor); 
// {value: "John", writable: true, enumerable: true, configurable: true}

// 修改屬性特徵
descriptor.writable = false;
descriptor.configurable = false;

Object.defineProperty(obj, 'name', descriptor);

// 現在 name 屬性已變為不可寫和不可配置
obj.name = 'Bob';
delete obj.name;

console.log(obj); // {name: "John"},不可修改和刪除
Enter fullscreen mode Exit fullscreen mode

Class

Class 是 ES6 中引入的一個新特性,它提供了一種更簡潔的方式來定義物件的屬性和方法。Class 本質上是一種特殊的函式建構子,它使用 class 關鍵字定義,並使用 constructor 方法定義屬性和方法。與傳統的函式建構子不同,class 定義的方法預設是不可枚舉的。

以下是一個範例說明 class 定義的方法預設是不可枚舉的:

class MyClass {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }
  sum() {
    return this.a + this.b; 
  }
}

const myObj = new MyClass(2, 3);

for (let key in myObj) {
  console.log(key); // Output: a, b
}

console.log(Object.keys(myObj)); // Output: [ 'a', 'b' ]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)