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

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

相关推荐
CN-Dust2 小时前
【C++专题】输出cout例题
开发语言·c++
时空系2 小时前
第6篇:多维数据盒——管理大量数据 python中文编程
开发语言·python·ai编程
charlie1145141912 小时前
嵌入式Linux驱动开发(7) 从虚拟设备到真实硬件 —— LED驱动硬件基础
linux·开发语言·驱动开发·内核·c
桔筐2 小时前
Vue3 v-model 双向绑定导致循环触发的坑
前端·javascript·vue.js
小短腿的代码世界2 小时前
QCefView深度解析:Qt应用中嵌入Chromium浏览器的终极方案
开发语言·qt
Reese_Cool2 小时前
【STL】蓝桥杯/天梯赛终极杀器!10个C++字符串核心技巧,暴力破解高频考点
开发语言·c++·蓝桥杯·stl
路光.2 小时前
ReferenceError:Can‘t find variable:structureClone
前端·javascript·html·vue2
我这一生如履薄冰~2 小时前
浏览器多窗口同开一页面,数据同步更新(纯前端方案)
前端·javascript
Rkgua3 小时前
实例成员和静态成员在对象中的用法
javascript
Momo__3 小时前
Web Speech API 语音识别与合成详解
前端·javascript