面向对象的几个概念
在进入正题前,先了解传统的面向对象编程(例如Java)中常会涉及到的概念,大致可以包括:
- 类:定义对象的特征。它是对象的属性和方法的模板定义。
- 对象(或称实例):类的一个实例。
- 属性:对象的特征,比如颜色、尺寸等。
- 方法:对象的行为,比如行走、说话等。
- 构造函数:对象初始化的瞬间被调用的方法。
- 继承:子类可以继承父类的特征。例如,猫继承了动物的一般特性。
- 封装:一种把数据和相关的方法绑定在一起使用的方法。
- 抽象:结合复杂的继承、方法、属性的对象能够模拟现实的模型。
- 多态:不同的类可以定义相同的方法或属性。
面向对象的三个基本特征是封装、继承、多态,接下来我们就来讲讲JavaScript的中怎么实现这三个基本特征。
JavaScript是一种基于对象(object-based)的语言, 几乎所有的东西都是对象, 但是它又不是一种真正的面向对象语言(OOP), 因为在ECMAScript2015 之前, JavaScript的语法中是没有 类(Class) 的。不过我们可以通过构造函数和原型去模拟它。
封装
根据定义,封装就是把属性和方法封装成一个对象,我们可以通常用构造函数来进行封装。例如:
1 | function Dog(name, color) { |
通过构造函数来定义一个”类”, 然后new一个实例对象出来,表面看没什么问题,但是这有点浪费内存,因为type属性和say()方法是一模一样的内容, 每次实例化都要再生成一遍,浪费内存;
1 | dogA.say === dogB.say // false |
解决办法就是 把这些不变的属性或方法直接定义在构造函数的 prototype 对象上, 然后通过原型链去继承。
1 | function Dog(name, color) { |
继承
现在有一个 “动物” 对象的构造函数
1 | function Animal() { |
还有一个 “狗” 对象构造函数
1 | function Dog(name, color) { |
如何实现 “狗” 继承 “动物”?
构造函数绑定
使用 call 或者 apply 方法, 将父对象的构造函数绑定到子对象上, 单纯地使用构造函数继承会造成内存的浪费。
1 | function Dog(name,color){ |
原型链继承
1 | function Animal() {}; Animal.prototype.species = "动物" Dog.prototype = Animal.prototype; Dog.prototype.constructor = Dog; // 构造函数指向本身, 不用建立新的Animal实例 var dogA = new Dog("朵朵","白色"); console.log(dogA.species); // "动物" |
这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Dog.prototype和Animal.prototype现在指向了同一个对象,那么任何对Dog.prototype的修改,都会反映到Animal.prototype。
组合使用原型链和借用构造函数
1 | // 父类构造函数 |
组合集成避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为了JavaScript中最常用的继承模式。 而且 instanceof 和 isPropertyOf ()也能够用于识别基于组合继承创建的对象。
注意: Student.prototype.constructor = Student; 这行代码避免由Student实例化的对象的 constructor 指向 Person, 造成继承链的混乱,
这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,那么,下一步必然是为新的 prototype 对象加上 constructor 属性,并将这个属性指回原来的构造函数。在支持Object.create()的浏览器中我们可以这样通过原型链去继承
1 | Student.prototype = Object.create(Person.prototype); |
对于不支持Object.create()的,我们可以写这样的
1 | function createObject(proto) { |
多态
就像所有定义在原型属性内部的方法和属性一样,不同的类可以定义具有相同名称的方法;方法是作用于所在的类中。并且这仅在两个类不是父子关系时成立(继承链中,一个类不是继承自其他类)。这个在上面的代码已经有所体现了,就不细说了。
ES6中的面向对象语法
1 | 'use strict'; |
类:class
是JavaScript中现有基于原型的继承的语法糖。ES6中的类并不是一种新的创建对象的方法,只不过是一种“特殊的函数”, 因此也包括类表达式和类声明, 但需要注意的是,与函数声明不同的是,类声明不会被提升。
类构造器:constructor
constructor()方法是有一种特殊的和class一起用于创建和初始化对象的方法。注意,在ES6类中只能有一个名称为constructor的方法, 否则会报错。在constructor()方法中可以调用super关键字调用父类构造器。如果你没有指定一个构造器方法, 类会自动使用一个默认的构造器。
类的静态方法:static
静态方法就是可以直接使用类名调用的方法,而无需对类进行实例化,当然实例化后的类也无法调用静态方法。 静态方法常被用于创建应用的工具函数。
继承父类:extends
extends关键字可以用于继承父类。使用extends可以扩展一个内置的对象(如Date),也可以是自定义对象,或者是null。
关键字:super
super关键字用于调用父对象上的函数。 super.prop和super[expr]表达式在类和对象字面量中的任何方法定义中都有效。
1 | super([arguments]); // 调用父类构造器 |
如果是在类的构造器中,需要在this关键字之前使用。