深入理解JavaScript原型与原型链:从对象创建到内存优化
本文深入解析JavaScript中的原型与原型链机制。从对象字面量的局限性出发,介绍工厂函数与构造函数的演进,详细阐述new关键字的执行步骤。重点讲解原型对象在共享方法、节省内存方面的作用,辨析显示原型与隐式原型的区别,并梳理Function与Object的特例及原型链查找规则,帮助开发者掌握JS核心对象模型。
一、为什么要使用原型?怎样去理解原型的出现
1、对象字面量创建对象的缺点
要理解原型,首先需明确其存在的必要性。在JavaScript早期,开发者常通过对象字面量来创建对象,例如:
const person = {
name: "wywy",
age: 21,
hobby: "听周杰伦"
}
这种方式虽然简洁直观,但在需要批量创建同类对象(如多个person对象)时,每次都要重复声明,工作量巨大。为解决此问题,引入了工厂函数的概念。
2、工厂函数
顾名思义,工厂函数如同流水线工厂,用于批量生产person对象,示例如下:
function createPerson(name,age,hobby){
const obj = {};
obj.name = name;
obj.age = age;
obj.hobby = hobby;
return obj;
}
const person1 = createPerson("zs", 23, "滑板");
const person2 = createPerson("ls", 22, "听歌");
console.log(person1);
console.log(person2);

通过调用该函数并传入属性值,相比对象字面量确实简化了代码。然而,工厂函数存在一个显著缺陷:无法区分对象的具体类型。在JavaScript中,复杂引用数据类型可细分为Array、Function、String等,但通过工厂函数创建的对象,控制台打印结果均为Object类型。若使用多个工厂函数创建多个实例,开发者将无法判断这些对象具体源自哪个工厂。为此,构造函数概念应运而生。
3、构造函数
构造函数与工厂函数相似,但JavaScript中并无单独名为“构造函数”的函数类别。构造函数总是与new关键字配合使用,更准确地说,它是被构造调用的函数。若不使用new关键字调用,它便与普通函数无异。
function CreatePerson(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
const person1 = new CreatePerson("zs", 23, "滑板");
const person2 = new CreatePerson("ls", 22, "听歌");
console.log(person1);
console.log(person2);

仔细观察,不难发现我们对上述工厂函数进行了以下改造:
1、将函数名首字母大写。在JavaScript中,若计划将函数作为构造函数使用,建议首字母大写以提示使用者。即便不大写,也不会产生语法错误。
2、取消了函数内部显式声明对象(如“const obj = {}”),并去掉了最后返回该对象的操作(“return obj”)。
3、在调用CreatePerson函数时,前面加上了new关键字。

此时打印对象,结果不再是Object,而是我们自定义的函数CreatePerson。这表明构造函数确实解决了对象类型无法判定的问题。那么,神奇的new关键字在后台执行了哪些操作?它主要完成了以下五件事:
1、在内存中创建一个新的对象。
2、将新对象的内部特性[[Prototype]]设置为函数CreatePerson的prototype属性值,即将函数的原型指针保存到[[Prototype]]中。
3、将函数内部的this指向这个新创建的对象。
4、执行函数内部的代码。
5、如果函数本身没有返回对象,则返回这个新创建的对象。
由此可见,构造函数在后台为我们做了大量工作。但这是否意味着构造函数就完美无缺了呢?并非如此。考虑一个person对象是否应该拥有方法?例如,我们为其添加一个说话的方法:
function CreatePerson(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
// 添加一个方法
this.sayHi = function() {
console.log("你好,我叫" + this.name)
}
}
const person1 = new CreatePerson("zs", 23, "滑板");
const person2 = new CreatePerson("ls", 22, "听歌");
console.log(person1);
console.log(person2);
然而,这样做会导致每个实例对象都创建一个独立的sayHi方法,且各实例上的方法互不相等。我们的初衷是让每个实例具备该功能即可,无需为每个实例创建独立的方法以浪费内存空间。通俗来说,若家有五个孩子都想玩Switch游戏,家长需为每个孩子买一台吗?当然,若家境优渥另当别论;一般家庭通常只买一台,孩子们轮流玩耍即可。
代码是对现实生活的抽象,我们是否也可以采取类似策略?即将方法添加到这些实例共同拥有的“父级”对象上。回顾new操作的第五步中的第二步,它正是JavaScript中的原型概念。这个原型便是这些实例的“父亲”。
经过上述铺垫,我们终于要正式讲解原型了!!!
二、使用原型
通过上述讲解,我们对原型的作用已有了大致理解:将实例对象所需的共有方法添加到原型上,而实例对象的私有属性则写在构造函数内部。接下来,我们对构造函数进行进一步改造:
function CreatePerson(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
// 添加一个方法
this.sayHi = function() {
console.log("你好,我叫" + this.name)
}
}
const person1 = new CreatePerson("zs", 23, "滑板");
const person2 = new CreatePerson("ls", 22, "听歌");
console.log(person1);
console.log(person2);
首先说明一点,代码并未按照“定义构造函数 -> 在原型上添加sayHi方法 -> 使用new创建实例”的顺序编写,而是先new创建了实例,随后在原型上添加方法。此举旨在说明原型具有动态性:即使先创建实例,后续给原型添加方法,实例依然可以访问该方法。此外,通过比较person1和person2的sayHi方法,我们发现它们是同一个方法引用。这样,我们完美解决了构造函数的问题。
三、原型概念辨析
首先需明确两个概念:
- 引用类型均具有对象特性,即可自由扩展属性。(引用类型包括:Object、Array、Function、Date、RegExp)
每个函数function都有一个显示原型prototype,每个实例对象都有一个隐式原型__proto__
function Fn() { // 内部语句:this.prototype = {}
}
// 1、每个函数function都有一个prototype,即显示原型(属性)
console.log(Fn.prototype);
// 2、每个实例对象都有一个__proto__,可称为隐式原型(属性)
var fn = new Fn(); // 内部语句: this.__proto__ = Fn.prototype
console.log(fn.__proto__);
两个准则:
在设计JavaScript原型与原型链时,需遵循以下两个准则:
准则一:原型对象(即Fn.prototype)的constructor指向构造函数本身。
准则二:实例对象(即fn)的__proto__指向其构造函数的显示原型。
function Fn() {}
var fn = new Fn();
// 原型对象的 constructor 指向构造函数本身
console.log(Fn.prototype.constructor === Fn); // true
// 对象的隐式原型的值为其对应构造函数的显示原型的值
console.log(Fn.prototype === fn.__proto__); // true
理解Function与Object特例
每个函数都是Function的实例,因此每个函数既拥有显示原型又拥有隐式原型,所有函数的隐式原型均指向Function.prototype;构造器Function的构造器是它自身。
// function Foo() {} 相当于 var Foo = new Function()
// Function = new Function() => Function.__proto__ = Function.prototype
// Object 作为构造函数时,其 __proto__ 内部属性值指向 Function.prototype
// Object.__proto__ = Function.prototype
// Function.constructor=== Function;//true
Object构造函数用于创建对象包装器。JavaScript中的所有对象均源自Object,所有对象都是Object的实例;所有对象从Object.prototype继承方法和属性,尽管这些属性和方法可能被覆盖。
// Fn的原型对象(Fn.prototype)也来自Object,故Fn.prototype.__proto__ = Object.prototype
function Fn() {}
原型链:
读取某个对象的属性时,会自动沿原型链进行查找。
- 1、先在自身属性中查找,若找到则返回。
- 2、若未找到,则继续沿着__proto__这条链向上查找,若找到则返回。
- 3、若最终未找到,则返回undefined。设置对象的属性值时,不会查找原型链;若当前对象中无此属性,则直接添加该属性并设置其值。方法一般定义在原型中,属性则通过构造函数定义在对象本身。
原型链是一个过程,而原型是原型链这个过程中的一个单位,贯穿整个原型链。
图解

四、原型链练习
//练习题1
function A(){
}
A.prototype.n = 1;
var b = new A();
A.prototype = {
n:2,
m:3
}
var c = new A();
console.log(b.n,b.m,c.n,c.m)
// 1 undefined 2 3
// 测试题2
var F = function() {
};
Object.prototype.a = function() {
console.log('a()');
};
Function.prototype.b = function() {
console.log('b()');
};
var f = new F();
f.a(); // a()
f.b(); // 报错:Uncaught TypeError: f.b is not a function
F.a(); // a()
F.b(); // b()
原型、原型链的意义与使用场景:
原型对象的作用在于存放实例中共有的属性和方法,从而大大减少内存消耗。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
