JavaScript 原型與原型鏈詳解
文章目錄
- JavaScript 原型與原型鏈詳解
- 一、基礎概念類
- 1.1 什么是原型?JavaScript 中如何訪問一個對象的原型?
- 1.2 構造函數、實例對象和原型對象之間的關系是什么?
- 1.3 prototype 和 **proto** 的區別是什么?
- 二、原型鏈機制類
- 2.1 什么是原型鏈?描述原型鏈的查找機制
- 2.2 代碼示例分析
- 三、構造函數與實例類
- 3.1 new 操作符執行時發生了什么?
- 3.2 手動實現 new 操作符
- 3.2.1apply邏輯
- 3.2.2`result instanceof Object ? result : obj;`解決的問題
- 四、繼承與原型鏈類
- 4.1 JS如何實現繼承?
- 原型鏈繼承
- 五、高級應用類
- 5.1 如何修改內置對象(如 Array)的原型?這樣做有什么風險?
- 5.1.1修改內置原型
- 5.1.2創建子類繼承內置對象(ES6 Class方式)
- 5.1.3使用Object.create創建原型鏈繼承
- 5.2 如何判斷一個屬性是對象自身的還是繼承自原型鏈的?
- 六、總結
一、基礎概念類
1.1 什么是原型?JavaScript 中如何訪問一個對象的原型?
**原型(Prototype)**是 JavaScript 實現繼承的基礎機制。每個 JavaScript 對象都有一個內部屬性 [[Prototype]]
(可通過 __proto__
或 Object.getPrototypeOf()
訪問),它指向該對象的原型對象。原型對象本身也是一個普通對象,同樣擁有自己的原型,這樣就形成了原型鏈。
訪問對象原型的方式:
- 對于構造函數:通過
Constructor.prototype
訪問 - 對于實例對象:
obj.__proto__
(非標準但廣泛支持)Object.getPrototypeOf(obj)
(標準方法)
function Person() {}
const p = new Person();// 訪問構造函數的原型
console.log(Person.prototype); // 訪問實例的原型
console.log(p.__proto__);
console.log(Object.getPrototypeOf(p));
1.2 構造函數、實例對象和原型對象之間的關系是什么?
三者關系可概括為:
- 構造函數:用于創建對象的函數,擁有
prototype
屬性 - 原型對象:通過
Constructor.prototype
訪問,包含共享屬性和方法 - 實例對象:通過
new Constructor()
創建,其__proto__
指向構造函數的原型
關系圖示:
構造函數 (Person)├── prototype (原型對象)│ ├── constructor (指回構造函數)│ └── 共享屬性和方法└── 實例化└── 實例對象 (p)└── __proto__ (指向原型對象)
關鍵點:
- 構造函數的
prototype
屬性指向原型對象 - 原型對象的
constructor
屬性指回構造函數 - 實例對象的
__proto__
指向構造函數的原型對象
1.3 prototype 和 proto 的區別是什么?
特性 | prototype | __proto__ |
---|---|---|
所屬對象 | 函數 | 對象(包括函數) |
作用 | 為構造函數定義共享屬性和方法 | 指向對象的原型,形成原型鏈 |
是否標準 | 是 | 否(非標準但廣泛支持) |
訪問方式 | Constructor.prototype | obj.__proto__ 或 Object.getPrototypeOf(obj) |
用途場景 | 實現繼承和共享方法 | 屬性查找機制 |
關鍵區別:
prototype
是函數特有的屬性,用于實現基于原型的繼承__proto__
是對象實例的屬性,指向其構造函數的原型對象
二、原型鏈機制類
2.1 什么是原型鏈?描述原型鏈的查找機制
原型鏈是由對象的 __proto__
鏈接形成的鏈條結構,它允許對象訪問其原型上的屬性和方法,直到 Object.prototype.__proto__
(值為 null
)為止。
原型鏈查找機制:
- 訪問對象屬性時,首先在對象自身查找
- 如果自身不存在該屬性,則通過
__proto__
查找其原型對象 - 繼續沿原型鏈向上查找,直到找到屬性或到達原型鏈頂端(
null
) - 如果最終未找到,則返回
undefined
這種機制實現了 JavaScript 的繼承和屬性共享。
2.2 代碼示例分析
function Person() {}
Person.prototype.name = "Alice";let p = new Person();
console.log(p.name); // "Alice"
console.log(p.hasOwnProperty("name")); // false
執行過程解析:
p.name
訪問時:- 首先在
p
對象自身查找name
屬性 → 未找到 - 通過
p.__proto__
查找Person.prototype
→ 找到name: "Alice"
- 返回
"Alice"
- 首先在
p.hasOwnProperty("name")
:hasOwnProperty
是檢查屬性是否為對象自身的屬性name
實際存在于Person.prototype
上,而非p
對象自身- 返回
false
三、構造函數與實例類
3.1 new 操作符執行時發生了什么?
new
操作符創建實例時,背后執行了以下步驟:
- 創建一個新的空對象
{}
- 將新對象的
__proto__
指向構造函數的prototype
屬性 - 將構造函數的
this
綁定到新對象,并執行構造函數 - 如果構造函數返回一個對象,則返回該對象;否則返回新創建的對象
完整過程示例:
function Person(name) {this.name = name;
}const p = new Person("Alice");
等價于:
function myNew(Constructor, ...args) {// 1. 創建空對象let obj = {};// 2. 設置原型鏈obj.__proto__ = Constructor.prototype;// 3. 綁定this并執行構造函數let result = Constructor.apply(obj, args);// 4. 返回結果(優先返回對象,否則返回新對象)return result instanceof Object ? result : obj;
}const p = myNew(Person, "Alice");
3.2 手動實現 new 操作符
function myNew(Constructor, ...args) {// 1. 創建一個新對象,并將其原型指向構造函數的prototypelet obj = Object.create(Constructor.prototype);// 2. 調用構造函數,將this綁定到新對象,args是一個數組let result = Constructor.apply(obj, args);// 3. 如果構造函數返回了一個對象,則返回該對象;否則返回新對象return result instanceof Object ? result : obj;
}
3.2.1apply邏輯
apply(thisArg,argsArray)
接收 兩個參數:
thisArg
(必需):函數運行時綁定的this
值。argsArray
(可選):一個數組或類數組對象,包含傳遞給函數的參數列表。如果省略或為null
/undefined
,則相當于傳遞空數組。
argsArray
的核心作用是解耦 myNew
和構造函數 Constructor
之間的參數關系:
myNew
不需要關心具體有多少參數,只需將所有額外參數打包到args
數組中。Constructor
可以自由定義自己需要的參數,通過this
接收并處理。
3.2.2result instanceof Object ? result : obj;
解決的問題
作用:處理構造函數 Constructor
的返回值,確保最終返回的對象符合預期
在 JavaScript 中,構造函數(通過 new
調用的函數)可以顯式返回一個值。這個返回值可以是:
- 一個對象(包括數組、函數、普通對象等)。
- 一個原始值(如
number
、string
、boolean
、null
、undefined
)。
如果構造函數返回一個對象,new
操作符會忽略默認創建的實例對象,直接返回這個指定的對象;如果返回原始值,則忽略返回值,仍然返回默認創建的實例對象。
示例:構造函數返回對象 vs 原始值
// 情況1:構造函數返回對象
function Person1() {return { name: "Bob" }; // 返回一個新對象
}
const p1 = new Person1();
console.log(p1); // { name: "Bob" }(不是 Person1 的實例)// 情況2:構造函數返回原始值
function Person2() {return 123; // 返回原始值
}
const p2 = new Person2();
console.log(p2); // Person2 的實例(不是 123)
四、繼承與原型鏈類
4.1 JS如何實現繼承?
原型鏈繼承
核心:將子類的原型指向父類的實例
JavaScript 中通過原型鏈繼承實現繼承的基本模式:
function Parent() {this.parentProperty = "Parent Value";
}Parent.prototype.parentMethod = function() {console.log("Parent Method");
};function Child() {this.childProperty = "Child Value";
}// 關鍵步驟:將Child的原型指向Parent的實例
Child.prototype = new Parent();const c = new Child();
console.log(c.parentProperty); // "Parent Value"
c.parentMethod(); // "Parent Method"
繼承關系圖示:
Child實例 (c)├── __proto__ → Child.prototype (Parent的實例)├── __proto__ → Parent.prototype├── constructor → Parent└── parentMethod└── childProperty (來自Child構造函數)
注意:這種繼承方式存在一些問題(如引用類型共享、無法向父類構造函數傳參等),現代開發更推薦使用 ES6 的 class
和 extends
語法。
五、高級應用類
5.1 如何修改內置對象(如 Array)的原型?這樣做有什么風險?
5.1.1修改內置原型
修改方式示例:
// 添加自定義方法到Array原型
Array.prototype.customMethod = function() {console.log("This is a custom method");
};const arr = [1, 2, 3];
arr.customMethod(); // "This is a custom method"
潛在風險:
- 全局污染:修改內置原型會影響所有使用該內置對象的代碼
- 命名沖突:可能與未來 JavaScript 版本新增的方法名沖突
- 兼容性問題:可能導致與其他庫或框架的不可預期交互
- 維護困難:使代碼行為變得不可預測,增加調試難度
最佳實踐:
- 避免直接修改內置原型
- 如需擴展功能,考慮使用工具函數或創建子類
- 如果必須修改,添加前綴以減少沖突風險(如
myCustomMethod
)
5.1.2創建子類繼承內置對象(ES6 Class方式)
這是最推薦的方式,既擴展了功能,又不會影響原生對象。
1. 基礎繼承示例
// 創建繼承自Array的自定義類
class CustomArray extends Array {// 添加自定義方法customSum() {return this.reduce((acc, val) => acc + val, 0);}// 可以添加更多自定義方法customMax() {return Math.max(...this);}
}// 使用示例
const arr = new CustomArray(1, 2, 3, 4);
console.log(arr.customSum()); // 輸出: 10
console.log(arr.customMax()); // 輸出: 4
console.log(arr instanceof Array); // true (仍然屬于Array類型)
5.1.3使用Object.create創建原型鏈繼承
更底層的實現方式,不使用ES6 class語法。
// 創建基于Array的新對象
const EnhancedArray = function(...items) {const arr = [...items];return Object.setPrototypeOf(arr, EnhancedArray.prototype);
};// 設置原型鏈
EnhancedArray.prototype = Object.create(Array.prototype);
EnhancedArray.prototype.constructor = EnhancedArray;// 添加自定義方法
EnhancedArray.prototype.customSum = function() {return this.reduce((acc, val) => acc + val, 0);
};// 使用示例
const arr = EnhancedArray(1, 2, 3);
console.log(arr.customSum()); // 輸出: 6
console.log(arr instanceof Array); // true
5.2 如何判斷一個屬性是對象自身的還是繼承自原型鏈的?
判斷方法:
-
hasOwnProperty()
方法:- 只檢查對象自身的屬性,不檢查原型鏈
const obj = { a: 1 }; console.log(obj.hasOwnProperty('a')); // true console.log(obj.hasOwnProperty('toString')); // false
-
Object.getOwnPropertyNames()
:- 返回對象自身的所有屬性名(不包括原型鏈)
const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyNames(obj)); // ["a", "b"]
-
in
操作符:- 檢查屬性是否存在于對象或其原型鏈中
const obj = { a: 1 }; console.log('a' in obj); // true console.log('toString' in obj); // true
綜合示例:
function checkProperty(obj, prop) {if (obj.hasOwnProperty(prop)) {console.log(`${prop} 是對象自身的屬性`);} else if (prop in obj) {console.log(`${prop} 是繼承自原型鏈的屬性`);} else {console.log(`${prop} 不是對象的屬性`);}
}const obj = { a: 1 };
checkProperty(obj, 'a'); // "a 是對象自身的屬性"
checkProperty(obj, 'toString'); // "toString 是繼承自原型鏈的屬性"
checkProperty(obj, 'b'); // "b 不是對象的屬性"
六、總結
JavaScript 的原型和原型鏈機制是其面向對象編程的核心,理解這些概念對于掌握 JavaScript 至關重要:
- 原型是對象間共享屬性和方法的機制
- 原型鏈實現了屬性的查找和繼承
- 構造函數、實例和原型三者構成了 JavaScript 對象系統的基礎
- new 操作符通過特定步驟創建實例并建立原型鏈
- 原型鏈繼承是 JavaScript 實現繼承的主要方式
- 謹慎操作內置原型,避免潛在風險
- 準確判斷屬性來源對調試和維護非常重要