關於構造函數、原型、繼承、原型鏈你須要知道的一切

關於構造函數、原型、繼承、原型鏈你須要知道的一切

1.構造函數與普通函數

構造函數的寫法其實和普通函數並無區別,區別只是調用的時候,構造函數能夠經過new建立對象,爲了加以區分,咱們一般約定構造函數首字母大寫:javascript

function Person(name){
  this.name=name
  this.say = function() {
    console.log(this.name)
  }
}

2.原型

構造函數都會有一個屬性prototype,能夠打印下Person.proptotype,打印出能夠發現是一個對象,這就是原型對象了。每一個原型對象上都會有一個屬性constructor,這個屬性指向的是構造函數。java

接下來經過new Person建立一個實例對象,打印這個對象,會發現他有一個__proto__屬性,這個屬性指向的是原型對象,但這個屬性不是一個標準屬性,只是實例對象和原型之間的一個橋樑,是實例對象內部找原型對象使用的,代碼中不使用,總結一下構造函數、原型、實例對象的三者關係:web

3.實例對象

接下來,咱們建立兩個實例對象(new 構造函數出來的對象就是實例對象):svg

var per1 = new Person("張三")
var per2 = new Person("李四")
per1.say()
per2.say()

new的意思其實就是建立一個空對象,再把構造函數中的屬性和方法,放到這個空對象中,而後返回這個對象,上述代碼可以正常執行,可是有個問題,函數屬於複雜數據類型,會開闢內存空間存放,上述那種寫法會致使每建立一個對象就多一個say函數的空間,這實際上是一種浪費,由於函數功能都同樣,咱們只須要一塊空間就行,因此一般方法咱們都定義在原型上:函數

Person.prototype.say = function() {
    console.log(this.name)
  }

定義在原型上,實例對象爲何可以訪問到呢?緣由在於咱們上文說過的__proto__屬性,執行的時候會先尋找per對象上有沒有say方法,發現沒有,就會去原型對象上找,而__proto__屬性就是找到原型的橋樑。this

如此一來,原型上可能有不少方法,例如:spa

Person.prototype.say1 = function() {
    console.log(this.name)
  }
Person.prototype.say2 = function() {
    console.log(this.name)
  }
Person.prototype.say = function() {
    console.log(this.name)
  }
...

每一個方法都這樣定義,可能比較繁瑣,咱們能夠簡化寫法,由於prototype自己就是對象,因此能夠寫成:prototype

Person.prototype = {
  say1: function () {},
  say2: function () {},
  say: function () {
    console.log(this.name)
  }
}

看上去好像沒問題,可是執行 per.say3() 發現並無效果。緣由在於咱們前面說過,每一個原型對象都有一個constructor屬性指向構造函數,因此做出以下修改:code

Person.prototype = {
  constructor: Person,
  say1: function () {},
  say2: function () {},
  say: function () {
    console.log(this.name)
  }
}
var per = new Person("xz")
per.say3()

如今執行上述代碼,可以正常運行。orm

4.繼承

繼承的意思,就是孩子可以使用父親的屬性和方法,同時又有本身獨特的屬性和方法。先定義一個Child類:

function Child() {}

可能有同窗會想,繼承直接讓Child.prototype=Person.prototype不就能直接使用父類的原型上的方法了嗎,是能夠用,但這有個問題,若是Child想有本身的方法,這會致使Person的原型會跟着同步改變,這顯然是不合理的。

咱們上文說過,實例對象的__proto__指向的是原型對象,那利用這點咱們可讓

Child.prototype = new Person("張三")
var child1 = new Child()
child1.say() // 張三
var child2 = new Child()
child2.say() // 張三

能夠發現經過這種方法,雖然繼承了父類的方法,可是全部name都是「張三」,這不合理,因此屬性的繼承通常使用call方法實現繼承:

function Child(name) {
  Person.call(this, name)
}
Child.prototype = new Person("張三")
var child1 = new Child("老王")
child1.say() // 老王
var child2 = new Child("小吳")
child2.say() // 小吳

js中的繼承通常是屬性使用call 方法使用父類實例對象這種組合繼承。Child也能夠有本身的屬性和方法:

function Child(name, sex) {
  Person.call(this, name)
  this.sex = sex
}
Child.prototype = new Person("張三")
Child.prototype.say = function () {
  console.log(this.sex)
}
var child = new Child("老王", "male")
child.say() //male

雖然是同名方法,但咱們能夠發現,child的say是調用的本身的say方法,由於js的機制是先在本身的對象上找,找不到纔會經過__proto__一層層往下找,建議打印child觀察一下結構。

同理,Child也能夠有本身的子類,他們之間也是經過__proto__繼承,這樣就造成了原型鏈。調用方法會沿着這個原型鏈一直找,這是咱們爲何能夠調用.toString()之類方法的緣由,由於String.prototype上定義了toString方法。

原型鏈

既然這樣,咱們能夠擴展內置對象,例如:

String.prototype.say = function () {
  console.log("hello")
}
var str = "xz"
str.say() // hello

這個擴展方法固然沒有任何意義,只是演示用,一般處理Date對象時,能夠擴展format方法。