JS的构造函数、原型与实例:从基础到精通

182 阅读5分钟

引言

在JavaScript的世界里,构造函数、原型和实例是构成面向对象编程(OOP)的核心概念。这些概念不仅让JavaScript拥有了强大的对象创建能力,还为开发者提供了一种高效、灵活的方式来组织代码。本文将带你深入了解这三个概念,从基础到精通,揭开它们背后的神秘面纱。

屏幕截图 2024-11-25 213105.png

1. 构造函数:对象的制造机

在JavaScript中,构造函数是一种特殊的函数,用于创建并初始化对象。构造函数通常首字母大写,以区别于普通函数。当我们使用new关键字调用构造函数时,它会执行以下操作:

  1. 创建一个新的空对象:这个对象将成为构造函数的实例。
  2. 将这个新对象的内部属性[[Prototype]]链接到构造函数的prototype属性:这是原型链的基础。
  3. 将构造函数中的this指向这个新对象:这样我们就可以在构造函数内部通过this来给新对象添加属性和方法。
  4. 执行构造函数内的代码:初始化新对象的属性。
  5. 如果构造函数没有显式返回其他对象,则返回这个新对象:这确保了即使构造函数内部没有返回值,我们仍然能得到一个新对象。

例如,定义一个简单的构造函数Person

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

通过new关键字创建Person的实例:

const person = new Person('小明', 20);
console.log(person.name); // 输出: 小明
console.log(person.age);  // 输出: 20

在这个例子中,Person构造函数接收两个参数nameage,并将它们赋值给新对象的相应属性。通过new关键字调用Person构造函数后,我们得到了一个具有nameage属性的新对象。

2. 原型:共享的秘诀

每个函数都有一个prototype属性,该属性是一个对象,包含可以由特定类型的所有实例共享的属性和方法。当通过构造函数创建一个新实例后,该实例可以通过原型链访问到构造函数原型上的属性和方法。

例如,给Person构造函数的原型添加一个eat方法:

Person.prototype.eat = function() {
  console.log(`${this.name}正在吃饭`);
};

现在,任何Person的实例都可以调用eat方法:

const xiaoming = new Person('小明', 20);
xiaoming.eat(); // 输出: 小明正在吃饭

这种方式不仅节省了内存(因为方法不需要在每个实例上重复定义),而且使得方法可以在所有实例间共享。这意味着无论创建多少个Person实例,它们都会共享同一个eat方法,而不是每个实例都有一份独立的副本。

3. 实例:个性与共性的结合

实例是根据构造函数创建的具体对象。每个实例都拥有自己的一套属性,但共享同一个原型上的方法。这种设计既保证了实例之间的差异性,又实现了资源的有效利用。

例如,我们可以基于相同的Person构造函数创建不同的实例,每个实例都有自己的名字和年龄,但共享同一个eat方法:

const xiaohong = new Person('小红', 19);
xiaohong.eat(); // 输出: 小红正在吃饭

在这个例子中,xiaohongxiaoming是两个不同的Person实例,它们各自有自己的nameage属性,但都共享同一个eat方法。这种设计使得代码更加高效,同时也保持了实例之间的独立性。

4. 三者的关系:构造函数、原型与实例

  • 构造函数:用于创建对象的模板,定义了对象的基本结构。
  • 原型:构造函数的prototype属性,包含了所有实例共享的方法和属性。
  • 实例:根据构造函数创建的对象,每个实例都有自己独特的属性值,但共享原型上的方法。

在JavaScript中,构造函数、原型和实例之间的关系构成了其独特的面向对象编程模式——原型继承。这种模式与传统的基于类的继承不同,它更加灵活,能够更好地适应JavaScript的动态特性。

4.1 构造函数与普通函数的区别

虽然构造函数本质上仍然是函数,但它们有一些重要的区别:

  • 调用方式:构造函数通常使用new关键字调用,而普通函数则直接调用。
  • this的指向:在构造函数中,this指向新创建的实例;而在普通函数中,this的指向取决于函数的调用方式。
  • 返回值:构造函数默认返回新创建的实例,而普通函数的返回值取决于函数体中的return语句。

例如,考虑以下代码:

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

Person('小明', 20); // 普通函数调用,不会创建新对象
const person = new Person('小明', 20); // 构造函数调用,创建新对象
console.log(person.name); // 输出: 小明
console.log(person.age);  // 输出: 20

在第一个调用中,Person作为普通函数被调用,this指向全局对象(在浏览器中是window),因此不会创建新对象。在第二个调用中,Person作为构造函数被调用,this指向新创建的实例,从而创建了一个新的Person对象。

4.2 原型链的工作原理

在JavaScript中,每个对象都有一个内部属性[[Prototype]],它指向另一个对象。这个被指向的对象称为原型对象。通过原型链,对象可以访问其原型对象上的属性和方法。

例如,考虑以下代码:

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

Person.prototype.eat = function() {
  console.log(`${this.name}正在吃饭`);
};

const xiaoming = new Person('小明', 20);
xiaoming.eat(); // 输出: 小明正在吃饭

在这个例子中,xiaoming[[Prototype]]属性指向Person.prototype。当我们调用xiaoming.eat()时,JavaScript引擎会首先在xiaoming对象本身上查找eat方法。如果没有找到,它会沿着原型链向上查找,直到找到eat方法为止。

4.3 原型的动态性

JavaScript的原型是动态的,这意味着我们可以在运行时修改原型对象,这些修改会立即反映到所有实例上。

例如,考虑以下代码:

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

Person.prototype.eat = function() {
  console.log(`${this.name}正在吃饭`);
};

const xiaoming = new Person('小明', 20);
xiaoming.eat(); // 输出: 小明正在吃饭

// 动态添加一个新方法
Person.prototype.sleep = function() {
  console.log(`${this.name}正在睡觉`);
};

xiaoming.sleep(); // 输出: 小明正在睡觉

在这个例子中,我们在创建xiaoming实例后,动态地给Person.prototype添加了一个sleep方法。尽管xiaoming是在sleep方法添加之前创建的,但它仍然可以访问到这个新方法。这是因为xiaoming[[Prototype]]属性始终指向Person.prototype,而Person.prototype的变化会立即反映到所有实例上。

4.4 原型链的查找机制

当我们在一个对象上访问一个属性或方法时,JavaScript引擎会按照以下步骤进行查找:

  1. 自身查找:首先在对象本身上查找属性或方法。
  2. 原型查找:如果在对象本身上没有找到,JavaScript引擎会沿着原型链向上查找。
  3. 继续查找:如果在当前原型对象上也没有找到,JavaScript引擎会继续沿原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(即null)。

例如,考虑以下代码:

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

Animal.prototype.move = function() {
  console.log(`${this.name}在移动`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name}在叫`);
};

const dog = new Dog('小白', '哈士奇');
dog.move(); // 输出: 小白在移动
dog.bark(); // 输出: 小白在叫

在这个例子中,Dog构造函数继承了Animal构造函数的原型。dog实例可以访问move方法,因为它通过原型链找到了Animal.prototype上的move方法。同时,dog实例也可以访问bark方法,因为它直接定义在Dog.prototype上。

5. ES6 Class 语法糖

ES6引入了class语法糖,使得JavaScript的面向对象编程更加简洁和直观。实际上,class只是对构造函数和原型的一种语法糖封装。

例如,以下代码展示了如何使用class语法糖定义一个Person类:

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

  eat() {
    console.log(`${this.name}正在吃饭`);
  }
}

const xiaoming = new Person('小明', 20);
xiaoming.eat(); // 输出: 小明正在吃饭

在这个例子中,class定义了一个Person类,其中constructor方法是构造函数,eat方法是原型方法。实际上,这段代码等价于以下ES5代码:

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

Person.prototype.eat = function() {
  console.log(`${this.name}正在吃饭`);
};

const xiaoming = new Person('小明', 20);
xiaoming.eat(); // 输出: 小明正在吃饭

6. 总结

通过本文的介绍,我们深入了解了JavaScript中的构造函数、原型和实例。这些概念是JavaScript面向对象编程的核心,它们共同构成了JavaScript独特的原型继承机制。

  • 构造函数:用于创建对象的模板,定义了对象的基本结构。
  • 原型:构造函数的prototype属性,包含了所有实例共享的方法和属性。
  • 实例:根据构造函数创建的对象,每个实例都有自己独特的属性值,但共享原型上的方法。

理解这些概念不仅有助于我们编写更高效、更灵活的代码,还能帮助我们更好地理解JavaScript的底层机制。希望本文能为你在JavaScript面向对象编程的道路上提供一些有价值的指导。

20200229174423_bzukt.jpg