对象继承
什么是继承
继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法
实现继承就需要完成两件事
- 子类可以获取父类的属性和方法
- 子类可以追加属性方法
怎么实现继承
对于js 来说,每个对象都可以添加属性和方法,所以我们更关注的是如何获取父类的属性和方法
根据js 的 原型链
的特性 我们可以通过原型链
来获取父类的属性和方法
原型链为什么可以获取父类的属性和方法
当试图得到一个对象的某个属性时,
如果这个对象本身没有这个属性,
那么会去它的*proto*(即他的构造函数的prototype)中寻找。
如果没有,则会接着往上找,一直上溯到Object.prototype,知道null为止
也就是说所有对象都继承Object.prototype的属性,Object.prototype的原型是null,null没有任何属性和方法。
所以 只要把父类的属性和方法放在子类的构造函数的prototype
或者在之前的构造函数的prototype
,就可以获取到父类的属性和方法
现有的继承方法
- 原型链继承
- 借用构造函数继承 伪造对象 或经典继承
- 组合继承(组合原型链继承和借用构造函数继承)(常用)
- 原型式继承
- 寄生式继承
- 寄生组合式继承(最理想)
- ES6继承 (class)(extends)
现在来实现对以下构造函数的继承
function Person(){
this.name = 'asa' ;
this.age= '11';
this.class = ['en','math'];
this.sayName = function(){
alert(this.name);
}
}
Person.prototype.like = 'fruit';
Person.prototype.sayHi = function(){
alert('Hi');
}
1. 原型链继承 -- 【原型链】
顾名思义 直接使用原型链继承。
首先创建一个对象,
1.基本方式创建对象
var a = {a:b};
var b = new Object();
2.构造函数创建对象
function Child (name) {
this.name = name;
}
图2 为没有这一行 我们先不看继承的person
Child.prototype = new Person();
生成两个实例
var per1 = new Child("la");
var per2 = new Child("laa");
输出一下就发现已经获取到父类的属性和方法
console.log(per1.like); // fruit
console.log(per1.age); //11
console.log(per1.name); //la
per1.sayName();
per1.sayHi();
console.log(per2.like); // fruit
console.log(per2.age); //11
console.log(per1.name); //laa
per2.sayName();
per1.sayHi();
观察原型链
查看原型链 就相当与 a ,b 继承自Object
构造函数生成的实例
看图可以知道 Child实例Per
可以获取到 Child.prototype
上的属性和方法
其实这就相当于与一个继承 Per 继承 Function Child的原型对象
了
为什么构造函数生成的对象 会继承构造函数的原型对象
呢 这就是new
干的事了
new
会把新生成的对象的__proto__
指向构造函数的原型对象
但是我们要实现的继承 是对一个指定的类型继承,我们还要继承Person
查看上面的关键一行代码 就发先他就干了一件事~
Child.prototype = new Person();
如下图
橙色是记这个代码之前的原型链
由于 Child.prototype = new Person();
所以 Child的原型就指向了Person实例
的原型对象
好了这就完成了指定类的继承,这就是最基本的原型链继承
这种办法最为简单但是也有很多问题,所以一般都不会单独使用哒
//问题
per1.calss.push('PE');
console.log(per1.class) // ['en','math','PE'];
console.log(per2.class) //['en','math','PE'];
问题:
- 引用类型的问题 【所有实例继承的引用类型都指向同一个地址 所以per1 操作的时候 Per2 的值也会改变】
- 继承时不能向父类中传递参数
2. 借用构造函数继承 伪造对象 或经典继承
为了解决原型链继承的问题就有了经典继承的方法
怎样解决引用类型的问题
,那就是每个实例都有独立的属性,而不是去获取原型链上的属性,这样就不会相互影响了,在实例生成的时候就立刻给这个实例附上所有属性和方法
代码:
function Child2(name){
Person.call(this,"jer",10)// 调用了 父构造函数 可以传参 提高自由度
// Person2.call(this,"") 多个构造函数 多继承
this.name = name;
}
这个实际上就不是把属性和方法放在原型上 ,而是直接添加到每个子实例中 添加方法就是运行一遍父构造函数,运行的时候还可以传参,就同时解决了第二个传参的问题
var per3 = new Child2('rr');
var per4 = new Child2('ss');
验证一下 就发现 Person 已经不在 实例的原型上了,因为我们只是把构造函数当做普通函数运行了一遍
console.log(per3 instanceoof Person)
验证一下 果然就不会产生两个实例的属性已经互不影响了
per3.calss.push('PE');
console.log(per3.class) // ['en','math','PE'];
console.log(per4.class) //['en','math'];
原型链 与上面没有继承父类的原型链长得一模一样 ,但是每个实例上都添加了方法和属性,这就产生了新的问题。这样就会发现
问题1
我们看着两个函数,根据上面的代码运行后 就生产了两个作用一致代码一致并且不会变动的函数,这就会造成冗余了
其实原型链那种方式 这两个函数也是有冗余的,但是那种方法可以把这个函数放在超类的原型上,所以实际没有这种问题
per3.sayName();
per4.sayName();
问题2
Person 不在原型链上 所以Person的原型对象上的方法也就不能被子类实例所使用啦
per3.sayHi(); // 会报错 没有该函数
per4.sayHi();
问题 :
- 冗余问题
- 继承时不能向父类中传递参数
由于以上问题,我们一般也不单独使用借用构造函数哒~
3.最常用的方法 -- 组合继承
那么,既然两种方法互补,那么合成大法来了---组合继承
想一下 把sayName
这种公用的方法,或者公用的属性,放在原型链上,用原型链方式继承
把class
这种独有的属性,借用构造函数继承,就很完美了~
代码:
借用构造函数
继承属性 此时 Person调用一次
function Child3(name){
Person.call(this,name);
}
组合原型继承
方法 new的时候又调用一次
Child3.prototype = new Person();
var per5 = new Child3("xiaomi");
var per6 = new Child3("xiaoming");
输出一下就发现已经获取到父类的属性和方法
console.log(per1.like); // fruit
console.log(per1.age); //11
per1.sayName();
console.log(per2.like); // fruit
console.log(per2.age); //11
per2.sayName();
验证一下 这两个实例的属性也是互不影响了
per3.calss.push('PE');
console.log(per3.class) // ['en','math','PE'];
console.log(per4.class) //['en','math'];
而且可以获取 原型链上的方法 可以有公共的方法
per5.sayHi();
per6.sayHi();
组合继承很好的避免了原型链和借用构造函数的缺陷,融合了他们的优点
问题
不过组合继承也有一个小小的问题,那就是超类构造函数会运行两次
以下几个有时间再好好看,现在还不太理解,暂时放着 //todo
4.原型式继承
5.寄生式继承
6.寄生组合式继承(最理想)
7.ES6继承 (class)(extends)
es6 出了新的语法糖class
之间通过使用extends
关键字,这比通过修改原型链实现继承,要方便清晰许多
新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。实际上还是运用上面的组合继承的方法。通过原型链继承方法,借用了构造函数实现专有属性。
代码:
先来看class
class
我的理解是 class
更像是构造函数的原型对象,他可以找到构造函数,并可以存放属性和方法
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
console.log(this.x);
}
}
es5 实现 你就发现 class
实际就是一个构造函数 和 原型上的方法组成~ 不过就是更简单清晰了
function Point(x,y){
this.x = x;
this.y = y;
}
Point.prototype.toString(){
console.log(this.x);
}
再来看extends
class Colorpoint extends Point {
//这个就是默认方法 使用new 生成实例时 会调用这个方法, 会把Point加在原型链上
//如果未定义 会自动添加
constructor(x,y,color){
//子类必须在constructor方法中调用super方法,否则新建实例时会报错
//这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象。
super(x,y); //调用父类构造函数(Point.prototype.constructor.call(this,x,y))
this.color = color
//隐式返回 this
// 如果显示返回对象 就是该对象
}
toString(){
//通过 super调用父类的方法
return this.color + ' ' + super.toString();
}
}
差异
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。