前端面试笔记-- js重要概念: 继承
js继承
JS的继承也是其非常强大的特性之一,在面试中也是常被问及的知识点。s的继承可大致分为6种,笔者想通过一个具体的例子记述js的继承方式。
说到继承,那必然要有父类和字类,我们先写一个父类的构造函数,代码如下:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
这里的父类构造函数是Animal, 定义了动物的name属性、一个实例方法sleep, 同时,构造函数的显式原型prototype上定义了一个原型方法eat。通过
js的继承方式,要实现的是字类继承父类的属性和方法,比如:子类为Cat, 通过继承,子类Cat构造的实例对象myCat除了有子类构造函数Cat的属性和方法,也要包含父类的属性和方法。
1、原型链继承
原型链继承的核心只有一句话: 将父类的实例作为子类的原型
在本文的例子中即为:
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// 测试
var cat = new Cat();
console.log(cat.name); // 'cat' 来自Cat.prototype.name
console.log(cat.eat('fish')); // cat正在吃: fish
console.log(cat.sleep()); // cat正在睡觉!
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
学习js的继承方法,一定要注意区分对比不同的继承方式的优缺点。
原型链继承优点、特点:
- 简单,易于实现
- 父类新增原型方法和原型属性子类都可以访问到
- 子类构造的实例也是父类构造的实例(参见测试代码
cat instanceof Animal
)
原型链继承缺点:
- 子类可以重写父类原型上的方法:
Cat.prototype._proto_.eat = null
。此时,父类上的原型方法eat已经被重置为null
. - 所有Cat实例化对象都一样,都共享有原型对象的属性及方法。修改一个,其他的实例对象也改变。
2、借助构造函数继承
话不多说,上代码:
function Cat(name, age){
Animal.call(this, name);
this.age = age;
}
// 测试
var cat = new Cat('Tom', 1);
console.log(cat.name); // Tom
console.log(cat.sleep()); // Tom正在睡觉!
console.log(cat.eat('fish')); // Uncaught TypeError: cat.eat is not a function
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
优点:
- 可以实现多继承(call多个父类对象)
- 创建子类实例时,可以向父类传递参数
- 解决了原型链继承中,子类实例共享父类引用属性的问题
缺点:
- 只能继承父类构造函数的方法和属性,不能继承父类显式原型上的属性和方法
- 每次实例化子类,都要调用一次父类的构造函数,对于父类构造函数中的方法,继承时,每次都要创建一次,无法实现函数的复用。
3、组合继承
也就是结合原型链继承和构造函数继承的优点。话不多说,还是上代码:
function Cat(name, age){
Animal.call(this, name);
this.age = age;
}
Cat.prototype = new Animal();
// 测试
var cat = new Cat('Tom', 1);
console.log(cat.name); // Tom
console.log(cat.sleep()); // Tom正在睡觉!
console.log(cat.eat('fish')); // Tom正在吃:fish
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
优点:
结合了原型链继承和构造函数继承的各自优势:
- 通过原型链继承父类的原型方法;
- 借助构造函数,继承父类属性。
这也是比较常用的js继承方法,可以满足大多数的需求,一定要牢记哦~
缺点:
无论什么情况下,都会调用两次父级构造函数:一次是在创建子级原型的时候,另一次是在子级构造函数内部
(未完待续~)
续:回头再看这个问题,有更深一点的理解,因此又默了一遍,贴出来对比一下前后的理解:
- 原型链继承: 父类的实例作为子类的原型
Child.prototype = new Father()
回忆原型链:
function Person() {this.name = 'tom'}
Person.prototype.constructor === Person // 构造函数的显式原型的constructor指向构造函数本身
person = new Person() // 实例
person.__proto__ === Person.prototype
因此缺点:
Child.prototype.__proto__ === father.__proto__ === Father.protoType
// 因此,麻烦了
Child.prototype.__proto__ = null
Father.protoType === null // true
也就是,子类可以重写父类原型上的方法,
牵一发动全身:
所谓继承,是通过prototype实现的,所有子类实例化对象共享有原型对象的属性及方法。修改一个,其他的实例对象也改变。
- 构造函数继承
核心是 子类构造函数调用父类构造函数,通过call
function Child() {
Father.call(this)
}
优点和缺点都很明显。因为这种操作相当于,每次去new一个子类的实例,都调用一次父类构造函数,所以:
优点: 子类的属性和方法是单独的,不会互相影响,
缺点:因为属性和方法都是单独的,无法实现复用,每个子类都有父类实例函数的副本,影响性能
且无法继承父类的原型属性和方法
- 组合继承–结合原型链和构造函数
核心:
function Child() {
Father.call(this)
}
Child.prototype = new Father();
// 如果要给子类构造函数的原型对象添加原型属性,放在上一句代码之后
Child.prototype.eat = function() {
console.log('正在吃饭')
}
优点当然就是前两者的优点,缺点:调用了两次父类构造函数
寄生继承,来源于原型式继承
原型式继承:
ES5的Object.create