从构造函数创建对象到原型

一、使用工厂函数创建对象(使用普通函数创建对象)

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);

运行结果如下

image

二、构造函数创建对象

构造函数作用与工厂函数一致,都是创建对象,但是构造函数的代码更加简洁。

使用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)

运行结果如下

image

三、通过将普通函数与构造函数合并在一个代码里面查看对比

/*声明一个空函数*/
function fn(){};
let res1 = fn();
let res2 = new fn();
console.log(res1,res2);

运行结果如下

image

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)

运行结果如下

image

可以看到

console.log(p1.eat === p2.eat)

打印出来是:false

这是为什么呢?

这段代码有个典型问题:每次创建 Person 实例时,eat 函数都会被重新创建一份(堆中会有两个一模一样的 eat 函数),造成内存浪费。

1. 内存分配核心逻辑

 
    • 栈(Stack):存储基本类型值、引用类型的指针(地址),特点是读取快、空间小、自动释放。
      • 这里 p1p2 是变量名,存储在栈中,它们的值是指向堆内存中对象的内存地址(比如 0x1000x200)。
       
    • 堆(Heap):存储引用类型(对象、函数等),特点是空间大、可动态分配、需要手动 / 垃圾回收释放。
      • new Person() 创建的两个对象(包含 nameageeat 属性)都存储在堆中,每个对象有独立的内存地址,且 eat 函数也会为每个对象单独创建一份。
       
 

2. 内存分布流程图,如下

image

第一种优化解决方案:使用全局函数解决构造函数中方法定义的资源浪费问题

// 使用全局函数
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)

运行结果如下

image

结果是:true

分析这段代码的核心优化点是:
 
    • eat 对应的函数提取为独立变量 fn,堆中只创建一份函数体(地址 0x001
    • p1.eatp2.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: 29eat: 0x001(指向堆里的函数)
      • Person 对象 2(p2 指向):存储 name: '处长'age: 58eat: 0x001(和 p1 共享同一个函数地址)
       
 

2. 内存分布流程图

image

再添加代码,验证一下p1.eat、p2.eat、fn三者之间的关系 ,如下

console.log(p1.eat === p2.eat)
console.log(p1.eat === fn);
console.log(p2.eat === fn);

运行结果如下

image

总结

 
    1. 栈中存储所有变量名(fn/p1/p2),值都是指向堆的内存地址,而非实际的函数 / 对象内容;
    2. 堆中仅存在一份 console.log('我要上班!') 的函数体,p1p2eat 属性共享这个函数的地址;
    3. 这种写法是:函数挂载在全局变量 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

运行结果如下

image

这段代码的设计优势:
 
    1. 函数集中管理:fn1fn2 仅在堆中创建唯一一份,通过 obj 统一管理,避免了为每个 Person 实例重复创建函数;
    2. 引用复用:p1.eatp2.eat 都指向同一个 fn1 函数地址,p1.learnp2.learn 都指向同一个 fn2 函数地址,极大节省内存;
    3. 扩展性好:如果后续需要新增 / 修改函数,只需修改 obj 中的对应函数,所有引用该函数的 Person 实例都会生效。

这段代码的设计缺点:

  • 问题本质:你定义的 obj 是一个全局(或外层)变量,会占用全局命名空间,容易和其他代码的变量名冲突(比如其他地方也定义了叫 obj 的变量,就会覆盖)。
  • 延伸问题:如果只想让 Person 相关的实例使用这些函数,却把 obj 暴露在全局,相当于 “把内部逻辑暴露到外部”,不符合封装性原则;如果想给多个构造函数复用不同的函数集合,还得定义多个类似 obj1obj2 的对象,管理成本变高。

1. 核心内存分配逻辑

 
    • 栈(Stack):存储变量名和对应的内存地址(指针),仅保存 “指向堆的引用”,不存实际内容
      • obj:栈中存储变量名,值是指向堆里 obj 对象的地址(如 0x001
      • p1:栈中存储变量名,值是指向堆里第一个 Person 对象的地址(如 0x100
      • p2:栈中存储变量名,值是指向堆里第二个 Person 对象的地址(如 0x200
       
    • 堆(Heap):存储所有引用类型(对象、函数)的实际内容,空间可动态分配
      • obj 对象(地址 0x001):包含 fn1fn2 两个属性,值分别是指向堆中对应函数的地址(0x0100x020
      • fn1 函数(地址 0x010):唯一一份,内容是 console.log('我要吃饭!')
      • fn2 函数(地址 0x020):唯一一份,内容是 console.log('我要学习!')
      • Person 对象 1(地址 0x100):包含 name: '科长'age: 29eat: 0x010learn: 0x020
      • Person 对象 2(地址 0x200):包含 name: '处长'age: 58eat: 0x010learn: 0x020
       
 

2. 内存分布流程图

image

总结

 
    1. 栈中仅存储变量名和 “地址指针”,所有对象、函数的实际内容都存储在堆中;
    2. obj 对象是函数的 “管理容器”,堆中仅存一份 fn1/fn2 函数,所有 Person 实例的 eat/learn 属性都复用这两个函数的地址;
    3. 这种 “集中管理函数 + 实例引用函数地址” 的方式,是 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)

运行结果如下

image

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
       
 

2. 内存分布流程图

image

(原型写法的核心优势)

 
    1. 极致节省内存eatlearn 函数仅在堆中创建一份,所有实例通过 __proto__ 原型链找到这两个函数,而非每个实例存储一份函数地址;
    2. this 指向稳定:调用 p1.eat() 时,函数内的 this 天然指向 p1(调用者),无需手动绑定,不会出现上下文丢失问题;
    3. 封装性好:方法挂载在 Person.prototype 上,和 Person 构造函数强绑定,不会污染全局命名空间;
    4. 继承友好:子类可通过原型链继承 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

运行结果如下

image

实例对象可以直接访问原型中的成员 ,其实都是通过__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();     // 输出“我要吃饭”

运行结果如下

image

原型除了可以挂载方法函数外,也可以挂载属性,如下所示

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 = '哺乳动物';

运行结果如下

image

可以看到,原型中挂载了eat 、learn方法,还挂载了type属性。

总结:

1. prototype :属于构造函数,指向原型对象
   * 作用:解决资源浪费+变量污染

2. __proto__ :属于实例对象,指向原型对象
   * 作用:可以让实例对象访问原型中的成员

3. constructor:属于原型对象,指向构造函数

image

六、原型使用的注意点

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();

运行结果如下

image

再看一个最常见的原型覆盖场景,通常用于批量为实例添加方法 / 属性:

// 定义一个构造函数
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

运行结果如下

image

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());

原型覆盖的注意事项

 
  1. 覆盖 vs 扩展
    • 扩展:Person.prototype.newMethod = function() {}(保留原有原型方法)
    • 覆盖:Person.prototype = { ... }(完全替换,原有方法丢失)
     
  2. constructor 丢失
     
    覆盖原型后,constructor 会指向 Object 而不是原构造函数,建议手动显式设置(如示例中 constructor: Person)。
  3. 影响范围
    • 覆盖构造函数的 prototype:仅影响覆盖后创建的实例,覆盖前的实例不受影响。
    • 覆盖实例的 __proto__:仅影响当前实例。
 
 
 
 
 
 
posted @ 2026-01-30 15:37  chenlight  阅读(3)  评论(0)    收藏  举报