超详细通俗讲解js继承

原型链继承

实现:让子类构造函数的原型指向父类构造函数的实例。子类实例不仅会有父类构造函数中的属性,还会继承父类原型上的属性。

因此对于父类原型上的引用类型属性,所有实例都可修改。修改了子类构造函数的的原型对象,还失去了原先默认的constructor属性。即子类原型的 constructor不指向子类构造函数,而指向父类构造函数。

下例中继承关系:SuperType.prototype <= SuperType的实例们,例如instance1。。。其中一个实例 <= SubType.prototype <= SubType的实例们,例如instance2。

重点:切断了子类实例与子类构造函数之间的关系,instance2.constructor === SuperType。即SubType.prototype本应该通过constructor指向SubType,但是现在由于SubType.prototype通过原型链_prpto_指向SuperType.prototype, 而SuperType.prototype的contructor指向SuperType。

即:SubType的实例 => SubType.prototype => SuperType.prototype(通过_proto_一步一步向上访问原型链)

重点1:通过构造函数new出来的对象都具有相同的属性方法,即父类SuperType的实例都有a = 1的属性,instance1与子类SubType.prototype都有,这样子类的实例即SuperType的实例也有,即instance2也有。

重点2:instance1与SubType.prototype分属于父类的不同实例,对他们两个实例进行分别赋值不会影响到彼此。即设置SubType.prototype.m = 10; instance1仍然没有m = 10;属性。

重点3:何时添加属性会屏蔽原型中的同名属性,何会修改原型中的属性。对于父类原型上定义的属性b,父类,父类实例,子类,子类实例都可以访问到。若其是复杂数据类型,即使通过子类实例也可以修改。创建对象模式中原型模式有同样的注意点,需注意。

注意: 用赋值运算符操作同名属性就是重写,相当于实例自己为自身添加属性,不会影响到原型;

用其他方法就是修改原型属性,所以也会影响到其他原型的实例。

区分:修改的是实例本质还是原型上的方法。通过构造函数构造出来的实例都有属于自己 一套和构造函数一样 属性,实例彼此之间不共享。原型上的属性才是共享,修改的话会影响所有实例。

重点理解:构造函数生成的实例,每个实例都有构造函数中的属性方法。原型上的属性方法可以访问,但是不是自己独有,可以对复杂数据类型修改,简单数据类型无法修改,相当于直接利用赋值运算符在实例上定义,会覆盖原型上属性,所以不会修改到原型上的简单数据类型。当然可以通过实例._proto_来修改。

步骤:

  1. 首先执行11行,实例化SuperType,指向SubType.prototype,这个时候SubType.prototype上有属性a。当然也可以 访问到父类原型上的属性d, e,但是不是SubTYpe.protytype自己的,是所有SuperType实例共享的。
  2. 执行12行,实例化SubType,指向instance2,这个时候instacce2上有SubType构造函数上的属性b;当然也可以访问到SubType.prototype上的属性,只不过是所有SubType实例共享。

 

function SuperType() {
    this.a = 1;
}
SuperType.prototype.d = [1, 2, 3];
SuperType.prototype.e = ['a'];
var instance1 = new SuperType();

function SubType() {
    this.b = 2;
}
SubType.prototype = new SuperType();
var instance2 = new SubType();

// 子类实例修改父类原型上的复杂数据类型   =》 修改!!!!!!!!!!
instance2.d.push(5);
console.log(instance2.d) // 子类实例修改
console.log(instance1.d) // 父类实例也会修改:[1, 2, 3, 5]

// 子类重写属性,会屏蔽掉父类原型中的属性,仅重写本次子类实例的属性  =》 重写!!!!!!!!!!!!!
instance2.e = ['a', 'b']  
console.log(instance2.e) // ['a', 'b']
console.log(instance1.e) // ['a'] 父类别的实例仍然不变

 

重点4:给子类原型添加属性的代码 一定要放在替换原型的语句 之后。因为替换掉原型后,与原先的原型就断了关系,所以访问不到之前添加的属性了。

function SuperType() {
    this.a = 1;
}
SuperType.prototype.d = [1, 2, 3];

function SubType() {
    this.b = 2;
}
// 先给子类原型添加属性f
SubType.prototype.f = 10;
// 再换掉原型
SubType.prototype = new SuperType();
var instance2 = new SubType();

// 此时访问不到之前为原型添加的属性了,因为与之前的原型断了关系,现在instance._proto_ => SubType.prototype = SuperType
console.log(instance2.f) // undefined

 

重点5:通过原型链实现继承时,不能使用对象字面量创建原型方法。

即赋值运算符就是重写,例如构建继承关系时SubType.prototype = new SuperType();就是重写了子类的原型,切断了与之前原型的联系;若是后面再使用赋值运算符进行操作,则又是重写,又会切断上一次 最新 建立 好的原型联系。

function SuperType() { this.a = 1; } var instance1 = new SuperType(); SuperType.prototype.d = [1, 2, 3]; function SubType() { this.b = 2; } SubType.prototype = new SuperType(); // 通过对象字面量创建属性,相当于重写原型,会上一步刚建立好的继承关系 SubType.prototype = { f: 1 }; var instance2 = new SubType(); console.log(instance2.a) // undefined

 

通过点的方式来引用,而不是赋值操作来重写

function SuperType() {
    this.a = 1;
}
var instance1 = new SuperType();
SuperType.prototype.d = [1, 2, 3];

function SubType() {
    this.b = 2;
}

SubType.prototype = new SuperType();
// 通过对象字面量创建属性,相当于重写原型,会上一步刚建立好的继承关系
SubType.prototype = {
    f: 1
};
var instance2 = new SubType();

console.log(instance2.a) // undefined

缺点:

1、父类原型上引用类型的数据也会被所有数据共享,但是修改一个实例上的属性,其他实例也会受影响。

2、创建子类型实例时,不能向父类的构造函数中传递参数。即创建出来的子类 实例都是具有相同值的实例,不能通过向构造函数中传参得到不同属性值的实例。例如下面所有的子类实例都具有a = [1, 2, 3],而不能满足 想要所有实例都具有属性a,但是属性值不同。(缺点同创建对象时的原型模式)

function SuperType() {
    this.a = [1, 2, 3];
}
var instance1 = new SuperType();

function SubType() {
    this.b = 2;
}

// 子类SubType.prototype变成了SuperType的一个实例,所以自己也有a = [1, 2, 3]的属性,子类的所有实例也共享这一属性
SubType.prototype = new SuperType();

var instance2 = new SubType();
var instance3 = new SubType();
instance2.a.push(4)

console.log(instance1.a) // [1, 2, 3]
console.log(instance2.a) // [1, 2, 3, 4]
console.log(instance3.a) // [1, 2, 3, 4]

构造函数继承

实现:在子类构造函数内部通过call() , apply() 调用父类构造函数。

虽说只是通过call()调用,但是this每次指向都是不同的调用对象,即每个对象拥有的都是独有一份的,也相当于拥有了

下例中,子类构造函数生成的对象instance2, instance3生成的过程中都会进行初始化,都会执行SuperType.call(this);即两个实例都保有(继承)了父类的属性,最终结果是每个实例都有a = [1, 2, 3]的副本,各不影响。既然实例本身也有属性了,后面实例instance1修改a,只是修改自己本身的属性,而不会影响到其他。

优点:传递参数。子类可以向父类构造函数传递参数。

缺点:方法都在构造函数中定义,无法做到函数复用

function SuperType() {
    this.a = [1, 2, 3];
}
var instance1 = new SuperType()

function SubType() {
    // 在子类构造函数内部通过call()调用父类构造函数从而实现继承
    SuperType.call(this);
}

var instance2 = new SubType();
var instance3 = new SubType();
instance2.a.push(4)

console.log(instance1.a) // [1, 2, 3]
console.log(instance2.a) // [1, 2, 3, 4]
console.log(instance3.a) // [1, 2, 3]

优点:传递参数。子类可以向父类构造函数传递参数。

function SuperType(name) {
    this.name = name;
}

function SubType() {
    // 在子类构造函数内部通过call()调用父类构造函数从而实现继承
    SuperType.call(this, 'JJ');
}
var instance1 = new SubType();
console.log(instance1.name); //JJ

缺点:无法做到函数复用

实例化的子类对象instance2, instance3 本身都有sayName()方法,这是实例化的时候从父类中继承的,每个实例人手一份,互不影响。但是这些方法是相同的功能,每个实例都有会造成内存浪费,没有做到函数复用。相同功能的函数应该做到复用共享使用。

同理,子类构造函数在继承完父类的构造函数后,也可定义子类自己的方法,但是由于在构造函数中定义,所以所有子类都会有此方法,也没有造成函数复用。

function SuperType() { this.a = [1, 2, 3]; this.sayName = function() { console.log('hello') } } var instance1 = new SuperType() function SubType() { SuperType.call(this); } // 实例化的instance2 instance3 都是由子类构造函数创建的,都有属性a 和方法sayName()。 // 对于实现同样功能的方法sayName则显得重复,没有做到复用 var instance2 = new SubType(); var instance3 = new SubType(); instance2.sayName(); // hello instance2.sayName(); //hello

 

缺点:子类实例无法访问父类原型中的属性方法

说到底,构造函数继承只是通过call()来借用父类构造函数 中的属性方法,但是不是继承关系,所以肯定访问不到父类原型中的值。

function SuperType() {
    this.a = [1, 2, 3];
    this.sayName = function() {
        console.log('hello')
    }
}
var instance1 = new SuperType()

function SubType() {
    SuperType.call(this);
}

// 实例化的instance2 instance3 都是由子类构造函数创建的,都有属性a 和方法sayName()。
// 对于实现同样功能的方法sayName则显得重复,没有做到复用
var instance2 = new SubType();
var instance3 = new SubType();
instance2.sayName(); // hello
instance2.sayName(); //hello

组合继承

实现:使用原型链实现对原型方法的继承,实现函数复用;借助构造函数实现对实例属性的继承,从而保证每个实例都有自己的属性。

重点:手动修复constructor默认指向。这样instance2.constructor === SubType。否则会等于SuperType,因为重写了子类原型。

缺点:会调用两次父类构造函数。第一次调用,会继承父类原型和父类构造函数中的属性,都被定义在子类原型上。

第二次调用会继承父类构造函数中属性,这一次定义在子类实例上。因此实例化子类对象会重复定义两次父类构造函数属性,且相同,最后子类实例上还会屏蔽掉子类原型上的,会造成空间浪费。

function SuperType() {
    this.name = name;
    this.a = [1, 2, 3];
}
SuperType.prototype.color = 'red'

function SubType(name, age) {
    // 继承父类构造函数中的属性
    SuperType.call(this, name); // 第二次调用父类构造函数
    this.age = age;
}

// 继承父类原型中的属性
SubType.prototype = new SuperType(); // 第一次调用父类构造函数
// 手动修复constuructor指向,维护原先的默认constructor指向
SubType.prototype.constructor = SubType;
var instance2 = new SubType('JJ', 20);

 

原型式继承

实现:首先有一个对象作为基础,通过函数可以借助原型对其进行浅拷贝。不需要通过new调用,内部已经返回对象了。

优点:在不想兴师动众的创建构造函数,只想让一个对象与另一个对象保持类似的情况下

缺点:包含引用类型的属性会共享响应的值,就像原型链一样。因为是通过prototype继承才可以访问到模板对象的属性方法,并不是自己独有一份的,只是访问并不可以独有,所以对于模板对象上的复杂数据类型,子类对象可以修改;但对于简单数据类型,由于是通过赋值运算符操作修改,所以相当于直接子类对象自己定义同名属性,重写了模板对象的属性,会屏蔽掉模板对象的属性。同原型链继承。

function object(o) {
    // 创建一个临时性的构造函数
    function F() {}
    // 将传入的对象作为这个构造函数的原型
    F.prototype = o;
    // 返回临时构造函数的实例
    return new F();
}
var person = {
    a: 1,
    b: [3, 4, 5]
}
var person2 = object(person);
person2.b.push(6);

console.log(person.b); // [3, 4, 5, 6]
console.log(person2.b); // [3, 4, 5, 6]

es6实现:利用Object.create()

var person = { a: 1, b: [3, 4, 5] } var person2 = Object.create(person)

 

寄生式继承

实现:创建一个仅用于封装继承过程的函数,不通过new调用(内部已经返回对象了)。类似原型式继承的一种封装,但是函数内部还可以增强对象,添加新方法。类似于下例,浅拷贝传进去的original,但是又自己增加了属性c。

缺点:为对象添加函数时不能做到函数复用。若函数中增强对象添加的是方法sayName(),那么调用createPerson生成的实例都有相同的方法,从而实现内存浪费,没有做到复用。与构造函数模式类似。

即包装函数类似于构造函数,每个实例创建都要调用它,包装函数上的每个属性方法每个实例都有一份。若是包装函数上定义方法,则后面生成的实例每个都有,相当于没有做到函数复用。同构造函数继承。

function createPerson(original) {
    var clone = object(original)
    clone.c = 10;
    return clone;
}
var person = {
    a: 1,
    b: [3, 4, 5]
}
// 不通过new调用
var person3 = createPerson(person)

寄生组合式继承

优点:解决了组合继承两次调用父类构造函数的缺点。

第一次调用父类构造函数,让子类原型指向父类构造函数实例,这一步让子类原型上拥有父类构造函数属性,能访问到父类原型上方法。

而第二次调用子类构造函数生成子类实例时,子类构造函数中通过call也可访问到父类构造函数中的方法。由于是通过call来访问,所以其中this指向不同的子类实例,所以相当于子类实例也都拥有一套父类构造函数的方法。

综上,子类实例上有父类构造函数的属性,子类原型上也有,重复了,且会屏蔽。

既然后面子类实例上也会屏蔽子类原型上的,所以在子类原型上这一步就不需要定义,直接让子类原型访问到父类原型就可以了。

实现:

注意:手动修复constructor指向,这样instance2.constructor === SubType,否则是指向SuperType

function SuperType() {
    this.name = name;
    this.a = [1, 2, 3];
}
SuperType.prototype.color = 'red'

function SubType(name, age) {
    // 继承(拥有)父类构造函数中的属性
    // 虽说只是调用,但是this每次指向都是不同的调用对象,即每个对象拥有的都是独有一份的,也相当于拥有了
    SuperType.call(this, name); 
    this.age = age;
}

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(SubType, SuperType) {
    var pro = object(SuperType.prototype);
    // 手动修复constructor指向,这样instance2.constructor === SubType,否则是指向SuperType
    pro.constructor = SubType; 
    SubType.prototype = pro;
}

// 调用此函数,将子类原型指向父类原型
// 继承(访问)父类原型中的属性
inheritPrototype(SubType, SuperType); 

var instance2 = new SubType('JJ', 20);
console.log(instance2.constructor)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值