从构造函数创建对象到原型
一、使用工厂函数创建对象(使用普通函数创建对象)
function createPerson(name,age,sex){
// 1、创建空对象
let p = {};
// 2、对象赋值
p.name = name;
p.age = age;
p.sex = sex;
// 3、返回创建好的对象
return p ;
}
let p1 = createPerson('科长',32,'女');
let p2 = createPerson('处长',38,'男');
let p3 = createPerson('局长',48,'女');
console.log(p1,p2,p3);
运行结果如下

二、构造函数创建对象
构造函数作用与工厂函数一致,都是创建对象,但是构造函数的代码更加简洁。
使用new关键字调用一个函数的才叫构造函数
// 构造函数
function Person(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
}
let person1 = new Person('科长',32,'女');
let person2 = new Person('处长',38,'男');
let person3 = new Person('局长',48,'女');
console.log(person1,person2,person3)
运行结果如下

三、通过将普通函数与构造函数合并在一个代码里面查看对比
/*声明一个空函数*/
function fn(){};
let res1 = fn();
let res2 = new fn();
console.log(res1,res2);
运行结果如下

new关键字会创建三件事:
1、创建空对象;
2、将this指向这个对象,即this = {};
3、对象的赋值
this.name = name;
this.age = age;
this.sex = sex;
4、返回这个对象,即return this
四、原型对象(构造函数方法浪费内存资源)
还是延用上面的示例,代码如下
function Person(name,age){
this.name = name;
this.age = age;
this.eat = function () {
console.log('我要上班!')
};
}
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
p1.eat()
p2.eat()
console.log(p1.eat === p2.eat)
运行结果如下

可以看到
console.log(p1.eat === p2.eat)
打印出来是:false
这是为什么呢?
这段代码有个典型问题:每次创建 Person 实例时,eat 函数都会被重新创建一份(堆中会有两个一模一样的 eat 函数),造成内存浪费。
1. 内存分配核心逻辑
-
- 栈(Stack):存储基本类型值、引用类型的指针(地址),特点是读取快、空间小、自动释放。
- 这里
p1、p2是变量名,存储在栈中,它们的值是指向堆内存中对象的内存地址(比如0x100、0x200)。
- 这里
- 堆(Heap):存储引用类型(对象、函数等),特点是空间大、可动态分配、需要手动 / 垃圾回收释放。
new Person()创建的两个对象(包含name、age、eat属性)都存储在堆中,每个对象有独立的内存地址,且eat函数也会为每个对象单独创建一份。
- 栈(Stack):存储基本类型值、引用类型的指针(地址),特点是读取快、空间小、自动释放。
2. 内存分布流程图,如下

第一种优化解决方案:使用全局函数解决构造函数中方法定义的资源浪费问题
// 使用全局函数
let fn = function (){
console.log('我要上班!')
}
function Person(name,age){
this.name = name;
this.age = age;
this.eat = fn;
}
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
p1.eat()
p2.eat()
console.log(p1.eat === p2.eat)
运行结果如下

结果是:true
分析这段代码的核心优化点是:
-
- 把
eat对应的函数提取为独立变量fn,堆中只创建一份函数体(地址0x001) p1.eat和p2.eat不再存储函数本身,而是存储指向同一个函数的地址0x001,避免了重复创建函数造成的内存浪费- 执行
p1.eat()或p2.eat()时,都会通过地址0x001找到堆里的同一个函数并执行,结果和原代码一致,但内存效率更高
- 把
1. 核心内存分配逻辑
-
- 栈(Stack):存储变量名和对应的指针 / 基本值
fn:存储在栈中,值是指向堆里匿名函数的内存地址(如0x001)p1/p2:存储在栈中,值是指向堆里两个 Person 对象的内存地址(如0x100/0x200)
- 堆(Heap):存储引用类型(函数、对象)
- 匿名函数
function(){console.log('我要上班!')}:唯一一份,存在堆地址0x001 - Person 对象 1(p1 指向):存储
name: '科长'、age: 29、eat: 0x001(指向堆里的函数) - Person 对象 2(p2 指向):存储
name: '处长'、age: 58、eat: 0x001(和 p1 共享同一个函数地址)
- 匿名函数
- 栈(Stack):存储变量名和对应的指针 / 基本值
2. 内存分布流程图

再添加代码,验证一下p1.eat、p2.eat、fn三者之间的关系 ,如下
console.log(p1.eat === p2.eat)
console.log(p1.eat === fn);
console.log(p2.eat === fn);
运行结果如下

总结
-
- 栈中存储所有变量名(
fn/p1/p2),值都是指向堆的内存地址,而非实际的函数 / 对象内容; - 堆中仅存在一份
console.log('我要上班!')的函数体,p1和p2的eat属性共享这个函数的地址; - 这种写法是:函数挂载在全局变量
fn上,会导致全局变量污染。
- 栈中存储所有变量名(
第二种优化解决方案:使用对象解决
// 使用对象
let obj = {
fn1 : function (){
console.log('我要吃饭!')
},
fn2 : function (){
console.log('我要学习!')
}
}
function Person(name,age){
this.name = name;
this.age = age;
this.eat = obj.fn1;
this.learn = obj.fn2;
}
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
p1.eat()
p2.eat()
console.log(p1.eat === p2.eat)
console.log(p1.eat === obj.fn1); //true
console.log(p2.eat === obj.fn2); //false
运行结果如下

这段代码的设计优势:
-
- 函数集中管理:
fn1、fn2仅在堆中创建唯一一份,通过obj统一管理,避免了为每个 Person 实例重复创建函数; - 引用复用:
p1.eat、p2.eat都指向同一个fn1函数地址,p1.learn、p2.learn都指向同一个fn2函数地址,极大节省内存; - 扩展性好:如果后续需要新增 / 修改函数,只需修改
obj中的对应函数,所有引用该函数的 Person 实例都会生效。
- 函数集中管理:
这段代码的设计缺点:
- 问题本质:你定义的
obj是一个全局(或外层)变量,会占用全局命名空间,容易和其他代码的变量名冲突(比如其他地方也定义了叫obj的变量,就会覆盖)。 - 延伸问题:如果只想让
Person相关的实例使用这些函数,却把obj暴露在全局,相当于 “把内部逻辑暴露到外部”,不符合封装性原则;如果想给多个构造函数复用不同的函数集合,还得定义多个类似obj1、obj2的对象,管理成本变高。
1. 核心内存分配逻辑
-
- 栈(Stack):存储变量名和对应的内存地址(指针),仅保存 “指向堆的引用”,不存实际内容
obj:栈中存储变量名,值是指向堆里obj对象的地址(如0x001)p1:栈中存储变量名,值是指向堆里第一个 Person 对象的地址(如0x100)p2:栈中存储变量名,值是指向堆里第二个 Person 对象的地址(如0x200)
- 堆(Heap):存储所有引用类型(对象、函数)的实际内容,空间可动态分配
obj对象(地址0x001):包含fn1、fn2两个属性,值分别是指向堆中对应函数的地址(0x010、0x020)fn1函数(地址0x010):唯一一份,内容是console.log('我要吃饭!')fn2函数(地址0x020):唯一一份,内容是console.log('我要学习!')- Person 对象 1(地址
0x100):包含name: '科长'、age: 29、eat: 0x010、learn: 0x020 - Person 对象 2(地址
0x200):包含name: '处长'、age: 58、eat: 0x010、learn: 0x020
- 栈(Stack):存储变量名和对应的内存地址(指针),仅保存 “指向堆的引用”,不存实际内容
2. 内存分布流程图

总结
-
- 栈中仅存储变量名和 “地址指针”,所有对象、函数的实际内容都存储在堆中;
obj对象是函数的 “管理容器”,堆中仅存一份fn1/fn2函数,所有 Person 实例的eat/learn属性都复用这两个函数的地址;- 这种 “集中管理函数 + 实例引用函数地址” 的方式,是 JavaScript 中节省内存、提升代码可维护性的常用技巧。
第三种优化解决方案:原型挂载方法
原型对象:当声明一个函数的时候,编译器会自动帮助你创建一个与之对应的对象,我们称之为原型对象。
原型对象的作用:解决构造函数内存资源浪费 + 全局变量污染
访问方法:构造函数.prototype
function Person(name,age){
this.name = name;
this.age = age;
}
// 把方法挂载到原型上,所有实例共享
Person.prototype.eat = function () {
console.log('我要吃饭!');
};
Person.prototype.learn = function () {
console.log('我要学习!')
}
// p1 p2是实例对象
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
console.log(p1.eat === p1.learn);
console.log(p1.eat === p2.eat);
console.log(p1.eat === p2.learn)
运行结果如下

1. 核心内存分配逻辑
-
- 栈(Stack):仅存储变量名和对应的内存地址(指针),不存储实际的对象 / 函数内容
p1:栈中存储变量名,值是指向堆里第一个 Person 实例的地址(如0x100)p2:栈中存储变量名,值是指向堆里第二个 Person 实例的地址(如0x200)Person:栈中存储构造函数名,值是指向堆里Person构造函数的地址(如0x001)
- 堆(Heap):存储所有引用类型的实际内容(构造函数、原型对象、实例对象、方法函数)
Person构造函数(地址0x001):内容是function Person(name,age){...},且自带prototype属性,指向原型对象地址0x010;Person.prototype原型对象(地址0x010):- 核心属性:
constructor(指向0x001,即 Person 构造函数)、__proto__(指向 Object.prototype); - 自定义方法:
eat(指向0x020函数地址)、learn(指向0x030函数地址);
- 核心属性:
eat函数(地址0x020):唯一一份,内容是console.log('我要吃饭!');learn函数(地址0x030):唯一一份,内容是console.log('我要学习!');- Person 实例 1(地址
0x100):仅存储自身属性name: '科长'、age: 29,通过__proto__指向原型对象0x010; - Person 实例 2(地址
0x200):仅存储自身属性name: '处长'、age: 58,通过__proto__指向原型对象0x010。
- 栈(Stack):仅存储变量名和对应的内存地址(指针),不存储实际的对象 / 函数内容
2. 内存分布流程图

(原型写法的核心优势)
-
- 极致节省内存:
eat、learn函数仅在堆中创建一份,所有实例通过__proto__原型链找到这两个函数,而非每个实例存储一份函数地址; - this 指向稳定:调用
p1.eat()时,函数内的this天然指向p1(调用者),无需手动绑定,不会出现上下文丢失问题; - 封装性好:方法挂载在
Person.prototype上,和 Person 构造函数强绑定,不会污染全局命名空间; - 继承友好:子类可通过原型链继承
Person.prototype上的所有方法,符合 JavaScript 原生继承设计。
- 极致节省内存:
五、原型对象、构造函数、实例对象、__proto__之间关系
__proto__:属于实例对象,指向原型对象
function Person(name,age){
this.name = name;
this.age = age;
}
// 把方法挂载到原型上,所有实例共享
Person.prototype.eat = function () {
console.log('我要吃饭!');
};
Person.prototype.learn = function () {
console.log('我要学习!')
}
// p1 p2是实例对象
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
// console.log(p1.eat === p1.learn);
// console.log(p1.eat === p2.eat);
// console.log(p1.eat === p2.learn)
console.log(Person.prototype === p1.__proto__); //true
运行结果如下

实例对象可以直接访问原型中的成员 ,其实都是通过__proto__来访问的。
下面验证一下
function Person(name,age){
this.name = name;
this.age = age;
}
// 把方法挂载到原型上,所有实例共享
Person.prototype.eat = function () {
console.log('我要吃饭!');
};
Person.prototype.learn = function () {
console.log('我要学习!')
}
// p1 p2是实例对象
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
// console.log(p1.eat === p1.learn);
// console.log(p1.eat === p2.eat);
// console.log(p1.eat === p2.learn)
console.log(Person.prototype === p1.__proto__); //true
// 实例对象可以直接访问原型中的成员 ,其实都是通过__proto__来访问的。
p1.eat(); // 输出“我要吃饭”
p1.__proto__.eat(); // 输出“我要吃饭”
运行结果如下

原型除了可以挂载方法函数外,也可以挂载属性,如下所示
function Person(name,age){
this.name = name;
this.age = age;
}
// 把方法挂载到原型上,所有实例共享
Person.prototype.eat = function () {
console.log('我要吃饭!');
};
Person.prototype.learn = function () {
console.log('我要学习!')
}
// 原型也可以挂载属性
Person.prototype.type = '哺乳动物';
// p1 p2是实例对象
let p1 = new Person('科长',29);
let p2 = new Person('处长',58);
console.log(p1,p2);
// console.log(p1.eat === p1.learn);
// console.log(p1.eat === p2.eat);
// console.log(p1.eat === p2.learn)
console.log(p1.type); // 输出"哺乳动物"
console.log(Person.prototype === p1.__proto__); //true
// 实例对象可以直接访问原型中的成员 ,其实都是通过__proto__来访问的。
p1.eat(); // 输出“我要吃饭”
p1.__proto__.eat(); // 输出“我要吃饭”
挂载的属性是:Person.prototype.type = '哺乳动物';
运行结果如下

可以看到,原型中挂载了eat 、learn方法,还挂载了type属性。
总结:
1. prototype :属于构造函数,指向原型对象
* 作用:解决资源浪费+变量污染
2. __proto__ :属于实例对象,指向原型对象
* 作用:可以让实例对象访问原型中的成员
3. constructor:属于原型对象,指向构造函数

六、原型使用的注意点
1、哪些属性可以放在原型中?
所有实例对象共享的成员。
2、对象访问原型的规则:就近原则
对象访问成员的时候,优先访问自身的已有成员,如果自身没有的话,才会访问原型的。
// 构造函数(原型载体)
function Person(name) {
this.name = name; // 实例自身的属性
}
// 给原型添加方法
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
// 给原型添加属性
Person.prototype.gender = '未知';
// 创建实例
const zhangsan = new Person('张三');
zhangsan.gender = '男'; // 实例自身添加 gender,覆盖原型
// 查找 gender:先找实例自身(有),不用找原型
console.log(zhangsan.gender); // 输出:男
// 查找 sayHello:实例自身没有,找 Person.prototype(有)
zhangsan.sayHello(); // 输出:你好,我是张三
// 查找 toString:Person.prototype 没有,找 Object.prototype(有)
console.log(zhangsan.toString()); // 输出:[object Object]
3、原型是可以覆盖的
原型覆盖的具体解释和示例
首先要明确两个容易混淆的概念:
-
__proto__:实例对象的原型指向(也可以通过Object.getPrototypeOf()/Object.setPrototypeOf()操作)prototype:构造函数的原型属性(只有函数才有)
两种原型都可以被覆盖,下面用具体示例说明:
1. 覆盖构造函数的 prototype 属性
先看一个基础的版本,如下
function Person(name,age){
this.name = name;
this.age = age;
}
// 把方法挂载到原型上,所有实例共享
Person.prototype.eat = function () {
console.log('这是原型的扩展方法!');
};
let p1 = new Person('厅长',89);
Person.prototype = {
eat:function () {
console.log('这是覆盖原型的方法');
}
}
let p2 = new Person('部长',99);
p1.eat();
p2.eat();
运行结果如下

再看一个最常见的原型覆盖场景,通常用于批量为实例添加方法 / 属性:
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 初始的原型方法
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
// 创建实例
const p1 = new Person("张三");
p1.sayHi(); // 输出:Hi, I'm 张三
// 覆盖整个 prototype(这就是原型覆盖)
Person.prototype = {
// 注意:覆盖后 constructor 会丢失,建议手动补回
constructor: Person,
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
},
run: function() {
console.log(`${this.name} is running`);
}
};
// 新实例使用覆盖后的原型
const p2 = new Person("李四");
p2.sayHello(); // 输出:Hello, my name is 李四
p2.run(); // 输出:李四 is running
// p2.sayHi(); // 报错:sayHi is not a function(原原型方法已被覆盖)
// 旧实例仍使用覆盖前的原型
p1.sayHi(); // 输出:Hi, I'm 张三
p1.sayHello(); // 报错:sayHello is not a function
运行结果如下

2. 覆盖实例的 proto 原型指向
也可以直接修改单个实例的原型指向:
const obj1 = { a: 1 };
const obj2 = { b: 2 };
// 初始原型指向 Object.prototype
console.log(obj1.toString()); // [object Object]
// 覆盖 obj1 的原型为 obj2
obj1.__proto__ = obj2;
// 或使用标准方法:Object.setPrototypeOf(obj1, obj2);
console.log(obj1.b); // 输出:2(从新原型上读取属性)
console.log(obj1.a); // 输出:1
// obj2 本身是一个普通对象,它的原型仍然是 Object.prototype。
// 但是,当把 obj1 的原型设为 obj2,obj1 的原型链就变成了:obj1 → obj2 → Object.prototype
// 所以 obj1.toString() 应该仍然可以工作,输出 "[object Object]"。
console.log(obj1.toString());
原型覆盖的注意事项
- 覆盖 vs 扩展:
- 扩展:
Person.prototype.newMethod = function() {}(保留原有原型方法) - 覆盖:
Person.prototype = { ... }(完全替换,原有方法丢失)
- 扩展:
- constructor 丢失:
覆盖原型后,
constructor会指向Object而不是原构造函数,建议手动显式设置(如示例中constructor: Person)。 - 影响范围:
- 覆盖构造函数的
prototype:仅影响覆盖后创建的实例,覆盖前的实例不受影响。 - 覆盖实例的
__proto__:仅影响当前实例。
- 覆盖构造函数的

浙公网安备 33010602011771号