类的声明和实例化
声明类有两种方法:
// es5构造函数里面的方法和属性
function Animal(name){
this.name = name;
}
// es6 类的构造函数,实例化的时候执行,new的时候执行
class Animal2 {
constructor(name){
this.name = name;
}
}
类的实例化只有一种方式
var a1 = new Animal('shape');
var a2 = new Animal2('cat');
面向对象的三大特征:封装、继承、多态
封装
我们平时所用的方法和类都是一种封装,当我们在项目开发中,遇到一段功能的代码在好多地方重复使用的时候,我们可以把他单独封装成一个功能的方法,这样在我们需要使用的地方直接调用就可以了
封装:封装的优势在于定义只可以在类内部进行对属性的操作,外部无法对这些属性指手画脚,要想修改,也只能通过你定义的封装方法
继承
继承:继承减少了代码的冗余,省略了很多重复代码,开发者可以从父类底层定义所有子类必须有的属性和方法,以达到耦合的目的。
继承有多种实现方式
借助构造函数实现继承
先看代码,Parent1 是父类,Child1 是子类。通过 Parent1.call(this, name)
改变了 this 指向,使子类继承了父类的属性,即 Child1 也有了 name 属性。
/*
构造函数实现继承
*/
function Parent1(name){
this.name = name;
}
function Child1(name, age){
Parent1.call(this, name)
this.age = age
}
var c1 = new Child1('bobo', 19)
console.log(c1)
运行程序,打印结果:
但是, 这种方式不能继承父类原型链上的属性,只能继承在父类显式声明的属性 。
看下面的代码,通过Parent1.prototype.say
给 Parent1 新增一个 say 方法,那么 Child1 能不能也继承呢?
function Parent1(name){
this.name = name;
}
Parent1.prototype.say = function(){
console.log('say hello')
}
function Child1(name, age){
Parent1.call(this, name)
this.age = age
}
var p1 = new Parent1('hehe')
var c1 = new Child1('bobo', 19)
console.log(p1)
console.log(c1)
输出结果如下:
Parent1 中有 say 方法,而 Child1 中没有,说明没有继承到。
借助原型链实现继承
实现原理是将 Child.prototype 赋值为一个新的 Parent 对象,即 Child2.prototype = new Parent2('bob')
function Parent2(name){
this.name = name;
this.arr = [1,2,3]
}
function Child2(age){
this.age = age
}
//重点在这句
Child2.prototype = new Parent2('bob')
c2 = new Child2(20)
c3 = new Child2(22)
console.log('c2', c2)
console.log('c3', c3)
控制台输出如下:
这种方法也有缺点,看 arr 属性是一个数组,如果创建两个实例对象 c2、c3,因为这两个实例对象的 arr 指向同一个引用,所以改变其中一个的值,另一个也会跟着改变。
我们来看下面的实验:c2.arr 和 c3.arr 值都是 [1,2,3],此时通过 c2.arr.push(4)
给 c2.arr 添加一个元素,c2.arr 变成了 [1,2,3,4],这没有问题。但再看 c3.arr ,也是 [1,2,3,4],这就有问题了,不是我们期望的。
组合方式优化
// 组合方式优化
function Parent5(name){
this.name = name
}
function Child5(name, age){
Parent5.call(this, name)
this.age = age
}
Child5.prototype = Object.create(Parent5.prototype)
Child5.prototype.constructor = Child5
var c8 = new Child5()
console.log(c8 instanceof Child5, c8 instanceof Parent5)
console.log(c8.constructor)
** Object.create()
**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。即返回一个带着指定的原型对象和属性的新对象。
Child5.prototype 和 Parent5.prototype 不再使用同一个引用,中间用一个新的对象隔离开了。
此时 Child5 还没有 constructor,所以要手动指定一个: Child5.prototype.constructor = Child5
至此,继承的完美解决方案诞生了。
es6继承的书写方法
class Father {
constructor(name) {
this._name = name;
} //实例方法,通过实例对象调用
getName() {
console.log(this._name);
}
// 静态方法不会被继承,并且是通过类名去调用的 static hitXiaoMing() {
console.log("打小明")
}
}
class Son extends Father {
constructor(name, age) {
//实例化子类的时候把子类的数据传给父类(这里的super必须有,super里的参数是所继承的父类实例化所需要的数据)
super(name);
this._age = age;
} }
var DaMing = new Father('大明');
Father.hitXiaoMing(); //打小明 DaMing.getName(); //大明
var XiaoMing = new Son('小明',15);
XiaoMing.getName(); //小明
特别提醒:继承会继承父类的实例属性和实例方法,并不会继承静态属性和静态方法,并且静态方法只能通过类名去调用。
多态
多态:多态实现了方法的个性化,不同的子类根据具体状况可以实现不同的方法,光有父类定义的方法不够灵活,遇见特殊状况就捉襟见肘了
多态的具体表现为方法重载和方法重写:
- 方法重载:重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数
- 方法重写:重写(也叫覆盖)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样
class Father {
constructor(name) {
this._name = name;
}
//实例方法,通过实例对象调用
getName() {
console.log(this._name);
}
work() {
console.log('我的工作是累死累活,赚钱养家')
}
// 静态方法不会被继承,并且是通过类名去调用的
static hitXiaoMing() {
console.log("打小明")
}
}
class Son extends Father {
constructor(name, age) {
//实例化子类的时候把子类的数据传给父类(这里的super必须有,super里的参数是所继承的父类实例化所需要的数据)
super(name);
this._age = age;
}
// 方法重写
work() {
console.log('我的工作是好好学习,天天向上。')
}
}
var DaMing = new Father('大明');
DaMing.work() // 我的工作是累死累活,赚钱养家。
var XiaoMing = new Son('小明',15);
XiaoMing.work(); // 我的工作是好好学习,天天向上。