原型和原型鏈在JavaScript的一個核心內容,它用於對象之間的屬性繼承,在面試的過程當中也會常常會問到這部分的知識,若是接觸過像Java這類的語言,並且只是對這個概念只知其一;不知其二的話,估計只能全靠猜,因此掌握原型和原型鏈是進階前端的一個重要關鍵點。這裏小編將從函數對象
、構造函數
、實例
、new
、prototype
、__proto__
、contructor
、class
這八個知識點來探索JavaScript的 原型 和 原型鏈。javascript
函數對象:使用
function
關鍵字或使用Function
構造函數建立的對象即爲函數對象。前端
「萬物皆對象」,在JavaScript中,函數是一個特殊的對象,它能夠像普通對象那樣子設置以及訪問自身的屬性,例如:java
// 普通對象
var stutent = {}
stutent.age = 18
console.log(stutent.age) // 18
// 函數對象
function teacher () {}
teacher.age = 50
console.log(teacher.age) // 50
複製代碼
構造函數:便是函數的自己,是函數的一個用法,能夠經過
new
關鍵字來建立對象。面試
實例:經過
new
和構造函數建立的對象就是實例。經過__proto__
指向原型prototype
,經過constructor
指向構造函數。數組
function Student (name, age, school) {
this.name = name
this.age = age
this.school = school
}
var student1 = new Student('啊俊俊', 23, '華軟')
複製代碼
在上述例子中,Student
方法便是 構造函數,使用 new
關鍵字加 Student
構造函數建立 student1
對象,student1
就是一個 實例。函數
prototype
& __proto__
原型和原型鏈的概念中,prototype
就是原型,能夠把它理解爲製做月餅的模子。prototype
是函數特有的屬性,普通的對象是沒有 prototype
的,看下面的例子:ui
var a = {}
var b = function () {}
function c () {}
console.log(a.prototype) // undefined
console.log(b.prototype) // { constructor: ƒ }
console.log(c.prototype) // { constructor: ƒ }
複製代碼
prototype
是用來幹嗎的?經過上文咱們瞭解到什麼是實例,可是每次經過構造函數建立的實例都是不同的,若是想讓多個實例之間具備共享屬性的話,僅靠構造函數是不夠的。this
在 ECMAScript 設計的時候,並無像Java那樣子設計成類的概念,而是經過構造函數的 prototype
來實現對象之間的共享屬性,看下面的例子:spa
function Student () {}
Student.prototype.school = '華軟'
var student1 = new Student()
var student2 = new Student()
console.log(student1.school) // 華軟
console.log(student2.school) // 華軟
複製代碼
在上面的代碼中能夠看到,定義了一個 Student
的構造函數,是一個空函數同時設置了該構造函數的原型 prototype
屬性 school
,經過該構造函數創造的兩個實例中,都繼承了原型中的 school
屬性。prototype
__proto__
又是什麼?在上文中咱們瞭解到如何讓多個實例之間具備共享屬性,但它共享的原理又是什麼呢?
原理就是經過構造函數建立出來的實例中,該實例的內部具備一個 __proto__
指針來指向構造函數的原型 prototype
。
咱們都知道,在JavaScript中,對象是在堆內存中保存的,像 var o = { name: 'a' }
中,變量 o
是一個指針並指向了 { name: 'a' }
的內存地址,判斷兩個對象變量是否相等其實是判斷這兩個變量指針是否指向同一個內存空間。
而在實例和原型之間的關係則是實例的 __proto__
指向了原型 prototype
,即 __proto__
和 prototype
指向了同一個內存空間,看下面的例子就能夠看出它們兩的關係:
function Student () {}
Student.prototype.school = '華軟'
var student1 = new Student()
console.log(student1.__proto__) // { school: "華軟", constructor: ƒ }
console.log(Student.prototype) // { school: "華軟", constructor: ƒ }
console.log(student1.__proto__ === Student.prototype) // true
複製代碼
那麼 new
其實是作了什麼呢,能夠用下面的代碼來理解 __proto__
的賦值過程:
function Student (name) {
this.name = name
}
Student.prototype.school = '華軟'
// var student1 = new Student('小明')
var student1 = {}
student1.__proto__ = Student.prototype
Student.call(student1, '小明')
複製代碼
首先用一張關係圖來表示 __proto__
、prototype
和對象存儲關係:
在上圖中能夠清晰的瞭解到二者之間的關係,雖然能夠經過實例訪問原型中的屬性,但不能經過實例直接修改或重寫原型的屬性,看下面的例子:
function Teacher () {}
Teacher.prototype.system = '軟件系'
var teacher1 = new Teacher()
var teacher2 = new Teacher()
console.log(teacher1.system) // 軟件系
console.log(teacher2.system) // 軟件系
teacher1.system = '外語系'
console.log(teacher1.system) // 外語系
console.log(teacher2.system) // 軟件系
複製代碼
在上面的例子中,經過構造函數 Teacher
建立的 teacher1
和 teacher2
兩個實例,在建立後訪問內部屬性 system
,JavaScript在執行時會先搜索實例中是否存在該屬性,若是有則馬上獲取並終止搜索,若是沒有則往實例的原型中繼續搜索。
因此上面代碼中前兩個 console
中打印的都是來自 Teacher
原型中的 system
,然後面執行了 teacher1.system = '外語系'
,此時 teacher1
實例修改的並非原型中的屬性,而是自身的system屬性,因此後面兩個打印的 system
分別來自實例自身和原型。若是想同時修改兩個實例的共享屬性的話就應該從原型上修改,以下:
function Teacher () {}
Teacher.prototype.system = '軟件系'
var teacher1 = new Teacher()
var teacher2 = new Teacher()
console.log(teacher1.system) // 軟件系
console.log(teacher2.system) // 軟件系
Teacher.prototype.system = '外語系'
console.log(teacher1.system) // 外語系
console.log(teacher2.system) // 外語系
複製代碼
什麼瞭解到修改原型修改多個實例中的共享屬性,但因爲對象是使用堆內存進行存儲的,變量指針指向對象所屬的內存空間,因此下面的這種狀況是不會修改實例的共享屬性:
function Teacher () {}
Teacher.prototype.system = '軟件系'
Teacher.prototype.saySystem = function () { console.log(this.system) }
var teacher1 = new Teacher()
teacher1.saySystem() // 軟件系
Teacher.prototype = {
system: '外語系',
studentNumber: 50,
saySystem: function () { console.log(this.system) },
sayStudentNumber: function () { console.log(this.studentNumber) }
}
teacher1.saySystem() // 軟件系
teacher1.sayStudentNumber() // TypeError: teacher1.sayStudentNumber is not a function
複製代碼
在上面的代碼中,重寫了 Teacher
構造函數的 prototype
原型,其實是讓 prorotype
指向了新的內存空間,但建立出來的實例的 __proto__
並不會一塊兒指向該內存空間,這並不僅是在原型裏是這樣子的機制,在普通對象中也是同樣。
話很少說,先看兩個例子:
// 例子一
function GirlFriend (name) {
this.name = name
}
GirlFriend.prototype = {
features: ['美', '長頭髮', '皮膚白'],
addFeatures: function (feature) {
this.features.push(feature)
}
}
var girlfriend1 = new GirlFriend('小花')
var girlfriend2 = new GirlFriend('小白')
girlfriend1.addFeatures('腿長')
console.log(girlfriend1.features === girlfriend2.features) // true
console.log(girlfriend1.features) // ["美", "長頭髮", "皮膚白", "腿長"]
console.log(girlfriend2.features) // ["美", "長頭髮", "皮膚白", "腿長"]
複製代碼
// 例子二
function GirlFriend (name) {
this.name = name
this.features = ['美', '長頭髮', '皮膚白']
}
GirlFriend.prototype = {
addFeatures: function (feature) {
this.features.push(feature)
}
}
var girlfriend1 = new GirlFriend('小花')
var girlfriend2 = new GirlFriend('小白')
girlfriend1.addFeatures('腿長')
console.log(girlfriend1.features === girlfriend2.features) // false
console.log(girlfriend1.features) // ["美", "長頭髮", "皮膚白", "腿長"]
console.log(girlfriend2.features) // ["美", "長頭髮", "皮膚白"]
複製代碼
上面兩個例子中,不一樣的地方就是 features
的位置,例子一是在原型上定義的,例子二是在構造函數中定義的,定義的位置不一樣,打印的結果就徹底不一樣。
例子一中,在構造函數的原型定義中就開闢了內存空間存儲了數組並用 features
指向該內存,new
的時候只是將兩個實例的 features
指向了那個內存,因此調用 girlfriend1
的addFeatures會將 girlfriend2
的也一塊兒修改。
例子二中,內存空間是在調用時才建立的,並讓 this.features
指向該內存,兩次調用就會建立兩次不一樣的內存,因此調用 girlfriend1
的addFeatures不會修改 girlfriend2
的。
constructor
在上述中,咱們瞭解完了 prototype
和 __proto__
,還有一個關鍵點就是 constructor
了,constructor
的概念比較簡單,它就是原型中的 constructor
指向構造函數,誰創造這個實例的,那麼這個實例的 constructor
就是誰,一張圖和一段代碼瞭解它們之間的關係。
實例.__proto__ === 構造函數.prototype
prototype.constructor = 構造函數
原型 === 構造函數.prototype
// 實例有__proto__,沒有prototype
// 構造函數有prototype
// 構造函數也有__proto__(Object構造函數除外)
function a () {}
var b = new a()
console.log(b.constructor) // ƒ a () {}
console.log(b.__proto__.constructor) // ƒ a () {}
console.log(b.constructor === a) // true
複製代碼
經過本文能夠了解到了原型的基本概念,爲了避免形成閱讀疲勞(懶得繼續碼字了),關於原型和原型鏈的相關內容分開兩篇,下篇將在這周內更新。