JS类的本质

975 阅读4分钟

类的三大特性

  • 封装
  • 继承
  • 多态

一般来说,面向对象语言都有类的概念,如Java、C++,JavaScript在ES6之前没有类的概念,是基于原型,虽然ES6新增了类,其本质也没有发生变化。

其实,JavaScript语言不能算真正的面向对象语言,对象仅仅只是它的一方面,还有更多其他的与之相比同样重要的数据类型。

封装继承从字面意思很容易理解,多态指的是父类定义了某个行为,而继承的子类可以重写父类行为方法。

类的机制

创造

我们先用一个通俗易懂的例子来理解下类和它的实例化创造。

以建造房子为例:在盖房子之前,建筑师一般会设计出一张图纸,图纸上房子的大体结构是必须要的,如客厅、主卧、次卧、厨房等的面积大小,主要功能的尺寸等都需要考虑进去,如果图纸是一个类,那设计的所有元素都是类的属性或方法。

图纸设计完成,相当于类定义好了之后,相关的属性方法都封装好了;然后开始实施,这也是类实例化的过程,理论上,我们可以按图纸重复建造,这就是类的继承;我们也可能会修改或完成图纸上的某些抽象定义,如客厅的家具如何摆放,不一定相同,这也是类的多态特性。

构造函数

类实例化对象就是通过类的构造函数来生成的。构造函数和类同名,并且名称一般都是大写开头,用于区别普通函数。它的任务就是初始化实例中所能获取到的信息。

思考如下类的伪代码:

class CoolBoy {
  skill = nothing
  CoolBoy (skill) {
    skill = skill
  }
  show () {
    output("Here's my skill: " + skill)
  }
}
const jay = new CoolBoy('play basketball')
jay.show() // 这是我的绝技:打篮球

在这里,类CoolBoy中存在一个函数CoolBoy,执行new CoolBoy('play basketball')的时候,实际上调用的就是它。构造函数会返回一个对象,这里赋值给变量jay

这里只是伪代码,模拟的是类底层的实现方式,在开发中类的构造函数,是通过constructor来替换,而不会在类中写重复的构造函数。

类的本质

受面向对象主流思想的影响,在我们使用JavaScript语言时,总会问:JavaScript语言到底是不是面向对象的语言?

如果是,那在最初好像没有类,或者类似类的概念啊;如果不是,那该如何改进呢?

所以,多年以来,在JavaScript中有一种奇怪的行为被不断的滥用,那就是模仿类,无论如何,都要弄出来类一样的东西,直到ES6,类应运而生。

思考如下代码:

function Foo (a) {
  this.a = a
}
const bar = new Foo(100)

我们再来回顾下执行new Foo(),我们实际做了些什么?

new Foo()

new关键字后面跟着Foo构造函数,本质上也是个普通函数,只是首字母大写用于语法的区分。其实就是调用构造函数Foo。

在这个过程中:

第一步:会创建一个全新的对象,当然不一定就会返回给bar变量,只有当函数没有其他返回对象的时候,new表达式调用的函数才会自动返回新创建的对象;

接着,这个对象会被执行[[Prototype]]连接,绑定到构造函数的prototype属性Foo.prototype,这就相当于每次创建的新对象的[[Prototype]]都指向了同一个位置,彼此间存在着关联。

我们先验证下:

function Foo () {
}
const a = new Foo()
const b = new Foo()
console.log(Object.getPrototypeOf(a) === Foo.prototype) // true
console.log(Object.getPrototypeOf(b) === Foo.prototype) // true

可以看出,结论正确。这样就与主流语言真正的类完全不一样,它们能进行物理上的复制,类和实例间能保持彼此间独立。

需要注意的是:new Foo()时调用的函数Foo实际上并没有创建关联,这个关联只是一个意外的副作用。

那我们不妨再思考下,JavaScript难道就没有能力把类实例化生成一个完全全新的对象吗?

这恐怕与最初设计时就一直用的原型继承思想有关。

再稍微解释下,这里仅仅用的是构造函数,只是更直白一点,用class关键字是一样的,因为new + class的话,执行new表达式本质上就是执行构造函数。