在搞懂了 this 指向之后,我们终于可以正式踏入 JS 面向对象编程 的大门。很多同学会疑惑:"JS 里没有 class 之前,到底怎么实现面向对象?" 答案就是 ------构造函数 + 原型。
本文将带你从 0 到 1 理解:
- 什么是构造函数,它和普通函数有什么区别?
- 为什么要用原型?原型解决了什么问题?
- 构造函数、实例、原型三者之间的关系是什么?
- 如何用构造函数 + 原型写出高效、可复用的面向对象代码?
一、构造函数:创建对象的 "工厂"
1. 什么是构造函数?
构造函数本质上就是一个普通函数,但约定俗成:
- 函数名首字母大写 (如
Person、Car),用来和普通函数区分; - 通过
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 件事:
- 创建一个空对象 :
const obj = {}; - 绑定原型 :把空对象的
__proto__指向构造函数的prototype; - 绑定 this :把构造函数的
this绑定到这个空对象上; - 返回对象:如果构造函数没有手动返回对象,就自动返回这个新对象。
可以用代码模拟一下:
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指向的对象; - 实例对象 :
p1、p2→ 通过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 继承的底层原理,也是面试中最常考的知识点之一,记得持续关注哦!
📌 本文代码可直接复制到浏览器控制台运行,建议动手修改几个案例,感受一下原型共享的效果。如果有疑问,欢迎在评论区留言讨论~