原型這塊知識點,其實在我們工作中的使用非常簡單。但是俗話説“面試造火箭,工作擰螺絲”,在面試中,面試官不時就會考察一些花裏胡哨的問題,所以,我們只有將這個概念和他的相關知識理解透徹,才能以不變應萬變。
-
兩個容易混淆但要分清的東西
-
每個普通對象都有內部隱式屬性
[[Prototype]](常見訪問名 proto) —— 它指向另一個對象(即原型對象)。所以原型對象名字的由來就是,一個對象有一個 prototype 屬性,就是原型屬性,而這個原型屬性本身又是一個對象,所以稱之為原型對象。
-
函數(作為構造函數)有
.prototype 屬性 —— 當你用new Fn()創建實例時,實例的[[Prototype]]會被設置為Fn.prototype。
總結:
.prototype是構造函數的屬性;[[Prototype]]/__proto__是普通對象實例的內部指針,二者在構造/實例化時建立聯繫,但不是同一個東西。
-
原型鏈:屬性查找的核心機制
當你訪問 obj.prop 時,JS 的查找流程如下:
- 先查看
obj自身是否有名為prop的自有屬性。有就返回。 - 沒有則沿着
obj.[[Prototype]](即obj.__proto__)去找,找到就返回。 - 若仍未找到則繼續沿着原型的
[[Prototype]](形成鏈)向上查找,直到null(查不到返回undefined)。
這就是所謂的 **原型鏈(prototype chain)**。
ES6
class是語法糖,本質仍用原型。
示例:
const grand = { greet() { return 'hi from grand'; } };
const parent = Object.create(grand);
parent.say = () => 'parent';
const child = Object.create(parent);
child.own = 1;
console.log(child.own); // 1 (own property)
console.log(child.say()); // 'parent' (從 parent 找到)
console.log(child.greet()); // 'hi from grand' (從 grand 找到)
console.log(Object.getPrototypeOf(child)); // parent
我們既可以通過構造函數的方式實現繼承,也可以通過純原型繼承(Object.create())的方式實現。
Object.getPrototypeOf(obj):安全地獲取[[Prototype]]。Object.setPrototypeOf(obj, proto):設置對象的原型。通常優先建議使用Object.create在創建時設置原型。
-
構造函數與
new的工作原理
當你寫 new F(arg):
- 新建一個空對象
obj。 - 這個空對象的
[[Prototype]]被設置為F.prototype。 - 執行
F,並把this指向obj。 - 若
F返回對象,則最終結果為該對象;否則返回obj。
因此,F.prototype 是實例繼承的方法/屬性的來源。
/**
* 模擬實現 new 操作符的函數
* @param {Function} Constructor 構造函數
* @param {...any} args 傳遞給構造函數的參數
* @return {*} 如果無返回值或者顯示返回一個對象,則返回構造函數的執行結果;如果顯示返回一個基本類型,則返回構造函數的實例
*/
function myNew(Constructor, ...args) {
// 1. 創建一個全新的空對象 2. 為這個空對象設置原型(__proto__)
// 可以使用 {},但是推薦使用 Object.create() 創建對象並設置原型
const instance = Object.create(Constructor.prototype)
// 3. 綁定構造函數的this為其新創建的空實例對象,並執行構造函數體
const result = Constructor.apply(instance, args)
const isObject = typeof result === 'object' && result !== null
const isFunction = typeof result === 'function'
// 4. 如果構造函數返回一個非原始值,則返回這個對象;否則返回創建的新實例對象
if (isObject || isFunction) return result
return instance
}
-
hasOwnProperty、in、Object.keys的區別
obj.hasOwnProperty('a'):只檢查自身屬性(不走原型鏈)。'a' in obj:檢查自身或原型鏈上是否存在屬性(包括不可枚舉的)。Object.keys(obj)/for...in:Object.keys返回自身可枚舉屬性數組;for...in會枚舉自身 + 可枚舉的繼承屬性(可用hasOwnProperty過濾)。
示例:
const p = {x:1};
const o = Object.create(p);
o.y = 2;
'x' in o // true
o.hasOwnProperty('x') // false
Object.keys(o) // ['y']
for (const k in o) { console.log(k); } // 'y' 'x'
-
instanceof如何工作
obj instanceof Constructor 檢查的是 Constructor.prototype 是否出現在 obj 的原型鏈上(通過 Object.getPrototypeOf 遞歸判斷)。
/**
* 模擬 instanceOf 的實現
* @param object 實例對象
* @param Constructor 構造函數(類)
* @return {boolean}
*/
function myInstanceOf(object, Constructor) {
// 初始獲取對象的原型
let proto = Object.getPrototypeOf(object)
while (true) {
// 遍歷到原型鏈頂端
if (proto === null) return false
// 找到匹配的原型
if (proto === Constructor.prototype) return true
// 繼續向上查找原型鏈
proto = Object.getPrototypeOf(proto)
}
}
-
覆蓋與讀取順序
如果對象自身有同名屬性,會遮蔽原型上的同名屬性:
const proto = {v:1};
const o = Object.create(proto);
o.v = 2;
console.log(o.v); // 2 (自身屬性優先)
delete o.v;
console.log(o.v); // 1 (回退到原型)
-
修改原型
你可以給原型添加/修改方法,所有繼承該原型的對象都會受影響:
Array.prototype.myLog = function(){ console.log(this.length); };
[1,2,3].myLog(); // 3
注意:
- 不要隨意修改內置對象(如
Object.prototype、Array.prototype)。修改 prototype 會影響所有實例,可能引入難以追蹤的副作用。這也是非常常見的一種網絡安全漏洞:原型污染。指攻擊者使用某種
-
單獨説説 constructor
上面的內容看起來是不是還挺簡單的。如果上面內容已經完全理解了,那麼再來看 construtor 屬性。
JavaScript 每個函數(構造函數)對象天生都會有一個 prototype 屬性,而這個 prototype 對象中,默認會有一個指向函數本身的屬性 —— constructor。
可以理解為:
constructor 是原型對象上一個指針,用來指向創建該實例的構造函數。
function Person(name) { this.name = name; }
console.log(Person.prototype.constructor === Person); // true
上述這段代碼還比較好理解,總之就是 prototype 這個對象身上有一個屬性叫做 constructor,這個 constructor 剛好指向原 構造函數。
接着這段代碼的思路,我們再來看看下面這段代碼:
function Person(name) { this.name = name; }
const p = new Person("Tom");
console.log(p.constructor === Person); // true
誒?不兒?constructor 不是 prototype 上的屬性嗎?實例對象上也有這個屬性嗎?
如果你能想到這裏,那説明之前的內容至少你已經學懂了。接下來讓我告訴你為什麼 p.constructor === Person?
原因其實也很簡單,因為:
p.constructor
= p.__proto__.constructor // 實例上沒有 constructor,會去原型 __proto__ 查找
= Person.prototype.constructor
= Person
為什麼上面的繼承方式我沒有説 constructor?
因為原型重寫後會丟失 constructor 指向,需要手動補回。看這段代碼:
function Animal() {}
Animal.prototype = {
eat() {}
};
乍眼一看,我們是為 Animal 構造函數添加了 eat 方法,但其實 ⚠️ 這樣做會 覆蓋原始默認的 prototype 對象,從而導致 constructor 丟失(變成 Object ==> { eat(){} } )。
console.log(Animal.prototype.constructor); // 此時是 Object,不是 Animal
所以,如果你非要這麼寫,還得自己補回 constructor:
function Animal() {}
Animal.prototype = {
constructor: Animal, // 手動補回構造函數
eat() {}
};
這樣你是不是明白了,為什麼上面的繼承方式我沒有説 constructor。不是不行,而是不太推薦。任何人都可以隨意改原型,導致 constructor 變得不可信。
ES6 class 的
constructor本質也是一樣的。
-
我是真的不想再談 Funciton 了
這一節完全可以不看,因為本質上還是上面的內容,但奈何總有面試官喜歡挖坑,也總有同學喜歡上當~
普通函數(非箭頭)天然可以作為構造函數。所以上面説的什麼 Object、Person 等等所有函數都是 Function 的實例。
console.log(Person.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true
Function.prototype 自身也是一個函數(內置),它的 prototype 與普通對象不同——記住 Function 本身是一個 constructor:
Function instanceof Function // true
Function.prototype instanceof Function // false (Function.prototype 是個普通函數對象)
Function.prototype.__proto__ === Object.prototype // true
Person (構造函數)
│
├── prototype → Person.prototype → { constructor: Person, ... } ✅
└── __proto__ → Function.prototype ✅
Function.__proto__ === Function.prototype // true
Function 自己也是一個函數,它也是自己構造出來的。這就像是先有雞還是先有蛋的問題 😂。