JavaScript(2)之——繼承

zhunny發表於2019-03-19

  在物件導向的程式設計思想中,繼承可以使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法等。這裡我們總結一些JavaScript中寫繼承的幾種方式,以及它們的優缺點。
(1)類式繼承

const Animal = function() {
    this.superBreath = true;
    this.types = ['cat', 'dog', 'snake'];
}
Animal.prototype.canBreath = function() {
    return this.superBreath;
}

const Cat = function(name) {
    this.name = name;
}

Cat.prototype = new Animal();  //類式繼承

const kitty = new Cat("kitty");

console.log(kitty instanceof Cat); //true
console.log(kitty instanceof Animal); //true

console.log(kitty.name); //kitty
console.log(kitty.canBreath()); //true
console.log(kitty.types); //[ 'cat', 'dog', 'snake' ]
kitty.types.push("duck");
const tom = new Cat("tom");
console.log(tom.types); //[ 'cat', 'dog', 'snake', 'duck' ]
複製程式碼

  當我們例項化一個父類時,建立了一個新的物件,將父類建構函式中的方法和屬性複製給這個物件,並將這個物件的[[prototype]]屬性指向了父類的原型物件。將這個新建立的物件賦值給子類的原型物件,子類的原型就可以訪問到父類的原型,還可訪問到從父類建構函式中複製的屬性和方法。
  缺點有兩個:第一個是子類的原型物件作為一個被父類例項化的物件,如果父類的建構函式中有引用型別的物件,那麼引用型別會被子類的所有例項化物件共享。第二個是因為子類靠例項化父類來實現繼承,因此繼承操作時無法想父類的建構函式傳參,父類建構函式的屬性無法初始化。
(2)建構函式繼承

const Animal = function() {
    this.superBreath = true;
    this.types = ['cat', 'dog', 'snake'];
}
Animal.prototype.canBreath = function() {
    return this.superBreath;
}

const Cat = function(name) {
    Animal.call(this); //建構函式繼承
    this.name = name;
}

const kitty = new Cat("kitty");

console.log(kitty instanceof Cat); //true
console.log(kitty instanceof Animal); //false

console.log(kitty.name); //kitty
console.log(kitty.canBreath()); //is not a function 
console.log(kitty.types); //[ 'cat', 'dog', 'snake' ]
kitty.types.push("duck"); 
console.log(kitty.types); //[ 'cat', 'dog', 'snake', 'duck' ]
const tom = new Cat("tom");
console.log(tom.types);//[ 'cat', 'dog', 'snake' ]
複製程式碼

  Animal.call(this)是建構函式繼承的關鍵,我們在呼叫Animal時強制把它的this繫結到Cat這個函式呼叫時的作用域,因此每次例項化子類Cat時,都會將Animal建構函式中的屬性與方法拷貝一次。但是這種方法沒有涉及到原型鏈的連結,因此例項化的子類物件無法訪問父類原型物件中的方法,要想被子類共享,只能放在父類建構函式中。每次都被拷貝一次,這樣違背了程式碼複用的原則,因此這種方法也不推薦使用。
(3)組合繼承
  組合使用就很好理解了,它將上面兩種繼承方式的優點集於一身,但同時也有一個小瑕疵。

const Animal = function() {
    this.superBreath = true;
    this.types = ['cat', 'dog', 'snake'];
}
Animal.prototype.canBreath = function() {
    return this.superBreath;
}

const Cat = function(name) {
    Animal.call(this); 
    this.name = name;
}

Cat.prototype = new Animal();

const kitty = new Cat("kitty");

console.log(kitty instanceof Cat); //true
console.log(kitty instanceof Animal); //true

console.log(kitty.name); //kitty
console.log(kitty.canBreath()); //true
console.log(kitty.types); //[ 'cat', 'dog', 'snake' ]
kitty.types.push("duck");
console.log(kitty.types); //[ 'cat', 'dog', 'snake', 'duck' ]
const tom = new Cat("tom");
console.log(tom.types); //[ 'cat', 'dog', 'snake' ] 
複製程式碼

  Animal.call(this)解決了引用型別共享和不能給父類建構函式傳參的缺點,Cat.prototype = new Animal();使得子類例項化物件可以共享父類原型物件中的屬性與方法。
  它的唯一缺點是呼叫了兩次父類建構函式,因此說它還不完美。
(4)原型式繼承

let Animal = {
    superBreath: true,
    animals: ['cat', 'dog']
}

let Cat = function() {
    this.subBreath = true;
    this.subAnimals = ['cat', 'dog', 'snakes']
}

Cat.prototype = Object.create(Animal);
Cat.prototype.constructor = Cat;

const kitty = new Cat();

console.log(kitty.superBreath); //true
console.log(kitty.animals); //[ 'cat', 'dog' ]
console.log(kitty.subBreath); //true
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes' ]
kitty.subAnimals.push("duck");
kitty.animals.push("elephant");
console.log(kitty.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes', 'duck' ]
const tom = new Cat();
console.log(tom.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(tom.subAnimals); //[ 'cat', 'dog', 'snakes' ]
複製程式碼

  原型式繼承的是對類式繼承的一個封裝,它的內部構造了一個空的函式,將這個函式的原型物件賦予父物件的值,然後返回一個空函式的例項物件。

function F(){};
F.prototype = o; //o是父物件
return new F();
複製程式碼

  因此可想而知,它的缺點與類式繼承是一樣的,父類的引用型別會被共用。只是它構造了一個空函式,開銷會小一點。
(5)寄生式繼承

let Animal = {
    superBreath: true,
    animals: ['cat', 'dog']
}

let Cat = function() {
    this.subBreath = true;
    this.subAnimals = ['cat', 'dog', 'snakes']
}

Cat.prototype = Object.create(Animal, { inherit: { value: "inherit" } });
Cat.prototype.constructor = Cat;

const kitty = new Cat();

console.log(kitty.inherit); //inherit
console.log(kitty.superBreath); //true
console.log(kitty.animals); //[ 'cat', 'dog' ]
console.log(kitty.subBreath); //true
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes' ]
kitty.subAnimals.push("duck");
kitty.animals.push("elephant");
console.log(kitty.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes', 'duck' ]
const tom = new Cat();
console.log(tom.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(tom.subAnimals); //[ 'cat', 'dog', 'snakes' ]
複製程式碼

  寄生式繼承顧名思義,它可以對繼承的物件進行擴充套件,為其增加新的屬性和方法。
(6)寄生組合式繼承
  目前為止,最完美的繼承方式:

let Animal = function() {
    this.superBreath = true;
    this.animals = ['cat', 'dog']
}
Animal.prototype.canBreath = function() {
    return this.superBreath;
}

let Cat = function() {
    Animal.call(this);
    this.subBreath = true;
    this.subAnimals = ['cat', 'dog', 'snakes']
}

Cat.prototype = Object.create(Animal.prototype);
console.log(Animal.prototype.constructor == Animal); //true
Cat.prototype.constructor = Cat;
console.log(Cat.prototype.constructor == Cat); //true

const kitty = new Cat();

console.log(kitty instanceof Animal); //true
console.log(kitty.canBreath()); //true
console.log(kitty.animals); //[ 'cat', 'dog' ]
kitty.animals.push("snakes");
console.log(kitty.animals); //[ 'cat', 'dog', 'snakes' ]
const miao = new Cat();
console.log(miao.animals); //[ 'cat', 'dog' ]
複製程式碼

  我們對比第三種組合繼承的方式來看,它用了寄生式與建構函式繼承的組合方式,使得父類建構函式少呼叫了一次。但是使用Object.create()方法還有一個小瑕疵,它需要拋棄預設的Cat.prototype,重新建立了一個物件,不能修改已有的預設物件。因此ES6之後又新增了一個輔助函式Object.setPrototypeOf(),直接修改物件的[[prototype]]關聯。

let Animal = function() {
    this.superBreath = true;
    this.animals = ['cat', 'dog']
}
Animal.prototype.canBreath = function() {
    return this.superBreath;
}

let Cat = function() {
    Animal.call(this);
    this.subBreath = true;
    this.subAnimals = ['cat', 'dog', 'snakes']
}

Object.setPrototypeOf(Cat.prototype, Animal.prototype)
console.log(Animal.prototype.constructor == Animal);
console.log(Cat.prototype.constructor == Cat);

const kitty = new Cat();

console.log(kitty instanceof Animal);
console.log(kitty.canBreath());
console.log(kitty.subAnimals);
複製程式碼

相關文章