原型這塊知識點,其實在我們工作中的使用非常簡單。但是俗話説“面試造火箭,工作擰螺絲”,在面試中,面試官不時就會考察一些花裏胡哨的問題,所以,我們只有將這個概念和他的相關知識理解透徹,才能以不變應萬變。

  1. 兩個容易混淆但要分清的東西

  2. 每個普通對象都有內部隱式屬性 ​[[Prototype]]​​(常見訪問名 ​proto —— 它指向另一個對象(即原型對象)。

    所以原型對象名字的由來就是,一個對象有一個 prototype 屬性,就是原型屬性,而這個原型屬性本身又是一個對象,所以稱之為原型對象。

  3. 函數(作為構造函數)有 ​.prototype​ 屬性 —— 當你用 new Fn() 創建實例時,實例的 [[Prototype]] 會被設置為 Fn.prototype

總結:.prototype 是構造函數的屬性;[[Prototype]]/__proto__ 是普通對象實例的內部指針,二者在構造/實例化時建立聯繫,但不是同一個東西。

  1. 原型鏈:屬性查找的核心機制

當你訪問 obj.prop 時,JS 的查找流程如下:

  1. 先查看 obj 自身是否有名為 prop 的​自有屬性​。有就返回。
  2. 沒有則沿着 obj.[[Prototype]](即 obj.__proto__)去找,找到就返回。
  3. 若仍未找到則繼續沿着原型的 [[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 在創建時設置原型。
  1. 構造函數與 new 的工作原理

當你寫 new F(arg)

  1. 新建一個空對象 obj
  2. 這個空對象的 [[Prototype]] 被設置為 F.prototype
  3. 執行 F,並把 this 指向 obj
  4. 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
}
  1. hasOwnPropertyinObject.keys 的區別

  • obj.hasOwnProperty('a'):只檢查自身屬性(不走原型鏈)。
  • 'a' in obj:檢查自身或原型鏈上是否存在屬性(包括不可枚舉的)。
  • Object.keys(obj) / for...inObject.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'
  1. 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)
    }
}
  1. 覆蓋與讀取順序

如果對象自身有同名屬性,會遮蔽原型上的同名屬性:

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 (回退到原型)
  1. 修改原型

你可以給原型添加/修改方法,所有繼承該原型的對象都會受影響:

Array.prototype.myLog = function(){ console.log(this.length); };
[1,2,3].myLog(); // 3

注意​:

  • 不要隨意修改內置對象(如 Object.prototypeArray.prototype)。修改 prototype 會影響所有實例,可能引入難以追蹤的副作用。

    這也是非常常見的一種網絡安全漏洞:原型污染。指攻擊者使用某種

  1. 單獨説説 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 本質也是一樣的。

  1. 我是真的不想再談 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 自己也是一個函數,它也是自己構造出來的。這就像是先有雞還是先有蛋的問題 😂。