目录
- 前言
- 正文内容
- 原型链继承
- 构造函数继承
- 组合继承[常用]
- 原型式继承
- 寄生式继承
- 寄生组合式继承[最高效]
- 总结
前言
在之前的学习中总能看到继承这个词,今天就拿出来单独学习一下
正文内容
原型链继承
javascript"> //父类
function Parent() {
this.name = '小A同学';
this.age = 18;
}
//子类
function Child() {
}
//原型链继承
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.name) //小A同学
console.log(child1.age) //18
这种方式比较好理解,但是有一个弊端,对于创建每一个 child 实例时都不能传参,如果想要传参就必须每一次重新在原型链继承里修改(Child.prototype = new Parent(这里添加参数)
),所以需要频繁传参时效率很低。
而且这样继承会让引用类型的属性被所有实例共享
javascript"> //父类
function Parent() {
this.name = ['小A同学','小B同学']; //将姓名改为数组,方便增加
}
//子类
function Child() {
}
//原型链继承
Child.prototype = new Parent();
var child1 = new Child();
child1.name.push('新转来的小C同学') //改变了 child1 实例的属性
console.log(child1.name)
var child2 = new Child();
console.log(child2.name)
emm可以看到我们仅仅在代码里改了 child1 的姓名,然后 child2 也变了…(形象化理解就是你爹生了五个孩子,你穿了一件衣服,其他兄弟姐妹身上也莫名其妙多出来一件衣服)所以除了某些特定场景下比较好用,一般的操作并不好用。
至于具体原因,我们通过简单的实验来看看
javascript"> //父类
function Parent() {
this.name = ['小A同学','小B同学'];
}
//子类
function Child() {
console.log(this) //打印每一次创建实例的时候的东西
}
//原型链继承
Child.prototype = new Parent();
var child1 = new Child();
child1.name.push('新转来的小C同学')
console.log("-----分割线-----")
var child2 = new Child();
显然没有东西,所以在进行console.log(child1.name)
和console.log(child2.name)
时在 Child
里找不到属性 name
,于是顺着原型链往上找,所以两个打印出来都是一样的。
构造函数继承
javascript"> //父类
function Parent() {
this.name = ['小A同学', '小B同学'];
}
//子类
function Child() {
Parent.call(this); //将 Parent 的指针指向 Child
}
var child1 = new Child();
child1.name.push('新转来的小C同学'); //新增一个
console.log(child1.name);
var child2 = new Child();
console.log(child2.name);
首先这个方式避免了所有的属性共享(夸!!)
这里也详细展开看看为什么可以避免属性共享
javascript"> //父类
function Parent() {
this.name = ['小A同学', '小B同学'];
}
//子类
function Child() {
Parent.call(this); //将 Parent 的指针指向 Child
console.log(this)
}
var child1 = new Child();
child1.name.push('新转来的小C同学'); //新增一个
console.log("------分割线------")
var child2 = new Child();
我们可以看到,在使用Parent.call(this)
改变了指针指向之后,每一次的Child
实例都有自己的 name
属性。
【----恍然大悟----】
我们继续往下看,这种继承方法除了不共享外,而且传参起来更加快捷,就像这样。
javascript"> //父类
function Parent(name) {
this.name = name;
}
//子类
function Child(name) {
Parent.call(this,name); //将 Parent 的指针指向 Child
}
var child1 = new Child("传入小A同学");
console.log(child1.name);
var child2 = new Child("传入小B同学");
console.log(child2.name);
但是这样的写法有一个弊端,就是如果构造函数稍微复杂一点,里面包含了一些方法,那么在创建实例时不管用没用到这些方法,JavaScript都会创建这些方法,会造成内存空间浪费,比如这个例子
javascript"> //父类
function Parent(name) {
this.name = name;
//这里有一个方法 fun
this.fun = function () {
}
}
//子类
function Child(name) {
Parent.call(this, name);
console.log(this) //打印一下当前this包含的内容
}
var child1 = new Child("小A同学");
console.log(child1.name);
var child2 = new Child("小B同学");
console.log(child2.name);
我们可以很清楚的看到,我们创建的实例并没有使用 fun 方法,但是 this 指针里已经包含了。
组合继承[常用]
这种继承方式就是为了解决以上两个方法的缺点而产生,结合了原型链继承和构造函数继承。
设计思路猜测(使用面向对象思想):一个类分为属性和方法,因为属性需要更高的自由度(例如传参以及每一个实例不同的值),所以不能采用原型链继承,而类里的方法属于每一个实例,所以可以直接绑定到原型链最顶端,不用的时候不由去创建,用的时候再顺着原型链去找。
javascript"> //父类
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
//子类
function Child (name) {
Parent.call(this, name); //构造继承
}
//原型链继承
Child.prototype = new Parent();
var child1 = new Child('小A同学');
child1.colors.push('black');
console.log(child1.name);
console.log(child1.colors);
var child2 = new Child('小B同学');
console.log(child2.name);
console.log(child2.colors);
通过结果,我们看到,首先支持传参,其次不会出现数据共享
我们再来看看是否会有类方法的赘余
javascript"> //父类
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
//子类
function Child (name) {
Parent.call(this, name); //构造继承
console.log(this)
}
//原型链继承
Child.prototype = new Parent();
var child1 = new Child('小A同学', '18');
console.log("-------分割线--------")
var child2 = new Child('小B同学', '20');
我们看到在创建实例时并没有我们在代码里写的getName
方法,那他究竟跑哪去了呢,我们展开原型链看看。
在通过 child1.getName()
调用时,会先在 Child
里找这个方法,但是没有,那就去父类里找(图中红框),还是找不到那就只能再去父类的父类里找(蓝框里),最后终于找到了!比起之前每次创建实例都要创建一个方法,这样的效率显然更高!!!是 JavaScript 中最常用的继承模式。
但还是有一个缺点,就是无论如何都会出现重复调用的情况
原型式继承
javascript"> function objcet(obj) {
function F() {
}
F.prototype = obj;
return new F();
}
首先区别一下,这不是原型链继承!!!!
这个东西实质上就是 Object.create()
方法,但是奈何 ES6 之前没有这种用法,所以就有了上面那段代码。
先来看看Object.create()
方法
javascript"> const cat = {
colors: ['white', 'black']
}
var cat1 = Object.create(cat)
var cat2 = Object.create(cat)
cat1.colors.push('blue')
console.log(cat1)
console.log(cat2)
console.log(cat1.colors)
console.log(cat2.colors)
通过Object.create(cat)
创建了一个原型为 cat 的空对象,并且属性会共享
然后我们看看ES6 之前的写法,也就是刚开始那段
先回忆一下Object.create()
得流程
- 接受一个对象A
- 返回一个新对象B
- 把 A 设置为 B 的原型
然后我们试验一下
javascript"> const cat = {
colors: ['white', 'black']
}
function objcet(obj) {
function F() {
}
F.prototype = obj;
return new F();
}
var cat1 = objcet(cat)
var cat2 = objcet(cat)
cat1.colors.push('blue')
console.log(cat1)
console.log(cat2)
console.log(cat1.colors)
console.log(cat2.colors)
和前面的结果一模一样
寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
javascript"> function createObj(o) {
var clone = Object.create(o); //通过调用函数创建一个新对象
clone.sayName = function () { //增加这个对象的方法函数
console.log('hi');
}
return clone; //返回这个对象
}
var person = {
name: "小A同学",
color: ["red", "blue", "green"]
};
var anotherPerson = createObj(person);
anotherPerson.sayName();
console.log(anotherPerson.name)
console.log(anotherPerson.color)
缺点也很清楚,每一次创建实例都会重复创建方法。
javascript"> function createObj(o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
console.log(clone) //打印一下看看
return clone;
}
var person = {
name: "小A同学",
color: ["red", "blue", "green"]
};
var Person1 = createObj(person);
console.log("----------分割线----------")
var Person2 = createObj(person);
寄生组合式继承[最高效]
寄生组合式继承顾名思义,就是寄生继承+组合继承的结合体,前面也说了,组合继承会有重复调用的问题,寄生继承会重复创建方法,所以我们要攻克这两个问题。
现在就要组合继承中的Parent.call(this, name);
和Child.prototype = new Parent();
哪一句可以找到别的方法替换掉,从而避免二次调用。
Parent.call(this, name);
是去不掉的,如果去掉了实例的属性就共享了
那就只能想办法替换 Child.prototype = new Parent();
,既然直接调用不行,我们找点间接访问的步骤
javascript">//父类
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
//子类
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('小A同学', '18');
console.log(child1);
这里的思路很清晰了,就是通过复制父类原型的办法避免直接调用父类构造函数,减少一次调用。那我们把这个思路结合到寄生式继承里面看看,寄生式继承的核心在于用来封装的那个函数,所以我们要在那里面动动手脚。
于是看看寄生组合式继承的思路:通过借用构造函数来继承属性(call),通过原型链的混成形式来继承方法(Object.create)
这里直接把《JavaScript高级程序设计》中的代码改一改拿来解释,因为学到这里还是不会写呜呜
javascript">// 实现继承的核心函数
function inheritPrototype(Child,Parent) {
function F() {}
//F()的原型指向的是Parent
F.prototype = Parent.prototype;
//Child的原型指向的是F()
Child.prototype = new F();
// 重新将构造函数指向自己,修正构造函数
Child.prototype.constructor = Child;
}
// 设置父类
function Parent(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
Parent.prototype.sayName = function () {
console.log(this.name)
}
}
// 设置子类
function Child(name, age) {
//构造函数式继承--子类构造函数中执行父类构造函数
Parent.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(Child, Parent)
// 添加子类私有方法
Child.prototype.sayAge = function () {
console.log(this.age);
}
var child1= new Child("小A同学",18)
可以看到,该有的属性都有,而且比起组合继承只调用一次构造函数更加高效。
梳理一下流程
总结
到这里终于学习完了,通过深入学习这些,得到的知识更多的是一种编程的思想和规范。
这一阶段的学习到这里也就告一段落,这十二篇看起来不是很多,但是真正自己钻研起来还是有很多内容。JavaScript中东西有很多,也许之后会更一个新的系列来继续学习,这个系列先到这里啦。