随着浏览器的发展,JS(JavaScript) 越来越受到人们的欢迎。它不再像以前只能做单一的渲染页面这样的事情了,在Chrome等现代浏览器上,你甚至可以用JS来做音视频的处理,是不是觉得很神奇?
不过今天我要讨论的并不是如何使用JS来做一些神奇的事儿,而是来重新认识一下JS中类的使用。对于这部分知识的理解,将为我们后面阅读Janus(一款WebRTC流媒体服务器)代码有着至关重要的作用。
JS中的function
在ES5以前,JS中并没有class关键字,那时候JS是如何表示一个类的呢?说来也奇怪,它使用function来表示。
我在了解这部分知识的时候也是觉得不可思意!实际上我早在2003年的时候就学习并使用JS了,那时候JS还很简单。虽然后来很久没有再碰过它,但印象中function一直是用来定义一个函数的,现在怎么又用来定义类了呢?
后来看了一些资料才逐渐理清,原来现在的JS中function
既可以用来定义函数,也可以用来定义类。有点类似于语文中的一语双关。
之所以function
有双层含义,是因为JS最开始并不支持面向对象开发模式。但随着技术的发展,面向对象的开发模式越来越受到人们的欢迎,JS为了能跟上时代,所以也必须支持面向对象开发。
不过JS在转向面向对象语言时面临一种选择,即从原生语言上支持class
,那JS解析器就要做大的调整,这可不是一时半会儿可以完成的。而如果在原有的基础上修改则要容易得多。
权衡利弊之后,JS大神
们还是决定在现有的基础上修改是最省时少力的。于是就借用了function
函数,把它看作是一个构造函数,这样就可以快速的将JS改造成面向对象的开发语言了。
以上就是JS中使用function
定义类的大致由头!下面我们就来看看在ES5上该如何定义一个类。
类及成员
在JS中如何定义一个类呢?实际上它与其它面向对象语言(如Java)是很类似的,只不过在Java中定义类用的是class
关键字,而在JS中用function
代替而以。代码如下:
1 | function classname() { |
在 Java 中,类的成员按安全性可以分为公有和私有,JS中是否也有类似的概念呢?答案是肯定的,接下来我们就来看一下在JS类中如何定义公有成员和私有成员吧。
JS中公有成员和私有成员的定义都是隐式的,不像Java有明确的public
和private
关键字来指明它们的权限。在JS类中直接定义的函数或变量都是私有成员,在类成员或函数前面加this
关键字的,则表式是公有成员。
我们来看个例子:
1 | function myclass (){ |
如果我们在浏览器执行上面的代码,在浏览器的debugger
中你一定可以看到这样一条错误信息“Uncaught TypeError: obj.testfunc is not a function”
,这说明通过obj
对像是无法访问到test()
函数的。
我们稍微调整一下这段代码,修改如下:
1 | function myclass (){ |
我们在test
函数前加上this.
,只做这一点点修改,这段代码就可以在浏览器上成功运行了。
由此我们可以知道,如果你想让外面访问对象中的成员(成员变量或成员方法),你就应该在这些成员前面加上this
关键字。反过来讲,如果你不想让外面访问到对象中的成员,则不要在这些成员前面加this
。
类方法
在Java中除了有对象成员外,还可以有类成员,比如在使用单例模式时,我们都会定义一个静态的成员。在JS中如何做到这点呢?我们来看个具体例子吧。还是刚才那个代码,我们在其基础上稍做修改即可。
1 | //定义类方法 |
在这段代码中,加入了类方法的定义及调用类方法的代码。从上述代码中我们可以知道,在JS中类方法是在类之外定义的,而不像Java在是类内加static
关键字。
类的原型prototype
在JS中,每个类都有一个类属性prototype
,用来指向类原型。或者你可以把它理解为指向类原型的地址。当我们想为这个类添加方法或成员变量的时候,就可以通过prototype来实现,只需修改prototype指向的内存地址的内容就可以达到添加成员的目的。
举个例子,假设我们定义了一个类如下:
1 | function myjs(){ |
目前在这个类中没有写任何方法或成员变量,只是定义了一个空类。下面我想修改这个类,给这个类增加一些内容,该怎么做呢?实现的方法很简单,修改prototype即可,看下面的例子你就明白了。
1 | function myjs(){ |
通过上面的代码,我们就给myjs
类增加了两个成员,即一个变量a
和一个方法test
。当我们生成myjs对象时,生成的对象中就有我们之前添加的成员变量和成员方法了。
类的继承
JS中没有专门用于类继承的语法,不过你可以通过上一节介绍的prototype来实现类继承
。在我们正式讲解继承之前,我们先了解一下prototype在内存中是如何表示的。如下图所示:
通过上图我们可以看到,使用function
定义的类并非真实的类
,更准确的说它应该是一个构造函数
。而类属性prototype
指向的才是类的真正地址。
可能很多同学会问JS是如何通过构造函数找到它所在的类的呢?其实这是C语言的一个小巧,其过程是JS调用浏览器,通过浏览器使用C语言中的技巧
获取构造函数所在类的地址,这对于浏览器来当然是小菜一碟。
了解了prototype
的物理意义后,接下来我们看看类生成的对象在内存中的情况,它与prototype之间的关系又是怎样的?如下图所示。
通过上图我们可以看到,JS在创建对象时会为每个对象分配内存空间。更为重要的一点是,多个相同类型的对象
会指向同一个prototype。
了解了上面的特性后,我们就可以利用prototype
来实现类的继承了。如何来做呢?我们再来举个例子。
首先,我们定义一个基类,如下:
1 | function parent() { |
然后,我们创建一个子类,并让子类的prototype指向父类,如下:
1 | function child() { |
执行上面的例子,我们通过浏览器的debugger
就可以看到如下结果:
1 | a = 1 |
说明child子类确实是继承了parent类。我们再深扒一下,对于上面这段代码表示的继承关系在内存中的物理意义是什么呢?如下图所示:
在JS中,正常情况下每生成一个对象,该对象的 __proto__
都指向该对象的原始类的地址。如上图所示parent对象的__proto__
指向parent.prototype,child对象的__proto__
指向child.prototype。
为什么会这样呢?要理解其中的奥秘,我们必须要知道JS中 new xxx
做了哪些事儿。实际上,new xxx
做了四件事儿,我们以上图中的new parent()
为例,它做的四件事儿如下:
1 | var obj = {}; |
在这步中,第二步是最关键的,它表明了新对象的__proto__
指向了哪里。这样我们就可以理解 “对象的 __proto__
都指向该对象的原始类的地址” 这句话了。
当我们理解了 new xxx
的真实含义之后,child.prototype = new parent()
这句代码的含义立马就清楚了,它的含义是改变 child.prototype
的指向, 让他重新指向parent
对象。
由于生成parent对象时,它的__proto__
指向了parent的原始类,因此child.prototype就与parent的prototype建立了连接。
在接下来创建obj_child
对象时,由于child.prototype已经指向了parent对象,因此obj_child.__proto__
也就指向了parent对象。此时通过 old_child 就可以访问到parent对象的内容了,从而也就达到了继承的目的。
ES6 中的类
大家对于在JS中使用function
方式定义类实在感到很厌烦,就不能与其它语言一样可以使用class
来定义类吗?在ES6时代,JS终于可以做到这一点了。
现在我们来看看在JS中该如何定义类吧,例子如下:
1 | class cls { |
上面的代码是不是看着就舒服多了?但实际上,ES6中的class
只是一个语法糖
。啥意思呢?也就是说虽然语法上JS改成了与其它面向对象语言一致的用法,但在JS内部还是使用的function
的机制来实现的。
公有成员与私有成员
使用 ES6 中的 class
定义类时,类中的成员默认都是公有成员,外面都可以直接访问到。当然在class中也可以使用#
来定义私有成员变量,但一般情况下我们很少用到。我们来看一下例子吧,在上面的代码中做一点修改即可:
1 | class cls { |
我们在上面代码中增加了 #a
变量,因#
表示的是私有成员,所以当我们创建对象 t
后,通过 t.a
是无法访问它的,此时只能通过cls类的成员方法do()
才能访问 #a
变量。
类的继承
接下来我们再来看看在 ES6 中如何实现类的继承
。在ES5中要实现类继承必须使用prototype
,如果你不从内存存储的角度去思考的话,就很难理解它是如何实现类继承
的。而在 ES6 中,类的继承就就像我们使用其它语言中的类继承一样,让我们一目了然。
我们来举个例子,你一看就明白了:
1 | class parent { |
上面的代码定义了两个类,一个父类parent
;一个子类child
。当我们创建 child
对象 c
时,首先会触发 child的构造函数。在child构造函数中,它首先调用 super()
方法,而该方法会调用parent
类的构造函数,从而将parent类中的a
属性进行初始化。之后又回到child构造函数中对b
属性进行初始化,至此所有的初始化工作完成,最终c
对象被创建出来。
当c对象创建好后,我们就可以直接访问它里边的 a
和 b
属性了,以上就是ES6中类继承的过程。其过程与其它面向对象语言完成一致,所以大家在使用它时会觉得非常自然。
小结
以上我对JS中 ES5 和 ES6 标准中的类
做的一些浅显的探究,在ES5 中类是通过function
创建了,由于JS最开始并不支持面向对象开发,所以在ES5中使用JS实现面向对象开发的方式让人觉得很诡异
。我在理解这部分知识时,也颇费了一翻周折。不过如果你对内存管理比较熟悉的话,从内存管理的角度去理解 ES5 中的类与继承就比较容易了。
对于 ES6 来说,类的定义与类的继承几乎完全照搬了 Java 的语法,所以我们在学习和使用它时就非常方便了。