原型/原型链/构造函数/实例/原型继承

358 阅读6分钟

原型

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象,并且这个属性是一个对象数据类型的值。每一个javascript对象(除null外)创建的时候,就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型中“继承”属性。

prototype 是一个函数对象特有的属性,它指向该函数的原型对象;而 __proto__ 则是所有 JavaScript 对象(包括函数对象)都具有的属性,它指向该对象的原型对象。另外,需要注意的是 __proto__ 在标准化之前是非标准属性,应该尽量避免使用。目前推荐使用 Object.getPrototypeOf(obj) 方法来获取一个对象的原型。

 function Parent () {}
     Parent.prototype.name = '父亲'
     Parent.prototype.sayName = function(){
       console.log(this.name)
}
let p = new Parent()
console.log(p)

image.png

原型链

原型链是 JavaScript 中的一个重要概念,它定义了对象之间继承关系的方式。每个对象都有一个 [[Prototype]] 内部属性,指向它的原型对象。当访问对象的属性时,如果该对象自身没有这个属性,就会沿着原型链往上查找直到找到为止。原型链的顶端是 Object.prototype,它是所有对象的祖先对象。 原型对象中有一个属性constructor, 它指向函数对象。

构造函数

构造函数模式的目的是创建一个可重复使用的对象模板,通过构造函数创建新的对象实例,并将属性和方法添加到这些对象中。这种模式使得对象可以有自己的属性和方法,而不必共享原型对象上的属性和方法。

  • 没有显示地创建对象
  • 属性和方法直接赋值给了this
  • 没有return

image.png

构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯    上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用  new关键字来调用。

function Person(name,age){
    this.name = name
    this.age = age
    this.sayName = function() {
        console.log(this.name)
    }
}

let p1 = new Person('张三',18)
let p2 = new Person('李四',28)

p1.sayName()
p2.sayName()

console.log(p1)
console.log(p2)
console.log(p1.constructor === Person)// true

实例

实例是通过构造函数创建的对象。每个实例都有自己独立的属性值,不同实例之间的属性值可以不同。但是它们都共享构造函数的原型对象,因此它们具有相同的方法和属性。

原型继承

实现的几种方式

  1. 原型链继承
  2. 构造函数继承
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承

原型链继承

通过将子类的原型对象指向父类的实例来实现继承

function Parent (){
    this.colors = ['red','green']
    name : '私有name'
}
function Child (){}
Child.prototype = new Parent()
// 创建实例
let c = new Child()
c.colors.push('blue')
console.log(c.colors)

let c2 = new Child()
console.log(c2.colors)
console.log(c2.name) // undefined

image.png

缺点:

  1. 原型中的引用类型值会被所有实例共享,当其中一个实例修改了该引用类型值时,其他实例也会受到影响。
  2. 无法向超类传递参数,因为原型是在实例化之前构建的。
  3. 子类无法访问超类的私有成员

构造函数继承

构造函数继承是通过在子类构造函数中调用父类构造函数来实现继承

function Parent (){
    this.colors = ['red','green']
}
Parent.prototype.name = '原型name'
function Child (){
    Parent.call(this)
}
// 创建实例
let c = new Child()
c.colors.push('blue')
console.log(c.colors)

let c2 = new Child()
console.log(c2.colors)
console.log(c2.name) // 无法访问

image.png 缺点:

  1. 无法继承超类原型上的属性和方法
  2. 每个实例都会单独复制一份超类的属性和方法,导致内存浪费。
  3. 子类无法访问超类原型上的方法

组合继承

组合继承是将原型链继承和构造函数继承结合起来使用,既可以继承父类原型上的方法,又可以在子类构造函数中传递参数给父类构造函数。并且实例之间互不影响

function Parent (name){
    this.name = name
    this.colors = ['red','blue']
}
Parent.prototype.sayHello = function() { 
    console.log('Hello, ' + this.name); 
};
function Child (name){
    Parent.call(this,name)
}
Child.prototype = new Parent()
// 创建两个实例
let c = new Child('儿子')
c.colors.push('green')
console.log(c.name)
console.log(c.colors)
c.sayHello() // 访问超类原型方法

let c2 = new Child('儿子2')
console.log(c2.name)
console.log(c2.colors)
c2.sayHello()// 访问超类原型方法

image.png

组合继承的主要缺点是使用了两种不同的继承方式,即原型继承和构造函数继承,这可能导致一些性能问题和内存浪费。

原型式继承

这是另一种继承,没有严格意义上的构造函数。思路是:通过使用一个已有的对象作为新对象的原型,并在该基础上进行修改和扩展来创建新对象。

let parent = {
  name: 'parent',
  sayHello: function() {
    console.log('Hello, ' + this.name);
  },
  colors:['red','green']
};

let child = Object.create(parent); // 将parent 作为child的原型
child.name = 'child';
child.sayHello(); 
child.colors.push('pink')
console.log(child.colors);

let child2 = Object.create(parent);
child2.name = 'child2'
child2.sayHello(); 
console.log(child2.colors);

image.png

原型式继承的缺点包括:

  1. 对象之间共享状态:由于原型对象被多个实例共享,因此更改一个实例的属性可能会影响其他实例的行为。
  2. 属性查找的性能问题:在对象实例中查找属性时,如果该属性存在于原型链的深层级别,则需要遍历整个原型链,这可能会导致性能下降。
  3. 不能改动自己的原型(因为返回new已经实例化了),所以也不能复用

寄生式继承

该继承方式与原型式继承比较接近,它的思路类似于寄生构造函数和工厂模式。寄生式继承是在原型式继承的基础上,增加了一个包装函数,用于增强新对象

function createChild(parent) {
  let clone = Object.create(parent); // 将parent 作为clone 的原型
  clone.age = 18;
  return clone; // 返回这个对象
}

let parent = {
  name: 'parent',
  sayHello: function() {
    console.log('Hello, ' + this.name);
  }
};
// 原型上添加方法
parent.__proto__.sayHello

let child = createChild(parent);
console.log(child.name); // 'parent'
console.log(child.age); // 18
child.sayHello(); // 'Hello, parent'

寄生式组合继承

寄生组合式继承解决了组合式继承两次调用的问题

// 父类构造函数
function Person(name) {
  this.name = name;
}

// 父类原型方法
Person.prototype.sayName = function() {
  console.log(this.name);
};

// 子类构造函数
function Child(name) {
  // 继承父类属性
  Person.call(this, name);
  this.age = [13,14];
}

// 寄生式继承父类原型
function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype); // 创建父类原型的副本
  prototype.constructor = subType; // 重置constructor指向子类
  subType.prototype = prototype; // 将副本赋值给子类原型
}

// 使用寄生组合式继承
inheritPrototype(Child, Person);

// 测试代码
let c1 = new Child('张三');
c1.sayName();
c1.age.push(15)
console.log(c1.age)

let c2 = new Child('李四')
console.log(c2.age

console.log(c1 instanceof Person); // 输出 "true"
console.log(c1 instanceof Child); // 输出 "true"

image.png

最优解class类继承