Stories

Detail Return Return

理解原型/原型鏈 - Stories Detail

對於搞前端的小夥伴來説,不管是新手還是老鳥,我想對於原型應該都被折騰過,總是雲裏霧裏的感覺,要是原型都沒搞明白,你還好意思説你是前端攻城獅?

關於對象

當一説到面向對象(Object-Oriented OO)時,你第一反應肯定想到類、對象、接口實現等概念,那我們這裏為啥已上來就説對象呢?因為ECMAScript裏沒有類,另外因為ECMAScript中的函數沒有簽名,所以也沒有接口

ECMAScript-262中對象定義為:“無序屬性的集合,其屬性可以是基本值、對象或者函數”。因此從數據結構的角度,可以把對象看成散列表(Hash Table)。

對象分類

從對象的創建方式上可以把對象分成:內置對象、宿主對象、自定義對象三大類。關於對象分類詳細點這裏。

特別需要強調的是,除了number、string、boolean、null、undefined、symbol這6中基本類型外,其它統統都是對象(引用類型),包括函數,所有的函數都是對象,反之則不成立

對象和函數的關係

對象的創建

前面説過,ECMAScript中沒有類,那怎麼創建對象呢?

對象字面量
// 方式一: 對象字面量
var zhangsan = {
    type: "人類",
    name: "張三",
    age: 18,
    greeting: function() {
        console.log(`hello I'am ${this.name}`);
    }
};
zhangsan.greeting(); // "hello I'am 張三"

該方式主要有一下幾個問題:

  • 當要創建多個變量的時候,不得不寫大量重複代碼;
  • 每個實例都會持有一個greeting函數,但實際上功能都一樣,沒有複用,浪費資源;
  • 創建所有“人類"(type="人類")的實例,type的值都是一樣的,但是每個實例還是持有一個獨立的副本;
  • 創建實例無法識別類型(也就是説創建的實例具體是啥類型不知道,只知道它是Object的實例)。
工廠模式
// 方式二: 工廠模式
function createPerson (name, age) {
    var p = new Object();
    p.type = "人類";
    p.name = name;
    p.age = age;
    p.greeting = greeting;
    return p;
}
var lisi = createPerson ("李四", 20);
lisi.greeting(); // "hello I'am 李四"
function greeting () {
    console.log(`hello I'am ${this.name}`);
}

方式二雖然進行了封裝,避免了創建時大量重複的代碼,也通過把greeting抽離到全局作用域而解決了多個實例持有多個greeting副本的問題,但同時也給全局空間引入了一個只有該類型實例才會引用的函數,污染了全局空間;最後它也米有解決對象識別問題。

// 方式三: 構造函數
function Person (name, age) {
    this.type = "人類";
    this.name = name;
    this.age = age;
    this.greeting = greeting;
}
var wangwu = new Person("王五", 24); // wangwu instanceof Person === true
wangwu.greeting(); // "hello I'am 王五"
function greeting () {
    console.log(`hello I'am ${this.name}`);
}

這個方式近乎完美了,解決了對象識別問題,但是任然沒有解決共享函數污染全局空間的問題;為了解決這個問題,下面請出我們的主角prototype(原型)。

原型&原型鏈

終於切入正題了,要解決上面方式三面臨的問題,就要有一個屬於構造函數專有(不用定義到全局污染全局空間),能夠為構造函數創建的所有對象實例所共享的對象。這個對象就是原型(或稱為原型對象)。

什麼是原型(prototype)

默認情況下,任何函數都有一個屬性prototype,它是一個指針,指向一個對象(原型對象),原型對象的用途是包含特定類型實例所共享的屬性和方法,默認原型對象只有一個constructor屬性,我們可以給它定義更多屬性和方法。

// 方式四: 原型法
function Person (name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.type = "人類";
Person.prototype.greeting = function () {
    console.log(`hello I'am ${this.name}`);
};
var wangwu = new Person("王五", 24); // wangwu instanceof Person === true
wangwu.greeting(); // "hello I'am 王五"

那上面的實例wangwu是怎麼找到原型對象裏定義的greeting的呢?原因是所有的對象都有一個內部指針,指向實例構造函數的原型對象,ECMAScript-262第5版中稱為[[Prototype]],雖然標準並沒有定義怎麼訪問這個內部指針,但是Firefox、Safari、Chrome在每個對象上都支持一個指向相同、名為__proto__指針屬性。

在chrome console裏查看wangwu的屬性如下圖:
[站外圖片上傳中...(image-1d07-1644313611733)]

原型鏈查找

當對象實例訪問某個屬性或調用某個方法時,首先在自有屬性裏找,找到則返回值或發起調用,沒有則沿着__proto__的指向往上找,直到最後查到Object.prototype,任然沒有查到,即終止並報錯。

對象實例、構造函數、構造函數的原型對象這三者的關係如下圖:

image

上圖中紅色的路徑及為查找方向,這條有__proto__指針串起來的鏈即為原型鏈(prototype chain)原型鏈的本質是一串順序指向原型對象的指針列表

原型的動態性

因為對象實例的__proto__僅僅是一個指向原型對象的指針,因此對原型對象的修改立即可以在實例上體現出來,哪怕這個實例在修改原型之前創建的:

Person.prototype.work = function () {
    console.log('work function');
}
// 這裏的wangwu是上面創建的實例,給原型增加work方法後,可以立即調用
wangwu.work(); // "work function"

但是如果重寫整個原型對象後,相當於為構造函數指定了新的原型對象,而已創建的實例的__proto__仍然指向舊原型對象,因此訪問不到在新原型裏定義的方法:

Person.prototype = {
    work: function () {
        console.log('work function');   
    }
};
// 報錯
wangwu.work(); // "wangwu.work is not a function"
// 在修改原型對象後創建的實例,因為獲取到的__proto__屬性是指向新原型的,因此不會報錯
var sanma = new Person('三毛', 30);
// 可以愉快的“工作”
sanma.work(); // "work function"

[圖片上傳失敗...(image-b3adea-1644313611733)]

覆蓋整個原型對象後,相當於上面圖中原來的prototype指向被切斷了,指向了新的原型。

小結一下

默認情況下(因為原型對象實際上是可寫的,因此可以被改變):

  1. 任何函數都有一個指向其原型對象的指針屬性prototype;
  2. 任何對象實例都有一個指向其構造函數原型對象的內部指針[[Prototype]](__proto__)
  3. 原型對象也是對象,因此也有__proto__(例如上圖中指向Object.prototype那個);
  4. 對象實例的__proto__指針指向構造函數的原型對象:wangwu.__proto__ === Person.prototype
  5. 原型對象的constructor屬性指向構造函數: Person.prototype.constructor === Person
  6. 構造函數和對象實例沒有直接聯繫,僅僅是都有一個指針屬性指向同一個原型對象。

對象實例識別(檢測)

我們知道,對於number、string、boolean、undefined、function這幾種類型值,可以通過typeof操作符簡單區分,但是對於除function外的引用類型實例和null,typeof都返回"object",但是再往細了區分,某個對象實例是神類型的實例,typeof就沒辦法了。

instanceof操作符

要識別具體的對象實例類型,就要用到instanceof操作符,格式為 instance instanceof Func, instance是待檢測實例對象,Func是一個構造函數,有了上面原型鏈的理解,那instanceof的檢測機制就簡單多了,只要在instance的原型鏈上某個__proto__指向了Func的原型對象,就返回true,否則返回false。即:

instance.__proto__...__proto__ === Func.prototype

另外也可以用Func.prototype.isPrototypeof(instance)、Object.getPrototypeof(instance) === Func.prototype來判斷。

console.log(wangwu instanceof Person); // true
console.log(wangwu instanceof Object); // true
console.log(Person.prototype.isPrototypeof(wangwu)); // true
console.log(Object.prototype.isPrototypeof(wangwu)); // true
console.log(Object.getPrototypeof(wangwu) === Person.prototype); // true
console.log(Object.getPrototypeof(wangwu) === Object.prototype); // false, 因為getPrototypeof函數只返回實例原型,而不會返回原型鏈上的其它原型

原型繼承

理解了原型,那原型繼承就很簡單了,需要擴展的類指向父類的原型即可,下面是簡單的原型繼承實現:

function Men() {
 // 
}


Men.prototype = Object.create(Person.prototype);
Men.prototype.constructor = Men;

特別注意,給prototype屬性賦值後,Men.prototype.constructor指向了Person,因此必須再把它指回Men。

Add a new Comments

Some HTML is okay.