- 说明:该文属于 大前端全栈架构白宝书专栏,目前阶段免费,如需要项目实战或者是体系化资源,文末名片加V!
- 作者:哈哥撩编程,十余年工作经验, 从事过全栈研发、产品经理等工作,目前在公司担任研发部门CTO。
- 荣誉:2022年度博客之星Top4、2023年度超级个体得主、谷歌与亚马逊开发者大会特约speaker、全栈领域优质创作者。
- 🏆 白宝书系列
在JavaScript中,继承是一种允许我们创建一个类(子类)从已有的类(父类)上继承所有的属性和方法的机制。这样的机制有助于我们复用和重用代码。
JavaScript原型链实现了继承。每一个对象都有一个内部属性[[prototype]],这个属性是一个链接/link(也就是一个指针),它指向了创建这个对象的函数的原型对象prototype。其实这就是“原型链”的起点,通过这个链子对象可以访问到父对象的属性。
在ES6之后,JavaScript又引入了基于类的继承,这种继承方式更接近于传统的面向对象语言,如Java,C++等。虽然JavaScript是基于原型的语言,但是为了更好地与其他面向对象编程语言交流和学习,ES6引入了class和extends关键字来实现基于类的继承。
但是无论是原型链继承还是ES6的类继承,其本质上都是原型继承,只是类继承更易于理解和使用。
先来看两个无关的类:People
和Vehicle
,这两个类分别描述人类和机动车。People
类含有的属性有:name(姓名)、age(年龄)、sex(性别),方法有:sayHello(打招呼)、sleep(睡觉);Vehicle
类含有的属性有:brand(品牌)、color(颜色)、engineType(发动机型号)、seatingCapacity(座位容量),方法有:move(移动)、whistle(鸣笛);可以看出他们的属性和方法几乎没有重复和相关的,所以他们是两个无关的类:
(上面两个图形是来源于UML的类的图形化表示法。UML,是一种统一建模语言,我们作为开发者能看懂图就可以了,不需要深入的了解UML的具体使用方式)
再来看两个有关的类:People
和Student
,这两个类分别描述人类和学生。Student
类含有的属性有:name(姓名)、age(年龄)、sex(性别)、studentNumber(学号)、school(学校),方法有:sayHello(打招呼)、sleep(睡觉)、study(学习)、exam(考试)。
我们可以发现:
这就是**“继承”**关系:Student类继承自People类
People是**“父类”(或“超类”、“基类”);Student是“子类”**(或”派生类“)
子类丰富了父类,让类描述的更具体,更细化
在UML中,用空心的箭头来描述继承关系,箭头指向父类:
更多继承关系的举例:
父类 | 子类 |
---|---|
People | Student、Teacher |
Vehicle(机动车) | Car(小轿车)、Truck(卡车)、Motocycle(摩托车) |
Applicance(家用电器) | Television(电视)、Refrigerator(冰箱) |
Publication(出版物) | Book(书籍)、Magazine(杂志) |
往往一个类只是一个继承”链“中的一环,比如子类也可以有自己的子类,父类也会有自己的父类。所以面向对象方法实际上是模仿了自然界中描述自然事物的方法,这样就使面向对象的编程方式非常容易被理解。
JavaScript 中如何实现继承?
实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法
使用JavaScript特有的原型链特性来实现继承,是普遍的做法
首先定义一个构造函数Peolpe
,我们可以在People.prototype
上定义一些人类的方法(比如sayHello()
、sleep()
等)然后new
出来一个People
的实例。然后让Student
构造函数的prototype
直接指向People
的实例(关键步骤),我们把study()
方法,和exam()
方法直接定义到People
的实例上。这样的巧妙之处在于如果我们new
出来一个student
(如Hanmeimei
),就可以行成了一个原型链,Hanmeimei
可以调用study()
和exam()
方法,也可以调用sayHello()
和sleep()
方法。
下面来敲一些这个demo:
// 父类
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
People.prototype.sayHello = function () {
console.log("你好,我是" + this.name + "我今年" + this.age + "岁了");
}
People.prototype.sleep = function () {
console.log("我要睡觉了,zzzzzz");
}
// 子类
function Student(name, age, sex, school, studentNumber) {
this.name = name;
this.age = age;
this.sex = sex;
this.school = school;
this.studentNumber = studentNumber;
}
// 关键语句,实现继承 这个语句一定要写在这个位置(位于定义子类语句和增加子类原型方法的中间)
Student.prototype = new People(); // 将子类的prototype指向父类的一个实例
Student.prototype.study = function () {
console.log(this.name + '正在学习');
}
Student.prototype.exam = function () {
console.log(this.name + '正在考试。。。');
}
var hanmeimei = new Student('韩梅梅', 12, '女', '实验小学', '10001');
hanmeimei.study();
hanmeimei.sayHello();
子类也可以重写父类的一些原型方法,比如我们将上述案例中的sayHello()方法在Student中重写
demo如下:
// 子类中重写sayHello()方法
Student.prototype.sayHello = function () {
console.log('敬礼!您好,我是' + this.name + ",我今年" + this.age + '岁了');
}
综上,我们可以总结出面向对象编程的特质如下(重点,一定要理解):
- 面向对象的本质:定义不同的类,让类的实例工作
- 面向对象的优点:程序编写更清晰、代码结构更严密、使代码更健壮更利于维护
- 面向对象经常用到的场合:需要封装和复用性的场合(组合性思维)
为了解决原型中包含引用类型值所带来问题和子类构造函数不优雅的问题,开发人员通常使用一种叫做”借助构造函数“的技术,也被称为”伪造对象“或”经典继承“
借助构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是要注意使用
call()
绑定上下文
优雅的在子类中使用父类的属性和方法:
function People(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
this.arr = [11, 22, 33]; // 引用类型值,父类的引用类型值会写到实例的身上
}
function Student(name, sex, age, school, sid) {
People.call(this, name, sex, age); // 使用call绑定上下文
this.school = school;
this.sid = sid;
}
Student.prototype = new People();
var xiaoming = new Student('小明', '男', '10', '实验小学', '100001');
xiaoming.arr.push('66')
console.log(xiaoming);
var xiaohong = new Student('小红', '女', '11', '实验小学', '100002');
console.log(xiaohong);
将通过原型链和借用构造函数的技术组合到一起,叫做组合继承,也叫做伪经典继承
组合继承是JavaScript中最常用的继承方式
示例代码:
// 父类
function People(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
}
People.prototype.sayHello = function () {
console.log("你好,我是" + this.name + ",我今年" + this.age + "岁了");
}
People.prototype.sleep = function name(params) {
console.log("我要睡觉了,zzzzzz");
}
// 子类
function Student(name, sex, age, school, sid) {
// 借助构造函数
People.call(this, name, sex, age);
this.school = school;
this.sid = sid;
}
// 实现继承,通过原型链
Student.prototype = new People();
Student.prototype.exam = function () {
console.log(this.name + '正在考试。。。');
}
var xiaoming = new Student('小明', '男', '10', '实验小学', '100001');
xiaoming.sayHello();
xiaoming.sleep();
xiaoming.exam();
组合继承的缺点:组合继承最大的问题就是无论什么情况下,都会调用两次父类的构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数的内部,这就会造成效率的浪费。
虽说组合继承有缺点,但无伤大雅,依然是JS中最常用的继承方式,一定要掌握这种继承的技巧。
在没有必要“兴师动众”地创建构造函数,而只是想让新对象与现有对象“类似”的情况下,使用Object.create()即可胜任,称为原型式继承
- 认识
Object.create()
方法
- IE9+开始支持
Object.create()
方法,可以根据指定的对象为原型创建出新对象Object.create()
让我们可以不借助任何构造函数就让一个对象的__proto__
指向另一个对象
Object.create()
还支持再传入一个参数,用于补充或重写新的属性,示例代码:
var obj1 = {
a: 11,
b: 22,
c: 33,
test: function () {
console.log(this.a + this.b);
}
};
var obj2 = Object.create(obj1, {
d: {
value: 44
},
a: {
value: 111
}
})
console.log(obj2.__proto__ === obj1);
console.log(obj2.a); // 111,因为重写了obj2的a属性,就会把从obj1继承的属性值覆盖
console.log(obj2.b); // 22
console.log(obj2.c); // 33
console.log(obj2.d); // 44
obj2.test(); // 133 (111+22=133)
Object.create()的兼容性写法----面试常考
如何在低版本浏览器实现Object.create()
方法呢?示例代码:
// 道格拉斯?克洛克福德写的一个函数,非常巧妙,面试常考
// 函数的功能就是以o为原型,创建新对象
function object(o) {
// 创建一个临时构造函数
function F() { }
// 让这个临时构造函数的prototype指向o,这样一来它new出来的对象,__proto__就指向了o
F.prototype = o;
// 返回F的实例
return new F();
}
var obj1 = {
a: 11,
b: 22
}
var obj2 = object(obj1);
console.log(obj2.__proto__ === obj1);
console.log(obj2.a);
console.log(obj2.b);
寄生式继承:编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法
寄生式继承仰赖一个函数,对象o需要传给这个函数,这个函数内部要创建一个新对象,这个新对象的
__proto__
指向o,函数内部对o进行“加工”,然后再输出一个新对象p,新对象上添加了一些预置的新方法新的对象感觉像是“寄生”在原来的对象上面,所以叫做“寄生”式继承
示例代码:
var obj1 = {
a: 11,
b: 22
}
var obj2 = object(obj1);
console.log(obj2.__proto__ === obj1);
console.log(obj2.a);
console.log(obj2.b);
var o1 = {
name: '小明',
age: 12,
sex: '男'
}
var o2 = {
name: '小红',
age: 11,
sex: '女'
}
// 寄生式继承
function f(o) {
// 以o为原型创建出新对象
var p = Object.create(o);
// 补充新方法
p.sayHello = function () {
console.log("你好,我是" + this.name + ",我今年" + this.age + "岁了");
}
// 补充新方法
p.sleep = function () {
console.log("我要睡觉了,zzzzzz");
}
return p;
}
var p1 = f(o1);
p1.sayHello();
var p2 = f(o2);
p2.sayHello();
总结:
- 寄生式继承就是编写一个函数,它可以“增强对象”,只要把对象传入这个函数,这个函数将以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法
- 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
- 和原型式继承相比,寄生式继承封装性更强,就像一个小工厂,可以对一个对象进行加工,产生一个新的对象
寄生式继承的缺点:
- 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,即“方法没有写到prototype上”
之前提到的组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数的内部。那么如何可以避免这个问题呢,利用寄生组合式继承就可以避免调用两次超类的构造函数问题。
寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式继承方法
寄生组合式继承背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已 。本质上,就是 使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
示例代码:
// 父类
function People(name, sex, age) {
this.name = name;
this.sex = sex;
this.age = age;
}
People.prototype.sayHello = function () {
console.log("你好,我是" + this.name + ",我今年" + this.age + "岁了");
}
People.prototype.sleep = function name(params) {
console.log("我要睡觉了,zzzzzz");
}
// 子类
function Student(name, sex, age, school, sid) {
// 借助构造函数
People.call(this, name, sex, age);
this.school = school;
this.sid = sid;
}
// 调用我们自己编写的inheritPrototype函数,
// 这个函数可以让Student类的prototype指向以“People.prototype为原型的一个新对象”
inheritPorpotype(Student, People);
// 子类中重写sayHello()方法
Student.prototype.sayHello = function () {
console.log('敬礼!您好,我是' + this.name + ",我今年" + this.age + '岁了');
}
Student.prototype.exam = function () {
console.log(this.name + '正在考试。。。');
}
var xiaoming = new Student('小明', '男', '10', '实验小学', '100001');
xiaoming.sayHello();
xiaoming.sleep();
xiaoming.exam();
instanceof运算符
:instanceof运算符用来检测“某个对象是不是某个类的”
总结
继承已经学习完毕了,下面我们来总结一下继承的几种方式:
其中组合继承是JavaScript中最常用的继承方式,一定要熟悉并掌握这几种继承方式,这些都是我们开发过程中常用的编码技巧。