彻底搞懂——js中实现继承的几种方式

继承的定义:

简单来说就是如果B继承了A,则B可以拥有A的属性和方法。A为B的父类(或超类,超类更准确一些,但为了直观理解,本文统称父类),B为A的子类。

实现继承的几种方式:

  1. 通过原型链(把子类的原型指向父类的实例,缺陷明显基本不单独使用)
  2. 利用构造函数(子类构造函数中使用call(或者apply)调用父类构造函数,缺陷明显基本不单独使用)
  3. 组合继承(原型链和构造函数方式的结合,优势互补)
  4. 原型式继承(利用原型可以基于已有的对象创建新对象,效果上与原型链的方式有类似,即引用值始终会在相关对象间共享)
  5. 寄生式继承(二次封装原型式继承,并拓展)
  6. 寄生式组合继承(引用类型继承的最佳模式)
  7. ES6中引入class关键字定义类,用extends实现继承(解决了上述6种方式的各种弊端,算个语法糖)

通过原型链:

关键思想:把子类的原型指向父类的实例,从而继承父类的构造函数内的属性和父类原型上的属性

// 创造一个超类型的构造函数Super(),为它设置静态属性name、原型方法getSuper()
function Super(){  
    this.name =["super"];
}
Super.prototype.getSuper = function(){
    return this.name;
}

// 再创造一个子类构造函数Sub()
function Sub(){}

Sub.prototype = new Super(); //继承,将Sub的原型对象Sub.prototype指向Super的实例

let sub1 =new Sub(); //创建Sub的实例sub1
sub1.name.push("sub1");

let sub2 =new Sub(); //创建Sub的实例sub2
sub2.name.push("sub2");
  
console.log(sub2.getSuper())//["super", "sub1", "sub2"] // 子类实例继承了父类的name属性和原型方法getSuper,并且子类实例共享父类引用属性name

优点:

  •  父类新增原型属性和方法,子类实例都能访问到
  •  简单、易用

缺点:

  •  无法实现多继承(一个子类继承成多个父类)
  •  创建子类实例的时候,无法向父类构造函数传参
  •  有子类实例共享父类引用属性的问题(因为子类的原型指向的是父类的一个实例,假如父类构造函数内的属性有一个是数组(引用类型),那么任一子类都可以操作这个数组,从而导致其他子类使用的这个数组也会发生变化,如上述代码)

利用构造函数:

关键思想:在子类构造函数中使用call(或者apply)调用父类构造函数实现父类构造函数内的属性和方法继承

// 父类
function Super(sex){
    this.name =["super"];
    this.sex = sex;
}
Super.prototype.getSuper = function(){
    return this.name;
}

// 子类
function Sub(sex){
    Super.call(this, sex); // 继承,在Sub中使用call去调用Super
}

let sub1 =new Sub('女'); //创建Sub的实例sub1
sub1.name.push("sub1"); //修改sub1的name属性值

let sub2 =new Sub(); //创建Sub的实例sub2
sub2.name.push("sub2"); //修改sub2的name属性值

// console.log(sub1.getSuper()) // Uncaught TypeError(不能继承原型链方法)
console.log(sub1.sex)  // 女 ,创建子类实例时,可以向父类传递参数
console.log(sub1.name)  // ['super', 'sub1'],子类实例继承了父类的name属性且可通过操作修改实例自身属性
console.log(sub2.name) //  ['super', 'sub2'],两个子类实例都继承了父类属性但修改子类实例自身属性并不影响父类和其它子类实例的相应属性

let super1 =new Super() 
console.log(super1.getSuper()) // ['super'],父类实例拥有父类构造函数内的属性和原型上的方法,修改子类实例属性时父类属性不受影响

优点:

  •  创建子类实例时,可以向父类传递参数
  •  可以实现多继承(在子类构造函数调用多个父类构造函数)
  •  解决了原型链继承中子类实例共享父类引用属性的问题(即使父类构造函数中有引用类型,在创建子类实例时,都会重新调用父类构造函数重新创建一份这个引用类型数据,重新申请引用类型的空间,不会互相影响篡改)

缺点:

  •  每次创建子类实例时,都要调用一次父类构造函数,影响性能
  •  只继承父类构造函数中的属性和方法,没有继承父类的原型链上的属性和方法

组合继承

关键思想:通过call/apply调用父类构造函数,继承父类自身的属性并保留传参的优点,避免了引用类型的属性被所有实例共享;然后通过将父类实例作为子类原型,原型链继承方法;

// 父类
function Super(sex){
    this.name =["super"];
    this.sex = sex;
}
Super.prototype.getSuper = function(){
    return this.name;
}
 
// 子类
function Sub(sex, age){
    Super.call(this, sex); // 继承父类构造函数中的属性/方法,第二次调用父类构造函数Super()
    this.age = age; // 子类自己的属性
}
Sub.prototype = new Super();  // 第一次调用父类构造函数Super()
Sub.prototype.sayAge = function() {  // 子类原型上新添的方法
    console.log(this.age);
};

// 测试 
let sub1 = new Sub('女', 12); //创建Sub的实例sub1
sub1.name.push("sub1"); //修改sub1的name属性值
console.log(sub1.name); // ['super', 'sub1']
console.log(sub1.getSuper()); // ['super', 'sub1']
console.log(sub1.sex); // 女
console.log(sub1.sayAge()); // 12
 
let sub2 = new Sub('男', 8); //创建Sub的实例sub2
sub2.name.push("sub2"); //修改sub2的name属性值
console.log(sub2.name); // ['super', 'sub2']
console.log(sub2.getSuper()); // ['super', 'sub2']
console.log(sub2.sex); // 男
console.log(sub2.sayAge()); // 8

let super1 = new Super; // 父类实例
console.log(super1.name); // ["super"]  

优点

解决了第一、二两种方法的明显缺点,实现子类既可以继承父类自身方法(父类构造函数上的方法),也可以继承父类原型链上的方法,没有子类实例共享父类引用属性的问题

缺点:

调用了两次父类的构造函数

第一次:Sub.prototype = new Super(),调用一次父类构造函数

第二次:Sub内使用call方法,又调用了一次父类构造函数,且之后每次实例化子类sub1、sub2...的过程中( new Sub() ),都会调用父类构造函数,则存在效率问题

原型式继承

关键思想:利用原型可以基于已有的对象创建新对象这一点(同时还不必因此创建自定义类型),来定义一个函数,普遍定义这个函数名为object(),来实现  新对象._proto_ --> 原对象,也就是原对象是新对象的原型,那么新对象当然就可以继承原对象的属性和方法啦,object()这个函数内部逻辑如下,本质上,object()是对传入的对象执行了一次浅拷贝:

function object (obj) {
  function F () {}; // 先创建了一个临时性的构造函数F
  F.prototype = obj; // 然后将传入的对象obj作为这个构造函数的原型
  return new F(); // 最后返回了这个临时类型的一个新实例,则新实例._proto_ = obj
}

举例说明实现过程和效果:

// 原对象
let person = {   
    name:"Nick",   // 基本类型属性
    friends:["wang","chen"]  // 引用类型属性
};

// 新对象1
let person1 = object(person); // 利用上述object()函数生成一个新对象
person1.name = "Amy";   // 更改新对象1的基本属性
person1.friends.push("zhang"); // 更改新对象1的引用属性

// 新对象2
let person2 = object(person); // let person2 = Object.create(person)效果一样
person2.friends.push("li");

console.log(person1.name); // Amy
console.log(person2.name); // Nick 基本类型属性不会相互影响
console.log(person1.friends); // wang,chen,zhang,li 引用类型属性相互影响了
console.log(person2.friends); // wang,chen,zhang,li
console.log(person.friends); // wang,chen,zhang,li

在没有必要创建构造函数,而是只想让一个对象与另外一个对象保持类似的情况下,原型式继承完全可以胜任。

缺点:原型式继承和原型链继承类似,区别:前者是完成了一次对 对象的浅拷贝,后者是对构造函数进行继承。缺点也是一致的:包含引用类型的属性值始终都会在相关对象间共享(避免引用属性共享的话只能通过构造函数继承属性而通过原型来继承方法,方法都是通用的所以可以共享使用,但引用属性共享会出问题所以一般不共享,不共享只能用构造函数,因为构造函数的每次new的实例都是深拷贝,实例之间互不影响)

补充知识点:在ES5中,新增了Object.create()方法规范化了原型式继承,它有两个参数,第一个参数是用作新对象原型的对象,第二个参数是可选的,是一个为新对象定义额外属性的对象。在传入一个参数的时候,这个方法是和object()方法一样的。

寄生式继承

关键思想:相当于给原型式继承外面再套了一个盒子。原型式继承封装了一个object()函数,寄生式继承沿用了这个object()函数,它用object()函数先创建一个新对象,再给这个新对象增加一些其它的方法或属性来增强丰富这个对象,整个这些过程用一个函数再包起来,就相当于给原型式继承外面再套了一层。(不过object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里替换object()使用)

实现过程:

// 原型式继承中的那个封装的函数
function object (obj) {
    function F () {};
    F.prototype = obj;
    return new F();
}
 
// 寄生式继承的核心思想代码
function createAnother(original){
    var clone = object(original); // 通过调用object()函数创建一个新对象
    clone.sayHi = function(){ // 添加新方法来增强这个对象
        console.log('hello')
    }
    return clone; //返回这个对象
}

// 举例
let person = {
    name:"Alvin",
    friends:['Yannis',"Lucy"]
}
 
let person1 = createAnother(person);
person1.sayHi(); // hello
console.log(person1.name); // Alvin
console.log(person1.friends); // Yannis, Lucy

优点:可添加新的属性和方法

缺点:跟利用构造函数模式一样,每次创建对象都会创建一遍方法。

寄生式组合继承:

关键思想:为了解决组合继承调用了两次父类的构造函数的缺点。不需要为了指定子类的原型而调用父类的构造函数(可理解为不需要显示的new操作),通过上面的寄生式继承方式来继承父类型的原型即可。所谓寄生组合式继承:通过借用构造函数来继承属性(call/apply),通过原型链的混成形式来继承方法(object()/Object.create())。

// 封装的object()函数,也可用Object.create()
function object (obj) {
  function F () {};
  F.prototype = obj;
  return new F();
}

// 使用寄生式继承来继承父类原型
function inheritPrototype(subType, superType){
    let protoType = object(superType.prototype);    // 创建父类原型的一个副本
    protoType.constructor = subType;                // 改善这个副本,让它的constructor指向子类构造函数
    subType.prototype = protoType;                  // 重写子类原型
}

// 父类
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}

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

inheritPrototype(SubType, SuperType);  // 子类通过函数inheritPrototype实现继承父类原型上的方法
SubType.prototype.sayAge = function(){   // 给子类原型添加新方法
    alert(this.age);
}

let instance = new SubType("Bob", 18);
instance.sayName();  // Bob
instance.sayAge();   // 18

inheritPrototype函数接收两个参数:子类型构造函数和超类型构造函数。
1. 创建超类型原型的副本。
2. 为创建的副本添加constructor属性,弥补因重写原型而失去的默认的constructor属性
3. 将新创建的对象(即副本)赋值给子类型的原型

这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且原型链仍然保持不变,instanceof 和isPrototypeOf()也能正常使用。

缺点:使用起来比较麻烦

ES6中引入class关键字定义类,用extends实现继承

ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

class Parent{
//属性
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    eat(){
    console.log('111')
    }
    show(){
    console.log('222')
    }
}

//ES6的继承

class Man extends Parent{
    constructor(beard,name,age){
    super(name,age)//super调用父类的构造方法!
    this.beard = beard;
    }
    work(){}
}

var p2 = new Man(10,"张家辉",40);
var p1 = new Man(10,"古天乐",41);
console.log(p1,p2)

//优缺点,代码简洁,但是有兼容性问题

JS继承的应用场景(摘自网络)

JS继承的话主要用于面向对象的编程中,使用场景的话还是以单页面应用或者JS为主的开发里,因为如果只是在页面级的开发中很少会用到JS继承的方式,与其继承,还不如直接写个函数来的简单直接有效一些。

想用继承的话最好是那种主要以JS为主开发的大型项目,比如说单页面的应用或者写JS框架,前台的所有东西都用JS来完成,整个站的跳转,部分逻辑,数据处理等大部分使用JS来做,这样面向对象的编程才有存在的价值和意义。

为什么要继承:通常在一般的项目里不需要,因为应用简单,但你要用纯js做一些复杂的工具或框架系统就要用到了,比如webgis、或者js框架如jquery、ext什么的,不然一个几千行代码的框架不用继承得写几万行,甚至还无法维护。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值