前言
原型鏈與繼承、作用域與閉包、單線程與異步並稱為前端的三座大山,均屬於 JavaScript 中基礎卻又十分複雜的部分,而且面試中也經常問到。
今天,我們就來詳細介紹一下原型鏈與繼承,聊聊它的概念、作用與用法。
如果掘友對此部分已經學過只是略微遺忘,可直接跳轉至原型鏈圖片看圖複習。
下面,讓我們循序漸進的介紹下原型鏈與繼承。
認識原型
在我們創建函數的同時,都會自動為其創建一個 prototype 屬性,指向函數的原型對象。所有的原型對象也會自動獲得一個名為 constructor 的屬性,指回與之關聯的構造函數。
我們所説的原型,一般指的都是 __proto__ 或 prototype 屬性,也有人將 prototype 屬性稱為顯示原型,__proto__ 稱為隱式原型,這兩個之間有什麼區別與關聯?
prototype 是我們在創建函數的同時,自動為該函數添加的屬性,會指向該函數的原型對象。
__proto__ 是所有對象都有的一個私有屬性,指向它的構造函數的原型對象
看這是不是一頭霧水,又多出了兩個新的概念:構造函數、原型對象又是什麼?
構造函數:構造函數也是函數,與普通函數的唯一區別就是調用方式不同。任何函數只要使用 new 操作符調用就是構造函數,而不適用 new 操作符調用的函數就是普通函數。一般構造函數的首字母會大寫,比如 Object() Array() Function() 等。
原型對象:原型對象是伴隨着構造函數一起被創建的,與普通的對象並無區別,但原型對象在創建時會自動獲得一個 constructor 屬性,指回與之關聯的構造函數。
每次使用構造函數創建一個實例對象,實例內部的 [[Prototype]] 指針就會被賦值為構造函數的原型對象。目前主流瀏覽器都在每個對象上暴露了 __proto__ 屬性,用以訪問該實例的原型。
看看代碼或許能更好的理解:
const obj = new Object()
console.log(obj['__proto__'] === Object.prototype) // true
console.log(Object.prototype.constructor === Object) // true
備註:通過Object.create(null)創建的對象屬於特例,其不具有__proto__屬性;es6 新增的箭頭函數,其不具有prototype屬性,也不能作為構造函數;Symbol、BigInt雖然不能通過new調用,但其具有prototype屬性。
認識繼承
你是否會疑問?當我們創建一個對象的時候,明明身上沒有任何屬性,但仍可以調用許多方法,如 toString hasOwnProperty……
const obj = {}
console.log(obj.toString()) // '[object Object]'
console.log(obj.hasOwnProperty('a')) // false
其實,所有對象都是 Object 的實例,obj = {} 與 obj = new Object() 完全等價。
我們在控制枱展開對象,發現其 [[Prototype]] 接口指向的就是 Object ,在其中也找到了上述的方法,為了方便區分,下稱普通對象為實例。
測試一下,實例身上的 toString 方法,指向的也就是其原型上的方法,在其原型上添加的屬性,也可以通過實例直接訪問。
const obj = {}
console.log(obj.toString === obj['__proto__'].toString) // true
obj['__proto__'].a = 1
console.log(obj.a) // 1
實例可以訪問其原型對象上的屬性與方法,這便是繼承。
但是,並不能通過實例修改其原型的屬性和方法,操作實例的屬性只會創建或修改實例身上的屬性。
如果實例與其原型有相同的屬性,那麼原型對象上的同名屬性將被隱藏。
const obj = {}
obj['__proto__'].a = 1
obj.a = 2
console.log(obj['__proto__'].a) // 1
console.log(obj.a) // 2
原型鏈
我們知道了每個實例都有一個原型對象,可以通過實例的 __proto__ 屬性訪問原型對象。
但原型對象也是對象,屬於上一層構造函數的實例,也會有 __proto__ 屬性,指向上一層的原型。
當我們訪問實例屬性的時候,如果實例上沒有,就會去訪問它的 __proto__,如果原型對象也沒有,就會訪問原型的原型,一層層向上訪問。我們把這一條由 __proto__ 屬性組成的原型路徑,稱作是原型鏈。
但是沿着原型一直向上是無窮無盡的。不禁會問:原型鏈的終點是什麼?
想解答這個問題,就要從實際出發,設計原型對象是為了方便我們的使用,繼承一些屬性和方法。
Object 是所有對象的基類,我們平時也根本不會直接去訪問或使用 Object 的原型,所以再為其設計上一級的原型對象毫無意義,所以 Object.prototype 的原型是 null,這便是原型鏈的終點。
控制枱打印 Object.prototype 也可以看出,其 __proto__ 屬性值為 null
函數也是對象
函數也是特殊的對象,所有函數都是 Function() 的實例。
你可能在平時根本沒有見過 Function()
但其實,以下三種創建函數的方式,除了第一種存在變量提升外,是完全等價的,我們只是習慣使用簡寫的方式。
function fun1(a, b) {
console.log(a, b)
}
fun1(1, 2) // 1 2
const fun2 = function (a, b) {
console.log(a, b)
}
fun2(1, 2) // 1 2
const fun3 = new Function('a', 'b', 'console.log(a,b)')
fun3(1, 2) // 1 2
函數也有 __proto__ 屬性,指向的是 Function 的 prototype,Function 的 prototype 也是對象,屬於 Object 的實例。
console.log(fun1['__proto__'] === Function.prototype) // true
console.log(Function.prototype['__proto__'] === Object.prototype) // true
不過除非特別指明,一般所説的函數的原型對象,指的是其 prototype 屬性。
使用原型
學會了原型,那麼要如何使用呢?
一般會有以下幾種用法:
檢查類型
instanceof 操作符就是根據原型來運作的,檢查某個對象是否為函數的實例,可以用來區分對象與數組。
const arr = []
const obj = {}
console.log(typeof arr) // 'object'
console.log(typeof obj) // 'object'
console.log(arr instanceof Array) // true
console.log(obj instanceof Array) // false
console.log(arr['__proto__'] === Array.prototype) // true
擴展原型
可以通過原型擴展實例的方法,比如我們覺得獲取一個數字的絕對值,每次都調用 Math.abs() 太麻煩了,可以直接擴展 Number 的原型。
Number.prototype.abs = function () {
return Math.abs(this.valueOf())
}
let num = -1
console.log(num.abs()) // 1
以及 Vue2 中的全局事件總線,也是擴展了 Vue 的原型。
// 創建全局事件總線
Vue.prototype.$bus = this
// 註冊事件
this.$bus.$on('eventName',(data)=>{})
// 觸發事件
this.$bus.$emit('eventName','data')
原型模式
可以通過構造函數來批量創建實例,並使它們共享屬性與方法。
function Person(name, age) {
if (name != undefined) this.name = name
if (age != undefined) this.age = age
}
Person.prototype.age = 18
Person.prototype.say = function () {
console.log(this.name, this.age)
}
const xiaoming = new Person('小明', 18)
xiaoming.say() // 小明 18
const xiaolan = new Person('小蘭', 17)
xiaolan.say() // 小蘭 17
const xiaohong = new Person('小紅')
xiaohong.say() // 小紅 18
console.log(xiaoming instanceof Person
&& xiaolan instanceof Person
&& xiaohong instanceof Person) // true
修改原型
可以修改原型以獲得方法。
const time = function () {
// 修改原型為數組,使用map方法
arguments['__proto__'] = Array.prototype
return arguments.map((item) => (String(item).length < 2 ? '0' + item : item)).join('-')
}
console.log(time(2022, 5, 20, 12, 0, 0))// 2022-05-20-12-00-00
但是一般不推薦直接修改原型的,因為修改原型容易導致邏輯的混亂,如果想獲取某個原型的方法,建議使用 Object.create() 繼承其實例。
const xiaoming = new Person('小明', 18)
xiaoming.say() // 小明 18
const xiaoming2 = Object.create(xiaoming)
xiaoming2.name = '小名'
xiaoming2.age = '19'
xiaoming2.say() // 小名 19
console.log(xiaoming2['__proto__'] === xiaoming) // true
對於上一個例子,推薦使用 Array.from() 轉化為數組。
const time = function () {
const arr = Array.from(arguments)
arr['__proto__'] = Array.prototype
return arr.map((item) => (String(item).length < 2 ? '0' + item : item)).join('-')
}
原型鏈圖片
用一張圖展示原型鏈的關係,希望看完本文,你也能輕鬆畫出下圖:
總結
各個原型之間的關係或許有點繞,但只要你理解了,其實並不難。
總結一下:
- 所有對象都有
__proto__屬性,指向它的原型對象,可以通過對象訪問其原型的屬性。 - 沿着
__proto__的路徑,就是原型鏈。 - 原型鏈的終點是
null(Object.prototype['__proto__']) - 所有函數在創建時都會自動創建它的原型對象,分配給函數的
prototype屬性。 - 所有函數也都是
Function的實例,它們的__proto__屬性指向Function的原型對象。 - 可以通過擴展函數的原型為其實例添加屬性與方法。
- 通過
Object.create(null)創建的對象不具有任何屬性;es6 新增的箭頭函數不具有prototype屬性,不能作為構造函數,但仍是Function的實例。
結語
如果文中有錯誤或不嚴謹的地方,請務必給予指正,十分感謝。
如果喜歡或者有所啓發,歡迎點贊關注,鼓勵一下新人作者。