(十)原型、原型链、继承

192 阅读8分钟

1.构造函数

在介绍原型前先了解下什么是构造函数?

构造函数也是函数,在表现形式上与函数没有什么区别,但在使用时构造函数需要用new关键字调用。另外,约定俗成构造函数首字母大写(不是必须

new关键字调用构造函数内部操作

当使用new关键字调用构造函数时,流程如下

  1. 创建新对象,该对象为空对象
  2. 新对象的原型__proto__指向构造函数的原型prototype
  3. this指向这个新对象
  4. 指向构造函数内部代码
  5. 如果构造函数没有指定返回复杂类型数据则返回this否则返回指定数据。

2.原型 [[prototype]]

什么是原型

每个对象(函数也算对象)中都会有原型,原型可以理解为对象之间的连接,是用于实现继承和属性共享的机制。
对象的原型指的是__proto__,函数的原型指的是prototype,而函数也算是对象所以也会有__proto__

  • 显式原型:可以使用函数.prototype的方式来获取函数的原型。
  • 隐式原型:用来获取对象的原型,在写测试代码时候可以使用对象.__proto__的方式来使用。

注:在JavaScript中,__proto__是一个非标准的属性,它用于访问对象的原型。它是一种在早期JavaScript实现中存在的属性,但目前它已经被ECMAScript标准所废弃, 可以使用Object.getPrototypeOf()方法来获取对象原型

对象和函数对象的原型指向

对于原型指向总归一句话:对象原型找其构造函数

1.由构造函数创建出的对象,其原型__proto__指向构造函数的原型prototype,其它对象形式指向Object.prototype,除了Object.prototype.__proto__
2.函数对象的原型prototype默认指向一个包含constructor的对象


  1. 创建对象方式可以由 字面量创建new构造函数 来创建。
    由于字面量创建对象是new Object()的简写形式,所以字面量创建对象的原型指向为构造函数Object的原型prototype

  2. 函数对象的显式原型prototype是一个对象,该对象存在一个不可枚举属性constructor,该属性的值为函数对象本身。函数对象的隐式原型__proto__指向的是构造函数Fucntionprototype

图片.png

function Person(){}

const p1 = new Person()

对于上方代码,原型指向情况如下

图片.png

对象属性or方法的getter查找规则

当获取对象上的属性or方法时会现在对象自身寻找,如果找寻不到则会在对象的原型上寻找。


给构造函数的原型添加属性or方法

为何要给构造函数的原型添加属性or方法

我们观察如下代码

function Person(name, age) {
  this.name = name;
  this.age = age;
  
  this.eating = function () {
    console.log(`${this.name}  在吃饭`);
  };
  
  this.runing = function () {
    console.log(`${this.name}  在跑步`);
  };
}
const p1 = new Person("p1", 18);

const p2 = new Person("p2", 18);

const p3 = new Person("p3", 18);

每次通过new来调用Person构造函数时,都会创建方法eatingruning,这样会浪费内存空间,完全没必要。
而我们在上方说过对象属性or方法的getter查找规则,根据这个规则我们可以直接构造函数的原型添加属性or方法,这意味着类的实例之间共享相同的方法代码,而不是复制多份。

那么如何给构造函数的原型添加属性or方法?


如何给构造函数的原型添加属性or方法

我们可以使用构造函数.prototype.xxx = xx这种方式一个个来给原型上添加属性or方法,但确定是每次都需要通过构造函数.prototype.xxx来添加,这样就比较麻烦,那么我们就可以使用构造函数.prototype = {}来整体赋值,但需要注意,原本构造函数.prototype是存在constructor属性的,并且是不可枚举的。那么我们需要单独来对constructor定义。代码如下:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype = {
  eating: function () {
    console.log(`${this.name}  在吃饭`);
  },

  runing: function () {
    console.log(`${this.name}  在跑步`);
  },
};

Object.defineProperty(Person, "constructor", {
  configurable: true,
  writable: true,
  enumerable: false,
  value: Person,
});

const p1 = new Person("p1", 18); //p1 在跑步

原型指向图解

原型链.jpg

特殊的几个原型指向

console.log(Object.__proto__ == Function.prototype); //true  
//因为Object也是一个函数对象,是new Fucntion()简写形式,其值为{construtor:Function,__proto__:object.prototype}

console.log(Function.prototype.__proto__ == Object.prototype);//true  

console.log(Object.prototype.__proto__ == null);//true

console.log(Function.__proto__ == Function.prototype);//true 因为Function也是一个函数对象,是new Fucntion()简写形式,

3.原型链[[prototype chain]]

首先要知道原型也是一个对象,原型也会有原型

什么是原型链

原型链是js实现继承的一种机制,其由一个接一个原型组成,如:(原型->原型的原型->原型的原型的原型-> ···),原型链最顶层是Object.prototype(还有另一种说法是null)。
当我们在对象上调用一个方法or使用一个属性时,如果对象自身没有会在原型链上寻找,找到则返回,直到最顶层还没有找到的话则返回undefined

4.继承

什么是继承

继承就是重复利用另一个对象的属性or方法,如下代码

function Student(name, age) {
  this.name = name;
  this.age = age;
}

Student.prototype.eating = function () {
  console.log("可以吃");
};

Student.prototype.running = function () {
  console.log("可以跑");
};

Student.prototype.learning = function () {
  console.log("可以学习");
};

  


function Teacher(name, age) {
  this.name = name;
  this.age = age;
}

Teacher.prototype.eating = function () {
  console.log("可以吃");
};

Teacher.prototype.running = function () {
  console.log("可以跑");
};

Teacher.prototype.teaching = function () {
  console.log("可以教学");
};

我们看上面的两个构造函数StudentTeacher,这两个构造函数都有属性nameage及方法eatingruning,如果再有其它构造函数如工人、司机等岂不是还要再重新定义一遍?那我们有没有办法来避免代码的重复?有,那就是继承。我们可以定义个构造函数为Person,这个构造函数具有共有属性,其它构造函数来继承Person就可以了。

如何实现继承

1.原型链继承

原理:利用在实例上寻找不到会在其原型上寻找的方式

优点:实现了方法继承

缺点:

  1. 只实现了方法继承,没有实现属性继承

  2. Student的原型添加方法会同时添加到Person的原型上

  3. 类型会为Person

function Person() {}
Person.prototype.eating = function () {
  console.log("吃饭");
};

Person.prototype.running = function () {
  console.log("跑步");
};

function Student() {
  this.name = name
}

// 该行代码实现了原型链继承
Student.prototype = new Person()

Student.prototype.learning = function () {
  console.log("学习");
};

const stu1 = new Student('stu1')

图片.png

2.借用构造函数式继承

原理:使用call来更改this绑定的方式来属性继承

优点:实现了属性继承

缺点:不能继承方法

function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Student(name, age, sno) {
  // 该行实行了借用构造函数式继承
  Person.call(this, name, age);
  this.sno = sno;
}


let stu = new Student("stu", 18, "18452");

图片.png

3.组合式继承

原理:原型式继承和借用构造函数式继承的组合形式

优点:实现了属性继承和方法继承

缺点:

  1. 会调用两次Person构造函数
  2. Student原型添加的方法会出现在Person的原型上
  3. stu对象实例上和原型同时出现nameage属性

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.eating = function () {
  console.log("吃饭");
};

Person.prototype.running = function () {
  console.log("跑步");
};

function Student(name, age, sno) {
  // 调用构造函数式继承
  Person.call(this, name, age);
  this.sno = sno;
}

// 原型链继承
Student.prototype = new Person();

Student.prototype.learning = function () {
  console.log("学习");
};

let stu = new Student("stu", 18, "18452");

图片.png

4.原型式继承

原理:使用一个空对象作为中介,将某个对象赋值给空对象的构造函数的原型

优点:实现了属性继承和方法继承

缺点:

  1. 只能对象方式继承,而不是构造函数式继承
  2. 不能传参,只能通过stu1.name的方式去修改,并且修改完成之后只是给stu1实例上的name修改,原型上还存在name属性并且值为person
  3. 如果是引用类型,会多个实例的引用类型属性指向相同,存在篡改的可能
// create函数实现原型式继承
function createObject(obj) {
  function foo() {}
  foo.prototype = obj;
  return new foo();
}

const person = {
  name: "person",
  friends: ["a", "b"],
  running: function () {
    console.log("跑步");
  },
};

// stu1继承自person
const stu1 = createObject(person);
stu1.name = "stu1";
stu1.friends.push("c");


// stu2继承自person
const stu2 = createObject(person);
stu2.name = "stu2";
console.log(stu2.friends); //Array(3) [ "a", "b", "c" ]

stu1:

图片.png
stu2:

图片.png


5.寄生式继承

原理:原型式继承和工厂函数结合

优点:实现了属性继承和方法继承,并且在原型式基础上可以传递参数

缺点:

  1. 依旧只是对象上继承
  2. 依旧如果是引用类型,会多个实例的引用类型属性指向相同,存在篡改的可能
function createObject(obj, name, age) {
  function foo() {}
  foo.prototype = obj;
  
  foo.name = name;
  foo.age = age;
  foo.friends = ["a", "b", "c"];
  return new foo();

}

const person = {
  name: "person",
  friends: ["a", "b"],
  running: function () {
    console.log("跑步");
  },
};

// stu1继承自person
const stu1 = createObject(person, "stu1", "18");
stu1.name = "stu1";
stu1.friends.push("c");

// stu2继承自person
const stu2 = createObject(person, "stu2", "20");
stu2.name = "stu2";
console.log(stu2.friends); //Array(3) [ "a", "b", "c" ]

stu1:

图片.png
stu2:

图片.png


6.寄生式组合继承 !!!最终方案

原理:原型式继承和调用构造函数式继承的结合,使用Object.create方法来代替原型式继承的createObject方法

function Person(name, age, friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
}

Person.prototype.eating = function () {
  console.log(this.name + `在吃饭`);
};

Person.prototype.running = function () {
  console.log(this.name + `在跑步`);
};

function Student(name, age, friends, sno) {
  // 实现了属性继承
  Person.call(this, name, age, friends);
  this.sno = sno;
}

  


Student.prototype = Object.create(Person.prototype, {
//修改construtor指向类型
  constructor: {
    writable: true,
    configrable: true,
    value: Student,
  },
});

// 上方的Object.create相当于下方伪代码,这样实现了方法继承
//Student.prototype = { constructor: Student, __proto__: Person.prototype };

Student.prototype.learning = function () {
  console.log(this.name + `在学习`);
};

let stu = new Student("stu1", "18", [{ one: "bob" }], "1106");

图片.png