js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承

简介: js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承


前言

作为对象三大特性(继承、封装、多态)之一的继承,到底是什么呢?简而言之,继承就是可以使得子类具有父类的各种方法和属性。

接下来我们就详细描述一下前端的js继承。

原型链继承

原型链继承的定义

先说说什么是原型链继承。通过prototype实现继承的即为原型链继承

比如说想让子类Child构造函数继承父类Person构造函数的属性和方法

先让Child.prototype = new Person(),简而言之:让新实例的prototype 等于父类的实例。这就是原型链继承

原型链继承是常见的继承方式之一。其基本思想就是通过原型继承多个引用类型的属性和方法。什么是原型链?每个构造函数都会有一个原型对象,调用构造函数创建的实例会有一个指针__proto__指向上一个原型对象,并从中继承方法和属性,这个原型可能是另一个类型的实例,所以内部可能也有一个指针指向更上一层的原型,这样一层一层,最终指向null,这种关系被称为原型链。

注意上面的说法,原型上的方法和属性被 继承 到新对象中,并不是被复制到新对象,我们看下面这个例子。

function Foo(name) {
  this.name = name;
}
Foo.prototype.getName = function() {
    return this.name;
}
Foo.prototype.length = 3;
let foo = new Foo('muyiy'); // 相当于 foo.__proto__ = Foo.prototype
console.dir(foo);

原型上的属性和方法定义在 prototype 对象上,而非对象实例本身。当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。

比如调用 foo.valueOf() 会发生什么?

1、首先检查 foo 对象是否具有可用的 valueOf() 方法。

2、如果没有,则检查 foo 对象的原型对象(即 Foo.prototype)是否具有可用的 valueof() 方法。

3、如果没有,则检查 Foo.prototype 所指向的对象的原型对象(即Object.prototype)是否具有可用的 valueOf() 方法。这里有这个方法,于是该方法被调用。

prototype 和 proto 的区别,其中原型对象prototype 是构造函数的属性,proto 是每个实例上都有的属性,这两个并不一样,但 foo.proto 和 Foo.prototype 指向同一个对象。

这次我们再深入一点,原型链的构建是依赖于 prototype 还是 __proto__呢?

Foo.prototype 中的 prototype 并没有构建成一条原型链,其只是指向原型链中的某一处。原型链的构建依赖于 proto,如上图通过 foo.__proto__指向 Foo.prototype,foo.proto.proto 指向 Bichon.prototype,如此一层一层最终链接到 null。

可以这么理解 Foo,我是一个 constructor,我也是一个 function,我身上有着 prototype 的 reference,只要随时调用 foo = new Foo(),我就会将foo.__proto__ 指向到我的 prototype 对象。

不要使用 Bar.prototype = Foo,因为这不会执行 Foo 的原型,而是指向函数 Foo。 因此原型链将会回溯到 Function.prototype 而不是Foo.prototype,因此 method 方法将不会在 Bar 的原型链上。

function Foo() {
  return 'foo';
}
Foo.prototype.method = function() {
  return 'method';
}
function Bar() {
  return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函数
let bar = new Bar();
console.dir(bar);
bar.method(); // Uncaught TypeError: bar.method is not a function

instanceof 原理及实现

instanceof 运算符用来检测 constructor.prototype 是否存在于参数object 的原型链上。

function C(){} 
function D(){} 
 
var o = new C();
 
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

instanceof 原理就是一层一层查找 proto,如果和constructor.prototype 相等则返回 true,如果一直没有查找成功则返回 false。

instance.[__proto__...] === instance.constructor.prototype

知道了原理后我们来实现 instanceof,代码如下。

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
   var O = R.prototype;// 取 R 的显示原型
   L = L.__proto__;// 取 L 的隐式原型
   while (true) { 
       // Object.prototype.__proto__ === null
       if (L === null) 
         return false; 
       if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true 
         return true; 
       L = L.__proto__; 
   } 
}
 
// 测试
function C(){} 
function D(){} 
 
var o = new C();
 
instance_of(o, C); // true
instance_of(o, D); // false

最简单的原型链继承示意:

// 定义父类构造函数Person
function Person (name, age) {
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 父类构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// 定义子类构造函数Child
function Child (realName, realAge) {
  this.realName = realName;
  this.realAge = realAge;
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person(); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
                                
// 创建子类实例
var childExample = new Child('张三', 20);
childExample.run(); // 原型链继承,可以继承父类构造函数里面以及父类原型链上面的属性和方法,如子类Child实例可以调用父类构造函数Person里面以及原父类型链的run和work方法
childExample.work();

执行结果:

我们可以看到实例childExample继承了父类Person的run方法以及父类原型链上的work方法

构造函数、class类、原型链的注意事项

  1. 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用,否则会报错
// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个实例共享,即构造函数(或class)的每个实例,它们在构造函数(或class)的属性都是独立的,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// Person.run(); // 没有实例化直接调用构造函数、class类、原型链上的方法,会报错
const personExample1 = new Person('张三', 20);
personExample1.run();
  1. 构造函数(或class)的属性和方法不被多个该类的实例共享,即构造函数(或class)的每个实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响


  2. 原型链上面的属性和方法可以被多个实例共享
// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
// 构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// Person.run(); // 没有实例化直接调用构造函数、class类、原型链上的方法,会报错
const personExample1 = new Person('张三', 20); // 构造函数(或class)的属性和方法不被多个实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,互不影响
const personExample2 = new Person('李四', 30);
personExample1.run();
personExample2.run();
personExample1.work();
personExample2.work();

原型链继承的优缺点

原型链继承的优点
  1. 当子类通过prototype继承了父类的实例之后(即原型链继承),子类的实例可以继承子类的构造函数的属性,子类原型的属性,父类构造函数属性,父类原型的属性
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
// 构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
// 通过原型链继承,子类的实例可以继承子类的构造函数的属性,子类原型的属性,父类构造函数属性,父类原型的属性,如子类Child实例可以调用子类构造函数的callPhone方法,父类构造函数Person里面run方法和父类原型链里的work方法
childExample.callPhone();
console.log(childExample.realName);
childExample.run(); 
childExample.work();
原型链继承的缺点
  1. 原型链继承,创建子类实例时,无法向父类的构造函数传参。
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
// 构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
// 通过原型链继承,子类的实例可以继承子类的构造函数的属性,子类原型的属性,父类构造函数属性,父类原型的属性,如子类Child实例可以调用子类构造函数的callPhone方法,父类构造函数Person里面run方法和父类原型链里的work方法
childExample.callPhone();
console.log(childExample.realName);
childExample.run(); 
childExample.work();
  1. 当子类通过prototype继承了父类的实例之后(即原型链继承),其父类的实例属性会成为子类的原型属性,如果父类实例的属性是引用类型的时候,子类创建的所有实例都会共享这些属性,修改某一个实例的这个引用类型的属性,其他实例的属性值也会被修改
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
  this.arr = [1, 2, 3]
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
// 父类构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
                               
// 创建子类实例
var childExample1 = new Child('张三', 20);
var childExample2 = new Child('赵六', 23);
// 当子类通过prototype继承了父类的实例之后(即原型链继承),其父类的实例属性会成为子类的原型属性,如果父类实例的属性是引用类型的时候,子类创建的所有实例都会共享这些属性,修改某一个实例的这个引用类型的属性,其他实例的属性值也会被修改
childExample1.arr.push(4)
console.log(childExample1.arr);
console.log(childExample2.arr);
console.log(childExample1.realName);
  1. 继承单一,原型链继承只能继承一个父类,其他父类会被最后一个父类覆盖
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 定义父类构造函数Father
function Father (fatherName) {
  this.fatherName = fatherName;
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
// 父类构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
}
// 原型链继承只能继承一个父类,其他父类会被最后一个父类覆盖
Child.prototype = new Person('李四', '王五');
Child.prototype = new Father('孙七');
                               
// 创建子类实例
var childExample1 = new Child('张三', 20);
console.log(childExample1.fatherName);
childExample1.work(); // 调用报错,父类Person被Father覆盖,所以子类实例无法再调用父类Person的属性和方法
  1. 子类型的原型上的 constructor 属性被重写了
    首先看没有继承Person的Child实例,可以看到该实例有constructor属性。且_proto_只有2层(展示上只有2层,实际会有很多层,直到null)
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
console.log(childExample);

  1. 当Child继承了Person之后,可以看到Child实例被改写了,丢失了constructor属性,而且Child原型对象_proto_的constructor变成了父类的Person,且多了一层_proto_(多的这层属于父类Person的原型对象,可以说Child的原型对象(或者说是原型链)已经发生了改变)
    简而言之就是Child.prototype 指向了 Person.prototype,而Person.prototype.constructor 指向了 Person,所以Child.prototype.constructor 指向了 Person。
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
console.log(childExample);

  1. 解决这个问题的办法就是重写 Child.prototype.constructor 属性,指向自己的构造函数Child。
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
// 新增,重写 Child.prototype 的 constructor 属性,指向自己的构造函数 Child,解决原型链继承后,子类的constructor属性被重写的问题
Child.prototype.constructor = Child;
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
console.log(childExample);
  1. 给子类型原型添加属性和方法必须在替换原型之后,原因就是第四点,因为原型链继承会导致子类型的原型被覆盖
    以下代码,alertName方法在替换原型之前添加,alertAge方法在替换原型之后添加,最终结果,由于原型链继承,子类的原型被替换,所以alertName不存在,alertAge可以正常调用
// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 给子类型原型添加属性和方法必须在替换原型之后,因为原型链继承会导致子类型的原型会被覆盖,此时alertName将被覆盖
Child.prototype.alertName = function () {
  console.log('在替换原型之前')
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
// 重写 Child.prototype 的 constructor 属性,指向自己的构造函数 Child,解决原型链继承后,子类的constructor属性被重写的问题
Child.prototype.constructor = Child;
// 给子类型原型添加属性和方法必须在替换原型之后,因为原型链继承会导致子类型的原型会被覆盖,此时alertName将被覆盖,alertAge可以正常调用
Child.prototype.alertAge = function () {
  console.log('在替换原型之后')
}
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
console.log(childExample);
childExample.alertAge();
childExample.alertName();

原型链继承备注

在哪个构造函数的原型(prototype)上挂载属性或方法,这个属性或方法就会挂载在哪里。

比如,在子类Child的原型上挂载alertName方法,在父类Person的原型上挂载work方法,子类Child继承父类Person,alertName和work方法的位置分别位于子类的原型和父类的原型中

// 定义父类构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
// 构造函数Person的原型上挂载work方法
Person.prototype.work = function () {
  console.log(`执行work`);
}
// 定义子类构造函数Child
function Child (name, age) {
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}
// 让子类构造函数Child的原型设置为父类Person的实例
Child.prototype = new Person('李四', '王五'); // 原型链继承,可以继承构造函数里面以及原型链上面的属性和方法。
                               
// 创建子类Child实例
var childExample = new Child('张三', 20); // 原型链继承,创建子类实例时,并不能直接给父类的构造函数传参
// 给子类型原型添加属性和方法必须在替换原型之后,因为原型链继承会导致子类型的原型会被覆盖
Child.prototype.alertName = function () {
  console.log('执行alertName')
}
console.log(childExample);
childExample.alertName();
childExample.work();

构造函数继承

构造函数继承的定义以及简单示例

我们之前说过原型链继承有以下缺点:

1、在创建子类实例的时候,不能给父类的构造函数传参

2、父类实例的属性是引用类型的时候,子类创建的所有实例都会共享这些属性,修改某一个实例的这个引用类型的属性,其他实例的属性值也会被修改

3、继承单一,原型链继承只能继承一个父类,其他父类会被最后一个父类覆盖

那么构造函数继承就可以解决上述原型链继承的问题:

在子类的构造函数内通过call或者apply调用父类的构造函数,即可实现构造函数继承(在子类 函数中做了父类函数的自执行(复制))

// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
  this.arr = [1, 2, 3]
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
Person.prototype.work = function () {
  console.log(`执行work`);
}
function Father () {
  this.alertFather = function () {
    console.log(`alertFather`)
  }
}
// 定义构造函数Child
function Child (name, age) {
  Person.call(this, name, age); // 构造函数继承可以给父类传参
  Father.call(this); // 构造函数继承可以继承多个父类
  this.realName = name;
  this.realAge = age;
  this.callPhone = function () {
    console.log(`执行callPhone`)
  }
}                              
var childExample1 = new Child('张三', 20);
var childExample2 = new Child('赵六', 23);
childExample1.arr.push(4)
console.log(childExample1.arr);
console.log(childExample2.arr);
childExample1.run();
childExample1.alertFather();

构造函数继承的优缺点

构造函数继承的优点
  1. 解决了原型链继承关于父类传参、父类引用类型的属性共享、只能继承一个父类的缺点
构造函数继承的缺点
  1. 只能继承父类构造函数的属性,没有继承父类原型的属性,如下,子类通过构造函数继承,无法继承父类原型上的work方法
// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
  this.arr = [1, 2, 3]
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
Person.prototype.work = function () {
  console.log(`执行work`);
}
function Father () {
  this.alertFather = function () {
    console.log(`alertFather`)
  }
}
// 定义构造函数Child
function Child (name, age) {
  Person.call(this, name, age); // 构造函数继承可以给父类传参
  Father.call(this); // 构造函数继承可以继承多个父类
  this.realName = name;
  this.realAge = age;
}                              
var childExample1 = new Child('张三', 20);
childExample1.work(); // 报错,构造函数继承只能继承父类构造函数的属性,没有继承父类原型的属性
  1. 无法实现构造函数的复用。(每次创建子类实例,都要重新调用父类构造函数)
  2. 每个子类新实例都有父类构造函数的所有属性和方法,臃肿。
// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
  this.arr = [1, 2, 3]
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
Person.prototype.work = function () {
  console.log(`执行work`);
}
function Father () {
  this.alertFather = function () {
    console.log(`alertFather`)
  }
}
// 定义构造函数Child
function Child (name, age) {
  Person.call(this, name, age); // 构造函数继承可以给父类传参
  Father.call(this); // 构造函数继承可以继承多个父类
  this.realName = name;
  this.realAge = age;
}                              
var childExample1 = new Child('张三', 20);
console.log(childExample1);

组合继承(组合原型链继承和构造函数继承)(常用)

组合继承的定义以及简单示例

单纯的原型链继承和单纯的构造函数继承都各有优缺点,我们可以将两种继承方式结合起来,使得它们优缺点互补,这就是组合继承

// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
  this.arr = [1, 2, 3]
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
Person.prototype.work = function () {
  console.log(`执行work`);
}
function Father () {
  this.alertFather = function () {
    console.log(`alertFather`)
  }
}
// 定义构造函数Child
function Child (name, age) {
  Person.call(this, name, age); // 构造函数继承可以给父类传参
  Father.call(this); // 构造函数继承可以继承多个父类
  this.realName = name;
  this.realAge = age;
}
Child.prototype = new Person('王五', 40);
var childExample1 = new Child('张三', 20);
childExample1.work();
childExample1.run();

组合继承(组合原型链继承和构造函数继承)的优缺点

组合继承(组合原型链继承和构造函数继承)的优点
  1. 可以继承父类原型上的属性,可以给父类传参,可复用(借助原型链继承,可以解决构造函数继承的缺点,就不需要每次创建子类实例都重新调用父类的构造函数)。
  2. 每个新实例引入的构造函数属性是私有的(借助构造函数继承,解决原型链继承的缺点,可保证子类实例继承的父类属性为该子类实例私有,而不是所有子类实例共享属性)。
组合继承(组合原型链继承和构造函数继承)的缺点
  1. 调用了两次父类构造函数(即原型链继承和构造函数继承分别调用了一次父类构造函数,消耗内存)
  2. 子类的构造函数会代替prototype原型上的那个父类构造函数(这也是下图为何run函数打印的是张三而不是王五的原因)
// 定义构造函数Person
function Person (name, age) {
  // 构造函数里面的方法和属性
  // 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
  // 构造函数(或class)的属性和方法不被多个该类的实例共享,即每个构造函数(或class)的实例,它们在构造函数(或class)的属性都是独立的,属于实例的自有属性,互不影响
  this.name = name;
  this.age = age;
  this.run = function () {
    console.log(`执行run, 姓名:${name},年龄:${age}`)
  }
  this.arr = [1, 2, 3]
}
// 原型链上面的属性和方法可以被多个实例共享
// 构造函数、class类、原型链上的方法和属性都叫实例方法,实例方法必须要实例化之后,通过实例才能调用
Person.prototype.work = function () {
  console.log(`执行work`);
}
function Father () {
  this.alertFather = function () {
    console.log(`alertFather`)
  }
}
// 定义构造函数Child
function Child (name, age) {
  Person.call(this, name, age); // 构造函数继承可以给父类传参
  Father.call(this); // 构造函数继承可以继承多个父类
  this.realName = name;
  this.realAge = age;
}
Child.prototype = new Person('王五', 40);
var childExample1 = new Child('张三', 20);
childExample1.work();
childExample1.run();

原型式继承

原型式继承的定义以及简单示例

原型式继承主要就是用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。

原型式继承的性质就是即使不自定义类型也可以通过原型实现对象之间的信息共享

适用环境: 你有一个对象,想在它的基础上再创建一个新对象。你需要把这个对象先传给object() ,然后再对返回的对象进行适当修改。类似于 Object.create()只传第一个参数的时候,本质上就是对传入的对象进行了一次浅复制,缺点就是新实例的属性都是后面添加的,无法复用

function CreateObj(o){
  function F(){}
  F.prototype = o;
  console.log(o.__proto__ === Object.prototype);
  console.log(F.prototype.constructor === Object); // true
  return new F();
}
var person = {
  name: 'xiaopao',
  friend: ['daisy','kelly']
}
var person1 = CreateObj(person);
// var person2 = CreateObj(person);
person1.name = 'person1';
// console.log(person2.name); // xiaopao
person1.friend.push('taylor');
// console.log(person2.friend); // ["daisy", "kelly", "taylor"]
// console.log(person); // {name: "xiaopao", friend: Array(3)}
person1.friend = ['lulu'];
// console.log(person1.friend); // ["lulu"]
// console.log(person.friend); //  ["daisy", "kelly", "taylor"]
// 注意: 这里修改了person1.name的值,person2.name的值并未改变,
// 并不是因为person1和person2有独立的name值,
// 而是person1.name='person1'是给person1添加了name值,并非修改了原型上的name值
// 因为我们找对象上的属性时,总是先找实例上对象,没有找到的话再去原型对象上的属性。
// 实例对象和原型对象上如果有同名属性,总是先取实例对象上的值

原型式继承的优缺点

原型式继承的优点
  1. 类似于复制一个对象,用函数来包装。
原型式继承的缺点
  1. 所有实例都会继承原型上的属性。
  2. 无法实现复用。(新实例的属性都是后面添加的)

寄生式继承

寄生式继承的定义以及简单介绍

与原型式继承比较接近的一种继承方式是寄生式继承,类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。简而言之就是寄生式继承就是给原型式继承外面套了个壳子

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。

var ob = {
  name: 'xiaopao',
  friends: ['lulu','huahua']
}
function CreateObj(o){
  function F(){};  // 创建一个构造函数F
  F.prototype = o;
  return new F();
}
// 上面CreateObj函数 在ECMAScript5 有了一新的规范写法,Object.create(ob) 效果是一样的 , 看下面代码
var ob1 = CreateObj(ob);
var ob2 = Object.create(ob);
console.log(ob1.name); // xiaopao
console.log(ob2.name); // xiaopao
function CreateChild(o){
  var newob = CreateObj(o); // 创建对象 或者用 var newob = Object.create(ob)
  newob.sayName = function(){ // 增强对象
      console.log(this.name);
  }
  return newob; // 指定对象
}
var p1 = CreateChild(ob);
p1.sayName(); // xiaopao

寄生式继承的优缺点

寄生式继承的优点
  1. 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
寄生式继承的缺点
  1. 没用到原型,无法复用。(通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似)

寄生组合式继承

寄生组合式继承的定义以及简单介绍

最常用的继承方式,也是最佳的,组合继承会调用两次父类构造函数,存在效率问题。其实本质上子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function object(person) {
  function F(params) {}
  F.prototype = person
  return new F()
 }
 function inheritPrototype(SubType,SuperType) {
  let prototype = object(SuperType.prototype) //生成一个父类原型的副本
 
  //重写这个实例的constructor
  prototype.constructor = SubType
 
  //将这个对象副本赋值给 子类的原型
  SubType.prototype = prototype
 }
 
 function SuperType(name) {
   this.name = name;
   this.colors = ["red","blue","green"];
 }
 SuperType.prototype.sayName = function() {
   console.log(this.name);
 };
 function SubType(name, age) {
   SuperType.call(this, name);
   this.age = age;
 }
 
 //调用inheritPrototype函数给子类原型赋值,修复了组合继承的问题
 inheritPrototype(SubType, SuperType);
 
 SubType.prototype.sayAge = function() {
   console.log(this.age);
 };

寄生组合式继承的优缺点

寄生式继承的优点
  1. 通过寄生方式,去掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点。
function Father() {
  this.name = '张三';
}
function Son() {
  Father.call(this)
}
Son.prototype = createObject(Father.prototype)
Son.prototype.constructor = Son;
function createObject(o) {
  function fn(){};
  fn.prototype = o;
  return new fn;
}

class继承

class和class继承属于ES6的新特性,class继承主要通过extends关键字实现继承

class Parent {
  constructor() {
    this.name = '王五';
  }
  reName() {
    console.log(this.name);
  }
}
class Child extends Parent {
  constructor() {
    super()
  }
}
var child1 = new Child()
var child2 = new Child()


目录
相关文章
|
5天前
|
JavaScript 前端开发
前端 JS 经典:原型和原型链
前端 JS 经典:原型和原型链
11 0
|
8天前
|
前端开发 JavaScript
前端 JS 经典:Class 面向对象
前端 JS 经典:Class 面向对象
11 1
|
8天前
|
前端开发 JavaScript
前端 js 经典:class 类
前端 js 经典:class 类
7 2
|
8天前
|
前端开发 JavaScript
前端 js 经典:原型对象和原型链
前端 js 经典:原型对象和原型链
19 1
|
8天前
|
JavaScript
js中如何使用工厂方式和构造函数创建对象,web开发项目实例
js中如何使用工厂方式和构造函数创建对象,web开发项目实例
|
9天前
|
JavaScript 前端开发
JavaScript 原型链继承:掌握面向对象的基础
JavaScript 原型链继承:掌握面向对象的基础
|
9天前
|
JavaScript 前端开发
JavaScript构造函数模式:创建对象的另一种方式!
JavaScript构造函数模式:创建对象的另一种方式!
|
11天前
|
JavaScript 前端开发
在JavaScript中,函数原型(Function Prototype)是一个特殊的对象
【5月更文挑战第11天】JavaScript中的函数原型是一个特殊对象,它为所有函数实例提供共享的方法和属性。每个函数在创建时都有一个`prototype`属性,指向原型对象。利用原型,我们可以向所有实例添加方法和属性,实现继承。例如,我们定义一个`Person`函数,向其原型添加`greet`方法,然后创建实例`john`和`jane`,它们都能调用这个方法。尽管可以直接在原型上添加方法,但推荐在构造函数内部定义以封装数据和逻辑。
21 2
|
11天前
|
JavaScript 前端开发
JavaScript 中最常用的继承方式
【5月更文挑战第9天】JavaScript中的继承有多种实现方式:1) 原型链继承利用原型查找,但属性共享可能引发问题;2) 借用构造函数避免共享,但方法复用不佳;3) 组合继承结合两者优点,是最常用的方式;4) ES6的class继承,是语法糖,仍基于原型链,提供更直观的面向对象编程体验。
25 1
|
11天前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念
【5月更文挑战第9天】JavaScript继承有优点和缺点。优点包括代码复用、扩展性和层次结构清晰。缺点涉及深继承导致的复杂性、紧耦合、单一继承限制、隐藏父类方法以及可能的性能问题。在使用时需谨慎,并考虑其他设计模式。
17 2