JavaScript继承

文章详细阐述了JavaScript中的四种继承方式:原型链继承导致引用类型属性被共享,构造函数继承解决了属性不共享但无法继承prototype属性,组合继承结合两者但有重复调用构造函数的缺点,而寄生组合继承通过优化减少了冗余,但仍存在Child.prototype属性丢失的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 原型链继承

// 定义父类
function Parent() {
    this.name = "parent";
}

// 在父类的原型上定义方法
Parent.prototype.getName = function() {
    return this.name;
}

// 定义子类
function Child() {
    this.name = "child";
}

// 子类继承父类,这里是关键,实现原型链继承
Child.prototype = new Parent();

// 实例化子类
var child1 = new Child();

console.log(child1.getName()); // 输出 "child"

原型链继承的一个主要问题是包含引用类型值的原型属性会被所有实例共享。 换而言之,如果一个实例改变了该属性,那么其他实例的该属性也会被改变

比如以下代码就会出问题:我明明只想修改了 child1 的arr,我不想修改 child2 的 arr。但是实际结果却是, child2 的 arr 也被修改了

      // 定义父类
      function Parent() {
        this.arr = [1, 2, 3];
      }

      // 定义子类
      function Child() {}

      // 子类继承父类,这里是关键,实现原型链继承
      Child.prototype = new Parent();

      // 实例化子类
      var child1 = new Child();
      var child2 = new Child();

      child1.arr.push(4);

      console.log(child1.arr); 
      console.log(child2.arr);

上述问题能否避免呢?答案是可以的。我们可以通过构造函数继承来避免上述问题。

2.构造函数继承

构造函数继承,通过使用 callapply 方法,我们可以在子类型构造函数中执行父类型构造函数,从而实现继承。比如以下例子,child1 和 child2 都继承了 Parent 的 sayHello() 方法。

这种继承方式的好处是,原型属性不会被共享(所以不会出现上述问题)。我们可以尝试console.log() child1 和 child2 的 sayHello方法, 通过 === 判断得知,他们并不是同一个方法。换而言之, 修改其中任何一个 sayHello ,不会影响另一个 sayHello。

      // 父类
      function Parent() {
        this.sayHello = function () {
          console.log("Hello");
        };
      }

      Parent.prototype.a = "我是父类prototype上的属性";
      // 子类
      function Child() {
        Parent.call(this);
      }

      // 创建两个 Child 实例
      var child1 = new Child();
      var child2 = new Child();

      console.log(child1.sayHello === child2.sayHello); // 输出 false

那么这种方式的缺点是什么呢?它不能继承父类 prototype 上的属性。比如以下例子,父类的prototype 上有个 a 属性(它是一个字符串)。按理来说,child 类继承了 parent 类,因此 child 实例化的对象也得有 a 属性,但 console.log(child1.a) 之后可以看出,child1.a 是 undefined 。

      // 父类
      function Parent() {
        this.sayHello = function () {
          console.log("Hello");
        };
      }

      Parent.prototype.a = "我是父类prototype上的属性";
      // 子类
      function Child() {
        Parent.call(this);
      }

      var child1 = new Child();
      var child2 = new Child();
      var parentObj = new Parent();

      console.log(parentObj.a);
      console.log(child1.a);

为了解决以上缺点,我们引入了组合继承

3.组合继承

组合继承可以理解为 原型链继承 + 构造函数继承 。 可以看以下例子:

    // 父类
      function Parent() {
        this.sayHello = function () {
          console.log("Hello");
        };
      }

      Parent.prototype.a = "我是父类prototype上的属性"; 

      // 子类
      function Child() {
        Parent.call(this);
      }

      Child.prototype = new Parent();

      var child1 = new Child();

实际上,只是在构造函数继承的基础上,加了一行代码:Child.prototype = new Parent();

这就解决了构造函数继承的缺点。 构造函数继承不能继承父类 prototype 上的属性。但是加上这行代码之后,每当我们在 child 对象上找不到属性,就会去 Child.prototype 上去寻找,而 Child.prototype 是一个 Parent 对象,如果 Parent 对象上还找不到,就会去 Parent.prototype 上去寻找。如此以来,组合继承就能继承父类 prototype 上的属性。

但是,组合继承有没有缺点呢?它也有缺点:调用了 2 次 Parent()。 它在 child 的 prototype 上添加了父类的属性和方法。

很多读者可能读不懂上面这句话,我们通过一个具体的例子来说明。child1 是一个子类对象,这个对象本身有 sayHello 方法。而 Child.prototype 上也有一个 sayHello 方法。

  • 为什么child1本身有 sayHello 方法?因为在 Child() 构造函数内部,我们调用了一次 Parent(), 这就给 child1 对象添加了 sayHello 方法。
  • 为什么 Child.prototype 上也有一个 sayHello 方法?因为 Child.prototype = new Parent();

细心的读者可能发现了,我们每次调用 child1.sayHello() 的时候,永远是调用的child1本身有 sayHello 方法。换句话说,Child.prototype 上 sayHello 方法我们根本不需要。

那么有没有办法解决这个问题呢?于是我们引入了寄生组合继承

4.寄生组合继承

     // 父类
      function Parent() {
        this.sayHello = function () {
          console.log("Hello");
        };
      }

      Parent.prototype.a = "我是父类prototype上的属性";
      // 子类
      function Child() {
        Parent.call(this);
      }


      // 创建一个没有实例方法的父类实例作为子类的原型
      Child.prototype = Object.create(Parent.prototype);
      // 修复构造函数的指向
      Child.prototype.constructor = Child;

      var child1 = new Child();

我们发现,只需要在组合继承的基础上,把 Child.prototype = new Parent(); 替换为   Child.prototype = Object.create(Parent.prototype);  

这两者有什么区别呢? Object.create(Parent.prototype)  创造了一个空对象,这个空对象的__proto__ 是 Parent.prototype 。所以它继承了 Parent 原型链上的属性和方法。由于我们删除了Child.prototype = new Parent(); 我们不再调用 Parent() 构造函数,因此 Child.prototype 不再包含 Parent 的属性和方法。所以第三小节部分提到的问题就解决了,Child.prototype 上不再有 sayHello 方法。

那么寄生组合继承有没有缺点呢?当然也有缺点。看以下例子:

      // 父类
      function Parent() {
        this.sayHello = function () {
          console.log("Hello");
        };
      }

      Parent.prototype.a = "我是父类prototype上的属性";
      // 子类
      function Child() {
        Parent.call(this);
      }

      Child.prototype.childFunction = () => {
        console.log("我是child方法");
      };
      // 创建一个没有实例方法的父类实例作为子类的原型
      Child.prototype = Object.create(Parent.prototype);
      // 修复构造函数的指向
      Child.prototype.constructor = Child;

      var child1 = new Child();
      child1.childFunction();

我们在 Child.prototype 上添加了 childFunction 。所有通过 Child() 创建的实例对象,都应该有该childFunction() 方法。但是当我们调用 child1.childFunction() 的时候却报错,这是为什么呢?因为 Child.prototype = Object.create(Parent.prototype); 这一行代码使 Child.prototype 指向了一个新对象, 原来对象上的属性和方法都会丢失。

所以寄生组合继承的缺点就是,Child.prototype 的原始属性和方法会丢失。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

codereasy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值