原型和原型鏈是 JavaScript 中實現對象繼承和屬性查找的核心機制。為了更深入地理解它們,我們需要從底層原理、實現機制以及實際應用等多個角度進行分析。
1. 原型(Prototype)
1.1 什么是原型?
- 每個 JavaScript 對象(除
null
外)都有一個內部屬性[[Prototype]]
,指向它的原型對象。 - 原型對象也是一個普通對象,它包含可以被其他對象共享的屬性和方法。
- 通過
__proto__
(非標準,但廣泛支持)或Object.getPrototypeOf()
可以訪問對象的原型。
1.2 構造函數與原型
- 每個函數(構造函數)都有一個
prototype
屬性,指向一個對象,這個對象是該構造函數實例的原型。 - 當使用
new
關鍵字創建實例時,實例的[[Prototype]]
會指向構造函數的prototype
對象。
function Person(name) {this.name = name;
}// 在構造函數的原型上添加方法
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};const person1 = new Person('Alice');
person1.sayHello(); // 輸出: Hello, my name is Alice
- 在上面的例子中:
Person.prototype
是person1
的原型對象。person1.__proto__
指向Person.prototype
。
2. 原型鏈(Prototype Chain)
2.1 什么是原型鏈?
- 原型鏈是由對象的
[[Prototype]]
連接起來的鏈式結構。 - 當訪問對象的屬性或方法時,如果對象本身沒有該屬性,JavaScript 會沿著原型鏈向上查找,直到找到該屬性或到達原型鏈的頂端(
null
)。
2.2 原型鏈的頂端
- 所有對象的原型鏈最終都會指向
Object.prototype
,而Object.prototype
的[[Prototype]]
是null
。 - 如果在整個原型鏈中都找不到屬性,則返回
undefined
。
console.log(person1.toString()); // 輸出: [object Object]
- 在上面的例子中:
person1
本身沒有toString
方法。- JavaScript 引擎會沿著原型鏈查找:
person1
-> 沒有toString
。person1.__proto__
(即Person.prototype
) -> 沒有toString
。Person.prototype.__proto__
(即Object.prototype
) -> 找到toString
,調用它。
3. 原型鏈的深度分析
3.1 原型鏈的構建
- 原型鏈是通過
[[Prototype]]
連接起來的。 - 當我們創建一個對象時,它的
[[Prototype]]
會指向某個原型對象。
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
- 如果我們手動修改
[[Prototype]]
,可以構建自定義的原型鏈。
const parent = { name: 'Parent' };
const child = Object.create(parent); // child.__proto__ 指向 parent
console.log(child.name); // 輸出: Parent
3.2 原型鏈的查找機制
- 當訪問對象的屬性時,JavaScript 引擎會按照以下步驟查找:
- 檢查對象自身是否有該屬性。
- 如果沒有,沿著
[[Prototype]]
向上查找。 - 重復這個過程,直到找到屬性或到達
null
。
const grandparent = { familyName: 'Smith' };
const parent = Object.create(grandparent);
const child = Object.create(parent);console.log(child.familyName); // 輸出: Smith
- 在上面的例子中:
child
自身沒有familyName
。child.__proto__
(即parent
)也沒有familyName
。parent.__proto__
(即grandparent
)有familyName
,返回Smith
。
4. 原型鏈與繼承
4.1 基于原型的繼承
- JavaScript 通過原型鏈實現繼承。
- 子類的原型對象指向父類的實例,從而繼承父類的屬性和方法。
function Person(name) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};function Student(name, grade) {Person.call(this, name); // 調用父類構造函數this.grade = grade;
}// 繼承父類原型
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 修復構造函數指向const student1 = new Student('Bob', 10);
student1.sayHello(); // 輸出: Hello, my name is Bob
- 在上面的例子中:
Student.prototype
的原型指向Person.prototype
。student1
可以訪問Person.prototype
上的方法。
4.2 原型鏈的局限性
- 原型鏈繼承是單向的,子類可以訪問父類的屬性和方法,但父類不能訪問子類的屬性和方法。
- 如果原型鏈過長,屬性查找的性能會受到影響。
5. 原型鏈的實際應用
5.1 共享方法
- 將方法定義在原型上,可以避免每個實例都創建一份方法的副本,節省內存。
function Person(name) {this.name = name;
}Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};const person1 = new Person('Alice');
const person2 = new Person('Bob');// person1 和 person2 共享同一個 sayHello 方法
console.log(person1.sayHello === person2.sayHello); // true
5.2 擴展內置對象
- 可以通過修改內置對象的原型來擴展其功能。
Array.prototype.last = function() {return this[this.length - 1];
};const arr = [1, 2, 3];
console.log(arr.last()); // 輸出: 3
6. 總結
- 原型:每個對象都有一個
[[Prototype]]
,指向它的原型對象。 - 原型鏈:通過
[[Prototype]]
連接起來的鏈式結構,用于屬性查找。 - 繼承:通過原型鏈實現對象之間的繼承。
- 性能:原型鏈過長會影響查找性能,需謹慎設計。
理解原型和原型鏈是掌握 JavaScript 面向對象編程的關鍵,也是深入理解 JavaScript 運行機制的基礎。