0%

js类的探究

javascript

随着浏览器的发展,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
2
3
function classname() {
...
}

在 Java 中,类的成员按安全性可以分为公有私有,JS中是否也有类似的概念呢?答案是肯定的,接下来我们就来看一下在JS类中如何定义公有成员和私有成员吧。

JS中公有成员和私有成员的定义都是隐式的,不像Java有明确的publicprivate关键字来指明它们的权限。在JS类中直接定义的函数或变量都是私有成员,在类成员或函数前面加this关键字的,则表式是公有成员。

我们来看个例子:

1
2
3
4
5
6
7
8
9
function myclass (){

function test(){
console.log("testA function");
}
}

var obj = new myclass();
obj.test();

如果我们在浏览器执行上面的代码,在浏览器的debugger中你一定可以看到这样一条错误信息“Uncaught TypeError: obj.testfunc is not a function”,这说明通过obj对像是无法访问到test()函数的。

我们稍微调整一下这段代码,修改如下:

1
2
3
4
5
6
7
8
9
function myclass (){

this.test = test() { //这里加了this 关键字
console.log("testA function");
}
}

var obj = new myclass();
obj.test();

我们在test函数前加上this.,只做这一点点修改,这段代码就可以在浏览器上成功运行了。

由此我们可以知道,如果你想让外面访问对象中的成员(成员变量或成员方法),你就应该在这些成员前面加上this关键字。反过来讲,如果你不想让外面访问到对象中的成员,则不要在这些成员前面加this

类方法

在Java中除了有对象成员外,还可以有类成员,比如在使用单例模式时,我们都会定义一个静态的成员。在JS中如何做到这点呢?我们来看个具体例子吧。还是刚才那个代码,我们在其基础上稍做修改即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义类方法
myclass.init = function(){
console.log("class method!");
}

function myclass (){

this.test = test() { //这里加了this 关键字
console.log("testA function");
}
}

//调用类方法
myclass.init();

//创建对象
var obj = new myclass();
obj.test();

在这段代码中,加入了类方法的定义及调用类方法的代码。从上述代码中我们可以知道,在JS中类方法是在类之外定义的,而不像Java在是类内加static关键字。

类的原型prototype

在JS中,每个类都有一个类属性prototype,用来指向类原型。或者你可以把它理解为指向类原型的地址。当我们想为这个类添加方法或成员变量的时候,就可以通过prototype来实现,只需修改prototype指向的内存地址的内容就可以达到添加成员的目的。

举个例子,假设我们定义了一个类如下:

1
2
3
function myjs(){

}

目前在这个类中没有写任何方法或成员变量,只是定义了一个空类。下面我想修改这个类,给这个类增加一些内容,该怎么做呢?实现的方法很简单,修改prototype即可,看下面的例子你就明白了。

1
2
3
4
5
6
7
8
9
10
11
12
function myjs(){

}

myjs.prototype.a = 5;
myjs.prototype.test = function() {
console.log("this is a function of myjs object!");
}

var obj = new myjs();
console.log("myjs.a == " + obj.a );
obj.test();

通过上面的代码,我们就给myjs类增加了两个成员,即一个变量a和一个方法test。当我们生成myjs对象时,生成的对象中就有我们之前添加的成员变量和成员方法了。

类的继承

JS中没有专门用于类继承的语法,不过你可以通过上一节介绍的prototype来实现类继承。在我们正式讲解继承之前,我们先了解一下prototype在内存中是如何表示的。如下图所示:
prototype

通过上图我们可以看到,使用function定义的类并非真实的,更准确的说它应该是一个构造函数。而类属性prototype指向的才是类的真正地址。

可能很多同学会问JS是如何通过构造函数找到它所在的类的呢?其实这是C语言的一个小巧,其过程是JS调用浏览器,通过浏览器使用C语言中的技巧获取构造函数所在类的地址,这对于浏览器来当然是小菜一碟。

了解了prototype的物理意义后,接下来我们看看类生成的对象在内存中的情况,它与prototype之间的关系又是怎样的?如下图所示。
对象在内存中

通过上图我们可以看到,JS在创建对象时会为每个对象分配内存空间。更为重要的一点是,多个相同类型的对象会指向同一个prototype。

了解了上面的特性后,我们就可以利用prototype来实现类的继承了。如何来做呢?我们再来举个例子。

首先,我们定义一个基类,如下:

1
2
3
4
5
6
function parent() {

}

//给基类添加一个新的属性 a
parent.prototype.a = 1;

然后,我们创建一个子类,并让子类的prototype指向父类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function child() {

}

child.prototype = new parent();

//再给子类添加一个属性 b
child.prototype.b = 2;

//创建子类
var obj_child = new child();

//访问子类的属性
console.log("a = " + obj_child.a);
console.log("b = " + obj_child.b);

执行上面的例子,我们通过浏览器的debugger就可以看到如下结果:

1
2
a = 1
b = 2

说明child子类确实是继承了parent类。我们再深扒一下,对于上面这段代码表示的继承关系在内存中的物理意义是什么呢?如下图所示:

js继承

在JS中,正常情况下每生成一个对象,该对象的 __proto__ 都指向该对象的原始类的地址。如上图所示parent对象的__proto__指向parent.prototype,child对象的__proto__指向child.prototype。

为什么会这样呢?要理解其中的奥秘,我们必须要知道JS中 new xxx 做了哪些事儿。实际上,new xxx 做了四件事儿,我们以上图中的new parent()为例,它做的四件事儿如下:

1
2
3
4
var obj = {};
obj.__proto__ = parent.prototype; //即parent
parent.call(obj); //调用parent的构造函数
return 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
2
3
4
5
6
7
8
9
10
11
12
class cls {
constructor(arg){
this.a = arg;
}

do(){
console.log("a value ==" + this.a);
}
};

var t = new cls('hello');
t.do();

上面的代码是不是看着就舒服多了?但实际上,ES6中的class只是一个语法糖。啥意思呢?也就是说虽然语法上JS改成了与其它面向对象语言一致的用法,但在JS内部还是使用的function的机制来实现的。

公有成员与私有成员

使用 ES6 中的 class 定义类时,类中的成员默认都是公有成员,外面都可以直接访问到。当然在class中也可以使用#来定义私有成员变量,但一般情况下我们很少用到。我们来看一下例子吧,在上面的代码中做一点修改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class cls {
#a;
constructor(arg){
this.#a = arg;
}

do(){
console.log("a value ==" + this.#a);
}
};

var t = new cls('hello');
console.log("#a= " + t.#a);
t.do();

我们在上面代码中增加了 #a 变量,因#表示的是私有成员,所以当我们创建对象 t 后,通过 t.a 是无法访问它的,此时只能通过cls类的成员方法do()才能访问 #a变量。

类的继承

接下来我们再来看看在 ES6 中如何实现类的继承。在ES5中要实现类继承必须使用prototype,如果你不从内存存储的角度去思考的话,就很难理解它是如何实现类继承的。而在 ES6 中,类的继承就就像我们使用其它语言中的类继承一样,让我们一目了然。

我们来举个例子,你一看就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class parent {
constructor(){
this.a = 'hello';
}
};

class child extends parent {
constructor() {
super();
this.b = 'world';
}
}

var c = new child();

console.log(c.a + " " + c.b);

上面的代码定义了两个类,一个父类parent;一个子类child。当我们创建 child 对象 c时,首先会触发 child的构造函数。在child构造函数中,它首先调用 super()方法,而该方法会调用parent 类的构造函数,从而将parent类中的a属性进行初始化。之后又回到child构造函数中对b属性进行初始化,至此所有的初始化工作完成,最终c对象被创建出来。

当c对象创建好后,我们就可以直接访问它里边的 ab 属性了,以上就是ES6中类继承的过程。其过程与其它面向对象语言完成一致,所以大家在使用它时会觉得非常自然。

小结

以上我对JS中 ES5 和 ES6 标准中的做的一些浅显的探究,在ES5 中类是通过function创建了,由于JS最开始并不支持面向对象开发,所以在ES5中使用JS实现面向对象开发的方式让人觉得很诡异。我在理解这部分知识时,也颇费了一翻周折。不过如果你对内存管理比较熟悉的话,从内存管理的角度去理解 ES5 中的类与继承就比较容易了。

对于 ES6 来说,类的定义与类的继承几乎完全照搬了 Java 的语法,所以我们在学习和使用它时就非常方便了。

参考

阮一峰

欢迎关注我的其它发布渠道