這篇文章主要介紹JavaScript實現繼承的方式:
- 物件冒充
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
function MAN(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
this.say = function(school, zhuanye) {
console.log(this.name + "," + this.sex + ",今年" + this.age + "歲!在" + school + "學習" + zhuanye);
}
}
MAN.prototype.cao = function() {
console.log(this.name + "," + this.sex + ",都已經" + this.age + "了,再不瘋狂我們就老了!")
}
function WOMAN(name, sex, age) {
//方法1:物件冒充
//this.man = MAN;
//this.man(name, sex, age);
//方法2:call
//MAN.call(this,name, sex, age);
//方法3:apply
//MAN.apply(this,[name, sex, age]);
//方法4:bind
//MAN.bind(this)(name, sex, age);
MAN.bind(this)(name, sex, age);
}
//方法5:prototype
WOMAN.prototype = new MAN();
//注意:方法1,2,3,4只能繼承this.XXX=XXX的屬性,不能繼承XXX.prototype.xxx==function(){},註釋掉方法5可以看到
//方法5只能繼承XXX.prototype.xxx==function(){}的屬性,不能繼承this.XXX=XXX的屬性,註釋掉方法1,2,3,4可以看到
var man = new MAN("張三", "男", 26);
man.say('藍翔技校', '電氣焊');
man.cao();
var woman = new WOMAN("小紅", "女", 18);
woman.say('清華大學', '挖掘機');
woman.cao();
</script>
</head>
<body>
</body>
</html>
複製程式碼
- 類式繼承
- 建構函式繼承
- 組合繼承
- 寄生組合式繼承
- extends繼承
1、類式繼承
簡單的類式繼承:
// 宣告父類
function Animal() {
this.name = 'animal';
this.type = ['pig', 'cat'];
}
// 為父類新增共有方法
Animal.prototype.greet = function(sound) {
console.log(sound);
}
// 宣告子類
function Dog() {
this.name = 'dog';
}
// 繼承父類
Dog.prototype = new Animal();
var dog = new Dog();
dog.greet('汪汪'); // "汪汪"
console.log(dog.type); // ["pig", "cat"]
複製程式碼
在上面的程式碼中,我們建立了兩個類Animal和Dog,而且給Animal.prototype
原型上新增了一個greet共有方法,然後通過new
命令例項化一個Animal,並且賦值給Dog.prototype
原型。
原理說明:在例項化一個類時,新建立的物件複製了父類的建構函式內的屬性與方法並且將原型__proto__
指向了父類的原型物件,這樣就擁有了父類的原型物件上的屬性與方法。
不過,通過類式繼承方式,有兩個缺點。
第一個是引用缺陷:
dog.type.push('dog');
var dog2 = new Dog();
console.log(dog2.type); // ["dog", "cat", "dog"]
複製程式碼
通過上面的執行結果,我們看到當通過dog例項物件修改繼承自Animal中的陣列type(引用型別)時,另外一個新建立的例項dog2也會受到影響。
第二個是我們無法為不同的例項初始化繼承來的屬性,我們可以修改一下上面的例子:
functionAnimal(color) {
this.color = color;
}
...
Dog.prototype = new Animal('白色');
...
console.log(dog.color); // "白色"
console.log(do2.color); // "白色"
複製程式碼
通過上面的程式碼可以看到,我們無法為不同dog賦值不同的顏色,所有dog只能同一種顏色。
2、建構函式繼承
建構函式繼承方式可以避免類式繼承的缺陷:
// 宣告父類
function Animal(color) {
this.name = 'animal';
this.type = ['pig','cat'];
this.color = color;
}
// 新增共有方法
Animal.prototype.greet = function(sound) {
console.log(sound);
}
// 宣告子類
function Dog(color) {
Animal.apply(this, arguments);
}
var dog = new Dog('白色');
var dog2 = new Dog('黑色');
dog.type.push('dog');
console.log(dog.color); //"白色"
console.log(dog.type); // ["pig", "cat", "dog"]
console.log(dog2.type); // ["pig", "cat"]
console.log(dog2.color); //"黑色"
複製程式碼
首先要知道apply
方法的運用,它是可以更改函式的作用域,所以在上面的例子中,我們在Dog子類中呼叫這個方法也就是將Dog子類的變數在父類中執行一遍,這樣子類就擁有了父類中的共有屬性和方法。
但是,建構函式繼承也是有缺陷的,那就是我們無法獲取到父類的共有方法,也就是通過原型prototype
繫結的方法:
dog.greet(); // Uncaught TypeError: dog.greet is not a function
複製程式碼
3、組合繼承
組合繼承其實就是將類式繼承和建構函式繼承組合在一起:
// 宣告父類
function Animal(color) {
this.name = 'animal';
this.type = ['pig','cat'];
this.color = color;
}
// 新增共有方法
Animal.prototype.greet = function(sound) {
console.log(sound);
}
// 宣告子類
function Dog(color) {
// 建構函式繼承
Animal.apply(this, arguments);
}
// 類式繼承
Dog.prototype = new Animal();
var dog = new Dog('白色');
var dog2 = new Dog('黑色');
dog.type.push('dog');
console.log(dog.color); //"白色"
console.log(dog.type); // ["pig", "cat", "dog"]
console.log(dog2.type); // ["pig", "cat"]
console.log(dog2.color); //"黑色"
dog.greet('汪汪'); //"汪汪"
複製程式碼
在上面的例子中,我們在子類建構函式中執行父類建構函式,在子類原型上例項化父類,這就是組合繼承了,可以看到它綜合了類式繼承和建構函式繼承的優點,同時去除了缺陷。
可能你會奇怪為什麼組合式繼承可以去除類式繼承中的引用缺陷?其實這是由於原型鏈
來決定的,由於JavaScript引擎在訪問物件的屬性時,會先在物件本身中查詢,如果沒有找到,才會去原型鏈中查詢,如果找到,則返回值,如果整個原型鏈中都沒有找到這個屬性,則返回undefined。
也就是說,我們訪問到的引用型別(比如上面的type)其實是通過apply
複製到子類中的,所以不會發生共享。
這種組合繼承也是有點小缺陷的,那就是它呼叫了兩次父類的建構函式。
5、寄生組合式繼承
寄生組合式繼承強化的部分就是在組合繼承的基礎上減少一次多餘的呼叫父類的建構函式:
functionAnimal(color) {
this.color = color;
this.name = 'animal';
this.type = ['pig', 'cat'];
}
Animal.prototype.greet = function(sound) {
console.log(sound);
}
functionDog(color) {
Animal.apply(this, arguments);
this.name = 'dog';
}
/* 注意下面兩行 */
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.getName = function() {
console.log(this.name);
}
var dog = new Dog('白色');
var dog2 = new Dog('黑色');
dog.type.push('dog');
console.log(dog.color); // "白色"
console.log(dog.type); // ["pig", "cat", "dog"]
console.log(dog2.type); // ["pig", "cat"]
console.log(dog2.color); // "黑色"
dog.greet('汪汪'); // "汪汪"
複製程式碼
在上面的例子中,我們並不像建構函式繼承一樣直接將父類Animal的一個例項賦值給Dog.prototype,而是使用Object.create()
進行一次淺拷貝,將父類原型上的方法拷貝後賦給Dog.prototype,這樣子類上就能擁有了父類的共有方法,而且少了一次呼叫父類的建構函式。
Object.create()的淺拷貝的作用類式下面的函式:
functioncreate(obj) {
functionF() {};
F.prototype = obj;
returnnew F();
}
複製程式碼
這裡還需注意一點,由於對Animal的原型進行了拷貝後賦給Dog.prototype,因此Dog.prototype上的constructor
屬性也被重寫了,所以我們要修復這一個問題:
Dog.prototype.constructor = Dog;
複製程式碼
6、extends繼承
Class和extends
是在ES6中新增的,Class
用來建立一個類,extends
用來實現繼承:
classAnimal{
constructor(color) {
this.color = color;
}
greet(sound) {
console.log(sound);
}
}
classDogextendsAnimal{
constructor(color) {
super(color);
this.color = color;
}
}
let dog = new Dog('黑色');
dog.greet('汪汪'); // "汪汪"
console.log(dog.color); // "黑色"
複製程式碼
在上面的程式碼中,建立了父類Animal,然後Dog子類繼承父類,兩個類中都有一個constructor構造方法,實質就是建構函式Animal和Dog。
不知道你有沒有注意到一點,我在子類的構造方法中呼叫了super方法,它表示父類的建構函式,用來新建父類的this物件。
注意:
ES6 子類必須在
constructor
方法中呼叫super
方法,否則新建例項時會報錯。這是因為子類沒有自己的this物件,而是繼承父類的this物件,然後對其進行加工。如果不呼叫super方法,子類就得不到this物件。應該注意和ES5的繼承原理上的區別!