一. 原型
我的理解是,原型是一個物件。是一個什麼物件呢?是一個模板物件。其他物件可以透過這個模板物件繼承屬性和方法。
假設有兩個物件 B 和 A,B 物件繼承了 A 物件,那麼 A 就是 B 的原型。我們透過這種機制實現了物件之間的共享和繼承,從而避免了重複定義相同的屬性和方法。
原型定義了一些公用的屬性和方法,利用原型建立出來的新物件例項會共享原型的所有屬性和方法。
有點難理解,沒問題,我們舉個栗子:
function Person(name) { this.name = name; } Person.prototype.greet = function() { console.log(`Hello, my name is ${this.name}`); }; const person1 = new Person('Feng'); const person2 = new Person('Bob'); person1.greet(); // 輸出:Hello, my name is Feng person2.greet(); // 輸出:Hello, my name is Bob
在這個例子中,greet
方法是定義在 Person.prototype
上的,因此 person1
和 person2
都可以呼叫它,但它們不會在每個例項上各自儲存一個 greet
方法。
它們共享同一個方法。
讓我詳細解釋一下小夥伴們所看到的內容,以及它與原型之間的聯絡。
1. length: 0
length: 0
表示陣列當前的長度為 0。陣列的 length
屬性是一個特殊的屬性,它會動態反映陣列中的元素數量。在這個例子中,陣列是空的,所以 length
為 0。
2. [[Prototype]]: Array(0)
[[Prototype]]
是每個 JavaScript 物件都擁有的隱式原型屬性。在陣列中,[[Prototype]]
指向 Array.prototype
,這是所有陣列共享的原型物件。
Array.prototype
是一個包含所有陣列方法的物件,比如.push()
,.pop()
,.map()
,.filter()
等等。這些方法定義在Array.prototype
上,因此所有陣列都可以透過原型鏈繼承並呼叫這些方法。當我們建立一個數組時,比如
const arr = []
,這個陣列物件的原型鏈就指向Array.prototype
,從而使這個陣列可以使用所有陣列方法。
[[Prototype]]: Array
的意思是這個陣列的原型是 Array.prototype
,並且 Array.prototype
是一個包含各種陣列方法的物件。 Array(0)
是因為陣列長度為0。
3. 展開後的陣列方法
當我們展開 [[Prototype]]
後,會看到所有與陣列相關的方法,比如 .concat()
, .slice()
, .splice()
等等。這些方法是定義在 Array.prototype
上的,所以所有的陣列例項都可以使用它們。
每當我們呼叫 arr.push()
之類的方法時,JavaScript 引擎會先在陣列 arr
上查詢這個方法。如果找不到,就會沿著原型鏈在 Array.prototype
上查詢,直到找到為止。
這些方法之所以能在陣列物件上呼叫,是因為 JavaScript 透過原型鏈把 Array.prototype
賦給了所有的陣列物件。
4. 原型與內建物件的關係
JavaScript 中的內建物件(如陣列、字串、函式等)都透過原型機制來實現方法共享。每個內建物件都有一個對應的原型物件,比如:
Array.prototype
:所有陣列的原型。String.prototype
:所有字串的原型。Function.prototype
:所有函式的原型。
這些原型物件都定義了該物件的常用方法和屬性。透過原型鏈,所有相應型別的物件都能繼承並使用這些方法和屬性。例如,Array.prototype
定義了 .push()
,因此所有陣列物件都可以透過原型鏈使用 .push()
。
二. 原型鏈
在 JavaScript 中,原型鏈(prototype chain)是物件繼承的核心機制之一。
原型也可以有自己的原型,依次向上,直到找到一個原型為 null
的物件為止。這樣的一個原型的層次結構就被稱為“原型鏈”。
透過原型鏈,JavaScript 實現了物件之間的繼承,當一個物件訪問某個屬性時,如果該屬性不在物件本身上,JavaScript 會順著原型鏈向上查詢,直到找到該屬性或達到原型鏈的頂端 null
。
1. 原型鏈的工作原理
當我們訪問物件的某個屬性時,JavaScript 引擎會首先檢查該物件自身是否具有這個屬性。如果沒有,JavaScript 引擎就會沿著原型鏈向上查詢該屬性。
查詢的過程:
第一步:JavaScript 引擎會首先在物件自身(即例項物件)中查詢這個屬性。
第二步:如果該屬性不在例項物件中,JavaScript 引擎會沿著物件的
__proto__
指向的原型物件(建構函式的prototype
)中查詢。第三步:如果在該原型物件中找不到,繼續沿著原型鏈向上查詢,找原型的原型,直到找到該屬性或者到達原型鏈的頂端
null
。
這個過程保證了物件能夠繼承其原型上的屬性和方法。
2. 原型鏈的示例
爲了更好地理解原型鏈,我們來看一個簡單的栗子:
function Animal(name) { this.name = name; } Animal.prototype.speak = function() { console.log(`${this.name} makes a sound.`); }; function Dog(name) { Animal.call(this, name); // 呼叫父類建構函式,繼承屬性 } Dog.prototype = Object.create(Animal.prototype); // 繼承方法 Dog.prototype.constructor = Dog; Dog.prototype.speak = function() { console.log(`${this.name} barks.`); }; const dog = new Dog('Buddy'); dog.speak(); // 輸出:Buddy barks
在這個例子中:
Animal
是一個建構函式,它定義了一個例項屬性name
,並在其原型上定義了一個方法speak
。Dog
繼承了Animal
,並重寫了speak
方法。dog
是Dog
的例項,但它可以呼叫Animal
原型鏈上的方法,因為Dog.prototype
是透過Object.create(Animal.prototype)
繼承了Animal.prototype
。
當你執行 dog.speak()
時,JavaScript 會先檢查 dog
物件本身有沒有 speak
方法,發現有,便直接呼叫。
如果沒有,它會順著原型鏈去 Dog.prototype
和 Animal.prototype
中查詢 speak
方法。
3. 原型鏈的層級
原型鏈的層級可以理解為物件與物件之間的繼承關係。每個物件可以繼承自另外一個物件,而被繼承的物件可以再繼承自其他物件,這樣的層次就形成了鏈式結構。
具體來看,以下是原型鏈的分層結構:
function Person(name) { this.name = name; } Person.prototype.greet = function() { console.log(`Hello, my name is ${this.name}`); }; const alice = new Person('Alice'); // 原型鏈關係 console.log(alice.__proto__ === Person.prototype); // true console.log(Person.prototype.__proto__ === Object.prototype); // true console.log(Object.prototype.__proto__ === null); // true
在這個例子中,alice
是 Person
的例項物件,它的原型指向 Person.prototype
。Person.prototype
本身是一個物件,它的原型指向 Object.prototype
,而 Object.prototype
的原型為 null
,這是原型鏈的終點。
原型鏈的層級順序可以總結如下:
alice
的原型是Person.prototype
。Person.prototype
的原型是Object.prototype
。Object.prototype
的原型是null
。
4. 原型鏈的好處
共享屬性和方法:透過原型鏈,多個例項物件可以共享同一個原型物件上的屬性和方法,避免了每個例項都建立一份獨立的副本,從而節省記憶體。
動態繼承:原型鏈允許物件在執行時動態地繼承屬性和方法。如果原型物件上的屬性或方法發生了變化,所有透過原型鏈繼承該屬性或方法的例項物件都會立即感知到這種變化。
層級繼承:透過原型鏈,JavaScript 實現了簡單的多級繼承機制。一個物件可以繼承自另一個物件,而這個物件本身又可以繼承自其他物件。
原型鏈的侷限性
效能問題:由於 JavaScript 引擎需要沿著原型鏈逐層查詢屬性或方法,原型鏈越長,查詢的速度就越慢。如果某個屬性位於原型鏈的較高層次,查詢它的成本會比較高。因此,過長的原型鏈可能會影響效能。
屬性遮蔽:如果物件的自身屬性和原型上的屬性同名,那麼物件自身的屬性會“遮蔽”原型鏈上的屬性。這意味著你只能訪問物件自身的屬性,而無法訪問原型鏈上的同名屬性。
const obj = { name: 'Alice' }; Object.prototype.name = 'Prototype Name'; console.log(obj.name); // 輸出:Alice (原型鏈上的 name 被遮蔽)
在這個例子中,儘管 Object.prototype
上有一個 name
屬性,但由於 obj
自身有一個同名的 name
屬性,所以它會優先返回自身的屬性,而不會去查詢原型鏈上的屬性。
三. 原型模式
原型模式(Prototype Pattern)是一種建立型設計模式,它的核心思想是透過克隆一個已經存在的物件來建立新的物件,而不是直接例項化一個新的物件。這種模式常用於避免昂貴的物件建立過程或複雜的物件初始化。
在原型模式中,一個物件會被當作原型,新的物件透過複製該原型來建立。這種方式可以快速生成一系列相似的物件,而無需透過建構函式或類去重新建立每一個物件。
1. 原型模式的工作原理
原型模式依賴於“原型”物件。原型物件作為一個模板,用於建立新物件。透過克隆(clone) 原型物件,新的物件不僅可以繼承原型物件的所有屬性和方法,還能根據需要進行定製和修改。
在傳統的物件導向程式設計中,我們通常會透過類來建立物件。例如,我們會定義一個類,然後透過類的建構函式例項化出多個物件。但在某些情況下,建立物件的過程可能很複雜,耗時較長。原型模式透過現有的物件模板建立新物件,從而避免了這些問題。
2. JavaScript 中的原型模式
在 JavaScript 中,由於其天生支援原型繼承,所以非常適合實現原型模式。在 JavaScript 中,我們可以透過 Object.create()
方法或手動克隆物件的方式來實現原型模式。
使用 Object.create() 實現原型模式
Object.create()
是 JavaScript 中專門用來基於現有物件建立新物件的方法。這種方式完美地契合了原型模式的思想:透過一個原型物件建立新的物件,而不是透過建構函式來建立。
const carPrototype = { drive() { console.log(`${this.make} ${this.model} is driving.`); } }; // 建立一個原型物件 const myCar = Object.create(carPrototype); myCar.make = 'Toyota'; myCar.model = 'Corolla'; myCar.drive(); // 輸出:Toyota Corolla is driving.
在這個例子中,carPrototype
作為一個原型物件,myCar
是透過克隆 carPrototype
建立的新物件,並繼承了 carPrototype
的 drive
方法。透過這種方式,myCar
可以直接使用 carPrototype
中定義的方法,而無需重複定義。
手動克隆物件實現原型模式
雖然 JavaScript 提供了 Object.create()
來簡化原型模式的實現,但我們也可以透過手動克隆物件來實現相似的功能。手動克隆通常涉及到淺複製和深複製的概念。
淺複製:複製物件的引用,克隆後的物件和原物件共享相同的內部屬性和方法。
深複製:遞迴複製物件的所有屬性和方法,克隆後的物件與原物件相互獨立。
function cloneObject(obj) { const newObj = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; } const carPrototype = { make: 'Toyota', model: 'Corolla', drive() { console.log(`${this.make} ${this.model} is driving.`); } }; const clonedCar = cloneObject(carPrototype); clonedCar.make = 'Honda'; clonedCar.model = 'Civic'; clonedCar.drive(); // 輸出:Honda Civic is driving.
在這個例子中,cloneObject()
函式手動克隆了 carPrototype
物件,並建立了一個新的 clonedCar
物件。這個新物件繼承了原型物件的屬性和方法,但與原型物件獨立存在。