JavaScript原型与原型链详解:从基础到实战
本文深入解析JavaScript中的原型机制,涵盖prototype属性、构造函数模式、工厂模式及原型模式的优缺点。详细讲解原型链的查找规则、__proto__的作用,以及如何通过组合模式实现最优的对象创建方案,帮助开发者理解JS面向对象的核心原理。
JavaScript原型机制基础
原型是什么
Function类型拥有一个名为prototype的属性,直译为“原型”。该属性是一个指针,指向一个包含若干属性和方法的对象。这些属性和方法会被该函数创建的所有实例(对象)所共享。
基于上述概念,我们可以推导并编写出如下代码示例:
function Person(){
...
}
Person.prototype = {
country : 'china',
sayName : function(){
...
}
}
首先创建一个Function类型的实例person,其prototype属性指向一个具体对象。该对象内的属性和方法将被person函数生成的所有实例共享。具体表现为:
person1 = new Person();
person2 = new Person();
person1和person2均是通过Person构造函数生成的实例,它们共同拥有country属性和sayName方法。这是因为它们各自拥有一个指向Person.prototype所指向对象的指针(即__proto__)。需要注意的是,__proto__并非ECMAScript标准属性,而是Chrome、Firefox等浏览器自行实现的内部属性,主要用于辅助理解prototype机制,实际开发中不建议直接依赖此属性。

关于原型的更多具体用法,将在后续章节详细展开。
JavaScript对象创建模式
创建对象的模式
接下来,我们将探讨JavaScript中创建对象的各种常用方法及其优缺点。
1. 工厂模式
工厂模式抽象了创建具体对象的过程,通过函数封装创建对象的细节,从而避免重复代码。实现代码如下:
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("jiangshui","22","engineer");
上述代码成功创建了一个person对象。工厂模式解决了重复创建相似对象的问题,但未能解决对象识别问题。它仅创建一个对象,无法区分该对象是基于人类模板还是动物模板创建,因此无法确定对象的类型。
2. 构造函数模式
通过定义自定义构造函数,可以明确定义自定义对象类型的属性和方法。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = jpb;
this.sayName = function(){
alert(this.name);
};
};
var person1 = new Person(...);
3. 构造函数模式与工厂模式的区别:
- 没有显式创建对象。
- 直接将属性和方法赋值给this对象。
- 没有return语句。
Person是Function类型的对象。使用new关键字后,会生成一个新对象。由于在函数内部将参数赋值给了this指针,这些参数便成为了新对象的属性或方法。
按照惯例,构造函数名称首字母大写。上述代码的执行步骤如下:
- 创建一个新对象。
- 将构造函数的作用域赋给新对象(因此this指向该新对象)。
- 执行构造函数中的代码。
- 返回新对象。
生成的实例默认包含一个constructor属性,指向构造函数,例如:
alert(person1.constructor == Person);
因此,使用构造函数模式可以区分对象类型,将其实例标识为特定的类型。
此外,构造函数本质上是普通函数,只是通过new关键字调用以返回新对象。如果不使用new直接执行,其行为与普通函数无异。例如,执行Person.sayName()会弹出window.name,因为此时函数在window上下文中执行,this指向window。
构造函数模式存在缺陷:每个实例都会重新创建一遍方法,导致不同实例上的同名函数不相等。例如:
person1.sayName == person2.sayName; //false
这意味着,由构造函数生成的每个对象实例,其属性和方法都是独有的,相当于复制了一份。属性独有是必要的,因为这是对象间差异的来源;但方法功能和代码相同,重复复制会浪费资源。
为了解决这个问题,我们可以将函数定义在外部,然后在构造函数中通过指针指向该函数。这样,生成的实例中,方法存储的是指向同一函数的指针,从而实现了函数的共享:
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
然而,这样做会导致函数变为全局函数,且与Person构造函数的关联性减弱,缺乏封装性。
接下来介绍原型模式。
原型模式详解
原型模式
前文已介绍部分原型基础知识。简而言之,每个函数都有一个prototype属性,指向一个对象(原型对象),该对象中可以存放属性和方法。函数生成的实例会有一个非标准属性(__proto__)指向该原型对象。
由此可知,prototype定义的属性和方法由所有实例共享。
这正好解决了构造函数模式中实例函数共享的问题。示例代码如下:
function Person(){
....
}
Person.prototype.name = "jiangshui";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //jiangshui
或者采用另一种写法:
Person.prototype = {
constructor : Person,
name : "jiangshui",
sayName : function(){
alert(this.name);
}
};
第二种方法覆盖了整个prototype对象,因此需要手动指定constructor属性以指向构造函数,否则它将指向Object。
梳理它们之间的关系:
- Person.prototype指向原型对象,可定义被所有实例共用的属性或参数。
- Person.prototype.constructor等于Person,即原型对象默认有一个constructor属性指向所属构造函数(注意:另一种写法会覆盖此属性,需重新指定)。
- person1 = new Person(),构造函数生成实例,该实例包含构造函数内容和原型对象内容。
- person1.__proto__指向创建该实例的原型对象(此属性不规范,建议避免使用)。
可以使用isPrototypeOf()方法确定对象之间的关系。例如:
Person.prototype.isPrototypeOf(person1);
当代码读取对象的某个属性时,会执行搜索。首先从当前对象开始,若未找到,则沿指针搜索原型对象,而不会搜索构造函数。实例可以访问但不能重写原型对象的值。如果实例中设置了与原型同名的属性,搜索过程将在实例中结束,不再访问原型对象,从而实现覆盖。因此,即使将属性设置为null,也表示实例中已存在该属性,无法访问原型对应属性。
因此,需要使用delete操作符完全删除实例属性,从而重新访问原型。
原型是动态的,对原型对象的任何修改都会立即反映在实例上。这是因为实例与原型之间存在松散链接关系,每次调用实例属性或方法时都会进行一次查询,若原型发生变化,查询结果也随之改变。
了解原型后,我们还可以为原生对象添加新方法或属性。Object、Array、String等原生引用类型与上述构造函数类似,我们可以利用prototype扩充它们的方法。例如:
String.prototype.startsWith = function(text){
return this.indexOf(text) == 0;
};
var msg = "Hello World";
msg.startsWith("Hello");
这段代码为String原生引用类型增加了一个startsWith方法,功能是检查字符串是否以传入参数开头。由于原型的动态性,只要执行一次,所有字符串类型变量都将获得该方法。
但不推荐使用此方法,若使用过多,会导致代码维护困难、混乱等情况。一般情况下,应先继承某个原生引用类型,再在新自定义类型上创建方法。关于继承,将在后续总结。
原型模式并非万能,原型中的所有属性和方法被所有实例共享,因此非常适合函数,但对于包含引用类型的属性,会产生冲突。例如:
function Person(){}
Person.prototype = {
constructor : Person,
friends : ["greg","jack"]
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("tom");
console.log(person2.friends);
在控制台输出中,你会看到person2的friends数组多了一个tom。这并非预期结果,但在定义person1的朋友时,确实影响到了实例person2。
因此,我们需要结合原型模式和构造函数模式来使用。
组合构造函数与原型模式
组合使用构造函数模式和原型模式
这是最常用的模式。构造函数用于定义实例属性,通过传递参数实现自定义;原型用于定义方法或所有实例共用的属性。这样既实现了自定义,又保证了共享,还避免了上述问题。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["greg","jack"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
};
var jiangshui = new Person("jiangshui","22","engineer");
实际应用示例
至此,你可能已理解原型的概念及对象创建方式,但会疑惑这些知识有何实际用途。确实,以往工作可能仅使用jQuery编写代码,无需封装生成对象。那么,这些究竟有什么用?
这种开发方式主要用于模块化和组件化开发。例如,常用的弹窗功能,你可以将弹窗相关代码抽象封装为一个组件。当需要使用弹窗时,只需传递参数生成一个弹窗实例即可调用,这比每次复制粘贴代码更为高效。
原型链与对象类型区分
原型对象和原型链
在JavaScript中,万物皆对象,但对象有区别,大致分为两类:普通对象(Object)和函数对象(Function)。
一般而言,通过new Function产生的对象是函数对象,其他对象都是普通对象。
举例说明:
function f1(){
//todo
}
var f2 = function(){
//todo
};
var f3 = new Function('x','console.log(x)');
var o1 = {};
var o2 = new Object();
var o3 = new f1();
console.log(
typeof f1,//function
typeof f2,//function
typeof f3,//function
typeof o1,//object
typeof o2,//object
typeof o3 //object
);
>> function function function object object object
f1属于函数声明,是最常见的函数定义方式;f2实际上是一个匿名函数,赋值给f2,属于函数表达式;f3不常见,但也是一种函数对象。
Function是JS内置对象。f1和f2在创建时,JS会自动通过new Function()的方式构建这些对象,因此,这三个对象都是通过new Function()创建的。
在JavaScript中创建对象有两种方式:对象字面量和使用new表达式。o1和o2的创建恰好对应这两种方式。重点讲解o3,如果用Java和C#的思路理解,o3是f1的实例对象,o3和f1是同一类型,至少我以前这么认为,其实不然……
那么如何理解呢?很简单,看o3是否通过new Function产生。显然不是,既然不是函数对象,那就是普通对象。
通过对函数对象和普通对象的简单理解后,我们再来了解JavaScript中的原型和原型链:
在JS中,每当创建一个函数对象f1时,该对象中都会内置一些属性,包括prototype和__proto__。prototype即原型对象,它记录着f1的一些属性和方法。
需要注意的是,prototype对f1是不可见的,也就是说,f1不会查找prototype中的属性和方法。
function f(){}
f.prototype.foo = "abc";
console.log(f.foo); //undefined
那么,prototype有什么用呢?其实prototype的主要作用就是继承。通俗一点讲,prototype中定义的属性和方法都是留给自己的“后代”用的,因此,子类完全可以访问prototype中的属性和方法。
想要知道f1是如何把prototype留给“后代”,我们需要了解JS中的原型链。此时,JS中的__proto__登场了。这个属性隐藏很深,以致于你经常见不到它,但它在普通对象和函数对象中都存在。它的作用是保存父类的prototype对象。JS在通过new表达式创建一个对象时,通常会把父类的prototype赋值给新对象的__proto__属性,这样,就形成了一代代传承……
function f(){}
f.prototype.foo = "abc";
var obj = new f();
console.log(obj.foo); //abc
现在我们知道,obj中__proto__保存的是f的prototype,那么f的prototype中的__proto__中保存的是什么呢?看下图:

如图所示,f.prototype的__proto__中保存的是Object.prototype。Object.prototype对象中也有__proto__,而从输出结果看,Object.prototype.__proto__是null,表示obj对象原型链的终结。如下图所示:

obj对象拥有这样一个原型链以后,当obj.foo执行时,obj会先查找自身是否有该属性,但不会查找自己的prototype。当找不到foo时,obj就沿着原型链依次去查找……
在上面的例子中,我们在f的prototype上定义了foo属性,这时obj就会在原型链上找到这个属性并执行。
什么时候该用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
