类的三大特性
- 封装
- 继承
- 多态
一般来说,面向对象语言都有类的概念,如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表达式本质上就是执行构造函数。