JavaScript 面向对象编程深度解析:构造函数、原型与类

6 阅读6分钟

JavaScript 是一种功能强大的客户端脚本语言,它不仅支持面向对象编程(OOP),而且其面向对象的方式与传统的如 Java 或 C++ 等静态类型语言有所不同。JavaScript 使用了一种基于原型的继承模型,这种模型更加灵活且易于理解。本文将围绕 JavaScript 中的对象创建方式——对象字面量、ES6 类(Class)以及构造函数——进行探讨,并解析它们之间的关系,特别是构造函数、原型对象和实例之间的联系

1. 对象字面量

对象字面量是一种简单且直接的创建对象的方式,通过花括号 {} 包裹键值对的形式来定义对象。这种方式非常适合快速创建少量的、结构简单的对象,但当需要创建多个具有相同属性和方法的对象时,这种方式就显得随意、不够灵活和高效了。

const person = {
  name: '宝宝',
  age: 3,
  sayHello: function() {
    console.log('你好,我是' + this.name);
  }
};

2. ES6 Class

随着 ECMAScript 6 (ES6) 的引入,JavaScript 开始支持使用 class 关键字来定义类,这使得 JavaScript 更加接近传统面向对象语言中的类概念。虽然在底层实现上仍然是基于原型,但是class提供了一种更直观的方式来封装数据和行为,使代码更加整洁和易于维护。

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

  sayHello() {
    console.log('你好,我是' + this.name);
  }
}

const person = new Person('宝宝', 3);
person.sayHello();

3. 构造函数

在 ES6 引入 class 之前,JavaScript 主要依赖于构造函数来模拟面向对象的行为。

构造函数本身就是一个普通的函数,但当它通过 new 关键字调用时,可以创建并返回一个新的对象实例。构造函数负责初始化新对象的属性。

下面将详细解释构造函数的相关概念,包括命名规范、this 关指针的作用、实例化的过程,以及如何区分构造函数和普通函数。

1. 构造函数的基本概念

  • 构造函数的命名规范

    • 构造函数通常首字母大写,以区别于普通函数。这是一种编程风格上的约定,有助于其他开发者更容易识别哪些函数是用来创建对象的。

    • 例如:

      function Person(name, age) {
        this.name = name;
        this.age = age;
      }
      
  • this 关指针

    • 在构造函数中,this 关键字指向新创建的实例对象。通过 this,我们可以为实例对象添加属性和方法。

    • 例如:

      function Person(name, age) {
        this.name = name;  // 为实例对象添加 name 属性
        this.age = age;    // 为实例对象添加 age 属性
      }
      
  • 实例化的过程

    • 使用 new 关键字调用构造函数时,JavaScript 会执行以下步骤:

      1. 创建一个新的空对象。
      2. 将这个新对象的 __proto__ 属性设置为构造函数的 prototype 属性。
      3. 将构造函数内部的 this 绑定到这个新对象。
      4. 执行构造函数中的代码,为新对象添加属性和方法。
      5. 返回新对象。
    • 例如:

// constructor
function Person(name, age) {
    this.name = name;
    this.age = age;
}
//每个函数都有一个原型对象
Person.prototype = {
    eat: function() {
        console.log(`${this.name}爱吃饭`);
        
    }
}
const baby = new Person('宝宝',3)
baby.eat();

2. 如何区分构造函数和普通函数

  • 调用方式决定是否为构造函数

是否为构造函数并不取决于函数的命名方式(首字母大写),而是取决于函数的调用方式。

如果一个函数是通过 new 关键字调用的,那么它就是作为构造函数使用的。

  • 首字母大写的约定

    • 尽管是否为构造函数不由命名方式决定,但首字母大写是一种常见的编程风格,有助于其他开发者更容易识别哪些函数是用来创建对象的。

    • 例如:

      function Person(name, age) {  // 构造函数
        this.name = name;
        this.age = age;
      }
      
      function sayHello() {  // 普通函数
        console.log('你好');
      }
      

3. 总结

  • 命名规范:构造函数通常首字母大写,这是一种编程风格上的约定,有助于识别。
  • this 关键字:在构造函数中,this 指向新创建的实例对象。
  • 实例化过程:使用 new 关键字调用构造函数时,JavaScript 会创建一个新对象,并将 this 绑定到这个新对象。
  • 区分方式:是否为构造函数取决于函数的调用方式,而不是命名方式。

通过这些概念的理解,我们可以更好地使用构造函数来创建和管理对象,从而实现更高效和灵活的面向对象编程。

4. 原型与原型链

在 JavaScript 中,每个函数都有一个 prototype 属性,这个属性指向一个对象,即原型对象。通过原型,可以实现方法的共享,从而提高性能。当我们使用构造函数或 class 创建对象时,新对象会有一个内部链接指向构造函数的原型对象。这意味着所有通过同一个构造函数或 class 创建的实例都可以访问到原型对象上的属性和方法。这种机制被称为原型链,它是 JavaScript 实现继承的核心。

在 ES5 中,类的构建实际上是通过构造函数和原型来实现的。构造函数负责初始化对象的属性,而原型负责定义对象的方法。这种方法的好处是所有实例共享同一份方法,从而节省内存。 在 ES5 中,可以通过直接设置构造函数的 prototype 属性来实现继承和方法共享。这种方式比 ES6 的 class 更加灵活,因为可以直接操作原型对象(cy)。

// 原型? cy
const cy = {
    playBasketball: function() {
        console.log('东理科比来了');
    }
}
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype = cy;

const baby = new Person('宝宝',3);
baby.playBasketball();
console.log(baby.__proto__ === cy);// 输出: true (共享同一个方法)

JavaScript 的面向对象模型是基于原型的,而不是基于类的。这意味着对象之间的继承关系是通过原型链来实现的,而不是通过类的层次结构。这种设计哲学类似于中国文化中以孔子为原型,而不是通过血缘关系来传承。

5. 构造函数、原型对象与实例的关系

构造函数:用于定义对象的初始状态(属性)。
原型对象:存储着所有实例共享的方法和属性,有助于减少内存消耗。
实例:通过构造函数或 class 创建的具体对象,拥有自己的属性,同时可以通过原型链访问到原型对象上的方法和属性。

通过上述三种方式,JavaScript 提供了丰富的面向对象编程工具,使得开发者可以根据不同的需求选择最适合的方式。无论是使用对象字面量快速创建单个对象,还是利用构造函数和原型链实现高效的对象复用,或者是采用 ES6 的 class 语法简化代码编写,JavaScript 的面向对象特性都为我们提供了极大的灵活性。