Skip to content

构造函数与原型链

OOP 的特性:封装、抽象、继承和多态。

区分JS与其他语言中的OOP

典型的面向对象编程:

  • 对象/实例是基于一个类来初始化的,这个类就像一个蓝图一样。
  • 所有的方法都会从类中复制一份到对象中。

JS 中的面向对象编程:

  • 对象会和一个原型对象连接(link)起来。
  • 原型继承:对象可以使用原型对象中的所有方法。
  • 这也叫做事件委托

一个具体的例子:

|300

考虑一些问题🙋:

  1. 如何创建原型对象?
  2. 如何将对象和原型对象连接起来?(原型继承/委托)
  3. 如何创建没有原型对象的对象?

带着这些问题,我们继续以下的概念。

  1. 构造函数
    1. 从一个函数中创建对象。
    2. 这就是 JS 内置对象,如Array, Map 和 Set 等使用的方法。
  2. ES6 的 class
    1. 现代构造函数。
    2. 原始构造函数的语法糖。
  3. Object.create()
    1. 直接讲对象和原型连接起来的方法。

构造函数

TIP

构造函数与普通函数唯一的区别就是构造函数可以使用 new 关键字。

那么, 当使用 new 来调用一个构造函数的时候,会发生什么?

  1. 创建一个新对象
  2. 调用该函数,函数中的 this 关键字指向上面这个刚创建的新对象
  3. 新对象连接到其自身的原型,即为新对象创建 __proto__ 属性,并将其指向构造函数的 prototype 属性指向的对象 Person.prototype
  4. 该函数返回刚刚创建的新对象,也是返回 this
js
const Person = function(firstName, birthYear){
    this.name = firstName; // 为新对象设置属性
    this.birth = birthYear;

	// 使用 new 调用时自动返回 `this`
}

const jonas = new Person('jonas', 1991);

console.log(jonas); // {name: 'jonas', birth: 1991}

对象的属性名与给构造函数传入的参数的名字没必要一样(如上面所示),但一般约定俗成是相同的。

TIP

this 指向的是实例化的那个对象。

验证一个对象是否是另一个对象的实例 instanceof

js
console.log(jonas instanceof Person); // true

如何为一个对象创建方法:

js
const Person = function(firstName, birthYear){
    this.firstName = firstName;
    this.birthYear = birthYear;

    // 虽然可以,但,在生产场景下,永远不要在构造函数内部创建方法
    // 如果在构造函数内部定义方法,JS 引擎会为每个对象单独创建一个新的函数实例,影响性能
    this.calAge = function(){
        console.log(2024 - this.birthYear);
    }
}

解决上述问题的方法就是使用原型链

原型与原型链

首先要明确的几点:

  1. JS 中所有函数自动地拥有一个名为 prototype 的属性。
  2. 通过构造函数创建的所有对象,拥有构造函数的 prototype 上的所有属性和方法。
js
const Person = function(firstName, birthYear){
    this.name = firstName; // 为新对象设置属性
    this.birth = birthYear;

	// 使用 new 调用时自动返回 `this`
}

const jonas = new Person('jonas', 1991);
console.log(jonas); // {name: 'jonas', birth: 1991}

也就是说,在上面这段代码中,jonas可以使用 Person.prototype 上的所有方法和属性。

通过原型继承的方法让对象拥有可以调用的方法:

js
Person.prototype.calAge = function(){ 
    console.log(2024 - this.birthYear);
}

const jiaqi = new Person('jiaqi', 1999);
jiaqi.calAge(); // 25

我们打印出 jiaqi 这个对象看看它的样子:

oop-2|500

可以看出,这个对象只有两个属性,calAge 这个方法是顺着原型链找到的。

TIP

任何对象总是可以访问它的构造函数的 prototype 中的方法和属性。

这是由于,对象内部有一个特殊的属性:__proto__ ,这一属性与其构造函数中的 prototype 属性相同:

js
console.log(jiaqi.__proto__ === Person.prototype); // true

这里有一个非常令人困惑的点,需要仔细思考:Person.prototype 并不是 Person 的原型,而是通过 Person 这个构造函数 new 出来的所有对象的原型。

仔细理解上面这句话,JS 内置对象也可以印证上面这句话:

js
let arrayInstance = [];
console.log(arrayInstance.__proto__ === Array.prototype); // true

|

从上面的图中可以看出对象和生成对象的构造函数之间的关系。

注意⚠️:这一关系在构造函数和 ES6 的 class 中都是相同的,但在 Object.create() 中是不同的。

原型链:

TIP

每个对象都有原型(__proto__ 属性),这一原型是通过相应构造函数 new 的时候,通过构造函数的 prototype 属性找到的。

上面👆图中的例子就是:jonas 找到了它的原型 Person.prototype,而 Person.prototype 本身也是个对象,也有自己的原型,即 Object.prototype ,此时就到了原型链的最顶层,Object.prototype.__proto__ 指向了 null

js
console.log(Object.prototype.__proto__); // null

用内置函数理解一下原型链:

js
const func = function(){

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

正是由于原型链这种继承方式,使得当我们为构造函数的 prototype 属性加上任意方法时,所有实例都可以调用它,并且不占用自身内存。

类之间的继承

TIP

手动创建原型链的过程。

用例:创建一个 Student 类来继承 Person 类。

继承属性

首先:子类继承父类的所有属性

js
const Person = function(firstName, birthYear){
    this.firstName = firstName;
    this.birthYear = birthYear;
};

Person.prototype.calAge = function(){
    console.log(2024-this.birthYear);
};

const Student = function(firstName, birthYear, course){
    // 继承 Person 的 firstName 和 birthYear
    Person.call(this, firstName, birthYear); 
    this.course = course;
}

注意,这里是通过 call 方法将子实例的 this 绑定到父类的属性上。

给子类绑定方法:

js
Student.prototype.introduce = function(){
    console.log(`hi, my name is ${this.firstName} and I study ${this.course}🖐️`);
}

调用该方法:

js
const jiaqi = new Student('jiaqi', 1999, 'cs');
jiaqi.introduce(); // hi, my name is jiaqi and I study cs🖐️

通过输出可以看出,到目前为止,子类的实例已经可以使用父类中定义的属性了。

TIP

这里并不是引用,而是创建了副本。

继承方法

除此之外,我们还希望子类能够调用父类 prototype 上的方法。

为此,我们需要手动创建一个原型链,具体如下图所示:

Object.create() 中讲到过,Object.create() 可以手动设置任何对象的原型对象,这正是我们这里需要的功能。

js
// 继承方法
Student.prototype =  Object.create(Person.prototype); // 放在添加其他方法的前面
Student.prototype.introduce = function(){
    console.log(`hi, my name is ${this.firstName} and I study ${this.course}🖐️`);
}

const jiaqi = new Student('jiaqi', 1999, 'cs');
jiaqi.calAge(); // 25

WARNING

注意,这里不能使用 Student.prototype = Person.prototype 这种方法,因为我们希望的是能够顺着原型链找到那个方法,而非直接指向那个方法。