JavaScript | 對象和繼承

1、 建立對象的幾種方式數組

  1. 工廠模式

工廠模式就是:定義一個「工廠函數」,每次調用這個函數就會獲得一個對象。工廠模式建立的對象,是一個函數結果而不能肯定類型。app

function createPerson(name,age){
    var o = new Object()
    o.name=name
    o.age=age
    return o
}

var p = createPerson('hhh',12)
//這時候p就至關於
{
    name:'hhh',
    age:12
}
//可是P是 沒有一個類型可言的。若是用typeof,只能是Object類型、

2.構造函數模式函數

構造函數構造函數,顧名思義,若是你還記得原型鏈的話,應該知道,每一個對象實例都有一個__proto__屬性,指向的是它們的原型。而原型裏有一個屬性是 constructor ,這個屬性其實就是這個構造函數。也就是說:測試

經過構造函數生成對象,其實就是先寫一個構造函數,這個構造函數的裏面的this,指的實際上是這個函數的prototype,即原型。構造函數定義好,那經過new這個構造函數,就能夠建立這個構造函數的原型的孩子,即對象實例。this

//定義構造函數
function Person(namem,age){
    this.name= name;
    this.age= age;
    //想一下這裏面的this指向的是誰?
    //原型鏈:每一個函數都有一個prototype屬性,每一個對象都有一個__proto__屬性
    //指向的 即是 `原型`
    //因此,這裏的this,其實指向的就是Person這個函數的原型:Person.prototype
    this.sayHello=function(){
        console.log("hello")
    }
}
//生成對象實例
var p = new Person('hhh',12)

//這裏,p.__proto__=== Person.prototype
//那你想一下,實例對象p的原型的構造函數(constructor)又是誰呢?
//就是Person()

這時候實例對象p是有類型可言的。它就是Person類型。prototype

這裏就出現了一個新規則:凡是經過new來調用的函數,會被看成構造函數。不然就是普通函數。指針

構造函數的缺點就是:每次生成 一個對象實例,至關於調用一次構造函數,每次調用構造函數,裏面的方法都會被從新建立一遍,形成資源浪費。(怎麼證實每一個建立的對象實例裏面的方法都是不等價的?)code

//1.首先,this.sayHello=function(){}這句話實際上是:
var tmp = new Function()
this.sayHello= tmp
//因此,每次調用構造函數,都會建立一個函數實例(即Function類型的對象實例)

//2.其次,經過測試
p1 = new Person('1',12)
p2 = new Person('2',21)
console.log(p1.sayHello===p2.sayHello)//結果是false

這個問題能夠經過把函數定義到外面來解決。在構造函數外面只聲明一次函數。對象

可是新問題又來了,這個聲明的函數就和對象、類型沒什麼關係了。繼承

3.原型模式

由於每一個建立的函數都有一個prototype屬性。這個屬性是一個指針,指向的是一個對象。也就是父親。

這個父親所擁有的全部的屬性和方法,均可以被孩子繼承。那麼,給這個父親添加屬性和方法,其實也在給孩子添加屬性和方法。

function Person(){
    //這是構造孩子的構造函數
}
Person.prototype.name = 'tmpname'
Person.prototype.age = 0
Person.protytype.sayHello = function(){
    console.log('hello')
}

var p1 = new Person()
var p2 = new Person()

//看這兩個對象實例,就是經過構造函數建立的孩子,他們其實都有name和age屬性
//p1和p2均可以訪問name 和 age,可是都是原型中的屬性值
//若是咱們給屬性值從新賦值,其實不是改變值,而是覆蓋掉孩子繼承的這個屬性

p1.name='ware'

//這句話的意思是,給p1一個name屬性,值爲ware,然而這個屬性因爲和原型中的屬性同名
//則會覆蓋掉原型中的這個屬性,而不是修改掉原型中的這個屬性值
//若是咱們想從新訪問原型中的屬性值,只須要把這個屬性delete掉就能夠了

delete p1.name

hasOwnProperty() 方法,能夠檢測對象的屬性是本身的仍是繼承自原型的

3.1 in操做符

in 操做符 在 經過對象可以訪問到給定屬性時 返回true

console.log('name' in p1) //true

同時使用hasOwnProperty()和In操做符可以肯定屬性是存在於對象中仍是原型中:

function whereProperty(obj,pro){
    console.log(!obj.hasOwnProperty(pro)&&(pro in obj)?'在原型裏':'在對象裏')
}

in 操做符能夠和for聯合使用,用來遍歷對象全部能訪問到的(包括原型中的) 可枚舉(enumerated)屬性。

經過keys()方法,也能夠達到相似效果。這個方法返回一個數組。

3.2 原型模式的簡寫

前面的例子能夠簡寫:

function Person(){
    
}

Person.prototype = {
    name:'tmpName',
    age:0,
    sayHello:function(){
        console.log('hello')
    }
}

可是這樣的寫法,至關於給Person的原型賦值了,而原來的寫法只是給Person的原型添加屬性。這是兩種概念。

默認的,咱們建立一個函數,同時會建立它的prototype對象。而這個函數自己,就是原型對象的construtor。

可是這樣的簡寫方式,至關於覆蓋掉了默認的prototype對象。因此,既然覆蓋掉了,而咱們重寫的時候,這個原型對象就沒有construtor屬性,那就會從Object類裏面繼承,由於

{
    name:'tmpName',
    age:0,
    sayHello:function(){
        console.log('hello')
    }

自己是一個Object類型的對象。

若是咱們但願之後經過這個構造函數建立的對象實例,能夠訪問construtor,而且指向的是Person,那咱們就應該在從新給原型賦值的時候,帶上constructor屬性。

Person.prototype = {
    constructor:Person,
    name:'tmpName',
    age:0,
    sayHello:function(){
        console.log('hello')
    }
}

不過直接寫明,會讓constructor屬性變爲可枚舉的。若是想要原來不可枚舉的效果,用Object.defineProperty() 這個方法。

Object.defineProperty(Person.prototype,'constructor',{
    enumerable:false,
    value:Person
})

對原型的操做(好比添加屬性、方法)是動態的,無論孩子是何時建立的,只要父親變了,孩子就會跟着變。

原型模式的缺點就是:全部的孩子在建立時,會有統一的屬性及屬性值。也就是說,沒有定製性了。

  1. 混合模式

所謂混合就是:構造函數定義和原型自定義兩種模式的混合。

構造函數定義,定義的是什麼?是當前構造函數可生成的實例的屬性和方法。

原型定義,定義的是什麼? 是原型的屬性和方法,共享於每一個實例。

構造+原型、動態原型、寄生構造、穩妥構造 四種方式。寄生構造模式只須要了解,用處不大。穩妥構造方式其實就是封裝對象屬性。

2、繼承

若是原型鏈沒有任何問題的話,繼承其實就是:全部的實例繼承其原型,或其原型鏈上面的全部父原型。

可是,不湊巧,原型鏈有個問題。

原型中定義的屬性,會被全部實例共享,除非實例對象裏覆蓋掉這個屬性。——這是對於基本數據類型而言。

原型中定義的「引用類型值」的屬性,會被全部實例共享。

那什麼是「引用類型值」 呢?

ECMAScript 變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型值指的是簡單的數據段,而引用類型值指那些可能由多個值構成的對象。引用類型的值是保存在內存中的對象。與其餘語言不一樣,JavaScript 不容許直接訪問內存中的位置,也就是說不能直接操做對象的內存空間。在操做對象時,其實是在操做對象的引用而不是實際的對象。爲此,引用類型的值是按引用訪問的。

也就是說,咱們若是在原型裏定義一個屬性——數組類型的。那孩子繼承的這個屬性實際上是這個屬性的引用。更改孩子中這個數組,意味着更改引用。

2.1 借用構造函數

借用構造函數,其實就是在 孩子的構造函數中,調用父親(原型)的構造函數。這樣,就把父親構造函數中定義的全部代碼都在子類的構造函數中執行了一遍。

function Father(){
    this.color=[1,2,3]
}

function Child(){
    Father.call(this)
}

//這時候用Child new一個對象實例,那對象實例就擁有了color這個屬性,並且是獨自擁有color的拷貝。

call 和 apply 講解 改變當前做用域中this對象。

這種繼承方式有構造函數模式的問題:方法都定義在構造函數裏,不可複用且資源浪費。

2.2 組合繼承

組合繼承 其實就是應用個 混合模式中的原型+構造函數模式。

function Father(name){
    this.name = name
    this.color = ['red','yellow','blue']
}

function Child(name,age){
    Father.call(this,name)
    this.age=age
}

Father.prototype.sayHello=function(){
    console.log(this.name)
}

//1.建立父構造函數  
//2.建立子構造函數並繼承父類中的name屬性
//3.給父類型的原型添加一個方法sayHello

Child.prototype = new Father()//給子類型添加一個原型,這個原型就是父類型的實例
Child.prototype.constructor = Child//肯定經過子類型生成的實例對象 是Child類型

//到這裏,全部經過new Child()建立的對象實例,都擁有了sayHello方法,各自擁有color/name/age屬性

2.3 原型式繼承/寄生式繼承

頗有意思的一個想法,道格拉斯·克羅克福德在2006年提出來的。咱們不急去了解它,先整理一下思路:按前面那些方式,到底建立一個繼承於父類的對象實例的本質是什麼?

本質很簡單:按照父親建立出孩子。不只要保證每一個孩子有本身的個性,還要保證每一個孩子同樣的地方不須要重複創造,並且單個孩子的某個動做,不會影響到父親以致於波及到其餘孩子。

逐條分析:

  1. 保證每一個孩子有本身的個性:孩子的構造函數就是幹這個事的。每一個孩子有本身的獨有屬性,那這些獨有屬性就在構造函數裏寫。其餘的都在父親(原型)裏繼承。
  2. 保證孩子同樣的地方不須要重複創造:每一個孩子都會說話、吃飯、睡覺,這些沒必要要在孩子的構造函數裏寫,只須要在父親(原型)裏寫就能夠了。
  3. 不會影響到父親波及其餘孩子:引用類型值的屬性。這些屬性若是是繼承的,那一個孩子更改了這個屬性,這個父親的全部孩子都會改變了。由於全部的孩子裏的這個屬性,都是引用,而不是值。

因此前面纔會有這些繼承方式,這些建立對象的方式。

道格拉斯這位兄弟有一天突發奇想,這世界上某個對象了,那經過現有的這個對象,是否是能夠直接建立新對象?

function child(FatherIns){
    function F(){}
    F.prototype = FaherIns
    return new F()
}

//本質是建立一個把FahterIns看成原型的  構造函數
//而後經過這個構造函數建立一個孩子

其實這種繼承方式的本質是:對象的深拷貝。而並不是嚴格的繼承。因此,這種繼承方式的前提是:1.有現成繼承的對象,2.不須要考慮類型 3.現有對象中若是存在引用類型值屬性,將會被全部孩子繼承。

因而,ES5爲此給Object增添了一個新方法:Object.create()用來建立新對象,接收兩個參數:1.用做新對象原型的對象,2.一個爲新對象定義額外屬性的對象。

而後,道哥又想,能不能給生成的對象添加方法呢?

而後就:

function child(fatherObj){
    var tmp = Object.create(fatherObj,{
        childPro:{
            value:'xxxxx'
        }
    })
    tmp.childMethod = function(){
        ...
    }
    return tmp;
}

這特麼就是寄生式繼承。

2.4 寄生組合式繼承

你覺得道哥思想的影響真的就這麼簡單麼?然而並非。回看一下組合式繼承。

組合式繼承的思路:

  1. 建立子類型的構造函數。

  2. 在構造函數中,調用父類的構造函數。

  3. 定義完構造函數以後,外面還要給子類型指定原型:Child.prototype = new Father()
  4. 咱們都知道指定原型形成的弊端就是失去constructor。因此再指定一下constructor. Child.prototype.constructor = Child
  5. 這時候繼承定義完成。

這時候咱們發現,Father()這個構造函數調用了兩次啊。並且,Child的prototype咱們實際上是不關心它的類型的。而且,Child.prototype可不能夠從一個現有的對象建立呢?徹底能夠啊。那這個現有的對象就是Father.prototype啊。

因此咱們就能夠把三、4步寫成:

var prototype = Object.create(Father.prototype)
Child.prototype = prototype
prototype.constructor = Child

看,這裏並無給Child一個經過Father()新建的實例,而是經過Father.prototype拷貝的實例。由於這個實例的類型並非咱們關心的。

相關文章
相關標籤/搜索