JavaScript:原型、原型链、继承

一、理解原型

1.1、人工智能解释

JavaScript中的原型是一种机制,它允许在创建对象时共享属性和方法。每个JavaScript对象都有一个原型对象,它包含一些公共属性和方法,可以被该对象和其他对象共享。

当你创建一个对象时,Javascript会从该对象的构造函数的原型对象中查找属性和方法。如果该属性或方法不存在,则Javascript会继续查找该对象原型对象的原型对象,以此类推,直到找到该属性或方法为止。

function Person(name, age) {  // 创建一个Person构造函数
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {   // 给Person增加一个greet方法
  return 'Hello, my name is ' + this.name + ' and I am ' + this.age + ' years old.';
};

var person1 = new Person('Alice', 25);
console.log(person1.greet()); // 输出 "Hello, my name is Alice and I am 25 years old."

在这个例子中,我们定义了一个Person构造函数和一个greet方法,并且将该方法添加到Person的原型对象中。然后我们使用Person构造函数创建了一个新的对象person1,并调用了该对象的greet方法。

值得注意的是,我们可以通过person1.__proto__来访问person1对象的原型对象,以及通过Person.prototype来访问Person构造函数的原型对象。原型指向的对象称为“原型对象”。

1.2、通俗理解

我们可以把原型比喻作“模型”、“模具”。

我们以“杯子”举例,杯子模具提供了杯子可以装水的基本能力。如果我们想要赋予杯子更多能力我们可以通过给杯子的原型prototype增加属性和方法,从而实现杯子制冷、制热、外观色彩等功能。

每个js对象(除null)创建的时候,都会与之关联另一个对象,这个对象就是我们说的原型,每一个对象都会从原型中“继承”属性。

每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法。

1.3、原型分为“隐式原型”和显示原型

1.3.1、隐式原型:JavaScript中每一个对象都有一个特殊的内置属性[[prototype]](隐式原型),这个属性指向的对象成为“隐式原型对象”。

1.3.2、显示原型:函数也是属于对象,也拥有属性,也有隐式原型([[prototype]])。但是函数还有一个属性prototype(显示原型) ,这个属性指向的对象称之为“显示原型对象”。prototype没有兼容性问题,可以直接使用。

1.4、原型作用

把对象的方法放到原型对象中,方法共享,节省空间。

1.5、prototype 与 __proto__

ES2019 对prototype 的定义

object that provides shared properties for other objects

在规范里,prototype 被定义为:给其它对象提供共享属性的对象。

也就是说,prototype 自己也是对象,只是被用以承担某个职能罢了。

在js中,每个函数都有一个prototype属性,这个属性指向函数的原型对象(函数也是个对象)。

这是每个对象(除null外)都会有的属性,叫做__ proto__属性,这个属性会指向该对象的原型。

二、原型链

在JavaScript中,每个对象都有一个原型对象(prototype),原型对象又有它自己的原型对象,这样就形成了一个原型链(prototype chain)。

当我们尝试访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript就会在该对象的原型中查找是否有该属性或方法。如果还没有找到,JavaScript会继续在原型的原型中查找,直到找到该属性或方法或原型链的顶端为止。如果在整个原型链中都没有找到该属性或方法,那么JavaScript就会返回undefined。

下面是一个示例,展示了构造函数、对象、原型和原型链之间的关系:

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

Person.prototype.greet = function() {
  console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
};

var person1 = new Person("Alice", 25);
person1.greet(); // 输出 "Hello, my name is Alice and I am 25 years old."

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

在这个例子中,我们定义了一个Person构造函数,然后创建了一个person1对象,并调用了该对象的greet方法。在访问person1.greet方法时,JavaScript会在person1的原型对象Person.prototype中查找该方法。

我们还使用console.log输出了person1.proto、Person.prototype.__proto__和Object.prototype.proto,并验证了原型链的结构。

需要注意的是,在ES6之后,还可以使用class和extends关键字来定义类和继承关系,但是它们的本质仍然是基于原型链的。

在开发代码中将原型的实例赋值给另一个对象,另一个对象再赋值给其他的对象,在实际的代码中对对象不同的赋值,就会形成一条原型链。

function Animal(weight) {
     this.weight = weight
}
Animal.prototype.name = 'animal'
var cat1 = new Animal()
var pinkCat = cat1
console.log(pinkCat.name); //animal
console.log(pinkCat.__proto__ === cat1.__proto__ == Animal.prototype) //true
var samllPinkCat = pinkCat
console.log(samllPinkCat.name);//animal
console.log(samllPinkCat.__proto__ == pinkCat.__proto__ === cat1.__proto__ == Animal.prototype);//true

三、继承

3.1、原型链继承

原型链继承是通过将一个对象的实例作为另一个对象的原型来实现继承。它是 JavaScript 中最原始的继承方式。

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

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

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

Student.prototype = new Person();

const student = new Student('John', 10);
student.sayHello(); // 输出:Hello, my name is John.
 

在上面的代码中,我们定义了一个 Person 构造函数,并将其原型上添加了一个 sayHello 方法。然后,我们定义了一个 Student 构造函数,它继承了 Person 构造函数。通过将 Person 的实例赋值给 Student 的原型属性,Student 构造函数就可以继承 Person 构造函数的属性和方法。

3.2、构造函数继承(借助call())

构造函数继承是通过在子类构造函数中调用父类构造函数来实现继承。这种继承方式可以避免父类原型上的属性和方法被子类实例共享的问题。

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

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}

const student = new Student('John', 10);
student.sayHello(); // 抛出 TypeError 错误

在上面的代码中,我们定义了一个 Person 构造函数和一个 sayHello 方法,以及一个 Student 构造函数。在 Student 构造函数中,我们通过调用 Person 的 call 方法来将 this 绑定到 Person 的构造函数中,从而实现对 Person 构造函数的继承。由于 call 方法只是调用了 Person 构造函数,并没有将 Person 的原型链赋给 Student 的原型链,所以 student 对象并没有继承 Person 的原型上的方法,因此在调用 sayHello 方法时抛出了 TypeError 错误。 

3.3、组合式继承(前两种组合)

组合继承是将原型链继承和构造函数继承结合起来使用的一种继承方式。它继承了父类构造函数的属性和方法,同时也继承了父类原型链上的属性和方法。

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

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

const student = new Student('John', 10);
student.sayHello(); // 输出:Hello, my name is John.

在上面的代码中,我们定义了一个 Person 构造函数和一个 sayHello 方法,以及一个 Student 构造函数。在 Student 构造函数中,我们通过调用 Person 的 call 方法来将 this 绑定到 Person 的构造函数中,从而实现对 Person 构造函数的继承。在 Student 的原型上,我们将其赋值为一个 Person 的实例,并将其 constructor 属性重新设置为 Student。这样,在 Student 的实例中就可以使用 Person 的属性和方法了。 

3.4、原型式继承

ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)

JavaScript 原型式继承是一种简单的继承方式,它可以通过一个已有的对象来创建新对象,新对象具有与已有对象相同的属性和方法。具体实现方式如下:

function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

const person = {
  name: 'John',
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}.`);
  }
};

const student = createObject(person);
student.grade = 10;

console.log(student.name); // 输出:John
student.sayHello(); // 输出:Hello, my name is John.

上面的代码中,我们定义了一个 createObject 函数,它接收一个对象作为参数,返回一个新对象。在 createObject 函数内部,我们创建了一个空函数 F,并将其原型对象指向传入的对象 obj,然后通过 new F() 创建一个新对象并返回。

我们通过 person 对象来创建了一个新的对象 student,student 对象继承了 person 对象的属性和方法。在这里,我们在 student 对象上添加了一个 grade 属性,这个属性不会影响 person 对象。

原型式继承的优点在于它可以方便地实现对象与对象之间的共享,而不必创建多个相似的对象。但是它也有一些缺点,例如如果原型链上的属性值被修改,那么所有继承该原型链的对象都会受到影响,可能会导致意外的结果。因此,对于需要继承的对象,最好使用 Object.create() 方法来创建一个新对象,它可以更好地控制属性的继承。

3.5、寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。

JavaScript 寄生式继承(Parasitic Inheritance)是一种基于原型式继承的模式,在原型式继承的基础上添加了一些额外的方法,以增强对象的功能。具体实现方式如下:

function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

// 寄生式继承
function createStudent(person) {
  const student = createObject(person);
  student.grade = 10;
  student.sayGrade = function() {
    console.log(`My grade is ${this.grade}.`);
  };
  return student;
}

const person = {
  name: 'John',
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}.`);
  }
};

const student = createStudent(person);
student.sayHello(); // 输出:Hello, my name is John.
console.log(student.grade); // 输出:10
student.sayGrade(); // 输出:My grade is 10.

在上面的代码中,我们定义了一个 createStudent 函数,它接收一个对象作为参数,返回一个新对象。在 createStudent 函数内部,我们通过调用 createObject 函数来创建一个新对象 student,然后为这个新对象添加了一个 grade 属性和一个 sayGrade 方法。最后,我们返回这个新对象。

通过寄生式继承,我们可以在原型式继承的基础上添加一些额外的方法和属性,以满足更具体的需求。但是,也需要注意继承链上可能存在的问题,以及方法和属性的命名冲突等问题。

3.6、寄生组合式继承

结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式。

JavaScript 寄生组合式继承(Parasitic Combination Inheritance)是一种常用的继承方式,它结合了原型式继承和构造函数继承的优点,以达到最优的继承效果。具体实现方式如下:

// 原型式继承
function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

// 构造函数
function Person(name) {
  this.name = name;
  this.friends = ['Alice', 'Bob'];
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

// 寄生组合式继承
function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}

Student.prototype = createObject(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.sayGrade = function() {
  console.log(`My grade is ${this.grade}.`);
};

const student = new Student('John', 10);
student.sayHello(); // 输出:Hello, my name is John.
console.log(student.friends); // 输出:["Alice", "Bob"]
student.sayGrade(); // 输出:My grade is 10.

在上面的代码中,我们先定义了一个 createObject 函数,用来实现原型式继承。然后定义了一个构造函数 Person,它有一个名字属性 name 和一个朋友列表属性 friends。我们把 sayHello 方法添加到 Person.prototype 上,以使得所有的 Person 实例都能够使用这个方法。

接下来,我们定义了一个构造函数 Student,它继承了 Person 构造函数。在 Student 构造函数内部,我们调用了 Person 构造函数,并传入了 name 参数,以设定 name 属性。然后,我们添加了一个 grade 属性,并把 sayGrade 方法添加到 Student.prototype 上,以使得所有的 Student 实例都能够使用这个方法。

最后,我们通过寄生式继承来实现了寄生组合式继承。具体来说,我们把 Student.prototype 设为一个 createObject(Person.prototype) 的实例,并重新设置了 Student.prototype 的 constructor 属性。这样,我们就实现了对 Person.prototype 的继承,并避免了重写 Student.prototype 对 Person.prototype 的修改。

总的来说,寄生组合式继承是一种高效且可靠的继承方式,可以解决构造函数继承和原型式继承的问题,并实现了一个完整的继承链。

3.7、ES6 class继承

ES6 提供了继承的关键字 extends

ES6 中引入了 class 语法糖,让原本基于构造函数和原型的继承方式更加友好和易于理解。class 继承的实现方式如下:

class Person {
  constructor(name) {
    this.name = name;
    this.friends = ['Alice', 'Bob'];
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }

  sayGrade() {
    console.log(`My grade is ${this.grade}.`);
  }
}

const student = new Student('John', 10);
student.sayHello(); // 输出:Hello, my name is John.
console.log(student.friends); // 输出:["Alice", "Bob"]
student.sayGrade(); // 输出:My grade is 10.

在上面的代码中,我们定义了一个 Person 类,它有一个构造函数和一个 sayHello 方法,用来输出个人信息。然后,我们定义了一个 Student 类,它通过 extends 关键字来继承 Person 类,并添加一个 grade 属性和一个 sayGrade 方法。在 Student 构造函数内部,我们使用 super 关键字来调用父类的构造函数,以初始化 name 属性。

最后,我们创建了一个 student 实例,并调用了它的 sayHello 和 sayGrade 方法,以输出个人信息。

总的来说,ES6 中的 class 继承语法糖更加简洁和易于理解,它隐藏了原型和构造函数的复杂性,提供了更加面向对象的编程方式。同时,它也避免了原型链继承的一些问题,如属性共享和不必要的属性复制。

在这里插入图片描述

四、相关内容

JavaScript:构造函数_snow@li的博客-CSDN博客

JavaScript:new操作符_snow@li的博客-CSDN博客

五、过程记录

记录在整理这篇文章时候看到的一些有意思的观点或内容,供参考:

5.1、主要就是“构造函数、原型对象、实例对象”三者的关系。

5.2、类是简化的原型,原型是简化的单向链表。

5.3、如果我们相信程序是简单的、可解释的,无非是“数据结构 + 算法”。那么,所有编程范式,语言风格,最终都将落实到具体的数据结构和算法上。

5.4、构造器(constructor)。

  function test(){}
  console.log(test.prototype.constructor === test);//true

六、本文首次借助CSDN创作助手,后边我会谈一谈自己对于借助AI创作的看法。

七、参考链接

JavaScript 的原型链

js的六种继承方式_js继承_Zang_WS的博客-CSDN博客

js中,什么是原型、原型链? - 知乎

https://www.cnblogs.com/bruce-gou/p/9658016.html

进阶必读:深入理解 JavaScript 原型 - 知乎

JS原型与原型链_原型和原型链_Lemon_dingding的博客-CSDN博客

继承与原型链 - JavaScript | MDN

猜你喜欢

转载自blog.csdn.net/snowball_li/article/details/122458205