JavaScript 原型与面向对象编程深度解析😎

59 阅读7分钟

在前端开发的世界里,JavaScript 是一门极具魅力且功能强大的语言。它的面向对象编程(OOP)机制有着独特的实现方式,尤其是原型(prototype)的概念,对于理解 JavaScript 的核心至关重要。今天,我们就来深入探讨 JavaScript 中的原型和面向对象编程。🎉

一、传统对象创建的痛点😫

在 ES6 引入 class 关键字之前,JavaScript 并没有传统意义上的类。在早期的代码中,我们常常看到通过对象字面量来创建对象,就像下面这样:

// ES6 之前不存在class JS 大型项目的企业级开发语言
var Person = {
    name: '蔡徐坤',
    hobbies: ['音乐','羽毛球','听歌']
}

var Jay = {
    name: '周杰伦',
    hobbies: ['音乐','羽毛球','听歌']
}

var JJLin = {
    name: '林俊杰',
    hobbies: ['音乐','羽毛球','听歌']
}

可以发现,当我们需要创建多个类似的对象时,使用对象字面量会导致大量的重复代码,创建过程十分繁琐。这显然不是一种高效的方式,于是 JavaScript 引入了构造函数和原型的概念。

二、构造函数与类的模拟🤖

在 JavaScript 中,虽然没有 class 关键字,但我们可以使用构造函数来模拟类的概念。构造函数本质上是一个普通的函数,只不过按照约定,函数名的首字母大写。下面是一个简单的 Person 构造函数的例子:

function Person(name, age) {
    // this 指向当前实例化的对象
    this.name = name;
    this.age = age;
}

=

在这个构造函数中,this 关键字指向通过 new 操作符创建的实例对象。当我们使用 new 关键字调用这个构造函数时,会发生一系列的操作:

  1. 创建一个新对象。

  2. 将新对象的 __proto__ 属性指向构造函数的 prototype 对象。

  3. 执行构造函数,并将 this 绑定到新对象上。

  4. 如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象。

下面是实例化的代码:

let hu = new Person('小胡',18);

三、原型与原型链🧬

每个函数都有一个 prototype 属性,这个属性的值是一个对象,被称为构造函数的原型对象。原型对象可以包含一些方法和属性,这些方法和属性可以被所有通过该构造函数创建的实例对象共享。例如:

// 类的方法
Person.prototype = {
    sayHello: function () {
        console.log(`Hello,my name is ${this.name}`);
    }
}

我们可以通过实例对象调用原型对象上的方法:

hu.sayHello();

在 JavaScript 中,每个对象都有一个 __proto__ 私有属性,它指向该对象的原型对象。当我们访问一个对象的属性或方法时,JavaScript 首先会在对象本身查找,如果找不到,就会沿着 __proto__ 指向的原型对象继续查找,直到找到该属性或方法,或者到达原型链的终点 null。这就是原型链的概念。例如:

console.log(hu.__proto__);//{ sayHello: [Function: sayHello] }
let o = {a:1};
console.log(o.__proto__);//[Object: null prototype] {}
console.log(o.toString());//[object Object]

原型链的工作原理🔄

原型链的查找过程可以用以下步骤来描述:

  1. 当访问一个对象的属性或方法时,JavaScript 首先检查该对象本身是否拥有该属性或方法。
  2. 如果对象本身没有该属性或方法,JavaScript 会检查该对象的原型对象(即 __proto__ 指向的对象)。
  3. 如果原型对象也没有该属性或方法,JavaScript 会继续沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的终点 null
  4. 如果到达原型链的终点仍然没有找到该属性或方法,则返回 undefined

原型链的示例🌰

让我们通过一个示例来更直观地理解原型链的工作原理:

// 定义一个构造函数
function Animal(name) {
    this.name = name;
}

// 为 Animal 的原型添加一个方法
Animal.prototype.speak = function() {
    console.log(this.name + ' makes a noise.');
};

// 定义一个继承自 Animal 的构造函数
function Dog(name) {
    Animal.call(this, name); // 继承实例属性
}

// 设置 Dog 的原型为 Animal 的实例,实现继承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 为 Dog 的原型添加一个方法
Dog.prototype.bark = function() {
    console.log(this.name + ' barks.');
};

// 创建一个 Dog 实例
let dog = new Dog('Buddy');

// 调用方法
dog.bark(); // 输出: Buddy barks.
dog.speak(); // 输出: Buddy makes a noise.

在这个示例中,dog 对象的原型链是这样的:

  • dog → Dog.prototype → Animal.prototype → Object.prototype → null

当我们调用 dog.bark() 时,JavaScript 首先在 dog 对象本身查找 bark 方法,找不到就到 Dog.prototype 中查找,找到了就执行。当我们调用 dog.speak() 时,JavaScript 在 dog 对象和 Dog.prototype 中都找不到 speak 方法,于是继续在 Animal.prototype 中查找,找到了就执行。

四、原型的灵活使用🔧

JavaScript 的原型机制非常灵活,我们可以动态地修改对象的原型。例如:

var a = {
    name:'孔子',
    contry:'中国'
}
hu.__proto__ = a;
console.log(hu.__proto__);
console.log(hu.contry);

在这个例子中,我们将 hu 对象的原型指向了 a 对象,这样 hu 对象就可以访问 a 对象的属性。

原型的动态性✨

JavaScript 的原型是动态的,这意味着我们可以在对象创建后动态地修改它的原型,并且这种修改会影响到所有继承自该原型的对象。例如:

function Person() {}

let person1 = new Person();
let person2 = new Person();

// 动态添加一个方法到原型
Person.prototype.sayHello = function() {
    console.log('Hello!');
};

// 两个实例都可以访问新添加的方法
person1.sayHello(); // 输出: Hello!
person2.sayHello(); // 输出: Hello!

使用 Object.create() 创建对象🛠️

除了使用构造函数和 new 操作符创建对象外,我们还可以使用 Object.create() 方法来创建对象。这个方法允许我们指定一个对象作为新创建对象的原型。例如:

// 创建一个原型对象
let animal = {
    speak: function() {
        console.log(this.name + ' makes a noise.');
    }
};

// 使用 Object.create() 创建一个继承自 animal 的对象
let dog = Object.create(animal);
dog.name = 'Buddy';

// 调用继承的方法
dog.speak(); // 输出: Buddy makes a noise.

这种方式创建的对象没有显式的构造函数,而是直接继承自指定的原型对象。

五、与其他语言的对比🐾

为了更好地理解 JavaScript 的原型机制,我们可以将其与 Java 等传统面向对象语言进行对比。在 Java 中,类是明确存在的,通过 class 关键字定义,例如:

package prototype;
// 定义 Person 类
public class Person {
    // 成员变量
    private int pupyAge;
    // 构造方法
    public Person(int pupyAge) {
        this.pupyAge = pupyAge;
    }
    // 公有方法
    public void say() {
        System.out.println("汪汪汪");
    }
}

Java 中的类和对象之间有明确的继承关系,而 JavaScript 中的对象和类(构造函数)之间没有严格的血缘关系,主要通过 __proto__ 来建立联系。

继承方式的对比🧩

  • Java:通过类的继承和接口实现来实现代码复用和多态性。子类继承父类的属性和方法,可以重写父类的方法。
  • JavaScript:通过原型链、构造函数、组合继承、寄生组合继承等多种方式实现继承。JavaScript 的继承更加灵活,但也更加复杂。

静态类型与动态类型的对比🔍

  • Java:是静态类型语言,需要在编译时指定变量的类型,类型检查发生在编译阶段。
  • JavaScript:是动态类型语言,变量的类型在运行时确定,类型检查发生在运行阶段。

六、总结📝

JavaScript 的原型和面向对象编程机制是其独特的魅力所在。通过构造函数和原型对象,我们可以模拟类的概念,实现对象的封装和继承。原型链的存在使得对象之间可以共享属性和方法,提高了代码的复用性。同时,JavaScript 原型的灵活性也为开发者提供了更多的可能性。希望通过本文的介绍,你对 JavaScript 的原型和面向对象编程有了更深入的理解😘。