将近20年前,Javascript诞生的时候,只是一种简单的网页脚本语言。如果你忘了填写用户名,它就跳出一个警告。
如今,它变得几乎无所不能,从前端到后端,有着各种匪夷所思的用途。程序员用它完成越来越庞大的项目。
Javascript代码的复杂度也直线上升。单个网页包含10000行Javascript代码,早就司空见惯。2010年,一个工程师透露,Gmail的代码长度是443000行!
编写和维护如此复杂的代码,必须使用模块化策略。目前,业界的主流做法是采用"面向对象编程"。因此,Javascript如何实现面向对象编程,就成了一个热门课题。
麻烦的是,Javascipt语法不支持"类"(class),导致传统的面向对象编程方法无法直接使用。程序员们做了很多探索,研究如何用Javascript模拟"类"。本文总结了Javascript定义"类"的三种方法,讨论了每种方法的特点,着重介绍了我眼中的最佳方法。
==============================================
Javascript定义类(class)的三种方法
作者:阮一峰
在面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称"实例")共有的属性和方法。
Javascript语言不支持"类",但是可以用一些变通的方法,模拟出"类"。
一、构造函数法
这是经典方法,也是教科书必教的方法。它用构造函数模拟"类",在其内部用this关键字指代实例对象。
function Cat() {
this.name = "大毛";
}
生成实例的时候,使用new关键字。
var cat1 = new Cat();
alert(cat1.name); // 大毛
类的属性和方法,还可以定义在构造函数的prototype对象之上。
Cat.prototype.makeSound = function(){
alert("喵喵喵");
}
关于这种方法的详细介绍,请看我写的系列文章《Javascript 面向对象编程》,这里就不多说了。它的主要缺点是,比较复杂,用到了this和prototype,编写和阅读都很费力。
二、Object.create()法
为了解决"构造函数法"的缺点,更方便地生成对象,Javascript的国际标准ECMAScript第五版(目前通行的是第三版),提出了一个新的方法Object.create()。
用这个方法,"类"就是一个对象,不是函数。
var Cat = {
name: "大毛",
makeSound: function(){ alert("喵喵喵"); }
};
然后,直接用Object.create()生成实例,不需要用到new。
var cat1 = Object.create(Cat);
alert(cat1.name); // 大毛
cat1.makeSound(); // 喵喵喵
目前,各大浏览器的最新版本(包括IE9)都部署了这个方法。如果遇到老式浏览器,可以用下面的代码自行部署。
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
这种方法比"构造函数法"简单,但是不能实现私有属性和私有方法,实例对象之间也不能共享数据,对"类"的模拟不够全面。
三、极简主义法
荷兰程序员Gabor de Mooij提出了一种比Object.create()更好的新方法,他称这种方法为"极简主义法"(minimalist approach)。这也是我推荐的方法。
3.1 封装
这种方法不使用this和prototype,代码部署起来非常简单,这大概也是它被叫做"极简主义法"的原因。
首先,它也是用一个对象模拟"类"。在这个类里面,定义一个构造函数createNew(),用来生成实例。
var Cat = {
createNew: function(){
// some code here
}
};
然后,在createNew()里面,定义一个实例对象,把这个实例对象作为返回值。
var Cat = {
createNew: function(){
var cat = {};
cat.name = "大毛";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};
使用的时候,调用createNew()方法,就可以得到实例对象。
var cat1 = Cat.createNew();
cat1.makeSound(); // 喵喵喵
这种方法的好处是,容易理解,结构清晰优雅,符合传统的"面向对象编程"的构造,因此可以方便地部署下面的特性。
3.2 继承
让一个类继承另一个类,实现起来很方便。只要在前者的createNew()方法中,调用后者的createNew()方法即可。
先定义一个Animal类。
var Animal = {
createNew: function(){
var animal = {};
animal.sleep = function(){ alert("睡懒觉"); };
return animal;
}
};
然后,在Cat的createNew()方法中,调用Animal的createNew()方法。
var Cat = {
createNew: function(){
var cat = Animal.createNew();
cat.name = "大毛";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};
这样得到的Cat实例,就会同时继承Cat类和Animal类。
var cat1 = Cat.createNew();
cat1.sleep(); // 睡懒觉
3.3 私有属性和私有方法
在createNew()方法中,只要不是定义在cat对象上的方法和属性,都是私有的。
var Cat = {
createNew: function(){
var cat = {};
var sound = "喵喵喵";
cat.makeSound = function(){ alert(sound); };
return cat;
}
};
上例的内部变量sound,外部无法读取,只有通过cat的公有方法makeSound()来读取。
var cat1 = Cat.createNew();
alert(cat1.sound); // undefined
3.4 数据共享
有时候,我们需要所有实例对象,能够读写同一项内部数据。这个时候,只要把这个内部数据,封装在类对象的里面、createNew()方法的外面即可。
var Cat = {
sound : "喵喵喵",
createNew: function(){
var cat = {};
cat.makeSound = function(){ alert(Cat.sound); };
cat.changeSound = function(x){ Cat.sound = x; };
return cat;
}
};
然后,生成两个实例对象:
var cat1 = Cat.createNew();
var cat2 = Cat.createNew();
cat1.makeSound(); // 喵喵喵
这时,如果有一个实例对象,修改了共享的数据,另一个实例对象也会受到影响。
cat2.changeSound("啦啦啦");
cat1.makeSound(); // 啦啦啦
(完)
ayanamist 说:
所谓的第三种方法,就是Builder模式啊……博主肯定没看过重构与模式方面的书……
2012年7月10日 00:38 | # | 引用
kk 说:
和build模式有啥关系。。第三种是闭包实现,看js good parts就知道了,不过闭包有闭包的问题。。所以现在一般还是用prototype实现。
要么再等等,class关键字就要出了;)
2012年7月10日 02:27 | # | 引用
aa 说:
马上就要有class关键字了
2012年7月10日 10:20 | # | 引用
Ariel 说:
建议大阮看看《七周七语言》中写的Io language,想必之后可以解释的更清楚一些。
2012年7月10日 10:24 | # | 引用
锐齿猱 说:
js的原型链确实奇葩,并难理解。
PS:这个值得参考一下: http://bonsaiden.github.com/JavaScript-Garden/zh/
2012年7月10日 11:21 | # | 引用
norion 说:
博主,三种方法的内存占用优劣性怎样呢,能不能分析一下。
2012年7月10日 16:17 | # | 引用
11 说:
入门级的。。。
2012年7月11日 14:18 | # | 引用
麦时 说:
阮老师好,我近来想着手翻译一些英文文章,不知老师是否有翻译技巧,翻译注意这些方面的书推荐呢?多谢。
2012年7月11日 21:38 | # | 引用
momo 说:
不同的功能,用不同方式吧
2012年7月11日 22:09 | # | 引用
文少 说:
致命缺点
alert(cat1 instanceof Cat);
通不过
2012年7月12日 10:15 | # | 引用
zhiyelee 说:
第三种方法的缺点也是很明显的,不能使用instanceof 判断类
2012年7月12日 10:21 | # | 引用
eyeseenull 说:
感觉 javascript 实现面向对象 有点臃肿,失去了轻巧灵活直接,不过复杂的东西总是需要章法。
2012年7月12日 15:16 | # | 引用
ghy 说:
感觉,怎么写都有点别扭
2012年7月12日 15:47 | # | 引用
Siwei 说:
使用 coffeescript 把。。。
2012年7月13日 08:23 | # | 引用
xuht 说:
第三种方式最大的好处是创建类和继承类很简单,但也有一个问题。
第三种方式本质上没有使用继承,每次的继承是重新创建的对象然后再增加属性,所以继承的层次如果多了,对象会搞得很大,这个不太好。
2012年7月13日 09:52 | # | 引用
穷小子 说:
js原生的还是很大用途啊,所谓网页汇编语言嘛
2012年7月13日 09:54 | # | 引用
Anna 说:
建议您读一读杨绛先生的《翻译的技巧》。
2012年7月13日 21:41 | # | 引用
Allen 说:
"它的主要缺点是,比较复杂,用到了this和prototype,编写和阅读都很费力。"
我咋觉得构造函数方法挺好呢?也许代码多写些了几行,但是很清晰,this与prototype也很好用啊
它的缺点到底是啥呢
2012年7月14日 14:09 | # | 引用
saturn 说:
峰哥您好:
最开始是如何进到这个博客的现在已经不知道了,这三年来,除偶尔不能上网的时候,我几乎每天都要到这里看看。不得不说,我从这里学习到了很多,您的辛勤劳动,帮助了很多人。不知道您对Windows编程有没有研究,最近正在学习Windows编程,尤其是MFC的使用,看了一些书,但是总还是没有头绪,不知道峰哥能不能写写这方面的东西,3Q in advance!
2012年7月14日 20:44 | # | 引用
土木坛子 说:
阮兄的博客是非常认真的,这在中文博客圈中少见。
2012年7月15日 03:51 | # | 引用
千面公子 说:
阮老师,你首页上最新留言处的js加载非常慢,是否考虑优化一下
2012年7月16日 11:26 | # | 引用
lib 说:
ruanyifeng已经被神话了。。
2012年7月20日 17:57 | # | 引用
Mead 说:
prototype和this怎么难理解?
我倒是觉得后面两种都非常难理解。
并且无法实现真正的对象化,var static={}这个是值形式,无法对象化。
2012年7月21日 13:29 | # | 引用
刘晓勇 说:
我只是一个平民,我们这里还有过年吃不上肉的人。以上仅是我的个人介绍,我想说的是:我反对一党执政,可有用吗?
2012年7月22日 19:54 | # | 引用
小Z 说:
来向峰哥学习的
2012年7月22日 21:56 | # | 引用
NinoFocus 说:
推荐大家可以看一下这篇文章 http://howtonode.org/object-graphs-2
2012年7月27日 19:34 | # | 引用
流年 说:
阮老师的博客我是经常来逛的,感觉阮老师对很多领域都有自己比较独到的见解,看完之后稍作思考便觉获益匪浅,这种受益并不局限于知识点或面,而是一种思维方式、思维角度的转换,往往能让我可以更全面的去考虑一件事。
希望阮老师以后能多多的把生活中的一些感触写下来。
2012年7月29日 20:11 | # | 引用
qtxie 说:
使用“极简主义法”,每个instance里都会有一份method的实例,不像prototype方式那样可以使用一份method实例就行了。
2012年8月 3日 10:13 | # | 引用
无意乂 说:
3.4数据共享那一节不是说了嘛,只要放在createNew外面就可以共享了。
var Cat = {
createNew: function(){
var cat = {};
cat.makeSound = this._makeSound; // 公有方法,共用
return cat;
},
_makeSound: function() { alert("喵喵喵"); }
};
只有公有的变量属性是不能共享的
2012年8月 9日 20:08 | # | 引用
lazyzzz 说:
极简主义,看起来很美好,有没有人分析下它的缺点?
2012年8月30日 10:23 | # | 引用
ToFishes 说:
这样同样会暴露一个调用方式: Cat._makeSound() ,这是好还是坏? 或者我想到这类似java类的static方法,不就是类名直接调用吗。
2012年9月 6日 14:15 | # | 引用
learner 说:
我是一名学生现在正在学js,不知道怎么去开始,请阮老师给一点建议,不知道从何开始学习?蛮烦了,谢谢
2012年9月23日 17:04 | # | 引用
sojuker 说:
个人感觉极简主义还有比较大的问题。特别是对于公有方法的处理上,有天生的缺陷一般。不知大家有好的解决方法吗?
var Cat = {
//公有属性
sound : "喵喵喵",
//公有方法定义1
showName : function (){ alert ("the cat's name is" + this.name); } //内部引用实例属性
//公有方法定义3,提高内聚程度,类似人工模拟 prototype
proto :{
showName : function (){ alert ("the cat's name is" + this.name); },
constructor : Cat //用于对象识别,最好能配置该属性的特性,使之不可枚举,不可配置
},
//模拟构建函数
createNew: function(name,age){ //利用参数提供初始化配置
var cat = {};
//设置实例属性
cat.name = name;
cat.age = age;
//公有方法定义2,手动维护关系链,而且不灵活
cat.showName = Cat.showName;
//相对来说更不优秀的方法,极度占用内存空间
cat.showName = function(){ alert ("the cat's name is" + this.name); };
//公有方法定义3,提高内聚程度
cat.proto = Cat.proto;
return cat;
}
};
//创建实例
var cat1 = Cat.createNew('Tom',8);
//访问公有方法1
Cat.showName.call(cat1);
//访问公有方法2(代码部署复杂,解耦程度低)
cat1.showName();
//访问公有方法3 (接近原生prototype实现)
cat1.proto.showName.call(cat1);
//经典模式下访问公有方法
cat1.showName();
2012年11月24日 17:39 | # | 引用
devan5 说:
习惯用 object.create() 稍加扩展和封装, 即可以实现继承, 私有方法/属性.
没有一种方法就能是银弹. 单纯使用以上三种, 都不能达到: 私有属性的实例之间共享.
第三种, 想法视点不错. 但是继承方面是缺陷的: 无论父类的共有私有方法/属性. 一并新建父对象实例.
2013年8月21日 09:53 | # | 引用
wteam_xq 说:
第三种方法摆脱了使用原型链的缺点(属性不能私有、创建、继承对象不够直观),但也暴露了没用原型链的弊端:每一次生成一个实例,都必须为重复的内容,多占用一些内存。看过cocos2d-html5的继承源码,感觉它就将原型链跟第三种方式结合了。
2013年8月31日 21:20 | # | 引用
玉慕瑕 说:
我学习了极简主义后,有个疑惑:如果我在对象的方法里须调用外部的方法,经此来减少对象方法对内存的重复占用,但又不能直接用inner.function = outer.function这样的形式,应该怎么实现呢?
2013年9月17日 12:05 | # | 引用
mlisp 说:
js本身就是基于对象,硬要造个类,浪费时间与精力。
animal = object clone
cat = animal clone
dog = animal clone
cat1 = cat clone
cat2 = cat clone
对象就是类,类就是对象;函数就是对象,对象就是函数。
2014年2月21日 13:12 | # | 引用
小河 说:
请有兴趣的人一起加入QQ群研究Java script技术,群号:23987720。(老师,借宝地网络一些有共同爱好的人)。
2014年10月17日 14:08 | # | 引用
xiejl 说:
方法二的Object.create()法是不是可以省略掉.
例如用下面的方式.
var xjl = {
name: "xie",
m1 : function() {
alert(10);
},
m2 : function() {
alert("姓名:" + xjl.name);
}
};
//方式一:通过新的对象调用
/* var x = xjl;
x.m1();
alert(x.name);
x.m2();*/
//方式二:直接调用
xjl.m1();
alert(xjl.name);
xjl.m2();
2015年3月21日 16:57 | # | 引用
climber 说:
第一种方法最好,符合一贯的编程习惯。
2015年4月23日 09:57 | # | 引用
夏云淋 说:
2015年4月30日 15:10 | # | 引用
jujube 说:
最后一种方法好棒啊!
2015年5月13日 12:53 | # | 引用
杯子里的影子 说:
最后那种方法在cocos2d-js下面就很常用。
然而在ES6面前它就跪了,这种风格的代码很难把逻辑转换成ES6啊。
2015年6月 9日 15:40 | # | 引用
ilaipi 说:
第三种方法能传参吗?
2015年6月25日 16:03 | # | 引用
red_iris 说:
js在自定义类的时候,能定义一个类,类名叫Object吗?
2015年8月13日 09:45 | # | 引用
TheStars 说:
在犀牛书(第六版)中9.5.1节是这样描述的:尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。
2015年11月15日 17:50 | # | 引用
yangtze 说:
之前看"Javascript 语言精粹"一头雾水,看了您的讲解,瞬间懂了,谢谢!
2015年12月17日 17:50 | # | 引用
Apolo 说:
看了半天,并不觉得后两种比第一中构造函数可读性强。
2016年10月31日 04:07 | # | 引用
麻瓜 说:
第三个 方式中,子类不能继承父类的类属性和类方法,怎么解决
2016年12月21日 16:11 | # | 引用
cmf41013 说:
第三种 是类的工厂,不能算是类的定义
2017年1月22日 09:40 | # | 引用
泳爸 说:
阮大神,您的例子我能看懂,但不符合我认为的类。我认为类中,共有的属性跟方法要共用。就是在内存中只存在一个。比如极简主义法中的cat.makeSound = function(){ alert("喵喵喵"); 这里猫叫按照道理来说是什么猫都会叫,所以按道理来说放在prototype里最好,公用。不然每次用都会给只新猫上添加这个方法,浪费了内存。这不符合我认为的‘类’
2017年9月13日 13:33 | # | 引用
lambdang 说:
级简的确不错 但还有一个问题怎么解决 就是多类继承 或者说mixin继承 如何实现
2017年11月27日 18:17 | # | 引用
龙在天 说:
不错不错,谢谢高人的传道受业解惑!
2018年1月10日 10:10 | # | 引用
kidz 说:
时隔多年js还是用上了class声明,开心!
2021年5月12日 16:43 | # | 引用