本文已参与「新人创作礼」活动,一起开启掘金创作之路。
关于继承的实现方式,有以下几种,我进行了汇总,代码举例以及优缺点总结。
1. 原型链继承
实现举例
function Animal() {
this.superType = 'Animal'
this.name = 'name' || '动物'
//实例方法
this.sleep = function () {
console.log(this.name + '正在睡觉')
}
}
//原型上的函数
Animal.prototype.eat = function (food) {
console.log(this.name + '正在吃' + food)
}
function Dog(name) {
this.name = name
}
// 改变Dog的prototype指向,指向了一个Animal实例,实现了原型继承
Dog.prototype = new Animal()
// 将Dog的构造函数指向自身
Dog.prototype.constructor = Dog
var doggie = new Dog('wangcai')
console.log(doggie.superType)//Animal
doggie.sleep()//wangcai正在睡觉
doggie.eat('骨头')//wangcai正在吃骨头
一图看懂原型继承
原来的构造函数Dog
的prototype
指向的是Dog
的原型对象,但是现在指向了Animal
的实例对象。也就是说构造函数Dog
的原型对象为Animal
的实例对象。
优点
- 实现简单,只需要设置子类的prototype为父类的实例即可
- 可以通过子类的实例直接访问父类原型链中的属性和函数
缺点
- 子类的所有实例将共享父类的属性,这样就会导致一个问题:如果父类中国的某个属性值为引用类型,某个子类的实例去修改这个属性的值,就会影响到其它实例的值。
- 在创建子类的实例的时候,无法向父类的构造函数中传递参数。
在通过
new
操作符创建子类的实例的时候,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类的关联操作,所以无法向父类的构造函数中传递参数。 - 在给子类的原型对象上添加属性或者是方法的时候,一定要放在
Student.prototype=new Person
语句的后面。 例如
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
}
//在Studnet.prototype = new Person();代码前给Student的prototype添加study方法。
Studnet.prototype.study = function () {
console.log("好好学习,天天向上");
};
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
var stu1 = new Studnet(1001);
stu1.study(); //stu1.study is not a function
原因:后面通过Studnet.prototype = new Person();
这行代码对Student
的原型对象进行了重写,所以导致study
方法无效了。
修改
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
}
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
//放在了Studnet.prototype=new Person语句的后面
Studnet.prototype.study = function () {
console.log("好好学习,天天向上");
};
var stu1 = new Studnet(1001);
stu1.study();
2. 构造函数继承
在子类的构造函数中,通过apply()
方法或者是call()
方法,调用父类的构造函数,从而实现继承功能。
实现举例
function Person() {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
}
function Studnet(id) {
this.id = id; // 学号
Person.call(this);
}
var stu1 = new Studnet(1001);
var stu2 = new Studnet(1002);
stu1.emotion.push("玩游戏");
console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
console.log(stu2.emotion); // ["吃饭", "睡觉", "学习"]
可以看到stu1
对象向emotion
数组中添加数据,并不会影响到stu2
对象。
优点
- 由于在子类的构造中通过
call
改变了父类中的this
指向,导致了在父类构造函数中定义的属性或者是方法都赋值给了子类,这样生成的每个子类的实例中都具有了这些属性和方法。而且它们之间是互不影响的,即使是引用类型。 - 创建子类的实例的时候,可以向父类的构造函数中传递参数。
缺点
- 子类只能继承父类中实例的属性和方法,无法继承父类原型对象上的属性和方法。
- 在父类的构造函数中添加一个实例方法,对应的子类也就有了该实例方法,但是问题时,每创建一个子类的实例,都会有一个父类中的实例方法,这样导致的结果就是占用内存比较大。
3. 拷贝继承
所谓的拷贝继承指的是先创建父类的实例,然后通过for...in
的方式来遍历父类实例中的所有属性和方法,并依次赋值给子类的实例,同时原型上的属性和函数也赋给子类的实例。
实现举例
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
function Studnet(id, age) {
var person = new Person(age);//创建父类实例
for (var key in person) {//遍历父类实例的属性或方法
if (person.hasOwnProperty(key)) {
this[key] = person[key];//实例上的属性或方法
} else {
Studnet.prototype[key] = person[key];//原型上的属性或方法
}
}
// 子类自身的属性
this.id = id;
}
var student = new Studnet(1001, 21);
student.study();
student.run();
在上面的代码中,创建了父类Person
,并且在该类中指定了相应的实例属性和实例方法,同时为其原型对象中也添加了方法。
在Studnet
这个子类中,首先会创建父类Person
的实例,然后通过for...in
来进行遍历,获取父类中的属性和方法,获取以后进行判断,如果person.hasOwnProperty(key)
返回值为false
,表示获取到的是父类原型对象上的属性和方法,所以也要添加到子类的prototype
属性上,成为子类的原生对象上的属性或者是方法。
优点
第一:可以实现向父类中的构造方法中传递参数。
第二:能够实现让子类继承父类中的实例属性,实例方法以及原型对象上的属性和方法。
缺点
父类的所有属性和方法,子类都需要复制拷贝一遍,所以比较消耗内存。
4. 组合继承
组合继承的核心思想是将构造函数继承与原型继承两种方式组合在一起。
实现举例
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
function Studnet(id, age) {//构造函数继承
Person.call(this, age);
this.id = id; //子类独有的属性
}
//原型继承
Studnet.prototype = new Person();
Studnet.prototype.constructor = Studnet;
var student = new Studnet(1001, 21);
student.run();
console.log("爱好是:" + student.emotion);
优点
-
通过
Person.call(this,ge)
这个行代码,可以将父类中的实例属性和方法添加到子类Student
中,另外通过Studnet.prototype = new Person();
可以将父类的原型对象上的属性和函数绑定到Student
的原型对象上。 -
可以向父类的构造函数中传递参数。
缺点
组合继承的主要缺点是父类的实例属性会绑定两次。
第一次是在子类的构造函数中通过call( )
函数调用了一次父类的构造函数,完成实例属性和方法的绑定操作。
第二次是在改写子类prototype
属性的时候,我们执行了一次new Person()
的操作,这里又将父类的构造函数调用了一次,完成了属性的绑定操作。
所以在整个组合继承的过程中,父类实例的属性和方法会进行两次的绑定操作。当然这里需要你注意的一点是:通过call()
函数完成父类中实例属性和方法的绑定的优先级要高于通过改写子类prototype
的方式。也就是说第一种方式会覆盖第二种方式
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
//实例方法
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
// 原型方法
Person.prototype.study = function () {
console.log(this.id + "号学生需要好好学习");
};
function Studnet(id, age) {
Person.call(this, age);
this.id = id; //子类独有的属性
}
Studnet.prototype = new Person();
Studnet.prototype.constructor = Student;
var student = new Studnet(1001, 21);
student.run();
console.log("爱好是:" + student.emotion);
student.study(); //调用父类的实例方法student
5. 寄生式组合继承
实现举例
function Person(age) {
this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
this.age = age;
this.study = function () {
console.log(this.id + "号同学要努力学习");
};
}
Person.prototype.run = function () {
console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
};
Person.prototype.study = function () {
console.log(this.id + "号学生需要好好学习");
};
function Studnet(id, age) {
Person.call(this, age);
this.id = id;
}
// 定义Super构造函数
function Super() {}
//Super.prototype原型对象指向了Person.prototype
Super.prototype = Person.prototype;
//Student.prototype原型对象指向了Super的实例,这样就去掉了Person父类的实例属性。
Studnet.prototype = new Super();
Studnet.prototype.constructor = Studnet;
var student = new Studnet(1001, 21);
student.run();
console.log("爱好是:" + student.emotion);
student.study();
在上面的代码中,创建了一个Super
构造函数,让Super.prototype
的原型指向了Person.prototype
,同时将Super
的对象赋值给了Student.prototype
,这样就去掉了Person
父类的实例属性。
通过寄生式组合继承解决了组合继承的问题。
平时实现可以使用组合继承,也可以使用寄生式组合继承。