动态

详情 返回 返回

聊聊原型鏈與繼承 - 动态 详情

前言

原型鏈與繼承、作用域與閉包、單線程與異步並稱為前端的三座大山,均屬於 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 ,在其中也找到了上述的方法,為了方便區分,下稱普通對象為實例。

image.png

測試一下,實例身上的 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

image.png

函數也是對象

函數也是特殊的對象,所有函數都是 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__ 屬性,指向的是 FunctionprototypeFunctionprototype 也是對象,屬於 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.jpg

總結

各個原型之間的關係或許有點繞,但只要你理解了,其實並不難。

總結一下:

  • 所有對象都有 __proto__ 屬性,指向它的原型對象,可以通過對象訪問其原型的屬性。
  • 沿着 __proto__ 的路徑,就是原型鏈。
  • 原型鏈的終點是 nullObject.prototype['__proto__']
  • 所有函數在創建時都會自動創建它的原型對象,分配給函數的 prototype 屬性。
  • 所有函數也都是 Function 的實例,它們的 __proto__ 屬性指向 Function 的原型對象。
  • 可以通過擴展函數的原型為其實例添加屬性與方法。
  • 通過 Object.create(null) 創建的對象不具有任何屬性;es6 新增的箭頭函數不具有 prototype 屬性,不能作為構造函數,但仍是 Function 的實例。

結語

如果文中有錯誤或不嚴謹的地方,請務必給予指正,十分感謝。

如果喜歡或者有所啓發,歡迎點贊關注,鼓勵一下新人作者。

user avatar dingtongya 头像 Leesz 头像 alibabawenyujishu 头像 yinzhixiaxue 头像 jingdongkeji 头像 qingzhan 头像 dirackeeko 头像 aqiongbei 头像 littlelyon 头像 zourongle 头像 longlong688 头像 anchen_5c17815319fb5 头像
点赞 170 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.