前言
其他編程語言如 Java 等使用 new 命令時,都會調用“類”的構造函數。但是,JavaScript沒有“類”,本身並不提供一個 class 實現(雖然在ES6中提供了class 關鍵字,但其只是語法糖,JavaScript仍然是基於原型的)。於是,JavaScript作了一個簡化的思想,new 命令後面跟的不是類,而是構造函數,用構造函數生成實例對象,但其缺點是無法共享屬性和方法。於是,就為構造函數設置了一個 prototype 屬性,這個屬性包含一個對象(prototype對象)。所有實例對象需要共享的屬性和方法都放在這個對象裏,那些不需要共享的屬性和方法就放在構造函數裏。
💡温馨提示:本文全文1986個字,推薦閲讀時間為10m,加油老鐵!
一、顯式原型(prototype)
1.1 介紹
每個函數在創建之後都會有一個名為prototype 屬性:
function Parent() {
}
Parent.prototype.name = 'kite';
let child = new Parent();
console.log(child.name);
這個屬性指向函數的原型對象(通過Function.prototype.bind方法構造出來的函數是個例外,它沒有prototype屬性),即調用構造函數創建的實例的原型,也就是例子中child的原型。
何為原型:每一個JavaScript對象(null 除外)都會和另一個對象相關聯,這個對象就是原型,上面也提到每個對象都會從原型中“繼承屬性”。
原型表明了構造函數和實例原型的關係。
1.2 作用
顯式原型用來實現基於原型的繼承與屬性的共享。
二、隱式原型(__proto__)
2.1 介紹
JS中任意對象都具有一個內置屬性[[prototype]],在ES5之前沒有標準方法訪問,大多數瀏覽器通過__proto__來訪問。ES5中有了對這個內置屬性標準的get方法:Object.getPrototypeOf(Object.prototype 是個例外,它的__proto__ 為 null)。
function Parent() {
}
let child = new Parent();
console.log(child.__proto__ === Parent.prototype); // true
2.2 作用
隱式原型構成原型鏈,同樣用於實現基於原型的繼承。舉例來説:
當我們訪問對象中的某個屬性時,如果在對象中找不到,就會一直沿着__proto__(原型的原型)依次查找,直到找到最頂層為止。
function Parent() {
}
Parent.prototype.name = 'dave';
let child = new Parent();
child.name = 'kite';
console.log(child.name); // 'kite'
delete child.name;
console.log(child.name); // 'dave'
上面的例子中,給對象child添加name屬性,當訪問name屬性時,找到了對象本身的屬性值kite。刪除name屬性之後,再次訪問name屬性,在對象中找不到該屬性,再在原型中尋找,找到dave。假設屬性在原型中也沒有找到該屬性,則會再去原型的原型中查找。
我們知道原型是個對象,既然是對象就可以用最原始的方式創建:
let obj = new Object();
原型就是通過Object 構造對象生成的。那 Object.prototype 的原型又是什麼?
Object.prototype.__proto__ = null; // true
null表示沒有對象,表明在此處可以停止查找了。
2.3 指向
__proto__ 指向創建這個對象的函數的顯式原型,其關鍵在於找到創建這個對象的構造函數。創建對象有三種形式:對象字面量;new(class);ES5的Object.create()。本質只有一種new。
2.3.1 對象字面量
let obj = {
name: 'ctt'
}
對象字面量聲明的對象繼承自 Object ,和 new Object一樣,其原型為
Object.prototype。而 Object.prototype 不繼承任何屬性和方法。
2.3.2 new
使用構造函數創建的對象,它的屬性繼承自構造函數。
具體分以下幾種情況:
- 內建對象
如Array(),它繼承於Array.prototype,Array.prototype為一個對象,這個對象由Object()這個構建函數創建。因此,Array.prototype.__proto__ === Object.prototype,原型鏈為:Array.prototype -> Object.prototype -> null。 - 自定義對象
-
默認情況下:
function Foo() {}; let foo = new Foo(); Foo.prototype.__proto__ === Object.prototype;
-
其他情況:
// 想讓Foo繼承Bar function Bar() { } Foo.prototype = new Bar(); Foo.prototype.__prototype__ = Bar.prototype; // 重新定義Foo.prototype Foo.prototype = { a: 1, b: 2 }; Foo.prototype.__proto__ = Object.prototype;
以上兩種情況改寫了Foo.prototype,所以Foo.prototype.constructor跟着改變,constructor和原構造函數 Foo切斷了聯繫。
- 構造函數
構造函數就是Function()的實例,因此構造函數的隱式原型指向
Function.prototype。引擎創建了Object.prototype,而後又創建了
Function.prototype,通過__proto__將兩者聯繫起來。
Function.prototype === Function.__proto__ ,其他所有的構造函數都可以通過原型鏈找到 Function.prototype ,並且 function Function() 本質也是一個函數,為了不產生混亂,將 function Function 的 __proto__ 聯繫到了Function.prototype 上。
2.3.3 class
在ES5中,每個對象都有一個 __proto__ 屬性,指向對應構造函數的prototype 屬性,而 class 作為構造函數的語法糖,同時具有 prototype 屬性和 __proto__ 屬性,所以同時存在兩條繼承鏈。
- 子類的
__proto__表示構造函數的繼承,總是指向父類; - 子類的
prototype屬性的__proto__表示方法的繼承,總是指向父類的
prototype。
class Parent {
}
class Child extends Parent {
}
Child.__proto__ === Parent;
Child.prototype.__proto__ === Parent.prototype;
作為一個對象,子類 Child 的原型(__proto__)是父類 Parent ;作為一個構造函數,子類的原型對象(prototype )是父類原型對象(prototype )的實例。
2.3.4 Object.create()
Object.create() 是ES5的方法,可以調用這個方法來創建一個新對象。新對象的原型就是傳入的第一個參數。
三、構造函數(constructor)
上面説明構造函數和實例都可以指向原型,接下來講的這個屬性就是讓原型指向構造函數的(沒有指向實例的屬性,因為構造函數可以生成多個實例),它就是constructor屬性,每個原型都有一個constructor屬性指向構造函數。
function Parent() {
}
console.log(Parent === Parent.prototype.constructor);
關於原型的各類關係總結如圖:
四、this 和 prototype 定義方法的區別
- 利用
this實現的方法,可以訪問類中的私有變量和私有方法。而利用原型對象實現的方法,無法訪問類中的私有變量和方法。 - 實例訪問對象的屬性或者方法時,將按照搜索原型鏈
prototype chain的規則進行。首先查找自身的靜態屬性、方法,繼而查找構造上下文的可訪問屬性、方法,最後查找構造的原型鏈。 this和prototype定義的另一個不同點是在內存中佔用空間不同。使用“this”關鍵字,實例初始化時為每個實例開闢構造方法所包含的所有屬性、方法和所需空間,而使用prototype定義的,由於prototype實際上是指向父級的引用,因此在初始化和存儲上比“this”節約資源。
五、總結
- 每個函數在創建之後都會有一個名為
prototype屬性,這個屬性指向函數的原型對象(顯式原型),所有實例對象需要共享的屬性和方法都放在這個對象裏; - 任意對象都具有一個內置屬性
__proto__指向創建這個對象的函數的顯式原型; - class 作為構造函數的語法糖,同時具有
prototype屬性和__proto__屬性,作為一個對象,子類Child的原型(__proto__)是父類Parent;作為一個構造函數,子類的原型對象(prototype)是父類原型對象(prototype)的實例。 - 每個原型都有一個
constructor屬性指向構造函數,即Parent === Parent.prototype.constructor
參考
JavaScript - 原型、原型鏈 · Issue #13 · cttin/cttin.github.io · GitHub