JavaScript中实现继承的几种方法

简介: JavaScript中实现继承的几种方法

首先,定义一个类

function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

1. 原型链继承

思想: 利用原型链来实现继承,超类的一个实例作为子类的原型

function Dog(){ }
Dog.prototype = new Animal();
Dog.prototype.name = '狗';
var dog = new Dog();
console.log(dog.name); // 狗
dog.eat('狗粮'); // 狗正在吃:狗粮
dog.sleep(); // 狗正在睡觉
console.log(dog instanceof Animal);  // true 
console.log(dog instanceof Dog);  // true

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例;
  2. 父类新增原型方法/原型属性,子类都能访问到;
  3. 简单,易于实现;

缺点:

  1. 可以在Dog构造函数中,为Dog实例增加实例属性。如果要新增原型属性和方法,则必须放在new Animal()这样的语句之后执行;
  2. 无法实现多继承;
  3. 来自原型对象的所有属性被所有实例共享;
  4. 创建子类实例时,无法向父类构造函数传参;

2. 构造函数继承

思想: 通过使用call、apply方法在新创建的对象上执行构造函数,用父类的构造函数来增加子类的实例

function Dog(name){
  Animal.call(this);
  this.name = name || '汪汪';
}
var dog = new Dog();
console.log(dog.name); // 汪汪
dog.sleep(); // 汪汪正在睡觉
console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true

特点:

  1. 解决了方法1中,子类实例共享父类引用属性的问题;
  2. 创建子类实例时,可以向父类传递参数;
  3. 可以实现多继承(call多个父类对象);

缺点:

  1. 实例并不是父类的实例,只是子类的实例;
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法;
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能;

3. 实例继承

思想: 为父类实例添加新特性,作为子类实例返回

function Dog(name){
  var instance = new Animal();
  instance.name = name || '汪汪';
  return instance;
}
var dog = new Dog();
console.log(dog.name); // 汪汪
dog.sleep(); // 汪汪正在睡觉
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // false

特点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果;

缺点:

  1. 实例是父类的实例,不是子类的实例;
  2. 不支持多继承;

4. 拷贝继承

function Dog(name){
  var animal = new Animal();
  for(var p in animal){
    Dog.prototype[p] = animal[p];
  }
  Dog.prototype.name = name || '汪汪';
}
var dog = new Dog();
console.log(dog.name); // '汪汪'
dog.sleep(); // 汪汪正在睡觉
console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true

特点:

  1. 支持多继承;

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性);
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到);

5. 原型式继承

思想:采用原型式继承并不需要定义一个类!!!,传入参数obj,生成一个继承obj对象的对象

var animal = {
  name: "汪汪",
  age: 2,
}
function F(o) {
  function C() {}
  C.prototype = o;
  return new C();
}
var dog = F(animal)
console.log(dog.name); // 汪汪
console.log(dog.age); // 2

特点:

  1. 直接通过对象生成一个继承该对象的对象;

缺点:

  1. 不是类式继承,而是原型式基础,缺少了类的概念;

6. 组合继承(推荐)

思想: 利用构造继承和原型链组合

function Dog(name){
  Animal.call(this);
  this.name = name || '汪汪';
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
var dog = new Dog();
console.log(dog.name); // 汪汪
dog.sleep(); // 汪汪正在睡觉
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // true

特点:

  1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法;
  2. 既是子类的实例,也是父类的实例;
  3. 不存在引用属性共享问题;
  4. 可传参;
  5. 函数可复用;

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了);

7. 寄生式继承

思想: 原型式+工厂模式 ,解决了组合继承两次调用构造函数的问题; 创建一个仅仅用于封装继承过程的函数,然后在内部以某种方式增强对象,最后返回对象;

//临时中转函数
function obj(o) {
  function Animal() {}
  Animal.prototype = o;
  return new Animal();
}
//寄生函数
function create(o){
  var Dog = obj(o);
  // 可以对f进行扩展
  Dog.sleep = function(){
    console.log(this.name + '在睡觉') 
  }
  return Dog;
}
var mydog = {
  name: '汪汪',
  age: 1,
};
var dog = create(mydog);
console.log(dog.name); // 汪汪
dog.sleep(); // 汪汪在睡觉

特点:

  1. 原型式继承的一种拓展

缺点:

  1. 依旧没有类的概念

8. 寄生组合继承(推荐)

思想: 结合寄生式继承和组合式继承,完美实现不带两份超类属性的继承方式

// 该实现没有修复constructor
function Dog(name){
  Animal.call(this);
  this.name = name || '汪汪';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Dog.prototype = new Super();
})();
var dog = new Dog();
console.log(dog.name); // 汪汪
dog.sleep(); // 汪汪正在睡觉
console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // true

补充:

// 定义一个类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉');
  }
  //实例引用属性
  this.features = [];
}
function Dog(name){}
Dog.prototype = new Animal();
var tom = new Dog('Tom');
var kissy = new Dog('Kissy');
console.log(tom.name); // "Animal"
console.log(kissy.name); // "Animal"
console.log(tom.features); // []
console.log(kissy.features); // []
tom.name = 'Tom-New Name';
tom.features.push('eat');
//针对父类实例值类型成员的更改,不影响
console.log(tom.name); // "Tom-New Name"
console.log(kissy.name); // "Animal"
//针对父类实例引用类型成员的更改,会通过影响其他子类实例
console.log(tom.features); // ['eat']
console.log(kissy.features); // ['eat']
// 原因分析:
// 1. 执行tom.features.push,首先找tom对象的实例属性(找不到),那么去原型对象中找,也就是Animal的实例。发现有,那么就直接在这个对象的features属性中插入值。
// 2. 在console.log(kissy.features); 的时候。同上,kissy实例上没有,那么去原型上找。
// 3. 刚好原型上有,就直接返回,但是注意,这个原型对象中features属性值已经变化了。

特点:

  1. 堪称完美;

缺点:

  1. 实现较为复杂;

9. ES6中的继承(强烈推荐)

思想: ES6中通过 Class ‘类’ 这个语法糖实现继承和Java等面向对象的语言在实现继承上已经非常相似,当然只是语法层面相似,本质当然依旧是通过原型实现的。

ES6实现继承是通过关键字extends、super来实现继承,和面向对象语言Java一样。

class Animal{
  constructor(name,age){
    this.name = name || 'Animal';
    this.age = age
    this.hobbies = ['睡觉','吃饭']
  }
  /* 下面的写法就等同于把方法挂在原型上 */
  static say(){ // 加了static 静态方法,只给类用的方法
    console.log('你好');
  } // 方法和方法之间不用加逗号 
  getname(){
    console.log(this.name);
  }
}
class Dog extends Animal{
  /*
    在子类constructor中添加属性的小技巧 
    专属于子类的属性写在参数的前面,父类的属性放在后面
    这样一来,就能直接使用...arg
    ...arg
    把函数中的多余的参数放入数组中体现出来。
  */
  constructor(food,...arg){
    super(...arg); // 等同于调用父类,把多余的参数(和父类一样的属性)放到super中,达到继承父类属性的目的
    /*
      在继承里,写constructor必须写super
      super下面才能使用this,super有暂存死区(super上面不能使用this,用了就报错)
    */
    this.food = food
  }
  static sayState(){
    console.log("我在睡觉")
  }
  sayAge(){
    console.log("我今年" + this.age + '岁');
  }
  sayFood(){
    console.log('我在吃'+this.food)
  }
}
var dog1 = new Dog('骨头',"汪汪",2);
Animal.say() // 你好
Dog.sayState();  // 我在睡觉
dog1.getname();  // 汪汪
dog1.sayAge(); // 我今年2岁
dog1.sayFood(); // 我在吃骨头
var dog2 = new Dog('狗粮',"旺财",1);
dog2.hobbies.push("玩耍");
dog2.getname();  // 旺财
dog2.sayAge(); // 我今年1岁
dog2.sayFood(); // 我在吃狗粮
console.log(dog1.hobbies)  // ['睡觉','吃饭']
console.log(dog2.hobbies)  // ['睡觉','吃饭','玩耍']


相关文章
|
8天前
|
JavaScript 前端开发 索引
JavaScript中的数组的内置方法全面讲解
JavaScript 数组提供了多种内置方法来高效操作数据。如 `push()` 和 `unshift()` 分别在数组尾部和头部添加元素;`pop()` 和 `shift()` 则移除尾部和头部的元素;`splice()` 可增删元素;`slice()` 创建子数组;`join()` 将数组转化为字符串;`indexOf()` 和 `includes()` 用于查找元素;`forEach()` 遍历数组。此外,`reverse()` 和 `sort()` 改变数组顺序;`fill()` 填充数组值;`slice()` 和 `concat()` 则分别用于创建子数组和合并数组。
17 2
|
7天前
|
前端开发 JavaScript
JavaScript——promise 是解决异步问题的方法嘛
JavaScript——promise 是解决异步问题的方法嘛
15 0
|
7天前
|
JavaScript 前端开发 索引
js遍历的方法与区别
js遍历的方法与区别
18 3
|
4天前
|
JSON JavaScript 前端开发
JavaScript实现字符串转json对象的方法
JavaScript实现字符串转json对象的方法
|
3天前
|
JavaScript 测试技术 索引
js数组方法汇总
js数组方法汇总
6 1
|
4天前
|
JavaScript 前端开发 索引
JS - includes 方法和 map 方法使用方式
这篇文章介绍了JavaScript中数组的`includes`方法和`map`方法的用法,包括它们的语法、参数说明和具体的示例代码。`includes`方法用于判断数组是否包含特定元素,而`map`方法用于对数组中的每个元素执行操作并返回新数组。
9 1
|
8天前
|
JavaScript API 索引
js中的reduce()方法 讲解 和实现
`reduce()` 方法对数组元素依次应用一个回调函数,将结果累计并最终返回单一值。语法为 `reduce(callback(accumulator, currentValue, currentIndex, array), initialValue)`。参数包括累计器(初次为初始值或首元素)、当前元素值、索引及数组自身。此方法需返回值供下一轮迭代使用。常见应用场景包括计算数组总和与平均值、统计元素频率、过滤与转换数组内容及去除重复项等。例如,可通过 `reduce()` 快速计算 `[1, 2, 3, 4, 5]` 的总和或对对象属性值求和。此外,还可自定义实现 `reduce()` 方法
26 1
|
4天前
|
JavaScript 前端开发
javascript中常见获取时间戳的方法
javascript中常见获取时间戳的方法
10 0
|
4天前
|
JavaScript 前端开发
js中this是指向的哪个全局变量,改变this指向的方法有什么?
js中this是指向的哪个全局变量,改变this指向的方法有什么?
6 0
|
4天前
|
前端开发 JavaScript
JavaScript 获取 HTML 元素方法
JavaScript 获取 HTML 元素方法
8 0

热门文章

最新文章