JS 入门通关手册(20):构造函数与原型:JS 面向对象第一课

在搞懂了 this 指向之后,我们终于可以正式踏入 JS 面向对象编程 的大门。很多同学会疑惑:"JS 里没有 class 之前,到底怎么实现面向对象?" 答案就是 ------构造函数 + 原型

本文将带你从 0 到 1 理解:

  • 什么是构造函数,它和普通函数有什么区别?
  • 为什么要用原型?原型解决了什么问题?
  • 构造函数、实例、原型三者之间的关系是什么?
  • 如何用构造函数 + 原型写出高效、可复用的面向对象代码?

一、构造函数:创建对象的 "工厂"

1. 什么是构造函数?

构造函数本质上就是一个普通函数,但约定俗成:

  • 函数名首字母大写 (如 PersonCar),用来和普通函数区分;
  • 通过 new 关键字调用,用来批量创建同类型对象
  • 内部通过 this 给实例对象添加属性和方法。

2. 基本写法

javascript

运行

复制代码
// 构造函数:首字母大写
function Person(name, age) {
  // this 指向 new 出来的实例对象
  this.name = name; // 实例属性
  this.age = age;   // 实例属性
  // 实例方法(不推荐这么写,后面会讲原因)
  this.sayHello = function() {
    console.log(`大家好,我是${this.name},今年${this.age}岁`);
  };
}

// 通过 new 调用构造函数,创建实例
const p1 = new Person("张三", 20);
const p2 = new Person("李四", 22);

console.log(p1.name); // 张三
p1.sayHello(); // 大家好,我是张三,今年20岁
console.log(p2.name); // 李四
p2.sayHello(); // 大家好,我是李四,今年22岁

3. new 操作符到底做了什么?

当你写 new Person() 时,JS 引擎会偷偷做 4 件事:

  1. 创建一个空对象const obj = {}
  2. 绑定原型 :把空对象的 __proto__ 指向构造函数的 prototype
  3. 绑定 this :把构造函数的 this 绑定到这个空对象上;
  4. 返回对象:如果构造函数没有手动返回对象,就自动返回这个新对象。

可以用代码模拟一下:

javascript

运行

复制代码
function myNew(constructor, ...args) {
  // 1. 创建空对象
  const obj = {};
  // 2. 绑定原型
  obj.__proto__ = constructor.prototype;
  // 3. 绑定 this 并执行构造函数
  const result = constructor.apply(obj, args);
  // 4. 返回对象(如果构造函数返回了对象,就返回它,否则返回 obj)
  return typeof result === "object" && result !== null ? result : obj;
}

// 测试
const p3 = myNew(Person, "王五", 25);
p3.sayHello(); // 大家好,我是王五,今年25岁

4. 构造函数的问题:内存浪费

上面的写法有一个严重问题 :每次创建实例时,sayHello 方法都会被重新创建一次。

javascript

运行

复制代码
const p1 = new Person("张三", 20);
const p2 = new Person("李四", 22);
console.log(p1.sayHello === p2.sayHello); // false → 两个不同的函数对象!

这意味着:

  • 每创建一个实例,就会多占一份内存来存方法;
  • 实例越多,内存浪费越严重;
  • 方法无法共享,修改一个实例的方法不会影响其他实例。

解决方案 :把方法放到原型上,让所有实例共享同一个方法。


二、原型:共享方法的 "公共仓库"

1. 什么是原型?

每个函数(除了箭头函数)都有一个 prototype 属性,它指向一个原型对象

  • 这个原型对象是所有由该构造函数创建的实例的公共祖先
  • 实例对象会通过 __proto__ 链接到这个原型对象;
  • 原型对象上的属性和方法,会被所有实例共享

2. 把方法放到原型上

javascript

运行

复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 把方法定义在构造函数的 prototype 上
Person.prototype.sayHello = function() {
  console.log(`大家好,我是${this.name},今年${this.age}岁`);
};

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

const p1 = new Person("张三", 20);
const p2 = new Person("李四", 22);

console.log(p1.sayHello === p2.sayHello); // true → 共享同一个函数!
p1.sayHello(); // 大家好,我是张三,今年20岁
p2.eat("火锅"); // 李四 正在吃 火锅

✅ 优点:

  • 所有实例共享同一个方法,内存极大节省
  • 修改原型上的方法,所有实例都会立刻生效;
  • 代码结构更清晰,属性和方法分离。

3. 原型的核心概念

  • 构造函数Person → 用来创建实例的函数;
  • 原型对象Person.prototype → 构造函数的 prototype 指向的对象;
  • 实例对象p1p2 → 通过 new 创建的对象;
  • 原型链接 :实例的 __proto__ 指向构造函数的 prototype

javascript

运行

复制代码
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

三、构造函数 + 原型:最佳实践写法

1. 标准写法:属性在构造函数,方法在原型

javascript

运行

复制代码
// 构造函数:负责初始化实例属性
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}

// 原型:负责定义共享方法
Person.prototype = {
  // 手动修正 constructor 指向(重要!)
  constructor: Person,
  sayHello() {
    console.log(`我是${this.name},今年${this.age}岁,性别${this.gender}`);
  },
  eat(food) {
    console.log(`${this.name} 爱吃 ${food}`);
  },
  work() {
    console.log(`${this.name} 正在努力工作`);
  }
};

const p1 = new Person("张三", 20, "男");
const p2 = new Person("李四", 22, "女");

p1.sayHello(); // 我是张三,今年20岁,性别男
p2.eat("草莓"); // 李四 爱吃 草莓
console.log(p1.work === p2.work); // true

⚠️ 注意:

  • 当你直接给 Person.prototype 赋值一个新对象时,会丢失原来的 constructor 属性,所以必须手动加一行 constructor: Person
  • 否则 p1.constructor 会指向 Object,而不是 Person

2. 如何判断原型关系?

JS 提供了几个方法来判断原型关系:

javascript

运行

复制代码
// 1. instanceof:判断实例是否属于某个构造函数
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Object); // true(所有对象都继承自 Object)

// 2. isPrototypeOf:判断某个原型是否在实例的原型链上
console.log(Person.prototype.isPrototypeOf(p1)); // true

// 3. Object.getPrototypeOf:获取实例的原型对象
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true

四、常见面试题与坑点

1. 构造函数忘记写 new 会怎样?

javascript

运行

复制代码
function Person(name) {
  this.name = name;
}

// 错误写法:没有 new
const p = Person("张三");
console.log(p); // undefined
console.log(window.name); // 张三 → this 指向全局,污染了全局变量!

原因 :没有 new 时,构造函数变成了普通函数调用,this 指向全局对象(浏览器中是 window)。

解决

  • 开启严格模式 'use strict',此时 this 指向 undefined,会直接报错;

  • 或在构造函数开头判断: javascript

    运行

    复制代码
    function Person(name) {
      if (!(this instanceof Person)) {
        return new Person(name);
      }
      this.name = name;
    }

2. 原型对象被覆盖后,constructor 指向丢失

javascript

运行

复制代码
function Person(name) {
  this.name = name;
}

// 直接覆盖 prototype
Person.prototype = {
  sayHello() {
    console.log(this.name);
  }
};

const p = new Person("张三");
console.log(p.constructor === Person); // false → 指向 Object
console.log(p.constructor === Object); // true

解决 :手动修正 constructor

javascript

运行

复制代码
Person.prototype = {
  constructor: Person, // 加上这一行
  sayHello() {
    console.log(this.name);
  }
};

3. 原型上的引用类型属性会被共享修改

javascript

运行

复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.hobbies = ["吃饭", "睡觉"];

const p1 = new Person("张三");
const p2 = new Person("李四");

p1.hobbies.push("打游戏");
console.log(p1.hobbies); // ["吃饭", "睡觉", "打游戏"]
console.log(p2.hobbies); // ["吃饭", "睡觉", "打游戏"] → 也被修改了!

原因hobbies 是数组(引用类型),所有实例共享同一个数组引用。

解决引用类型属性必须放在构造函数里,不要放在原型上:

javascript

运行

复制代码
function Person(name) {
  this.name = name;
  this.hobbies = ["吃饭", "睡觉"]; // 放在构造函数里,每个实例有自己的数组
}

五、总结:构造函数与原型核心关系

我们可以用一张图来总结三者的关系:

plaintext

复制代码
构造函数 Person
    ↓ prototype
Person.prototype(原型对象)
    ↑ __proto__
实例对象 p1 / p2
  • 构造函数 Person 通过 prototype 指向原型对象;
  • 实例对象 p1 通过 __proto__ 指向原型对象;
  • 原型对象通过 constructor 指回构造函数;
  • 实例对象通过原型链,共享原型对象上的方法。

一句话记忆

构造函数管属性,原型管方法;实例共享原型,内存更高效。


下一篇我们将深入讲解 原型链,它是 JS 继承的底层原理,也是面试中最常考的知识点之一,记得持续关注哦!

📌 本文代码可直接复制到浏览器控制台运行,建议动手修改几个案例,感受一下原型共享的效果。如果有疑问,欢迎在评论区留言讨论~

相关推荐
2501_945423542 小时前
C++与Rust交互编程
开发语言·c++·算法
小王不爱笑1322 小时前
Java Set 集合全家桶:HashSet、LinkedHashSet、TreeSet 详解与实战
java·开发语言
早點睡3902 小时前
ReactNative项目Openharmony三方库集成实战:@react-native-ohos/react-native-image-picker
javascript·react native·react.js
code_whiter2 小时前
C++2(类与对象上篇)
开发语言·c++
六元七角八分2 小时前
学习笔记二《JavaScript 流程控制》
javascript·笔记
Teable任意门互动2 小时前
中小企业进销存实战:Teable多维表格从零搭建高效库存管理系统
开发语言·数据库·excel·飞书·开源软件
En^_^Joy2 小时前
JavaScript Web API:DOM操作全解析
开发语言·前端·javascript
m0_743297422 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
代码改善世界2 小时前
【C++ 初阶】命名空间 / 输入输出 / 缺省参数 / 函数重载
开发语言·c++