由浅入深理解JavaScript类:从定义到继承
本文深入解析JavaScript中的类概念,涵盖类的定义、实例化、属性与方法的管理。详细阐述静态属性与方法的访问差异,解析构造函数执行流程及参数传递,并探讨基于原型链的继承实现方式,帮助开发者掌握JavaScript面向对象编程的核心技巧。
发布时间:2006-3-6
作者:泣红亭
简介:
原文出处:www.51js.com
说明:2004年6月15日
翻译:
近期在无忧脚本社区参与讨论时,发现虽回复了不少帖子,却未分享实质性内容,心中略感不安。为此撰写了此文,原计划发布在类封装区,但考虑到该板块关注度较低,且希望更多朋友能从中受益,故调整至此处发布。
什么是类?
什么是类?
许多初学者难以理解“类”的概念。实际上,类是对现实世界的抽象模拟,将其理解为“类别”或“类型”更为直观。例如,“人”是一个类,而具体的某个人则是该类的实例。尽管人类拥有超过六十亿的实例,但“人”这个类定义只有一个。有人可能会问,男性和女性也是人,为何说只有一个类?这涉及继承的概念,后文将详细讲解,请继续阅读。
如何创建类?
如何创建类?
在C++中,类通过class关键字声明。JavaScript与C++不同,它使用function关键字来定义类,这导致许多学习JScript的朋友容易混淆类与函数。在JScript中,两者确实存在相似之处,但长期使用后会自然区分。本文旨在帮助希望深入面向对象编程的朋友,暂不深入探讨过于复杂的细节。
请看以下类的定义示例:
function WuYouUser()
{
this.Name; //名字
}
上述代码定义了一个名为WuYouUser(无忧用户)的类,它包含一个属性:Name(名字)。Name即为WuYouUser类的一个属性。
类具有固定的属性结构,但类的实例拥有不同的属性值。例如,我属于“人”这个类,性别为男;我的女同学也属于“人”类,但她的性别属性值为女。
那么如何声明类的实例呢?非常简单:
var Wo = new WuYouUser(); //实例一:“我”
var Biyuan = new WuYouUser(); //实例二:“碧原”(Biyuan哥,不好意思。。。嘿嘿)
类的属性
类的属性
这个Wo(我)是WuYouUser类的一个实例,它拥有WuYouUser赋予的一切:Name属性、Sex属性以及Age属性。我们可以通过以下方式设置其属性:
Wo.Name = "泣红亭";
这很简单,试着运行以下代码:
window.document.write(Wo.Name);
可以看到,输出了我的名字:泣红亭。
同样设置一下碧原兄的属性:
Biyuan.Name = "碧原";
运行以下代码:
window.document.write(Biyuan.Name);
可以看到输出了"碧原"。这说明Biyuan与Wo同为WuYouUser类的实例,但却是不同的实体,具有不同的属性值。
属性可以设置默认值。无忧社区记录每位用户的发帖数量,我们也可以给WuYouUser类添加一个表示发帖数量的属性ArticleCount:
function WuYouUser()
{
this.Name;
this.ArticleCount = 0;
}
一个新注册的无忧用户,其发帖数量为0。在上述代码中,直接给属性ArticleCount赋值为0。
可以运行以下代码验证:
var Wo = new WuYouUser();
window.document.write(Wo.ArticleCount);
可以看到输出了0,说明ArticleCount属性成功设置了默认值0。
类的方法
类的方法
“方法”一词可能不易理解,将其视为“行为”会更直观。一个人有许多共同行为,如睡觉、吃饭、走路等。现在给WuYouUser类添加一个发帖的方法:
function WuYouUser()
{
this.Name;
this.ArticleCount = 0;
this.NewArticle = function()
{
/*
*
* 具体如何发帖大家都清楚,打字、加图片、点保存等。
* 具体发帖代码无需在此展示,我们只需了解方法的定义与使用。
* 这里实现一个最简单且重要的功能:将发帖数量加1!
* 注意:恐龙等级就是这样累积的,所以大家尽情发帖吧。。。
*/
this.ArticleCount++;
}
}
// 既然定义好了这个方法,我们来测试效果:
var Wo = new WuYouUser();
Wo.NewArticle();
document.write(Wo.ArticleCount); 可以看到输出了1,说明发帖成功!这真是具有历史纪念意义的一刻,离恐龙等级又近了一步。
静态属性
静态属性
静态属性又称公共属性,它不属于某个类的实例,而是直接属于类本身。
例如,无忧用户有一个属性:注册用户的总数,它属于整个无忧用户类,而不是属于泣红亭或任何特定用户。
静态属性的声明方法为:
类名.prototype.属性名 = 属性值;
例如,给WuYouUser类定义一个表示注册用户总数的Count属性:
WuYouUser.prototype.Count = 0;
那么如何读取它呢?有两种方法:
1. 直接使用 WuYouUser.prototype.Count
2. 使用Wo.Count
这两种方式没有区别,均得到0。
虽然读取方法有两种,但在修改时需特别小心,请看以下代码:
var Biyuan = new WuYouUser();
WuYouUser.prototype.Count++;
document.write(Wo.Count);
document.write(Biyuan.Count);
你会发现两者的Count属性都是1。这意味着修改WuYouUser.prototype.Count会影响所有实例的相应属性,原理在于Wo、Biyuan的Count属性与WuYouUser.prototype.Count本质上是同一个对象。
现在来看另一段代码:
var Biyuan = new WuYouUser();
Biyuan.Count++; //特别注意这里,这是直接修改Biyuan的Count属性
document.write(Biyuan.Count); // 输出 1
document.write(WuYouUser.prototype.Count); //输出 0
document.write(Wo.Count); //同样输出0,为什么?
可以看到,如果直接修改实例的静态属性值,其他实例甚至类的静态属性会与之不同步。这是因为直接修改时,该实例会生成一个属于自身的属性Count,此时Biyuan.Count不再与WuYouUser.prototype.Count相同,也不与Wo.Count相同。这个Count属性专属于Biyuan,后续修改仅影响它自己。
因此,除非有特殊需求,建议在读取或赋值时统一使用WuYouUser.prototype.Count这种方式,以确保万无一失。
静态方法
静态方法
与静态属性相似,它也有别称:公共方法,同样属于类本身。
静态方法的定义方式为:
类名.方法名 = function(参数1,参数2...参数n)
{
//方法代码
}
现在我们来定义一个无忧用户类的注册新用户静态方法:
WuYouUser.prototype.AddOne = function()
{
//*** 具体代码省略,给静态属性Count增加1,表示注册用户数量又加一
WuYouUser.prototype.Count++;
}
现在来看看如何使用它,同样有两种方法:
1. 直接使用WuYouUser.prototype.AddOne()
2. 使用某实例的AddOne()
这两种方法效果相同:
var Wo = new WuYouUser();
var Biyuan = new WuYouUser();
document.write(WuYouUser.prototype.Count); // 0
Wo.AddOne();
document.write(WuYouUser.prototype.Count); // 1
document.write(Wo.Count); // 1
document.write(Biyuan.Count); // 1
WuYouUser.prototype.AddOne();
document.write(WuYouUser.prototype.Count); // 2
document.write(Wo.Count); // 2
document.write(Biyuan.Count); // 2
//可以看出,无论是使用Wo.AddOne()还是WuYouUser.prototype.AddOne(),效果一致,均为给WuYouUser.prototype.Count加1。
现在再看一段代码:
function NewClass() //由于之前的WuYouUser类不适合此例,声明一个新类NewClass
{
this.Name = "泣红亭"; //这里默认值为我的名字
}
NewClass.prototype.ChangeName = function(NewName)
{
this.Name = NewName;
}
var Wo = new NewClass();
Wo.ChangeName("郑运涛"); //我的真名
可以看到Wo.Name确实已变为"郑运涛",此方法看似可用,但内部是否有玄机?
再看以下代码,类的定义及ChangeName定义不变,但修改后续代码:
NewClass.prototype.ChangeName("郑运涛");
document.write(NewClass.Name); //undefined,即未定义
document.write(NewClass.prototype.Name); //郑运涛
var Wo = new NewClass();
document.write(Wo.Name); //泣红亭
可以看到,我们并未定义NewClass.prototype.Name这个静态属性,但编译器自动添加了一个。
然而,再看输出Wo.Name,它并非"郑运涛",而是原来的默认值"泣红亭",这说明了什么?
其实很简单,查看NewClass的定义中已有Name属性,因此Wo也有自己的Name属性,它与NewClass.prototype.Name并非同一对象,因此保持原样。
那为何前一个例子运行Wo.ChangeName("郑运涛")能改变Wo.Name属性呢?其实这与修改Wo.Count值的道理相同,编译器自动给Wo增加了一个方法ChangeName,该方法代码与NewClass.prototype.ChangeName相同,但Wo.ChangeName是Wo实例特有的,而非NewClass.prototype.ChangeName。
分析可知,在静态方法中尽量避免使用this关键字引用实例本身的属性,除非有特殊目的且清楚内部运行机制。
若确实需要在静态方法中使用this,可直接将this作为参数传入:
NewClass.ChangeName = function(This,NewName) //注意这里是This,不是this
{
This.Name = NewName;
}
构造函数
构造函数
类在初始化时也是一个函数的执行过程,这个函数即为构造函数。请看以下代码:
function WuYouUser()
{
this.Name = "泣红亭"; //默认定义为泣红亭
alert(this.Name);
}
var Wo = new WuYouUser();//可以看到出现一个窗口显示泣红亭三个字
可以看出,类的定义不仅定义了其属性与方法,还可加入一些代码,这些代码即为该类的构造函数代码,在实例声明过程中执行。
实际上,类的属性与方法都是在构造函数中执行定义的,请看以下代码:
function WuYouUser()
{
this.Name = "泣红亭";
return;
this.Sex = "男";
}
var Wo = new WuYouUser();
document.write(Wo.Name); //泣红亭
document.write(Wo.Sex); //undefined,即未定义
看出什么?Sex属性在return;之后,而WuYouUser类的构造函数遇到return即停止运行,换句话说this.Sex = "男";这一行并未被执行,即Sex属性根本没有被定义。
构造函数可以有参数,参数值在声明实例时传入:
function WuYouUser(Name)
{
this.Name = Name;
}
var Wo = new WuYouUser("泣红亭");
document.write(Wo.Name); //泣红亭
构造函数不需要返回值,但若设置了返回值,可将其当作普通函数使用。 function Sum(a, b)
{
this.a = a;
this.b = b;
return this.a + this.b;
}
document.write(Sum(12, 23)); //输出的是12与23的和35
var Obj = new Sum(12,23);
document.write(Obj.a) // 12
document.write(Obj.b) // 23
感觉挺奇妙,对吧?我写这篇文章时也觉得挺奇妙的,呵呵。
但强烈建议不要将类当作函数使用!若需要的是函数,请直接写成函数而非类,以免混淆。
继承
继承
继承在面向对象编程中至关重要。虽然JavaScript并非真正的面向对象语言,而是类似VB的基于对象的语言,但它同样提供了继承机制。
文章开头提到的男人与女人,也是两个不同的类,却具有相同的属性和方法,这些共同特性来自“人”这个类,换句话说,男人与女人继承了“人”的所有特性。但男人与女人也有不同之处,编程语言中的继承同理,类A继承类B,则B为A的父类,A为B的派生类(子类)。例如,男人是人的派生类,人是男人的父类。最高一级的类称为基类,想象即可明白,男人继承自人,男孩继承自男人,人即是男孩的基类,男人是男孩的父类。
多重继承
题外:多重继承
这里涉及多重继承话题,但若仅学习JavaScript,无需继续阅读,因为JavaScript不提供多重继承,准确地说,没有简单标准的方法实现多重继承(虽有办法,但繁琐且无必要)。
C++中有多重继承概念,此处讨论JavaScript,故不展开,仅简述其思想供参考。
在男孩继承问题中,男孩不仅继承自男人,还继承自孩子(有男孩子,也有女孩子)这个类,因此,它同时继承了两个类:男人与孩子,这就是所谓的多重继承。
好,此话题暂止,回归主题。
先看第一个类的定义:
function A()
{
this.Name = "泣红亭";
alert(this.Name);
}
//这个类定义了一个属性Name,默认值为"泣红亭"
//现在看第二个类的定义:
function B()
{
this.Sex = "男";
alert(this.Sex);
}
//定义了一个属性Sex,默认值为"男"
//继承的方式就是 子类.prototype = new 父类();
//现在我们来让B类继承A类:
B.prototype = new A();
//运行这一段代码:
var Obj = new B(); //首先打开警告窗口显示"泣红亭",再显示"男"
可从上述结果看出B类继承了A类,拥有了A类的属性Name,并执行了A类的构造函数,且A类构造函数在B类构造函数执行前运行。因此,利用此机制可实现重写父类方法及重设父类某属性默认值:
function A()
{
this.Name = "泣红亭";
this.Show = function()
{
alert("这是A类的Show方法");
}
alert(this.Name);
}
function B()
{
this.Name = "郑运涛";
this.Show = function()
{
alert("这是B类的Show方法");
}
alert(this.Name);
}
B.prototype = new A();
var Obj = new B();
Obj.Show();
结果出现三次警告窗口,第一个内容为泣红亭,是执行A类构造函数里的alert(this.Name),此时Name属性值仍为"泣红亭",因为B类构造函数尚未执行;第二次内容为"郑运涛",这是B类里的alert(this.Name),因B类构造函数里给Name重赋值为"郑运涛";最后是调用Obj.Show(),执行的不是A类的Show方法(显示"这是A类的Show方法"),而是B类的Show(显示"这是B类的Show方法"),显然Show方法被重写了。
类作为对象时的属性与方法
类作为对象时的属性与方法(不知如何简洁表达,故用了这么长的题目)
不知在此讨论此话题是否有些混淆视听,但觉得不谈则文章不完整,因为文章目的是让人清楚类的方方面面。
看了这一小节的题目,或许你会觉得奇怪,类就是类,怎么会“作为一个对象”呢?在JavaScript里,一切皆对象,包括类!对象可有属性、方法,类同样可有,但这极易与前文所述的静态属性与静态方法混淆,需仔细区分两者。
定义一个类:
function WuYouUser()
{
this.Name = "泣红亭";
}
//定义类作为一个对象时的属性:
WuYouUser.Url = "http://www.51js.com"; //静态属性的定义是:WuYouUser.prototype.Url = "http://www.51js.com";
var Wo = new WuYouUser();
document.write(WuYouUser.Url); //http://www.51js.com
document.write(Wo.Url); //undefined,即未定义!注意这里的未定义
由此可知,Url属性是WuYouUser自身所有,修改它与其他类及子类完全无关。
引用类的属性仅有一种办法,即类名.属性名,修改亦然。
定义类作为一个对象时的方法:
WuYouUser.ChangeUrl = function()
{
this.Url = "http://51js.com";
}
//你可能会疑惑,这里的this是什么?因为ChangeUrl方法属于对象WuYouUser,故this指代WuYouUser本身。
//可运行以下代码测试:
document.write(WuYouUser.Url); // http://www.51js.com
WuYouUser.ChangeUrl();
document.write(WuYouUser.Url); // http://51js.com
明显ChangeUrl直接修改了WuYouUser.Url的值,故后边才能输出http://51js.com。
若此节未看懂,勿急,编程之事,许多东西只能意会不可言传,且我口才不佳,说不清楚,只要日后多写代码、多用类,自然会有体会,亦可参考JSVM代码,其中几乎每个类都用到类作为对象时的属性与方法。
此内容虽属javascript范畴,但应有助于理解类概念! [sweat] 希望会有帮助吧
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
