JavaScript 中的“造物”艺术:对象、构造函数与原型

232 阅读4分钟

走进JS,走近讶语

在JavaScript的世界里,对象和面向对象编程(OOP)是不可或缺的一部分。本文将带你深入了解JavaScript中对象的创建方式,特别是ES6中引入的class语法,以及传统的构造函数和原型的概念。希望通过这篇文章,你能对JavaScript的面向对象编程有一个更深入的理解。

1. 造对象

在JavaScript中,创建对象有多种方式,每种方式都有其特点和适用场景。

1.1 对象字面量

对象字面量是最简单、最直接的方式,适合创建简单的对象。

const person = {
  name: 'Alice',
  age: 25,
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

person.greet(); // 输出: Hello, my name is Alice

虽然这种方式简单易用,但它缺乏灵活性,不适合创建多个具有相同属性和方法的对象。

1.2 ES6 Class 语法

ES6引入了class关键字,使得JavaScript的面向对象编程更加直观和易于理解。

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

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

const alice = new Person('Alice', 25);
alice.greet(); // 输出: Hello, my name is Alice

class语法不仅封装了属性和方法,还提供了构造函数、继承等高级特性,是目前最常用的面向对象编程方式。

1.3 类的本质

类是抽象的概念,用于封装属性和方法。类实际上是模板,定义了一组对象的共同特征和行为。与对象字面量相比,类提供了更好的组织和复用性。

2. 构造函数

在ES5及之前,JavaScript并没有class关键字,而是通过构造函数来实现面向对象编程。

2.1 构造函数的基本概念

构造函数是一种特殊的函数,用于创建和初始化对象。构造函数的首字母通常大写,以区别于普通函数。

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

const bob = new Person('Bob', 30);
console.log(bob); // 输出: Person { name: 'Bob', age: 30 }

在构造函数中,this关键字指向新创建的实例对象。通过new运算符调用构造函数,可以完成对象的实例化过程。

2.2 构造函数 vs 普通函数

函数是否为构造函数,不是由首字母大写决定的,而是由new运算符决定的。首字母大写只是编程风格上的约定,有助于提高代码的可读性。

function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(this); // 输出当前实例化对象
}

// 使用new运算符调用构造函数
const alice = new Person('Alice', 25);

// 不使用new运算符,直接调用函数
Person('Charlie', 40); // 这样调用不会创建新的实例对象

输出示例: image.png 分析:

  • 使用 new 运算符:构造函数会创建一个新的实例对象,并将 this 绑定到这个新对象上。
  • 不使用 new 运算符:构造函数会将 this 绑定到全局对象上,导致属性被添加到全局对象上,而不是创建一个新的实例对象。

3. 原型

在JavaScript中,函数也是对象,每个函数对象都有一个prototype属性,用于存储所有实例共享的属性和方法。

3.1 原型的基本概念

通过原型,可以实现方法的共享,减少内存占用,因为如果把方法也书写在构造函数体内,随着创建的实例对象越来越多, 会造成额外的资源浪费。

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

// 在原型上定义方法
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const alice = new Person('Alice', 25);
alice.greet(); // 输出: Hello, my name is Alice
3.2 类的方法部分由原型完成

在ES6的class语法中,类的方法实际上也是定义在原型上的,

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

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

const alice = new Person('Alice', 25);
alice.greet(); // 输出: Hello, my name is Alice

4. 三者关系

在JavaScript中,构造函数、原型对象和实例对象之间有着密切的关系。

  • 构造函数:用于创建和初始化对象。
  • 原型对象:存储所有实例共享的属性和方法。
  • 实例对象:通过new运算符创建,拥有自己的属性,同时可以访问原型对象上的方法。
function Person(name, age) {
  this.name = name;
  this.age = age;
}

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

const alice = new Person('Alice', 25);
console.log(alice); // 输出: Person { name: 'Alice', age: 25 }

// 实例对象先在自己身上查找属性,找不到再去原型对象上查找
alice.greet(); // 输出: Hello, my name is Alice

5. JavaScript的面向对象设计哲学

JavaScript的面向对象编程是基于原型的,而不是传统的类继承。这种设计哲学类似于中国人以孔子为原型,强调的是对象之间的关联和共享,而不是严格的类层次结构。

image.png

const kobe = {
  name: 'Kobe Bryant',
  playBasketball: function() {
    console.log(`${this.name} is playing basketball`);
  }
};

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

Player.prototype = kobe;

const leBron = new Player('LeBron James');
leBron.playBasketball(); // 输出: LeBron James is playing basketball

为什么输出的是 LeBron James 而不是 Kobe Bryant

  1. 实例对象的属性优先级

    • 当你调用 leBron.playBasketball() 时,JavaScript 引擎会首先在 leBron 对象本身上查找 playBasketball 方法。
    • 如果在 leBron 对象上没有找到 playBasketball 方法,它会沿着原型链向上查找。
  2. 原型链的作用

    • leBron 的原型对象是 kobe,因为我们在 Player 构造函数中设置了 Player.prototype = kobe
    • 因此,当在 leBron 上找不到 playBasketball 方法时,JavaScript 引擎会在 kobe 对象上查找该方法。
  3. this 的绑定

    • 在 playBasketball 方法中,this 指向的是调用该方法的对象,即 leBron
    • 因此,this.name 实际上是 leBron.name,而不是 kobe.name

image.png

总结

通过本文的介绍,相信你对JavaScript中对象的创建方式、构造函数、原型以及面向对象的设计哲学有了更深入的理解。无论是使用对象字面量、构造函数还是ES6的class语法,都能根据具体需求选择合适的方式来实现面向对象编程。