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
Retry later

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Retry later