原型与继承
2024-06-04
共 3473 字
预计阅读 18 分钟

这一部分红宝书讲得很清楚了,配合红宝书增加图例即可

从对象创建讲起

对象是 JavaScript 中使用最多的数据类型,对象的创建需要实现两方面的内容:

首先是表示创建了一个什么类型的对象

其次是对象上共有的属性和方法应该在同一类型的对象中继承,减少重复代码。

字面量创建

const obj = {};
const obj1 = new Object();

工厂模式

步骤

  • 创建对象
  • 给对象赋值
  • 返回赋值完的对象

缺点

无法说明创建对象的类型

代码

function createPerson(name, job, age) {
  // 创建对象
  let o = new Object();
  // 给对象赋值
  o.name = name;
  o.job = job;
  o.age = age;
  o.sayName = function () {
    console.log(this.name);
  };
  // 返回赋值的对象
  return o;
}
const person1 = createPerson("张三", "fe-dev", 18);
const person2 = createPerson("李四", "be-dev", 18);
console.log(person1.constructor); // Object 无法说明创建对象类型

构造函数

通过 Constructor 标明对象类型,实例对象本身没有 constructor 属性,该属性取自原型对象上的 constructor。

步骤

  • 没有显式创建对象
  • 赋值
  • this 指向调用实例
  • 没有显式返回对象

缺点

定义在构造函数上的方法存在同名不可复用问题

代码

function Person(name, job, age) {
  // 没有显式创建对象
  // 赋值
  // this指向调用实例
  this.name = name;
  this.job = job;
  this.age = age;
  // 该方法会在每一个实例上定义一次,无法复用
  this.sayName = function () {
    console.log(this.name);
  };
  // 没有显式返回对象
}
const person1 = new Person("小明", "dev", 18);
console.log(person1); // Person
console.log(person1.constructor === Person); // true
console.log(person1.constructor === Object); // false
person1 instanceof Person; // true
person1 instanceof Object; // true

原型

基本概念

  • 构造函数 prototype 指向原型对象
  • 原型对象 constructor 指回构造函数(表示创建的对象类型)
  • 实例对象__proto__属性指向原型对象
  • 实例对象与原型对象有直接联系,同构造函数没有
  • 定义在原型上的方法及属性在实例对象中共享,且由于原型对象上 constructor 的存在能够标明对象类型
  • 若原型对象是另一个父对象的实例,原型对象的__proto__指向父对象的原型对象,否则默认为 Object 的实例,即__proto__指向 Object.prototype
  • 检索属性顺序:实例对象 ➡(构造函数的)原型对象 ➡ 原型对象的原型对象直到 Object 的原型对象 ➡null
  • instanceof 及 isProperty 是基于原型链的判断

proto

proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

图例

image-20240616172025974

这是最最基础的图,基于该图理解

说明

属性说明
constructor对象独有属性,说明对象是什么类型。
红宝书:一般认为 instanceof 操作符是确定对象类型更可靠的方式。
实例对象的 constructor 取自原型对象上的 constructor
prototypeObject that provides shared properties for other objects.
在规范里,prototype 被定义为:给其它对象提供共享属性的对象。
prototype 自己也是对象,只是被用以承担某个职能罢了。
这个属性存在于函数上,函数创建时创建其原型对象。
** proto **对象独有属性,指向它的原型对象 (创建时构造函数的原型对象)
属性
constructor
该属性每一个对象都有(实际是存在于原型对象上)
构造函数(包括 Object)都是 Function 的实例
实例对象该属性取值于原型对象指向的 constructor 属性
注意原型对象被手动指定时发生的实例对象类型识别错误
prototype
不太科学地说
第一列都有对应构造函数
第二列都是 Function 的实例
第三列是 Object 的实例
** proto **
指向对象创建时构造函数的原型对象
如果原型对象被修改了就只向新指定的原型对象

再到继承

JavaScript 的继承只有实现继承

原型链

原理

原理是将父构造函数的实例作为子构造函数的原型对象

⏰ SubType.prototype = new SuperType();

将使得 Subtype.prototype 的__proto__属性指向 SuperType.prototype,共享 SuperType 原型上的属性及方法

这个时候 Subtype 实例对象的 Constructor 为 SuperType,使用 constructor 检测会发生类型错误,使用 instanceof 检测结果无误。

实际效果:

SubType 实例对象的原型对象上包含了 SuperType 构造函数里面定义的属性与方法

SubType 实例对象的原型对象 的 原型对象上又包含了 SuperType 原型对象上包含的属性与方法

缺点

  • 定义在父构造函数的属性和方法变为子原型对象上的属性和方法,如果存在引用值,该引用值将在所有子构造函数实例对象中共享
  • 子类型在实例化的时候不能给父类型构造函数传参,对父构造函数调用的传值将体现在所有子类型实例对象上
  • 子类型实例对象的 constructor 会返回父构造函数

代码

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function () {
  return this.property;
};
function SubType() {
  this.subproperty = false;
}
// 继承 SuperType
// 这一步给SubType.prototype增加了property属性,导致原本应该给每一个实例对象的属性变为原型对象的属性
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};
// 这个时候SubType.prototype并没有constructor属性,于是继续往上找到了SuperType.prototype上的constructor属性
// 导致instance的constructor返回的是SuperType
let instance = new SubType();
console.log(instance.getSuperValue()); // true
instance.constructor === SuperType; // true

盗用构造函数

原理

解决原型包含引用值导致的继承问题,可以在子类构造函数中向父类构造函数传参。

本质是在子构造函数中调用父构造函数并改变 this 指向

function SubType(){

SuperType.call(this,[…args]);

}

使得 SubType 实例能够继承 SuperType 构造函数上的属性和方法,并且可以向父构造函数传参

缺点

  • 必须在构造函数中定义方法,实例化之后存在多个同名方法无法复用。
  • 子类不能访问父类原型上的方法。

代码

function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {
  // 继承 SuperType
  SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"

组合继承

本质是:原型链 + 盗用构造函数 = 组合继承

使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

实际上父构造函数的属性同时出现在子类实例及子类原型对象上,只是由于调用顺序原型对象上的属性检索不需要使用

而且子类型实例对象的 constructor 同样指的父类型,需要使用 instanceof 判断

原型式继承

Object.create()做的事情:

  • 修改了__proto__
  • 修改了 Conctructor,即定义了对象类型,如果用在继承上实际是导致 Constructor 指向错误
function object(o) {
  function F() {}
  F.prototype = o;
  // 返回对象的__proto__指向浅拷贝的o
  return new F();
}
function Person() {
  this.name = "人";
}
const person1 = new Person();
console.log(person1); // Person {name:'人'}
const person2 = object(Person.prototype);
console.log(person2); // F {}

发生了类型丢失,object 等价于 Object.create()实际用于对象拷贝

实现继承需要手动补充 prototype 的 constructor 指向

Object.create()第一个参数等价于 object 实现的继承,第二个参数给新对象定义额外属性的对象

寄生式继承

如果不修复子类的构造函数指向,会导致以下后果:

  1. 构造函数指向错误:子类的 constructor 属性会指向父类而不是子类本身。这意味着当你通过子类创建新对象时,constructor 属性会指向父类的构造函数,而不是子类的构造函数。这可能会导致一些意外的行为或错误。
  2. 类型判断错误:由于构造函数的指向错误,使用 instanceof 运算符进行类型判断时可能会出现错误结果。如果不修复构造函数指向,instanceof 运算符可能会认为子类的实例是父类的实例,而不是子类的实例。

通过修复子类的构造函数指向,即 ChildClass.prototype.constructor = ChildClass,可以确保构造函数指向子类本身,从而避免上述问题。这样做可以确保正确的类型判断和行为,以及更好的代码可读性和维护性。

function object(o) {
  function F() {}
  F.prototype = o;
  // 返回对象的__proto__指向浅拷贝的o
  return new F();
}

function createAnother(original) {
  let clone = object(original); // 通过调用函数创建一个新对象
  clone.sayHi = function () {
    // 以某种方式增强这个对象
    console.log("hi");
  };
  return clone; // 返回这个对象
}

寄生式组合继承

实际是原型式继承 + 修复了 Constructor 丢失 + 给子构造函数加属性强化然后返回罢了

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function () {
  console.log("My name is " + this.name);
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// 创建一个继承自 Animal 的对象,并在原型上添加额外的方法
function createDog(name, breed) {
  var dog = Object.create(Animal.prototype);
  dog.constructor = Dog;
  dog.name = name;
  dog.breed = breed;

  dog.bark = function () {
    console.log("Woof!");
  };

  return dog;
}

// 创建一个 Dog 对象
var myDog = createDog("Max", "Labrador");
myDog.sayName(); // 输出: My name is Max
myDog.bark(); // 输出: Woof!

核心实现

function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype; // 赋值对象
}

最后到 ES6 的类

本质还是原型 + 构造函数的封装

相关题目:new 构造符做了什么

  • 创建一个新的空对象。
  • 将这个新对象的__proto__设置为类的prototype对象。
  • 执行构造函数,并将this绑定到新创建的对象上。
  • 如果构造函数返回了一个对象,那么这个对象将成为new表达式的返回值;否则,返回新创建的对象。

对象拷贝

区分继承/复用和深浅拷贝(不保留原始构造关系,通常拷贝出来的对象只是 Object 的实例)

深拷贝操作并不会保留原始对象的构造函数。如果需要在深拷贝后的对象中保留原始的构造函数,可以手动设置 constructor 属性,例如 copyPerson.constructor = Person

看看 AI 的回答:

深拷贝和原型继承是两种不同的概念和用途,它们的区别如下:

  1. 定义和目的:深拷贝是创建一个对象的完全独立副本,副本中的所有属性值都是原对象属性的副本。深拷贝的目的是为了在不影响原对象的情况下,创建一个与原对象完全独立的对象。原型继承是通过原型链建立对象之间的关联关系,一个对象可以继承另一个对象的属性和方法。
  2. 复制方式:深拷贝是通过递归复制对象的所有属性值,包括嵌套对象和数组。深拷贝后的对象和原对象是完全独立的,修改其中一个对象的属性不会影响另一个对象。原型继承是通过将一个对象设置为另一个对象的原型来建立对象之间的关联。继承后的对象会共享原型对象的属性和方法,修改其中一个对象的属性可能会影响其他对象。
  3. 对象关系:深拷贝后的对象是完全独立的,它与原对象没有任何关联,它们是两个独立的对象。原型继承建立了对象之间的关联,继承后的对象可以访问原型对象的属性和方法,原型对象也可以通过原型链访问继承对象的属性。
  4. 使用场景:深拷贝适用于需要创建一个与原对象完全独立的对象,例如在修改对象时需要保留原始对象的副本,或者在传递对象时需要避免引用传递。原型继承适用于需要创建一个对象,并使其继承另一个对象的属性和方法,从而实现代码重用和继承关系的场景。

需要注意的是,深拷贝和原型继承都有各自的优缺点和适用场景。在选择使用哪种方式时,需要根据具体的需求和情况进行考虑。深拷贝可能会导致性能上的损耗,特别是对于嵌套层级很深的对象或包含大量数据的对象。原型继承可以方便地实现属性和方法的继承,但也可能导致对象之间的耦合性增加。

深拷贝

function deepClone(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  let clone = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }

  return clone;
}

关于 typeof 与 instanceof

instanceof

使用是 a instanceof b

本质是取 b.prototype 跟 a.proto比较,不对就跟 a.proto.proto比较直到 null

SubType.prototype 虽然被改为 SuperType 的实例,但是 subtype1 的proto指向了被修改的实例,导致返回的还是 true

至于 subtype1 的类型还是 SubType,初步判断这是作用域[[scope]]里面存储的 instance 里面为 SubType

function instance_of(obj, constructor) {
  const basicType = [
    "[object Undefined]",
    "[object Null]",
    "[object String]",
    "[object Boolean]",
    "[object Symbol]",
    "[object BigInt]",
  ];
  const objType = Object.prototype.toString.call(constructor);
}

image-20240617012236085

参考资料

感谢您的阅读,如果对文章内容有任何疑问或者建议,欢迎在掘金社区私信我