注意
首先前提是继承要做到,子类实例不能修改父类构造函数里的属性和方法,子类实例共享父类原型上的属性和方法(原型就是为了封装对象公共方法和属性而存在的)。在学习的时候总是搞混这两个概念,导致学的云里雾里的。
补充
声明对象的方式
//字面量声明
new obj={}
//构造函数创建
function obj(){}
let obj=new obj()
//通过Object()函数声明
let obj=new Object()
obj.a=''
obj.v=''
注意点:
- 对象之间不相等
- 对象是引用类型
- 对象的key都是字符串(将一个对象a作为另一个对象里的属性k名,那么这个对象a会变成[Object][Object]字符串)
原型链继承
对象能够访问到原型链上的属性和方法。 原型链继承的方式: new要被继承的构造函数,赋值给对象的原型。 就可以访问到Fn1的属性和方法了。
function Fn(){
this.a=1
this.fn=function(){}
}
function Fn1(){
this.b=2
this.fn1=function(){}
}
let fn1=new Fn()
//就是new了个对象给fn1的原型
fn1.__proto__=new Fn1()//Fn.prototype=new Fn1(),这两种写法是一个意思,因为
//Fn.prototype==fn1.__proto__
console.log(fn1.a,fn1.b);//1,2
new两个Fn()构造的对象fn3,fn4出来,尝试用fn3修改父类构造函数的属性
let fn3=new Fn()
let fn4=new Fn()
fn3.b="Fn1的b被修改了"
console.log(fn3.b,fn4.b);//Fn1的b被修改了 2
可以看到在fn3对象上新添加了父类同名属性b,覆盖了父类的属性
这里的属性b是基本类型变量,但是如果属性b是引用类型的变量,那么结果会不同。
function Fn(){
this.a=1
this.fn=function(){}
}
function Fn1(){
// this.b=2
this.b=[1,2,3,4]
this.fn1=function(){}
}
let fn1=new Fn()
Fn.prototype=new Fn1()
let fn3=new Fn()
let fn4=new Fn()
fn3.b.push(5)
console.log(fn3.b,fn4.b);//(5) [1, 2, 3, 4, 5] (5) [1, 2, 3, 4, 5]
子类对象共享一个prototype,所以fn3,和fn4的原型是同一个,而修改原型上的引用类型变量会引起所有子类该变量都改变(在上面父类的b属性已经改成数组,向数组里面push值,fn4的b属性数组也改变了。但是如果是给b赋值这种方式修改的话,不会引起所有子类共享变量改变,依旧是给fn3这个对象里添加一个同名属性覆盖原型上的属性)
function Fn(){
this.a=1
this.fn=function(){}
}
function Fn1(){
this.b=[1,2,3,4,5]
this.fn1=function(){}
}
Fn.prototype=new Fn1()
let fn3=new Fn()
let fn4=new Fn()
fn3.b="Fn1的b被修改了"
// fn3.b.push(6)
console.log(fn3,fn4);
构造函数继承
原型链继承会引发引用值共享的问题(引用类型,指向同一块内存空间)。解决方法,在子类构造函数里调用父类构造函数,改变this指向为子类实例,在子类里暴露父类的属性。
//构造函数继承
//明确问题:我们需要继承Fn1,并且在用Fn构建对象的时候,使得每个对象都能拿到
//继承的属性,且每个对象修改属性不会影响到其它对象
//Fn为子类,Fn1为父类,
function Fn(){
this.a=1
//把Fn1的this绑定到Fn上面,
//子类构造函数的this指向new出来的对象,也就是fn和fn1
// 在这里执行Fn1这个函数,并且改变Fn1的this为子类实例对象
//每个实例对象都会有Fn1里的属性,它们不再共享同一个数
//组c了
Fn1.call(this)
}
function Fn1(){
this.b=2
this.c=[1,2,3,4]
}
let fn=new Fn()
let fn1=new Fn()
fn.c.push(5)
console.log(fn,fn1)
构造函数的缺点,因为是直接在子类构造函数里调用父类的构造函数,如果在父类的原型上面添加了方法,子类是拿不到的。
组合继承(伪经典继承)
组合继承就是构造函数继承和原型链继承的结合,可以同时解决前面提到的两个问题。
function Fn(){
this.a=1
//把Fn1的this绑定到Fn上面
Fn1.call(this)
}
function Fn1(){
this.b=2
this.c=[1,2,3,4]
}
Fn1.prototype.say=function(){console.log(222);}
Fn.prototype=new Fn1()
let fn=new Fn()
let fn1=new Fn()
fn.c=2
console.log(fn,fn1)
组合继承里,向子类添加了两次属性
第一次是:Fn.prototype=new Fn1()
Fn的原型对象等于new Fn1()出来的值,注意,这个是挂载到原型上的。
第二次:Fn1.call(this)
这里new Fn1()创建实例对象的时候,向对象上添加了Fn1的属性,注意,是添加到对象身上的。
上面代码里,我在new两个实例后,用fn.c改变了一个实例的c(从父类继承来的)属性,可以看到没有影响到另一个实例继承来的c属性。
这里我分别注释了原型链继承和构造函数继承来检查。
问题:我们向实例对象里添加了两次从父类Fn1里继承来的属性,谁会生效呢?
上面说了,一个挂载到原型上,一个直接在对象自身添加,会先找自身的。
寄生组合继承
组合继承的缺点:调用了两次父类Fn1的构造函数。
通过Object.create(父类.prototype)来直接拿到父类的原型,而无需拿到父类的属性,这个方法是es5的,有些浏览器会存在兼容问题。
//寄生组合继承
function Fn(){
this.a=1
//把Fn1的this绑定到Fn上面
Fn1.call(this)
}
function Fn1(){
this.b=2
this.c=[1,2,3,4]
}
Fn1.prototype.say=function(){console.log(222);}
// Fn.prototype=new Fn1()
//通过Object.create(父类.prototype)来直接拿到父类的原型,而无需拿到父类的属性
Fn.prototype=Object.create(Fn1.prototype)
let fn=new Fn()
let fn1=new Fn()
fn.c=2
console.log(fn,fn1)
解决兼容问题:
原理
//解决兼容问题
function createObject(obj){
function F(){}
F.prototype=obj
return new F()
}
class方式继承
//class继承
class Person{
constructor(name,age){
this.name=name
this.age=age
}
speak(){
console.log(this.name+'在说话');
}
getAge(){
console.log(this.age);
}
}
//extends关键字继承父类
class Student extends Person{
constructor(name,age,sid){
//代表父类的构造方法,只能用在子类的构造函数中,用在其他地方就会报错;
//super虽然代表了父类的构造方法,但是内部的this指向调用这个函数的类的
//实例
//super要写在构造函数的最开始位置
super(name,age)
this.sid=sid
}
}
let p=new Student('xiaolee',12,20202765)
//子类继承父类的方法
p.speak()//xiaolee在说话
p.getAge()//12
p.getSId()//xiaolee的学号是20202765
子类可以重写父类的方法
class Person{
constructor(name,age){
this.name=name
this.age=age
}
speak(){
console.log(this.name+'在说话');
}
getAge(){
console.log(this.age);
}
}
class Student extends Person{
constructor(name,age,sid){
super(name,age)
this.sid=sid
}
getSId(){
console.log(this.name+"的学号是"+this.sid);
}
getAge(){
console.log("子类重写父类的方法");
}
}
let p=new Student('xiaolee',12,20202765)
p.speak()//xiaolee在说话
p.getAge()//子类重写父类的方法
p.getSId()//xiaolee的学号是20202765
总结几种继承方式的区别
-
原型链继承
- 将父类的实例添加到子类的原型上(子类实例共享一个子类原型),可以访问到父类构造函数的属性和方法,但是父类引用类型的属性是所有子类实例共享的。
-
构造函数继承
- 在子类构造函数里调用父类构造函数,相当于把父类的属性和方法添加到子类构造函数里了。但是因为是直接调用,父类this指向window,所以要显示的改变父类构造函数this指向为子类this。这个继承方式的问题是子类只能拿到父类构造函数里的属性和方法,不能拿到父类原型上的方法和属性。
-
组合继承
- 将上面两种方法结合起来。原型链继承承担拿到父类原型上的属性和方法的职责,构造函数继承承担拿到父类构造函数里的属性和方法的职责。
- 缺点是父类构造函数里的属性和方法被重复添加了。
-
寄生组合继承
- 将组合继承中原型链继承的部分改进了,把父类的原型挂载的一个函数上,返回这个函数的实例给子类的原型。
- 因为这个函数只挂载了父类的原型,所以子类的不会拿到父类的构造函数的属性和方法了。
-
class extends继承