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 继承的底层原理,也是面试中最常考的知识点之一,记得持续关注哦!

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

相关推荐
她说..3 小时前
Java 对象相关高频面试题
java·开发语言·spring·java-ee
watson_pillow3 小时前
c++ 协程的初步理解
开发语言·c++
庞轩px3 小时前
深入理解 sleep() 与 wait():从基础到监视器队列
java·开发语言·线程··wait·sleep·监视器
故事和你913 小时前
洛谷-算法1-2-排序2
开发语言·数据结构·c++·算法·动态规划·图论
这是个栗子4 小时前
TypeScript(三)
前端·javascript·typescript·react
白毛大侠5 小时前
理解 Go 接口:eface 与 iface 的区别及动态性解析
开发语言·网络·golang
李昊哲小课5 小时前
Python办公自动化教程 - 第7章 综合实战案例 - 企业销售管理系统
开发语言·python·数据分析·excel·数据可视化·openpyxl
Hou'5 小时前
从0到1的C语言传奇之路
c语言·开发语言
不知名的老吴5 小时前
返回None还是空集合?防御式编程的关键细节
开发语言·python
迈巴赫车主5 小时前
蓝桥杯3500阶乘求和java
java·开发语言·数据结构·职场和发展·蓝桥杯