JavaScript中的原型和原型鏈

提問

  • 原型是用來幹什麼的?
  • 爲何原型那麼重要?
  • 如何合理的使用原型?

多是以前開發和思考的層次比較淺,我的感受多數的開發工做與原型和原型鏈的關係不大,甚至不知道的狀況下也可以把任務完成。網上也有很多講解原型的文章,更有很多面經中提到這個考題,若是不是爲了炫技那必是開發中必需要掌握的一個知識點。最近也看了這方面的資料,就上面的三個問題總結一下本身的想法,其實學以至用纔是最棒的,目前開發經驗少不能體會原型的好處,只作記錄方便以後遇到問題可以知道該去查找哪方面的資料。 javascript

prototype、proto

prototype

幾乎全部函數都有一個顯示的 prototype 屬性,除了下面的這種狀況java

let fun = Function.prototype.bind()
複製代碼

prototype 屬性是一個對象,在函數聲明的時候就會自動建立,並且默認狀況下只有一個屬性 constructor 用來指向函數自己。
git

image.png

proto

每一個對象都會有的一個隱式原型屬性,指向了建立該對象的構造函數的原型。
es6

image.png

__proto__ 是在對象建立的時候添加屬性,連接到構造調用的函數的原型上的。當訪問對象上不存在的屬性的時候能夠順着這條鏈查找屬性。
new 一個對象的過程能夠解析爲一下4步:

  • 建立一個空對象 obj
  • obj 的 __proto__ 連接到函數的 prototype
  • 執行構造調用的函數,初始化屬性
  • 判斷函數返回值是否爲一個對象類型,是則返回該返回值,不然返回 obj
// new 語法的簡單模擬
var creatObject = function(constructor,...args) {
	var obj = Object.create(null)
  // 連接原型
	Object.setPrototypeOf(obj,constructor.prototype)
	res = constructor.apply(obj,args)
	return typeof res === 'object' ? res : obj
}
複製代碼

image.png

理解原型

在JavaScript中沒有類這個概念,所謂的繼承也是經過原型鏈來實現的,包括加入的 class 語法糖也是爲了更好的理解JavaScript中的「繼承」並無改變JavaScript中對象的工做機制,底層仍是原型。那麼很天然的想到原型鏈中也有相似繼承的好處。
個人理解上原型有兩個主要的做用:github

  • 用來理解對象上屬性訪問的過程
  • 用來判斷對象實例是否由某個函數建立

對象上屬性訪問的過程

在對象上查找屬性的時候,若對象自己不具有該屬性,則被查找的屬性會被委託在整個原型鏈上,只有當沒有更多的原型能夠查找的時候,才中止查找。
咱們定義一個對象 var o = {a:1,b:2} 咱們知道可使用 hasOwnProperty 來判斷某屬性是否對象自身擁有的屬性,可是在建立對象的時候並無建立 hasOwnProperty 方法,這個是如何經過 . 運算調用的呢?
app

image.png

咱們經過 o.__proto__===Object.prototype 能夠知道經過字面量的方式建立對象實例 o 的時候的函數是 Object 且如今 o 的原型鏈上有 Object.prototype 
image.png

咱們再查看一下 Object.prototype 發現這個對象中含有屬性 hasOwnProperty 
image.png

也就是說,在 o 中沒有找到 hasOwnProperty ,就順着原型鏈往上找,首先找 o.__proto__ 也就是 Object.prototype ,很巧在這裏就找到了(若是沒有找到就會一直順着這條鏈往上找),因而拿過來使用。
image.png

判斷對象實例是否由某個函數建立

constructor

前面略帶的提過這個屬性 constructor ,經過這個屬性能夠訪問建立該對象時所用的函數,經過這個咱們能夠判斷兩個實例是不是同一個函數建立而來的,我想這個屬性的做用差很少也應該是這樣了。
函數

image.png

instanceof

obj instanceof Foo 應該被理解爲檢測函數 Foo 是否出如今 obj 的原型鏈中,並非單純的理解過對象實例是否由某個函數構造調用建立。只要經過 __proto__ 一層一層的往上找可以找到這個函數的 prototype 就說明是這個函數建立的。
ui

image.png

image.png

可是當修改了原型鏈的時候可能檢查不許,如今 dog.__proto__!==Dog.prototype 並且順着 __proto__ 也找不到 Dog.prototype 因此這裏返回 false 
image.png

原型的一些注意點

遮蔽效應

當實例屬性和原型屬性中有同名屬性或者方法的時候優先使用實例屬性。下面實例屬性中和原型屬性中都有 say 函數,這裏使用實例屬性。順着原型鏈查找如今實例屬性中找到了就中止查找
this

image.png

函數原型之間的引用關係

對象與函數原型之間的引用關係是在對象建立的時候創建的,新建立的對象會根據當前的函數原型創建引用。
es5

image.png

繼承的正確實現方式

上面例子中用一個對象字面量重寫了 Dog.prototype 此時 constructor 屬性不見了,這個在實習繼承效果的時候也須要注意,咱們在修改完原型以後須要補上 constructor

// es5
function Animal() {}
Animal.prototype.say = function() {
  console.log('動物叫')
}
function Dog() {}
let dog = new Dog()
// 連接原型並加上constructor
Dog.prototype = Object.create(Animal.prototype, {
  constructor: {
    value: Dog, // 指向Dog
    enumerable: false,
    writable: true,
    configurable: true
  }
})
//es6 引入了class 和 extends 減小對原型覆蓋的反作用
class Animal{
  constructor(name) {
    this.name = name
  }
  say(){
    console.log('動物叫')
  }
}
class Dog extends Animal{
  constructor(name,type){
    super(name) // 調用父類構造函數
    this.type = type
  }
	say(){
    console.log('汪汪')
  }
}
var dog = new Dog('道格','中華田園犬')
dog.say() // 汪汪
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
複製代碼

抽離公共方法到函數原型

一些公共方法若是做爲實例方法建立在每建立一個實例的時候都會產生一個副本,不只佔內存並且修改起來麻煩。能夠在函數的原型對象上建立對象方法,這樣可使得一個方法被全部對象實例共享(能夠經過原型鏈訪問)。

// 翻轉字符串爲例
String.prototype.reversed = function(){
  return Array.from(this).reverse().join('')
}
"abcdef".reversed() // "fedcba"
複製代碼

參考資料

相關文章
相關標籤/搜索