由浅入深理解JavaScript类:从定义到继承

作者:半夏时间:2026-10-30 13:49:06浏览:438

本文深入解析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] 希望会有帮助吧

作者最新文章