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 關鍵字做了哪些事情
- 建立一個新的物件。
設定原型
// 動物
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);
設定物件原型
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
物件
在 mollypages.org 可以看到這張圖
Function.prototype
和Function.__proto__
都指向Function.prototype
Object.prototype
和Object.__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.prototype
和 Function.__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);
Object
和 Function
的雞和蛋的問題
導致 Function instanceof Object
和 Object 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.prototype
,CustomFunction.__proto__ == Function.prototype
Function.prototype
繼承Object.prototype
Function.prototype.prototype
是null
所以 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 關係是
- 先有
Object.prototype
(原型鏈頂端) Function.prototype
繼承Object.prototype
而產生- 最後,
Function
和Object
和其它建構函數繼承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.prototype
是Map
格式的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.prototype
和Function
沒有反向關係(正向Function
繼承Function.prototype
)
所以 Function.prototype
和 Function.__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__
則出現在所有物件。
參考資料
- Prototypal inheritance
- 重新認識 JavaScript: Day 24 物件與原型鏈 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- Object.setPrototypeOf() - JavaScript | MDN
- [第十七週] JavaScript 進階:為什麼要有原形鍊 Prototype Chain? | Yakim shu
- 从__proto__和prototype来深入理解JS对象和原型链 · Issue #9 · creeperyang/blog · GitHub
- Javascript Object Hierarchy
- 你懂 JavaScript 嗎?#19 原型(Prototype) | Summer。桑莫。夏天
- 15. [JS] 什麼是原型鏈? - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- JavaScript Prototype
- 原型基礎物件導向 · 從ES6開始的JavaScript學習生活
- proto VS. prototype in JavaScript - Stack Overflow