JavaScript原型与原型链深度解析:从基础到最佳实践
本文深入解析JavaScript中面向对象的核心机制——原型与原型链。详细阐述prototype、constructor与__proto__的关系,探讨属性屏蔽、动态性及引用类型陷阱。重点对比纯原型模式、组合构造函数模式及动态原型模式的优劣,帮助开发者掌握ECMAScript中自定义类型的最佳创建方式。
JavaScript原型基础概念
近期深入研习JavaScript,特别是在面向对象编程涉及原型概念时,产生了诸多思考。文中如有疏漏或偏差之处,恳请读者不吝指正。
JavaScript作为一门面向对象语言,自然具备继承特性。然而,JS中并不存在传统意义上的“类”概念,因此也没有类似Java中extends的语法。基于此,我认为JavaScript中的继承机制主要依托于原型链来实现。
那么,究竟什么是原型?在JavaScript中,函数本质上也是一种对象。当定义一个函数时,该函数会自动获得一个名为prototype的属性,即原型属性。这是一个指针,指向该函数的原型对象。该原型对象默认拥有一个constructor属性,指向拥有prototype属性的那个函数。
function Person(){}
Person.prototype={
// constructor:Person;
first_name:"guo",
hair_color:"black",
city:"zhengzhou",
act:function(){alert("eatting");}
};
以具体示例说明:首先创建一个名为Person的函数,该函数默认拥有prototype属性,指向Person.prototype对象。该对象包含一个默认的constructor属性,指向Person函数(注:此处原文指出默认指向Object,后续将对此进行修正说明)。
实例与原型链的关系
接下来探讨通过构造函数创建实例后的情况。
function Person(){}
Person.prototype={
first_name:"guo",
hair_color:"black",
city:"zhengzhou",
act:function(){alert("eatting");}
};
var boy=new Person();
var girl=new Person();
关键在于理解JavaScript中构造函数与普通函数的区别,即new关键字的使用。带有new操作符的函数调用才构成构造函数。当通过Person构造函数创建实例并分别赋值给boy和girl时,这两个实例对象会生成一个内部属性__proto__(在ECMAScript 5标准中可用[[prototype]]表示)。该属性指向构造函数的原型对象,即boy.__proto__指向Person.prototype(这与构造函数本身无直接关联)。此时,boy和girl可以通过点运算符访问原型对象中的属性,且二者共享原型对象的属性。我们可以通过isPrototypeOf()或Object.getPrototypeOf()(返回原型对象,即__proto__的值)来验证这一结论。
alert(Person.prototype.isPrototypeOf(boy)); //true
alert(Object.getPrototypeOf(boy).first_name); //"guo"
属性屏蔽与动态性
进一步验证:若在实例中定义一个与原型对象属性同名的属性,会发生什么?
var boy=new Person();
var girl=new Person();
boy.hair_color="red";
alert(boy.hair_color); //red
alert(girl.hair_color); //black
alert(Object.getPrototypeOf(boy).hair_color); //black
结果显示,实例中声明的同名属性会屏蔽原型对象中的对应属性。但这仅是覆盖关系,不会修改原型对象本身的属性(Object.getPrototypeOf(boy).hair_color仍为black),也不会影响其他共享该原型对象的实例(girl.hair_color仍为black)。同时,可以使用delete操作符删除实例中声明的属性,从而撤销屏蔽效果。此外,可利用hasOwnProperty()方法判断属性是存在于实例中(返回true)还是原型对象中(返回false)。
alert(boy.hasOwnProperty("hair_color")); //true
可以使用Object.keys()方法来枚举属性。
var key=Object.keys(Person.prototype);
alert(key);
原型模式的局限与修正
学习上述知识后,会发现使用前述方式声明原型对象存在一个问题:constructor属性不再指向Person。这与我们之前所述“原型对象的constructor默认指向拥有prototype属性的函数”相悖。原因在于:每创建一个函数,系统会自动创建一个prototype对象,该对象默认生成constructor属性。此处我们是对默认的prototype进行了重写,导致新的constructor指向了Object构造函数,而非Person。若constructor属性至关重要,则需显式指定constructor:Person。
接下来需要了解原型的动态性:对原型对象属性的修改会实时反映到实例中,无论实例的创建发生在属性修改之前还是之后。
function Person(){}
Person.prototype={
first_name:"guo",
hair_color:"black",
city:"zhengzhou",
act:function(){alert("eatting");}
};
var boy=new Person();
Person.prototype.hobby="basketball";
var girl=new Person();
alert(boy.hobby); //basketball
上述代码示例表明,即使对原型对象属性的修改发生在实例创建之后,boy实例依然能共享Person.prototype.hobby。
然而,这种情况仅适用于对原型对象属性的修改。若对原型对象属性进行完全重写,则实例的创建必须位于原型对象重写之后,否则会导致错误。
function Person(){}
var girl=new Person();
Person.prototype={
first_name:"guo",
hair_color:"black",
city:"zhengzhou",
act:function(){alert("eatting");}
};
var boy=new Person();
Person.prototype.hobby="basketball";
alert(boy.hobby); //basketball
alert(girl.first_name); //undefined
引用类型陷阱与组合模式
回到“屏蔽”问题,前文提到创建实例属性(与原型属性重名)会屏蔽原型属性,但不影响其他实例。这里存在一个误区:该结论仅适用于基本数据类型。当属性值为引用类型时,会出现严重问题,见如下代码。
function Person(){}
Person.prototype={
first_name:"guo",
hair_color:"black",
friends:["Nick","John"],
city:"zhengzhou",
act:function(){alert("eatting");}
};
var boy=new Person();
boy.friends.push("Mike");
var girl=new Person();
alert(boy.friends); //Nick,John,Mike
alert(girl.friends); //Nick,John,MIke
可见,前文结论不再适用。原因是friends存在于原型对象中,而非boy实例中,因此对friends的修改会影响整个环境(可通过boy.friends=[]在boy实例中创建同名属性来解决)。为此,我们需要引入组合使用构造函数模式与原型模式。
function Person(hair_color,city){
this.hair_color=hair_color;
this.city=city;
this.friends=["John","Nick"];
}
Person.prototype={
constructor:Person,
first_name:"guo",
act:function() {
alert("eatting");
}
};
var boy=new Person("black","zhengzhou");
var girl=new Person("red","shenyang");
boy.friends.push("Nick");
alert(girl.friends);
alert(boy.friends);
该模式是目前ECMAScript中使用最广泛、认可度最高的创建自定义类型的方法,甚至可视为默认模式。
高级原型模式:动态与稳妥
然而,对于从事其他面向对象语言开发的程序员而言,这种模式显得较为怪异。为了将所有信息封装到构造函数中,动态原型模式应运而生。动态模式主要通过if语句判断是否需要对原型对象进行初始化,以达到节省资源的目的。
此外,还有稳妥构造函数模式,旨在适应没有共享属性且不使用this的场景。
以上关于JavaScript原型的解析内容即为全部分享,希望能对各位有所帮助,也欢迎大家继续支持本站。
JavaScript异步发送HTTP请求时怎么优雅地处理错误
2026-11-05 17:10:44
5款主流JS在线运行工具对比:CodePen、JSFiddle与StackBlitz怎么选
2026-11-05 17:04:04
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
