本文首发自腾讯IMWEB社区:imweb.io/javascript
由于我在学校接触的第一门语言是cpp,是一个静态类型语言,而且实现面向对象直接就有class关键字,并且只讲了面向对象一种设计思想,致使我一直很难理解javascript语言的继承机制。java
JavaScript没有”子类“和”父类“的概念,也没有”类“(class)和”实例“(instance)的区分,全靠”原型链“(prototype chain)实现继承。c++
学的时候就很想吐槽,费了这么大的劲去模拟类,那js干吗不一开始就设计class关键字而是最开始仅将class做为保留字呢?(ES6以后有了class关键字,是原型的语法糖)es6
当时我一直怀疑,“js没有class是一种设计缺陷吗?”web
原来,JavaScript设计之初,设计里面全部的数据类型都是对象(object),最开始,JavaScript只想被设计成一种简易的脚本语言,设计者JavaScript里面都是对象,必需要有一种机制将全部对象联系起来,但若是引入“类”(class)的概念,那么就太“正式”了,增长了上手难度。编程
要实现继承,但又不想用类,那该怎么办呢?bash
JavaScript 的设计者Brendan Eich发现,能够像c++和Java语言中使用new命令生成实例。数据结构
因而new命令被引入到JavaScript,用来从原型对象生成一个实例对象。可是JavaScript没有“类”,原型对象该如何表示呢?编程语言
这时,他想到c++和java使用new命令时,都会调用“类”的构造函数(constructor),因而他作了个简化设计,在JavaScript中,new命令后面跟的不是类而是构造函数。函数
用构造函数生成实例对象,有一个缺点就是没法共享属性和方法。
每个实例对象,都有本身的属性和方法的副本。这不只没法作到数据共享,也是极大的资源浪费。
考虑到这一点,brendan Eich决定为构造函数设置一个prototype属性。
这个属性包含一个prototype对象(是的,prototype属性的值是prototype对象),全部的实例对象须要共享的属性和方法,都放在这个对象里面,那些不须要共享的属性和方法,就放在构造函数里。
实例对象一旦建立,将自动引用prototype对象的属性和方法,也就是说,实例对象的属性和方法,分红两种,一种是本地的,另外一种是引用的。
因为全部的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象同样。
若是没了解过c++、java或者其余的编程语言,我相信你看完上面这段内容应该会看睡着了吧!好的,咱们仍是直接来看看代码吧~
//原型链继承
// 父类
// 拥有属性 name
function parents(){
this.name = "JoseyDong";
}
// 在父类的原型对象上添加一个getName方法
parents.prototype.getName = function(){
console.log(this.name);
}
//子类
function child(){
}
//子类的原型对象 指向 父类的实例对象
child.prototype = new parents()
// 建立一个子类的实例对象,若是它有父类的属性和方法,那么就证实继承实现了
let child1 = new child();
child1.getName(); // => JoseyDong
复制代码
在只有一个 子类实例对象的时候,咱们貌似看不出什么问题。然而在实际场景中,咱们会建立不少实例对象来继承父类,毕竟继承得越多,被复写的代码量就越多嘛~
//原型链继承
// 父类
// 拥有属性 name
function parents(){
this.name = ["JoseyDong"];
}
// 在父类的原型对象上添加一个getName方法
parents.prototype.getName = function(){
console.log(this.name);
}
//子类
function child(){
}
//子类的原型对象 指向 父类的实例对象
child.prototype = new parents()
// 建立一个子类的实例对象,若是它有父类的属性和方法,那么就证实继承实现了
let child1 = new child();
child1.getName(); // => ["JoseyDong"]
// 建立一个子类的实例对象,在child1修改name前实现继承
let child2 = new child();
// 修改子类的实例对象child1的name属性
child1.name.push("xixi");
// 建立子类的另外一个实例对象,在child1修改name后实现继承
let child3 = new child();
child1.getName();// => ["JoseyDong", "xixi"]
child2.getName();// => ["JoseyDong", "xixi"]
child3.getName();// => ["JoseyDong", "xixi"]
复制代码
当不少时候,咱们的实例对象里的值是会虽具体场景而改变的。好比这个时候,咱们的child1除了joseydong之外,她的朋友又给她取了个新名字xixi,咱们改变了child1的name值。而child一、child二、child3是三个独立的个体,可是最后发现三个孩子都有了新名字!
这就表示,原型链继承里面,使用的都是同一个内存里的值,这样修改该内存里的值,其余继承的子类实例里的值都会变化。
这可不是咱们想要的效果,毕竟只有child1被赋予了新名字。而且,若是我想经过子类实例对象传递参数给父类,也是作不到的。
// 构造函数继承
function parents(){
this.name = ["JoseyDong"];
}
// 在子类中,使用call方法构造函数,实现继承
function child(){
parents.call(this);
}
let child1 = new child();
let child2 = new child();
child1.name.push("xixi");
let child3 = new child();
console.log(child1.name);// => ["JoseyDong", "xixi"]
console.log(child2.name);// => ["JoseyDong"]
console.log(child3.name);// => ["JoseyDong"]
复制代码
咱们使用构造函数的方法,就只修改了child1的名字,而child2和child3的name属性并无受影响~
同时,因为call()支持传递参数,咱们也能够在child中向parent传参啦~
// 构造函数实现继承
//子类向父类传参
function parents(name){
this.name = name;
}
//call方法支持传递参数
function child(name){
parents.call(this,name)
}
let child1 = new child("I am child1");
let child2 = new child("I am child2");
console.log(child1.name);// => I am child1
console.log(child2.name);// => I am child2
复制代码
好了,如今咱们经过构造函数实现继承弥补了用原型链实现继承的缺点,同时也是经过构造函数实现继承的优势:
1.避免了引用类型的属性被全部实例共享
2.能够在child中向parent传参
可是,这种方式也有缺点,由于方法都在构造函数中定义,每次建立实例都会建立一遍方法。
咱们发现,经过原型链实现的继承,都是复用同一个属性和方法;经过构造函数实现的继承,都是独立的属性和方法。因而咱们大打算利用这一点,将两种方式组合起来:经过在原型上定义方法实现对函数的复用,经过构造函数的方式保证每一个实例都有它本身的属性。
下面我再举个栗子,让你们感觉下组合继承的好处~
//组合继承
// 偶像练习生大赛开始报名了
// 初赛,咱们找了一类练习生
// 这类练习生都有名字这个属性,但名字的值不一样,而且都有爱好,而爱好是相同的
// 只有会唱跳rap的练习生才可进入初赛
function student(name){
this.name = name;
this.hobbies = ["sing","dance","rap"];
}
// 咱们在student那类里面找到更特殊的一类进入复赛
// 固然,咱们已经知道初赛时有了name属性了,而不一样练习生名字的值不一样,因此使用构造函数方法继承
// 同时,咱们想再让练习生们再介绍下本身的年龄,每一个子类还能够本身新增属性
// 固然啦,具体的名字年龄就由每一个练习生实例来定
// 类只告诉你,有这个属性
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
// 而你们的爱好值都相同,这个时候用原型链继承就好啦
// 每一个对象都有构造函数,原型对象也是对象,也有构造函数,这里简单的把构造函数理解为谁的构造函数就要指向谁
// 第一句将子类的原型对象指向父类的实例对象时,同时也把子类的构造函数指向了父类
// 咱们须要手动的将子类原型对象的构造函数指回子类
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;
// 决赛 kunkun和假kunkun进入了决赛
let kunkun = new greatStudent('kunkun','18');
let fakekun = new greatStudent('fakekun','28');
// 有请两位选手介绍下本身的属性值
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies) // => fakekunkun 28 ["sing", "dance", "rap"]
// 这个时候,kunkun选手说本身还有个隐藏技能是打篮球
kunkun.hobbies.push("basketball");
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap", "basketball"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies)// => fakekun 28 ["sing", "dance", "rap"]
// 咱们能够看到,假kunkun并无抄袭到kunkun的打篮球技能
// 而且若是这个时候新来一位选手,从初赛复赛闯进来的一匹黑马
// 能够看到黑马并无学习到kunkun的隐藏技能
let heima = new greatStudent('heima','20')
console.log(heima.name,heima.age,heima.hobbies) // => heima 20 ["sing", "dance", "rap"]
复制代码
能够看到,组合继承避开了原型链继承和构造函数继承的缺点,结合了二者的优势,成为了javascript中最经常使用的继承方式。
这种继承的思想是将传入的对象做为建立的对象的原型。
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
复制代码
咱们来实现下原型式继承,看看会不会有什么问题
// 原型式继承
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
let person = {
name:'JoseyDong',
hobbies:['sing','dance','rap']
}
let person1 = createObj(person);
let person2 = createObj(person);
console.log(person1.name,person1.hobbies) // => JoseyDong ["sing", "dance", "rap"]
console.log(person2.name,person2.hobbies) // => JoseyDong ["sing", "dance", "rap"]
person1.name = "xixi";
person1.hobbies.push("basketball");
console.log(person1.name,person1.hobbies) //xixi ["sing", "dance", "rap", "basketball"]
console.log(person2.name,person2.hobbies) //JoseyDong ["sing", "dance", "rap", "basketball"]
复制代码
这个时候咱们发现,修改了person1的hobbies的值,person2的hobbies的值也变了。
这是由于包含引用类型的属性值始终会共享相应的值,这点跟原型链继承同样~
而修改了person1.name的值,person2.name的值并未发生改变,并非由于person1和person2有独立的name值,而是由于person1.name = "xixi"这条语句是给person1实例对象添加了一个name属性,而它的原型对象上name值并无被修改,因此person2的name没有变化。由于咱们找对象上的属性时,老是先找实例对象,没有找到的话再找原型对象上的属性。实例对象和原型对象上若是有同名属性,老是先取实例对象上的值。
ESMAScript5新增了Object.create()方法规范化了原型式继承~
建立一个仅用于封装继承过程的函数,该函数在内部以某种形式来作加强对象,最后返回对象。
//寄生式继承
function createObj(o){
let clone = Object.create(o);
clone.sayName = function(){
console.log('hi');
}
return clone
}
let person = {
name:"JoseyDong",
hobbies:["sing","dance","rap"]
}
let anotherPerson = createObj(person);
anotherPerson.sayName(); // => hi
复制代码
固然,用寄生式继承来为对象添加函数,和借用构造函数模式同样,每次建立对象都会建立一遍方法。
前面咱们说了,组合继承是javascript最经常使用的继承模式。这里咱们先来回顾下组合式继承的代码:
//组合继承
function student(name){
this.name = name;
this.hobbies = ["sing","dance","rap"];
}
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;
let kunkun = new greatStudent('kunkun','18');
复制代码
组合继承最大的缺点是最调用两次父构造函数
一次是设置子类实例的原型的时候:
greatStudent.prototype = new student();
复制代码
一次是在建立子类型实例的时候:
let kunkun = new greatStudent('kunkun','18');
复制代码
在这个例子中,若是咱们打印一下kunkun这个对象,咱们就会发现greatStudent.prototype和kunkun都有一个属性为hobbies。
这其实就是实例对象和原型对象上的属性值重复了,而再找属性值的时候,在实例对象上找到了属性值就不会在原型对象上找了,而这部分原型对象上的值就实打实的浪费了存储空间。
那么咱们该如何精益求精,避免这一次重复调用呢?
若是咱们不使用greatStudent.prototype = new student(),而是直接让greatStudent.prototype访问到student.prototype呢?
看看如何实现:
// 寄生组合式继承
function student(name){
this.name = name;
this.hobbies = ["sing","dance","rap"];
}
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
//关键的三步 实现继承
// 使用F空函数当子类和父类的媒介 是为了防止修改子类的原型对象影响到父类的原型对象
let F = function(){};
F.prototype = student.prototype;
greatStudent.prototype = new F();
let kunkun = new greatStudent('kunkun','18');
console.log(kunkun);
复制代码
打印结果:
能够看到,kunkun实例的原型对象上再也不有hobbies属性了。
最后,咱们封装下这个继承方法:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
let prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当咱们使用的时候:
prototype(Child, Parent);
复制代码
引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:
这种方式的高效率体现它只调用了一次 Parent 构造函数,而且所以避免了在 Parent.prototype 上面建立没必要要的、多余的属性。与此同时,原型链还能保持不变;所以,还可以正常使用 instanceof 和 isPrototypeOf。开发人员广泛认为寄生组合式继承是引用类型最理想的继承范式。
总而言之就是,这种js实现继承的方式是最佳的。
然而,ES6以后经过extends关键字实现了继承。
// ES6
class parents {
constructor(){
this.grandmather = 'rose';
this.grandfather = 'jack';
}
}
class children extends parents{
constructor(mather,father){
//super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。
super();
this.mather = mather;
this.father = father;
}
}
let child = new children('mama','baba');
console.log(child) // =>
// father: "baba"
// grandfather: "jack"
// grandmather: "rose"
// mather: "mama"
复制代码
子类必须在 constructor 方法中调用 super方法,不然新建实例时会报错。这是由于子类没有本身的this 对象,而是继承父类的 this 对象,而后对其进行加工。
只有调用 super 以后,才可使用 this 关键字,不然会报错。这是由于子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。
ES5 的继承实质是先创造子类的实例对象 this,而后再将父类的方法添加到 this 上面(Parent.call(this))。
ES6 的继承机制实质是先创造父类的实例对象 this (因此必须先调用 super() 方法),而后再用子类的构造函数修改 this。
es6实现继承的核心代码以下:
function _inherits(subType, superType) {
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
复制代码
子类的 proto 属性:表示构造函数的继承,老是指向父类。 子类 prototype 属性的 proto 属性:表示方法的继承,老是指向父类的 prototype 属性。
除此以外,ES6 能够自定义原生数据结构(好比Array、String等)的子类,这是 ES5 没法作到的。