Javascript定义类(class)的三种方法

作者: 阮一峰

日期: 2012年7月 9日

将近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(); // 啦啦啦

(完)

留言(54条)

所谓的第三种方法,就是Builder模式啊……博主肯定没看过重构与模式方面的书……

和build模式有啥关系。。第三种是闭包实现,看js good parts就知道了,不过闭包有闭包的问题。。所以现在一般还是用prototype实现。
要么再等等,class关键字就要出了;)

马上就要有class关键字了

建议大阮看看《七周七语言》中写的Io language,想必之后可以解释的更清楚一些。

js的原型链确实奇葩,并难理解。

PS:这个值得参考一下: http://bonsaiden.github.com/JavaScript-Garden/zh/

博主,三种方法的内存占用优劣性怎样呢,能不能分析一下。

入门级的。。。

阮老师好,我近来想着手翻译一些英文文章,不知老师是否有翻译技巧,翻译注意这些方面的书推荐呢?多谢。

不同的功能,用不同方式吧

致命缺点
alert(cat1 instanceof Cat);
通不过

第三种方法的缺点也是很明显的,不能使用instanceof 判断类

感觉 javascript 实现面向对象 有点臃肿,失去了轻巧灵活直接,不过复杂的东西总是需要章法。

感觉,怎么写都有点别扭

使用 coffeescript 把。。。

第三种方式最大的好处是创建类和继承类很简单,但也有一个问题。
第三种方式本质上没有使用继承,每次的继承是重新创建的对象然后再增加属性,所以继承的层次如果多了,对象会搞得很大,这个不太好。

js原生的还是很大用途啊,所谓网页汇编语言嘛

引用麦时的发言:

阮老师好,我近来想着手翻译一些英文文章,不知老师是否有翻译技巧,翻译注意这些方面的书推荐呢?多谢。

建议您读一读杨绛先生的《翻译的技巧》。

"它的主要缺点是,比较复杂,用到了this和prototype,编写和阅读都很费力。"
我咋觉得构造函数方法挺好呢?也许代码多写些了几行,但是很清晰,this与prototype也很好用啊
它的缺点到底是啥呢

峰哥您好:

最开始是如何进到这个博客的现在已经不知道了,这三年来,除偶尔不能上网的时候,我几乎每天都要到这里看看。不得不说,我从这里学习到了很多,您的辛勤劳动,帮助了很多人。不知道您对Windows编程有没有研究,最近正在学习Windows编程,尤其是MFC的使用,看了一些书,但是总还是没有头绪,不知道峰哥能不能写写这方面的东西,3Q in advance!

引用saturn的发言:

峰哥您好:

最开始是如何进到这个博客的现在已经不知道了,这三年来,除偶尔不能上网的时候,我几乎每天都要到这里看看。不得不说,我从这里学习到了很多,您的辛勤劳动,帮助了很多人。不知道您对Windows编程有没有研究,最近正在学习Windows编程,尤其是MFC的使用,看了一些书,但是总还是没有头绪,不知道峰哥能不能写写这方面的东西,3Q in advance!



阮兄的博客是非常认真的,这在中文博客圈中少见。

阮老师,你首页上最新留言处的js加载非常慢,是否考虑优化一下

ruanyifeng已经被神话了。。

prototype和this怎么难理解?
我倒是觉得后面两种都非常难理解。
并且无法实现真正的对象化,var static={}这个是值形式,无法对象化。

我只是一个平民,我们这里还有过年吃不上肉的人。以上仅是我的个人介绍,我想说的是:我反对一党执政,可有用吗?

来向峰哥学习的

推荐大家可以看一下这篇文章 http://howtonode.org/object-graphs-2

阮老师的博客我是经常来逛的,感觉阮老师对很多领域都有自己比较独到的见解,看完之后稍作思考便觉获益匪浅,这种受益并不局限于知识点或面,而是一种思维方式、思维角度的转换,往往能让我可以更全面的去考虑一件事。
希望阮老师以后能多多的把生活中的一些感触写下来。

使用“极简主义法”,每个instance里都会有一份method的实例,不像prototype方式那样可以使用一份method实例就行了。

引用qtxie的发言:

使用“极简主义法”,每个instance里都会有一份method的实例,不像prototype方式那样可以使用一份method实例就行了。

3.4数据共享那一节不是说了嘛,只要放在createNew外面就可以共享了。
var Cat = {
createNew: function(){
var cat = {};
cat.makeSound = this._makeSound; // 公有方法,共用
return cat;
},
_makeSound: function() { alert("喵喵喵"); }
};

只有公有的变量属性是不能共享的

极简主义,看起来很美好,有没有人分析下它的缺点?

引用无意乂的发言:

3.4数据共享那一节不是说了嘛,只要放在createNew外面就可以共享了。
var Cat = {
createNew: function(){
var cat = {};
cat.makeSound = this._makeSound; // 公有方法,共用
return cat;
},
_makeSound: function() { alert("喵喵喵"); }
};

只有公有的变量属性是不能共享的

这样同样会暴露一个调用方式: Cat._makeSound() ,这是好还是坏? 或者我想到这类似java类的static方法,不就是类名直接调用吗。

我是一名学生现在正在学js,不知道怎么去开始,请阮老师给一点建议,不知道从何开始学习?蛮烦了,谢谢

个人感觉极简主义还有比较大的问题。特别是对于公有方法的处理上,有天生的缺陷一般。不知大家有好的解决方法吗?
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();

习惯用 object.create() 稍加扩展和封装, 即可以实现继承, 私有方法/属性.

没有一种方法就能是银弹. 单纯使用以上三种, 都不能达到: 私有属性的实例之间共享.

第三种, 想法视点不错. 但是继承方面是缺陷的: 无论父类的共有私有方法/属性. 一并新建父对象实例.

第三种方法摆脱了使用原型链的缺点(属性不能私有、创建、继承对象不够直观),但也暴露了没用原型链的弊端:每一次生成一个实例,都必须为重复的内容,多占用一些内存。看过cocos2d-html5的继承源码,感觉它就将原型链跟第三种方式结合了。

我学习了极简主义后,有个疑惑:如果我在对象的方法里须调用外部的方法,经此来减少对象方法对内存的重复占用,但又不能直接用inner.function = outer.function这样的形式,应该怎么实现呢?

js本身就是基于对象,硬要造个类,浪费时间与精力。

animal = object clone
cat = animal clone
dog = animal clone
cat1 = cat clone
cat2 = cat clone

对象就是类,类就是对象;函数就是对象,对象就是函数。

请有兴趣的人一起加入QQ群研究Java script技术,群号:23987720。(老师,借宝地网络一些有共同爱好的人)。

方法二的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();

第一种方法最好,符合一贯的编程习惯。

引用learner的发言:

我是一名学生现在正在学js,不知道怎么去开始,请阮老师给一点建议,不知道从何开始学习?蛮烦了,谢谢

最后一种方法好棒啊!

最后那种方法在cocos2d-js下面就很常用。
然而在ES6面前它就跪了,这种风格的代码很难把逻辑转换成ES6啊。

第三种方法能传参吗?

js在自定义类的时候,能定义一个类,类名叫Object吗?

引用文少的发言:

致命缺点
alert(cat1 instanceof Cat);
通不过

在犀牛书(第六版)中9.5.1节是这样描述的:尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。

之前看"Javascript 语言精粹"一头雾水,看了您的讲解,瞬间懂了,谢谢!

看了半天,并不觉得后两种比第一中构造函数可读性强。

第三个 方式中,子类不能继承父类的类属性和类方法,怎么解决

第三种 是类的工厂,不能算是类的定义

阮大神,您的例子我能看懂,但不符合我认为的类。我认为类中,共有的属性跟方法要共用。就是在内存中只存在一个。比如极简主义法中的cat.makeSound = function(){ alert("喵喵喵"); 这里猫叫按照道理来说是什么猫都会叫,所以按道理来说放在prototype里最好,公用。不然每次用都会给只新猫上添加这个方法,浪费了内存。这不符合我认为的‘类’

级简的确不错 但还有一个问题怎么解决 就是多类继承 或者说mixin继承 如何实现

不错不错,谢谢高人的传道受业解惑!

时隔多年js还是用上了class声明,开心!

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接