說起 JavaScript 的原型鏈,有一張上古神圖

(上圖來自 JavaScript Object Layout)
從圖中的關係說起
函式兩組、物件三組再加上衍生的兩組,圖中共 7 組關係
先看函式:
/**
* 函数的两组关系
*/
function Cat() {}
// 第零组关系
// 实例的constructor 是 构造函数
new Cat().constructor === Cat
// 构造函数的constructor 是 创建构造函数的东西
Cat.constructor === Function
// 第一组关系:实例的__proto__ 指向 构造函数的prototype
// 这组很容易记住,想想原型链继承的关键操作
new Cat().__proto__ === Cat.prototype
// 第二组关系:构造函数的prototype的constructor 还是自身
Cat.prototype.constructor === Cat
接著看物件:
/**
* 对象的三组关系
*/
// 第一组关系仍然成立
new Object().__proto__ === Object.prototype
// 第二组关系仍然成立
Object.prototype.constructor === Object
// 第三组关系:Object的prototype的__proto__ 是 null
Object.prototype.__proto__ === null
最後是衍生的:
/**
* 衍生的两组关系
*/
// 第四组关系:构造函数是new Function得到的
// 所以 构造函数的__proto__ 指向 Function的prototype
Cat.__proto__ === Function.prototype
// 同上
Object.__proto__ === Function.prototype
// 第五组关系:本来也应该同上,太变态了单独拿出来
// Function也是new Function得到的!!
// Function.constructor === Function 也就是说Function的构造函数是它自己,所以
// Function的__proto__ 指向 Function的prototype
Function.__proto__ === Function.prototype
// 第六组关系:Function的prototype的__proto__ 是 Object的prototype
Function.prototype.__proto__ === Object.prototype
怎麼理解 prototype、proto、constructor 的三角關係?
這麼理解:
- 建構函式是建立實例的機器
- 實例在建立過程中能獲得什麼屬性,取決於機器藏了哪些屬性
- 這坨屬性藏在建構函式的
prototype屬性上(稱之為原型物件) - 在
new實例的時候,給實例掛上__proto__讓實例能順藤摸瓜找到這坨屬性
特殊的:
- 建構函式與其
prototype有雙向關係,反向指回來的屬性叫constructor
怎麼區分 prototype 和 proto?
本質上,關鍵區別只有一點:給誰用
prototype是給實例用的原型物件,只有建構函式有prototype__proto__是指向自己原型物件的屬性,所有物件都有__proto__x.prototype與x.__proto__的區別是 前者是給實例用的(不new x牠就沒啥用),後者是自己要用的
二者都是跟原型打交道,那名字怎麼區分呢?
prototype是藏了一坨給(子類別)實例用的屬性,稱之為原型物件__proto__串起來了原型鏈,姑且稱之為原型
instanceof 是怎麼判別實例與類別(建構函式)的關係的?
MDN 有句話特別準確地說清楚了 instanceof:
The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object.
(摘自 instanceof)
不翻譯了,改一個字都顯得多餘。按這句話,我們就能自己實作一個 instanceof 了:
function insOf(obj, Ctor) {
let proto = obj;
// while (proto = obj.__proro__) {
while (proto = Object.getPrototypeOf(proto)) {
if (Ctor.prototype === proto) {
return true;
}
}
return false;
}
// Case 1
class A {}
insOf(new A(), A)
// Case 2
function Cat() {}
insOf(new Cat(), Cat)
insOf(new Cat(), Object)
稍微深究一下 constructor 和繼承
第零組關係中,我們知道實例的 constructor 是 建構函式:
// 实例的constructor 是 构造函数
new Cat().constructor === Cat
那實例的 constructor 屬性是從哪來的?
沒錯,是在 new Cat() 建立實例的時候,從建構函式的原型物件上抄過來的:
new Cat().constructor === Cat.prototype.constructor
所以,在經典的 ES5 繼承裡:
function extend(Sub, Super) {
// 1.把Super.prototype包进一个匿名对象的原型,返回这个匿名对象
var proto = Object.create(Super.prototype);
// 2.修正constructor属性
proto.constructor = Sub;
// 3.让子类实例获得匿名对象原型属性的访问权
Sub.prototype = proto;
}
改不改 constructor 指向,並不影響 instanceof 運算子的判斷結果,改過來只是為了讓 constructor 屬性值變正確:
// 如果不改constructor,子类实例的constructor仍然是A,而不是B
function Sub() {}
function Super() {}
var proto = Object.create(Super.prototype);
Sub.prototype = proto;
// 这就会显得很奇怪(实例的constructor不是构造函数了)
new Sub().constructor !== Sub;
// 所以,改过来
proto.constructor = Sub;
// 正常了
new Sub().constructor === Sub;
一些冷知識
1. 箭頭函式沒有原型物件(所以箭頭函式不能用做建構函式)
(() => 1).prototype === undefined
2. 原生物件的原型就不要深究了,不太確定
Math.__proto__ === Object.prototype
Math.max.__proto__ === Function.prototype
Window.__proto__ === EventTarget
console.__proto__ === ?
3. 比較函式的 prototype 有什麼作用?
npm 下載量很高的 is 模組中,有一行上古代碼:
if (type === '[object Function]') {
return value.prototype === other.prototype;
}
你沒有看錯,他比較了兩個函式的 prototype,以此為據判函式的相等性,隨便一想也知道不太靠譜,比如:
(x => x).prototype === (x => x + 1).prototype === undefined
所以呢我就 問函式庫作者了,幾個回合下來,發現比較 prototype 能用來判建構函式/Class 是否相同(但注意 prototype 是可篡改的):
class Dog {}
class Cat {}
function Button() {}
function Panel() {}
Dog.prototype !== Cat.prototype
Button.prototype !== Panel.prototype
如果函式不是建構函式或 Class,比較 prototype 就毫無意義,函式庫裡比較函式的 prototype 在 OOP 不怎麼盛行的 JS 裡作用十分有限
暫無評論,快來發表你的看法吧