js的幾種繼承

面向對象

1. 面向對象的三大特性

封裝
繼承
多肽

1.1 原型鏈的知識

原型鏈是面向對象的基礎,是很是重要的部分。有如下幾種知識:

2. 建立原型的幾種方法

2.1 方式一:字面量

var obj1 = {name:"江小白"}
var obj2 = new Object(name:"江小白")
上面的兩種寫法,效果是同樣的。由於,第一種寫法,obj11會指向Object。
  • 第一種寫法是:字面量的方式。
  • 第二種寫法是:內置的構造函數

2.2 方式二:經過構造函數

var M = function(name){
    this.name = name;
}
var obj3 = new M("asd asd)

2.3 方法三:Object.create

var p = {name:'lipiao'}
var obj3 = Object.create(p);
// 此方法建立的對象是原型鏈對象
第三種方法,這種方式裏,obj3是實例,p是obj3的原型(name是p原型裏的屬性),構造函數是Objecet 。

3. 原型鏈

6uK7on.png

  1. 構造函數經過 new 生成實例
  2. 構造函數也是函數,構造函數的prototype指向原型。(全部的函數有prototype屬性,但實例沒有 prototype屬性)
  3. 原型對象中有 constructor,指向該原型的構造函數

6ul0MT.png

  1. 實例的__proto__指向原型。也就是說,Foo.__proto__ === Foo.prototype。

聲明:全部的引用類型(數組、對象、函數)都有__proto__這個屬性。
Foo.__proto__ === Function.prototype的結果爲true,說明Foo這個普通的函數,是Function構造函數的一個實例。es6

3.2 原型鏈

原型鏈的基本原理:任何一個實例,經過原型鏈,找到它上面的原型,該原型對象中的方法和屬性,能夠被全部的原型實例共享。數組

Object對象是原型鏈的頂端。

原型能夠起到繼承的做用。原型裏的方法均可以被不一樣的實例共享:app

//給Foo的原型添加 say 函數
  Foo.prototype.say = function () {
      console.log('');
  }

原型鏈的關鍵:在訪問一個實例的時候,若是實例自己沒找到此方法或屬性,就往原型上找。若是仍是找不到,繼續往上一級的原型上找。函數

3.3 instanceof的原理

6u33Nj.png
instanceof的做用:用於判斷實例屬於哪一個構造函數。
instanceof的原理:判斷實例對象的__proto__屬性,和構造函數的prototype屬性,是否爲同一個引用(是否指向同一個地址)。this

注意1:雖說,實例是由構造函數 new 出來的,可是實例的__proto__屬性引用的是構造函數的prototype。也就是說,實例的__proto__屬性與構造函數自己無關。
    注意2:在原型鏈上,原型的上面可能還會有原型,以此類推往上走,繼續找__proto__屬性。這條鏈上若是能找到, instanceof 的返回結果也是 true。

好比說:es5

foo instance of Foo的結果爲true,由於foo.__proto__ === M.prototype爲true。
foo instance of Objecet的結果也爲true,爲Foo.prototype.__proto__ === Object.prototype爲true。
但咱們不能輕易的說:foo 必定是 由Object建立的實例`。這句話是錯誤的。咱們來看下一個問題就明白了。spa

3.4 分析一個問題

問題:已知A繼承了B,B繼承了C。怎麼判斷 a 是由A直接生成的實例,仍是B直接生成的實例呢?仍是C直接生成的實例呢?

分析:這就要用到原型的constructor屬性了。prototype

foo.__proto__.constructor === M的結果爲true,可是 foo.__proto__.constructor === Object的結果爲false。
因此,用 consturctor判斷就比用 instanceof判斷,更爲嚴謹。

4. new運算符

當new一個對象是發生了什麼
  • 建立一個新的空對象實例。
  • 將此空對象的隱式原型指向其構造函數的顯示原型。
  • 執行構造函數(傳入相應的參數,若是沒有參數就不用傳),同時 this 指向這個新實例。
  • 若是返回值是一個新對象,那麼直接返回該對象;若是無返回值或者返回一個非對象值,那麼就將步驟(1)建立的對象返回。

5 類的定義、類的聲明(繼承的本質:原型鏈)

5.1方式1、用構造函數模擬類(es5)

function Animal(){
    this.name="xiaoqi";//經過this,代表這是一個構造函數
}

5.2方式2、用class聲明(es6)的寫法

class Animal{
    constructor(){//能夠在構造函數裏寫屬性
        this.name = name;
    }
}

5.3 類的實列化

類的實例化很簡單,直接 new 出來便可。code

new Animal();

5.4繼承的幾種方式

繼承的本質是原型鏈,
繼承是面嚮對象語言的基礎概念,通常面嚮對象語言支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。ECMAScript中函數沒有簽名,所以沒法實現接口繼承。ECMAScript只支持實現繼承,而其實現繼承主要是靠原型鏈來實現。

5.4.1 原型鏈

原理:讓一個引用類型繼承另外一個引用類型的方法和屬性

具體實現以下:對象

// 父類
function Parent(){
    this.name = "xiaoqi";
    this.children = ["zhuzhu","chouzhu"]
}
Parent.prototype.getChildren = function(){
    console.log(this.childen);
}
// 子類
function Child(){

} 

Child.prototype = new Parent()
var child1 = new Child();
child1.children.push("hanhanzhu")
console.log(child1.getChildren())// Array ["zhuzhu", "chouzhu", "hanhanzhu"]

var child2 = new Child();
console.log(child2.getChildren())// Array ["hanhan", "chouzhu"]
優勢
  • 父類新增原型方法/原型屬性,子類都能訪問到
  • 簡單,易於實現
缺點
  • 沒法實現多繼承
  • 引用類型的屬性被全部實例共享
  • 在建立Child的實例的時候,不能向Parent傳參

5.4.2 盜用構造函數

原理:使用apply()和call()方法以新對象爲上下文執行構造函數,子類構造函數內部調用超類構造函數

具體實現以下:

//盜用構造函數
// 父類 
function Parent(name){
     this.name = name;
     this.colors = ["red","yellow"];
     this.getName = function(){
         return this.name;
     }
 }

//  子類
function Child(name){
    Parent.call(this,name);
}

var child1 = new Child("xiaoqi");
child1.colors.push("xiaopiao");
console.log(child.colors);//["red","yellow","xiaopiao"]

var child2 = new Child("xiaopiao");
child2.colors.push("xiaoqi")
console.log(child2.colors)//["red","yellow","xiaoqi"]
優勢
  • 避免了引用類型的屬性被全部實列共享,能夠向父級傳遞參數
  • 解決了原型鏈繼承中子類實例共享父類引用屬性的問題

建立子類實例時,能夠向父類傳遞參數
能夠實現多繼承(call多個父類對象)

缺點
  • 只能繼承父類的實例屬性和方法,不能繼承原型屬性和方法
  • 每次實列都會建立一遍方法,函數複用是一個問題

5.4.3 組合繼承(原型鏈+借用構造函數的組合繼承)

原理:經過借用構造函數實現對實例屬性的繼承。這樣,既可以保證可以經過原型定義的方法實現函數複用,又可以保證每一個實例有本身的屬性

具體實現以下:

//  組合繼承(原型鏈+借用構造函數的組合繼承)
function Parent(name,age){
    this.name= name;
    this.age = age;
    this.colors = ['red','green']
    console.log("parent")
}
Parent.prototype.getColors = function(){
    console.log(this.colors);
}

// 子類
function Child(name,age,grade){
    Parent.call(this,name,age)//建立子類實列會折行一次
    this.grade = grade;
}

Child.prototype = new Parent();//指定子類原型會執行一次
Child.prototype.constructor = Child;//校訂構造函數
Child.prototype.getName = function(){
    console.log(this.name)
}
var c = new Child("xiaoqi",88,99);
console.log(c.getName());
// 輸出:「Parent」,"Parent","xiaoqi"
優勢
  • 能夠繼承實例屬性/方法,也能夠繼承原型屬性/方法
  • 不存在引用屬性共享問題
  • 可傳參
  • 函數可複用
缺點
  • 建立子類時會調用兩次超類的構造函數

5.4.4 原型式繼承

原理:藉助原型能夠基於已有的對象建立新對象,同時還不比所以建立自定義類型

具體實現以下:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
在object()函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回這個臨時類型的一個新實例。本質上object()就是完成了一次淺複製操做
var person ={
    name:"xiaoqi",
    friends:["piaopiao","xiaopiao"]
}

var p1= object(person);
p1.name="xiaopiao"
p1.friends.push("heihei")

var p2=object(person);
p2.name = "xiaoqi"
p2.friends.push("haha")

console.log(p1.name)
console.log(person.friends)
//["piaopiao","xiaoxiao","heihei","haha"]
ECMAScript5經過新增Object.create()方法規範化了原型式繼承,這個方法接收兩個參數:一個用做新對象原型的對象和爲新對象定義屬性的對象
  • 注意Object.create()有兩個參數,第二個與Object.defineProperties()的第二個參數同樣
var person ={
    name:"xiaoqi",
    friends:["piaopiao","xiaopiao"]
}

var p1= Object.create(person);
p1.name="xiaopiao"
p1.friends.push("heihei")

var p2=Object.create(person);
p2.name = "xiaoqi"
p2.friends.push("haha")

console.log(p1.name)
console.log(person.friends)
//["piaopiao","xiaoxiao","heihei","haha"]

5.4.5 寄生式繼承

寄生式繼承是與原型式繼承緊密相關的一種思路,即建立一個僅用於封裝繼承函數過程的函數,該函數在內部以某種方式來加強對象,最後返回對象

具體實現以下:

function object(obj) {
    function F(){};
    F.prototype = obj;
    return new F();
}

function createAnother(original) {
    var clone = object(original); // 建立新對象
    clone.sayHi = function(){ 
        console.log('hello, world'); // 加強對象,添加屬性或法,這裏致使方法難以複用問題
    }
    return clone; // 返回新對象
}

var person = {
    name: 'alice',
    friends: ['Sherly', 'Taissy', 'Vant']
}

var p1 = createAnother(person);
p1.sayHi(); 

> "hello, world"

5.4.6 寄生組合式繼承

合繼承是 JavaScript最經常使用的繼承模式,其最大的問題是無論在什麼狀況下都會調用兩次超類構造函數:一次是在建立子類原型時,一次是在子類型構造函數內部。子類型最終會包含超類的所有實例屬性。
所謂寄生組合式繼承,即經過構造函數來繼承屬性,經過原型鏈繼承方法,背後的基本思路是:沒必要爲了指定子類的原型而調用超類的構造函數,咱們所須要的無非就是超類原型的一個副本而已

具體實現以下:

function Parent(name,age){
    this.name = name;
    this.age = age;
    console.log('parent')
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(name,age,grade){
    Parent.call(this,name,age);
    this.grade = grade;
}

// 寄生組合的方式
// 複製父類的原型對象
function create(original){
    function F(){};
    F.prototype = original;
    return new F();
}

//建立父類的原型副本,改變子類的原型,同時糾正構造函數
function inherit(subClass,superClass){
    var parent = create(superClass.prototype);
    parent.constructor = subClass;
    subClass.prototype = parent;
}

inherit(Child,Parent);

var child = new Child("xiaoqi",99,99)
// ‘parent’
寄生組合繼承的高效率在於它只調用了一次超類構造函數,同時還可以保持原型鏈不變,可以正常使用 instanceof 和 isPrototypeOf() 寄生組合繼承被廣泛認爲是引用類型最理想的繼承方式

5.4.7 加強型寄生組合繼承

寄生組合式繼承可以很完美地實現繼承,但也不是沒有缺點。inherit() 方法中複製了父類的原型,賦給子類,假如子類原型上有自定的方法,也會被覆蓋,所以能夠經過Object.defineProperty的方式,將子類原型上定義的屬性或方法添加到複製的原型對象上,如此,既能夠保留子類的原型對象的完整性,又可以複製父類原型
function Parent(name, age){
    this.name = name;
    this.age = age;
}

Parent.prototype.getName = function(){
    console.log(this.name)
}

function Child(name, age, grade){
    Parent.call(this, name, age);
    this.grade = grade;
}


function inherit(child, parent){
    let obj = parent.prototype;
    obj.constructor = child;
    for(let key in child.prototype){
        Object.defineProperty(obj, key, {
            value: child.prototype[key]
        })
    }
    child.prototype = obj;
}

inherit(Child,Parent);

var child = new Child("xiaoqi",99,99)
// ‘parent’

5.4.8 ES6中class 的繼承

S6中引入了class關鍵字,class能夠經過extends關鍵字實現繼承,還能夠經過static關鍵字定義類的靜態方法,這比 ES5 的經過修改原型鏈實現繼承,要清晰和方便不少。

ES5 的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制徹底不一樣,實質是先將父類實例對象的屬性和方法,加到this上面(因此必須先調用super方法),而後再用子類的構造函數修改this。

  • class關鍵字只是原型的語法糖,JavaScript繼承仍然是基於原型實現的
class Person {
            //調用類的構造方法
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            //定義通常的方法
            showName() {
                console.log("調用父類的方法")
                console.log(this.name, this.age);
            }
        }
        let p1 = new  Person('kobe', 39)
        console.log(p1)
        //定義一個子類
        class Student extends Person {
            constructor(name, age, salary) {
                super(name, age)//經過super調用父類的構造方法
                this.salary = salary
            }
            showName() {//在子類自身定義方法
                console.log("調用子類的方法")
                console.log(this.name, this.age, this.salary);
            }
        }
        let s1 = new Student('wade', 38, 1000000000)
        console.log(s1)
        s1.showName()
繼承方式 優勢 缺陷
原型鏈繼承 可以實現函數複用 1.引用類型的屬性被全部實例共享;2.建立子類時不能向超類傳參
借用構造函數 1. 避免了引用類型的屬性被全部實例共享; 2. 能夠在子類中向超類傳參 方法都在構造函數中定義了,每次建立實例都會建立一遍方法,沒法實現函數複用
組合繼承 融合了原型鏈繼承和構造函數的優勢,是Javascript中最經常使用的繼承模式 建立子類會調用兩次超類的構造函數
原型繼承 在沒有必要興師動衆地建立構造函數,而只是想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承徹底能夠勝任 引用類型的屬性會被全部實例共享
寄生式繼承 能夠加強對象 使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用形成效率下降,這一點與構造函數模式相似;同時存在引用類型的屬性被全部實例共享的缺陷
寄生組合繼承 使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用形成效率下降,這一點與構造函數模式相似;同時存在引用類型的屬性被全部實例共享的缺陷
相關文章
相關標籤/搜索