Prototype 原型

Node.js Prototype 原型

Prototype 原型鏈的原理

JavaScript 並不像 Java、C++ 這些知名的物件導向語言具有「類別」(class)來區分概念與實體(instance)或天生具有繼承的能力,而只有「物件」,因此只能利用設計模式來模擬這些功能

當初設計語言並沒有 Class,所以只能用建構函式 function( 可以當作 constructor ),所以就可以知道 prototype 的出現是為了達到所有 instance 都能共享 property & method。

function Person(name) {
    this.name = name;
    this.sayHi = function() {
        // new 出幾份 instance 就會有幾份,佔用記憶體空間
        console.log('Object', this.name, 'Hi');
    }
}

Person.prototype.sayHi = function(){
    // 用 prototype 實現共享 property & method
    console.log('Object', this.name, 'Hi');
}

若沒有寫 prototype 的話,只要 Function 或物件被 new 出多少 instance 就會需要多少空間

而共同 prototype 的函式是共用同個記憶體,所以 比較節省記憶體空間

當建立一個 Instance 時,JavaScript 會自動加上 __proto__ 屬性,告訴程式如果在原本的 Instance 找不到方法的時候,要去 __proto__

建構子

只要函式前有 new,這個函式就是建構子,只要函式前有 new 來做呼叫,就叫做建構子呼叫。

new 關鍵字做了哪些事情

  1. 建立一個新的物件。

設定原型

// 動物
let animal = {
    // 會吃
    eats: true
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// 設定兔子原型是動物
rabbit.__proto__ = animal;

// 兔子會吃:true
console.log( rabbit.eats );
// 兔子會跳:true
console.log( rabbit.jumps );
// 動物不一定會跳:undefined
console.log( animal.jumps );
// 兔子會跳:true
console.log( 'eats' in rabbit );
// 動物不一定會跳:false
console.log( 'jumps' in animal );

除了直接在 __proto__ 設定物件原型,也可以用使用物件方法設定原型

// 設定兔子原型是動物
rabbit.__proto__ = animal;
// 使用物件方法設定原型
Object.setPrototypeOf(rabbit, animal);

Prototype 原型

設定物件原型

Object.setPrototypeOf(obj, prototype)

加入新的原型方法

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// 設定兔子原型是動物
Object.setPrototypeOf(rabbit, animal)

// 兔子會走:animal walking
rabbit.walk();

加入新的物件繼承動物原型

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// 鳥
let bird = {
    // 會飛
    fly: true
}

// 設定兔子原型是動物
Object.setPrototypeOf(rabbit, animal)
// 設定鳥原型是動物
Object.setPrototypeOf(bird, animal)

// 兔子會走:animal walking
rabbit.walk();
// 鳥會走:animal walking
bird.walk();
// 兔子與鳥的原型都是一樣的 true
console.log(rabbit.__proto__ === bird.__proto__);

取得物件本身自己的鍵值 Object.keys

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// 鳥
let bird = {
    // 會飛
    fly: true
}

// 設定兔子原型是動物
Object.setPrototypeOf(rabbit, animal);
// 設定鳥原型是動物
Object.setPrototypeOf(bird, animal);

// [ 'jumps' ]
console.log(Object.keys(rabbit));
// [ 'fly' ]
console.log(Object.keys(bird));

取得物件所有鍵值,包含原型資料 for..in

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// 鳥
let bird = {
    // 會飛
    fly: true
}

// 設定兔子原型是動物
Object.setPrototypeOf(rabbit, animal);
// 設定鳥原型是動物
Object.setPrototypeOf(bird, animal);

// 取得兔子所有鍵值資料
// [rabbit] jumps
// [rabbit] eats
// [rabbit] walk
for(let prop in rabbit) {
    console.log(`[rabbit] ${prop}`);
}

// 取得鳥所有鍵值資料
// [bird] fly
// [bird] eats
// [bird] walk
for(let prop in bird) {
    console.log(`[bird] ${prop}`);
}

判斷是否是物件自己的屬性 hasOwnProperty

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// 鳥
let bird = {
    // 會飛
    fly: true
}

// 設定兔子原型是動物
Object.setPrototypeOf(rabbit, animal);
// 設定鳥原型是動物
Object.setPrototypeOf(bird, animal);

// 取得兔子所有鍵值資料
// [rabbit] Our: jumps
// [rabbit] Inherited: eats
// [rabbit] Inherited: walk
for(let prop in rabbit) {
    let isOwn = rabbit.hasOwnProperty(prop);

    if (isOwn) {
        console.log(`[rabbit] Our: ${prop}`);
    } else {
        console.log(`[rabbit] Inherited: ${prop}`);
    }
}

// 取得鳥所有鍵值資料
// [bird] Our: fly
// [bird] Inherited: eats
// [bird] Inherited: walk
for(let prop in bird) {
    let isOwn = bird.hasOwnProperty(prop);
    if (isOwn) {
        console.log(`[bird] Our: ${prop}`);
    } else {
        console.log(`[bird] Inherited: ${prop}`);
    }
}

刪除 prototype

let animal = {
    jumps: null
};
let rabbit = {
    __proto__: animal,
    jumps: true
};


// true
console.log( rabbit.jumps );

// 刪除兔子 jumps
delete rabbit.jumps;
// 顯示動物 jump:null
console.log( rabbit.jumps );

// 刪除動物 jumps
delete animal.jumps;
// undefined
console.log( rabbit.jumps );

prototype chain 原型鏈

prototype 可以持續不斷地繼承

建構函式有預設最底層的原型物件

// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

// [Object: null prototype] {}
console.log(rabbit.__proto__);

修改 prototype 後,可以用 prototype chain 找到更底層的原型物件

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

Object.setPrototypeOf(rabbit, animal);
// { eats: true, walk: [Function: walk] }
console.log(rabbit.__proto__);
// [Object: null prototype] {}
console.log(rabbit.__proto__.__proto__);
// null
console.log(rabbit.__proto__.__proto__.__proto__);

JavaScript 就是透過 prototype chain 原型鏈,去達到繼承的目的

方法取得順序

// 動物
let animal = {
    // 會吃
    eats: true,
    walk : () => {
        console.log(`animal walking`);
    }
};
// 兔子
let rabbit = {
    // 會跳
    jumps: true
};

Object.setPrototypeOf(rabbit, animal)
// animal walking
rabbit.walk();
// [object Object]
console.log(rabbit.toString());

rabbit.walk()

順序 尋找 所屬原型 有沒有找到
1 rabbit.walk() rabbit X
2 rabbit.__proto__.walk() animal V

rabbit.toString()

順序 尋找 所屬原型 有沒有找到
1 rabbit.toString() rabbit X
2 rabbit.__proto__.toString() animal X
3 rabbit.__proto__.__proto__.toString() Object V

__proto__prototype 比較

function Animal(name) {
    this.name = name;
    this.walk = () => {
        console.log(`[Animal] ${this.name} walking`);
    }
}

// 設定動物函式的 Prototype
Animal.prototype.run = () => {
    console.log(`[Animal Prototype] ${this.name} run`);
}

let MyAnimal = new Animal('Kitty');

// Animal { name: 'Kitty', walk: [Function (anonymous)] }
console.log(MyAnimal);
// [Animal] Kitty walking
MyAnimal.walk();
// [Animal Prototype] undefined run
MyAnimal.run();

// { run: [Function (anonymous)] }
console.log(MyAnimal.__proto__);
// [Object: null prototype] {}
console.log(MyAnimal.__proto__.__proto__);
// null
console.log(MyAnimal.__proto__.__proto__.__proto__);

在 ES 規範中,每個變數物件都有 __proto__,只有 Function 函數 才有 prototype 屬性

當建立 Function 函數 的時候,會自動加入 prototype 屬性

所以使用 new FunctionName() 的方式建立物件時,在建立完 Instance 後,會繼承 Function 函數 中所有的 prototype 屬性和方法,將自己的 __proto__ 指向 prototype

透過 prototype 儲存要共享的屬性和方法,也可以設定 prototype 去指向既有的物件

所以:

  • Function 本身就是函数,Function.__proto__ 標準的建構目標是 Function.prototype
  • Function.prototype.__proto__ 標準的建構目標是 Object.prototype
  • 每個函式的 prototype 物件,會有一個 constructor 屬性,指回到這個函式。例如 MyFunc.prototype 物件的 constructor 屬性,會指向 MyFunc函式
  • 每個物件都有一個 __proto__ 內部屬性,指向它的繼承而來的原型 prototype 物件

Prototype

mollypages.org 可以看到這張圖

  • Function.prototypeFunction.__proto__ 都指向 Function.prototype
  • Object.prototypeObject.__proto__ 都指向 Object.prototype
  • Object.prototype.__proto__ === null

結論

  • __proto__ 是物件實際在使用的原型鏈,去使用 prototype chain 原型鏈 解析方法
  • prototype 是函式物件在使用 new 建立物件時,用來建立 __proto__
function Animal(name) {
    this.name = name;
}


let myAnimal = new Animal('Kay');

// true
console.log(myAnimal.__proto__ === Animal.prototype);
// true
console.log(myAnimal.prototype === undefined);

prototype 原理

Object.prototype 是原型鍊的頂端物件

// true
console.log(Object instanceof Function);
// true
console.log(Function instanceof Object);
// true
console.log(Function.prototype.__proto__ == Object.prototype);
// false
console.log(Function.prototype == Object.prototype);

Function.prototypeFunction.__proto__ 為同一對象

// true
console.log(Function.prototype === Function.__proto__);
// true
console.log(Function.prototype === Object.__proto__);
// true
console.log(Function.prototype === Array.__proto__);

Object/Array/String 等等建構函式本質上和 Function 一樣,都是繼承 Function.prototype

Function.prototype 直接继承 root(Object.prototype)

// true
console.log(Object instanceof Function);
// true
console.log(Function instanceof Object);
// true
console.log(Function instanceof Function);

// true
console.log(Array instanceof Function);
// true
console.log(Array instanceof Object);
// false
console.log(Function instanceof Array);
// false
console.log(Object instanceof Array);

ObjectFunction 的雞和蛋的問題

導致 Function instanceof ObjectObject instanceof Function 都為 true 的原因

1. Function.prototype 不同於一般函式的函式

The Function prototype object is itself a Function object (its [[Class]] is “Function”) that, when invoked, accepts any arguments and returns undefined.

The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object (15.2.4). The initial value of the [[Extensible]] internal property of the Function prototype object is true.

The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object.

  • Function.prototype一般函式 一樣可以呼叫調用,接受任何的參數,且也可回傳 undefined
  • 一般函式Function 的 Instance 實例,表示 一般函式 繼承 Function.prototypeCustomFunction.__proto__ == Function.prototype
  • Function.prototype 繼承 Object.prototype
  • Function.prototype.prototypenull

所以 Function.prototype 是另類的函數,可以獨立優先於 Function 產生

// true
console.log(Object instanceof Function);
// true
console.log(Function instanceof Object);
// {}
console.log(Function.__proto__);
// [Object: null prototype] {}
console.log(Function.__proto__.__proto__);
// null
console.log(Function.__proto__.__proto__.__proto__);

// {}
console.log(Function.prototype);
// [Object: null prototype] {}
console.log(Function.prototype.__proto__);
// null
console.log(Function.prototype.__proto__.__proto__);

// undefined
console.log(Function.prototype.prototype);

2. Object 本身是建構函式,是 Function 的實例

The value of the [[Prototype]] internal property of the Object constructor is the standard built-in Function prototype object.

The value of the [[Prototype]] internal property of the Object prototype object is null, the value of the [[Class]] internal property is “Object”, and the initial value of the [[Extensible]] internal property is true.

Object.__proto__ 就是 Function.prototype

// true
console.log(Object instanceof Function);
// {}
console.log(Object.__proto__);
// [Object: null prototype] {}
console.log(Object.__proto__.__proto__);
// null
console.log(Object.__proto__.__proto__.__proto__);
// [Object: null prototype] {}
console.log(Object.prototype);
// null
console.log(Object.prototype.__proto__);


// {}
console.log(Function.prototype);
// {}
console.log(Function.__proto__);
// [Object: null prototype] {}
console.log(Function.__proto__.__proto__);

// true
console.log(Object.__proto__ == Function.prototype);
// true
console.log(Object.__proto__ == Function.__proto__);
// true
console.log(Object.__proto__.__proto__ == Function.__proto__.__proto__);

所以 Function 與 Object 關係是

  1. 先有 Object.prototype(原型鏈頂端)
  2. Function.prototype 繼承 Object.prototype 而產生
  3. 最後,FunctionObject 和其它建構函數繼承 Function.prototype 而產生

Object prototype 研究

Object.prototype 不是 Object 的實例

// 取得物件 prototype
const root = Object.prototype;

// [Object: null prototype] {}
console.log(root);
// null : 取得 root prototype
console.log(Reflect.getPrototypeOf(root));
// {} : 取得 Object prototype
console.log(Reflect.getPrototypeOf(Object));
// object : 取得 root 類型
console.log(typeof root);
// false : root 是否為 Object 實例
console.log(root instanceof Object);

Object.prototype 是對象,但不是透過 Object 函數建立的

// Object(0) []
console.log(Array.prototype);
// Object [Map] {}
console.log(Map.prototype);
// [Object: null prototype] {}
console.log(Object.prototype);
// {}
console.log(Function.prototype);
  • Array.prototype 是陣列格式的 Object
  • Map.prototypeMap 格式的 Object
  • Function.prototype 反過來繼承 {} (Object)
const funcPrototype = Function.prototype;
const func_prototype_ = Function.__proto__;
function test() {};

// true
console.log(test.__proto__ === funcPrototype);
// true
console.log(Array.__proto__ === funcPrototype);
// true
console.log(func_prototype_ === funcPrototype);
  • Function 本身也是 function
  • Function.prototype 是所有 function 的原型(包括 Function 自己的原型)
  • 但反過來,Function.prototypeFunction 沒有反向關係(正向 Function 繼承 Function.prototype

所以 Function.prototypeFunction.__proto__ 相同,不代表 Function 這個函數是由自己本身去建立的

是先有 Function.prototype 這個對象(也是函數),然後才有其他函數

Array.prototype 是陣列

// Object(0) []
console.log(Array.prototype);
// false
console.log(Array.prototype instanceof Array);

const arrayPrototype = Array.prototype;
arrayPrototype.push('Kay');
// Object(1) [ 'Kay' ]
console.log(arrayPrototype);
// true
console.log(Array.isArray(arrayPrototype));

ES6 使用類別實現繼承

class Animal {
    constructor (name){
        this.name = name
    }
    walk = () => {
        console.log(`[Animal] ${this.name} walking`);
    }
}

class Rabbit extends Animal {
    constructor (name){
        super(name)
    }

    jump = () => {
        console.log(`[Rabbit] ${this.name} jump`);
    }
}

let myRabbit = new Rabbit('Kay');

// [Animal] Kay walking
myRabbit.walk();
// [Rabbit] Kay jump
myRabbit.jump();

// Rabbit { walk: [Function: walk], name: 'Kay', jump: [Function: jump] }
console.log(myRabbit);
// Animal {}
console.log(myRabbit.__proto__);
// {}
console.log(myRabbit.__proto__.__proto__);
// [Object: null prototype] {}
console.log(myRabbit.__proto__.__proto__.__proto__);
// null
console.log(myRabbit.__proto__.__proto__.__proto__.__proto__);

常見問題

為什麼不直接用 __proto__ 取得原型物件?

__proto__ 雖然被幾乎所有的瀏覽器支援,但它仍是非標準屬性;透過 Object.getPrototypeOf 取得物件的原型是比較正確的方法。

什麼是 __proto__

由 ES6 開始成為 Object 的原生屬性,直接對 [[Prototype]] 進行讀寫。

什麼是 prototype

一個 Object,當 new 一個 instance 時,會被用作指向 __proto__ 作為 instance 繼承的屬性

prototype 只存在於 constructor functions,在 instance 上不存在。

相反的,__proto__ 則出現在所有物件。

參考資料