今天聊一个挺经典的面试题——“JavaScript 如何实现继承?”嗯,作为程序员,我倒是挺喜欢这种问题的,因为它不仅考察了基础知识的掌握程度,还能看你是否能够在实际开发中灵活运用这些知识点。毕竟在 JavaScript 这个语言里,继承可不单单是类的那点事,灵活度可是特别高的。
在我看来,继承是面向对象编程中一个非常重要的概念。简单来说,它允许一个对象“继承”另一个对象的属性和方法。就像我们在生活中,经常听到“父母遗传了孩子的眼睛”这样的说法,其实它和继承的概念是类似的。
如果有个类(比如“汽车”),然后通过继承,它的子类(比如“轿车”或“货车”)就可以自动继承父类的属性和方法,而且还可以有自己的特色。接下来,我就通过几个例子,给大家讲解一下 JavaScript 中的继承方式。
首先来简单回顾一下继承的基本概念。假设我们有一个“汽车”类,这个类有两个属性:颜色和速度,接着我们定义两个子类——“轿车”和“货车”。
“轿车”和“货车”会继承“汽车”的属性(颜色、速度),但是它们还会有自己的独特属性,比如“轿车”有一个后备厢,“货车”有一个大货箱。
// 基类:汽车
class Car {
constructor(color, speed) {
this.color = color;
this.speed = speed;
}
}
// 子类:货车
class Truck extends Car {
constructor(color, speed) {
super(color, speed); // 调用父类构造函数
this.container = true; // 新增属性
}
}
在这个例子中,Truck
继承了 Car
的属性,还能为 Truck
添加自己的属性,比如 container
。这是最基础的继承方式,但是,JavaScript 中的继承可不仅仅是这样,还有很多种实现方式。接下来,我们就来看看这些实现方式,尤其是当面试官问到这个问题时,你可以从哪几个方面展开讨论。
1. 原型链继承
原型链继承是最常见的一种实现方式。其原理是通过设置一个对象的原型为另一个对象的实例,从而让一个对象继承另一个对象的属性和方法。可以这么理解:当我们在子类中访问某个属性时,如果子类本身没有这个属性,它就会去父类的原型对象中找,直到找到为止。
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
var child1 = new Child();
child1.play.push(4);
var child2 = new Child();
console.log(child2.play); // [1, 2, 3, 4]
这里有个问题,虽然 child1
继承了 Parent
,但由于 child1
和 child2
都共享同一个原型对象,因此 child1
修改了 play
属性后,child2
也会受到影响。这是原型链继承的一大缺点。
2. 构造函数继承(借用构造函数)
为了解决原型链继承的问题,我们可以使用构造函数继承,也就是通过在子类构造函数内部调用父类构造函数。这样,子类会有自己的属性,而不会与其他实例共享。
function Parent() {
this.name = 'parent';
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {
Parent.call(this); // 借用父类构造函数
this.type = 'child';
}
var child = new Child();
console.log(child.name); // parent
console.log(child.getName()); // 错误:getName 没有继承
在这个例子中,Child
调用了 Parent.call(this)
,从而继承了父类的实例属性,但父类的原型方法并没有被继承。也就是说,Child
可以访问父类的属性(比如 name
),但无法访问父类原型上的方法(比如 getName()
)。这种方式的缺点是无法继承父类的原型方法。
3. 组合继承
组合继承将原型链继承和构造函数继承结合起来,它既能继承父类的实例属性,也能继承父类的原型方法。这样我们就能避免原型链继承中的属性共享问题,同时也能继承父类的原型方法。
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {
Parent.call(this); // 继承实例属性
this.type = 'child';
}
Child.prototype = new Parent(); // 继承原型方法
Child.prototype.constructor = Child; // 修正 constructor 指向
var child1 = new Child();
var child2 = new Child();
child1.play.push(4);
console.log(child1.play); // [1, 2, 3, 4]
console.log(child2.play); // [1, 2, 3]
console.log(child1.getName()); // parent
组合继承的优势在于,它解决了原型链继承和构造函数继承的缺陷。每个实例有自己的属性,原型方法被共享。唯一的问题是 Parent
构造函数会被调用两次——一次是在 Child
的构造函数中调用,另一次是在设置 Child.prototype
时调用。虽然这个性能开销不大,但如果父类构造函数非常复杂,可能会成为问题。
4. 原型式继承
原型式继承是一种通过 Object.create()
方法来实现继承的方式。它的原理是创建一个新对象,这个对象的原型指向父对象。相比于传统的继承方式,原型式继承更加简洁。
let parent = {
name: 'parent',
friends: ['p1', 'p2'],
};
let child = Object.create(parent);
child.name = 'child';
child.friends.push('p3');
console.log(child.name); // child
console.log(child.friends); // ['p1', 'p2', 'p3']
这种方式的问题在于,friends
是引用类型,所以 child
和 parent
会共享 friends
数组。如果一个实例修改了 friends
,另外一个实例也会受到影响。这是因为它们共享相同的内存地址。
5. 寄生式继承
寄生式继承是在原型式继承的基础上,通过扩展或者增强对象的方法和属性来实现。其优点是可以在继承时给新对象添加一些额外的功能。
let parent = {
name: 'parent',
friends: ['p1', 'p2'],
};
function createChild(parentObj) {
let child = Object.create(parentObj);
child.getFriends = function() {
return this.friends;
};
return child;
}
let child = createChild(parent);
child.friends.push('p3');
console.log(child.getFriends()); // ['p1', 'p2', 'p3']
通过这种方式,createChild
创建了一个继承自 parent
的新对象,并给它添加了 getFriends
方法。这种方法是对原型式继承的优化,可以避免引用类型的问题,但它依然存在对象共享的缺陷。
JavaScript 继承的方式有很多,每种方式都有其适用场景。
原型链继承简单,但是容易导致属性共享的问题;构造函数继承解决了这个问题,但无法继承父类的原型方法;组合继承是比较常见的做法,能兼顾两者的优点;原型式继承和寄生式继承则提供了一种更加灵活的方式,在某些场景下非常有用。最终,你应该根据项目的需求来选择合适的继承方式。
这就是面试官可能问的继承问题,你可以从基础概念到不同实现方式全方位地展示你的知识深度。希望大家下次面试碰到这个问题时,能一口气讲出来!
目前,对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥私藏精品 热门推荐 虎哥作为一名老码农,整理了全网最全《前端资料合集》。