概念

在javaScript中我们我们有两大编程思想,面向过程(POP)和 面向对象(OOP);

  • 面向过程:关心的是代码每一步的的实现,先去做什么然后再做什么

  • 面向对象:不管新具体的代码实现细节,只关心结果;好处:减少代码冗余;便于后期维护;

面向对象的特征

  • 继承 ->子类继承了父类的属性方法
  • 封装-> 将实现同样的代码段,放到一个函数中,可以重复使用,不需要关系代码实现的细节,实现代码的高内聚,低耦合
  • 多态

类和对象的关系

  • 类是对象的抽象,对象是类的具体
  • 类是对象的模板,对象是类的产品

如何创建类

使用class关键字,class后面跟上类名,类名的命名使用帕斯卡命名法(首字母大写)

如何实例化一个类

实例化一个类就是创建一个实例(对象),使用关键字new,后面跟上类名和实参列表,返回值是一个实例(对象)

构造器

  • 构造器就是类中自带的一个方法,可以理解为前缀方法
  • 当我们创建一个实例(对象)的同时,他会被自动调用,即出现new的时候,构造器自动执行;
  • 每个类都有构造器,即使我们不定义构造器也存在,为空
  • 构造器的方法是固定的,为constructor
  • 因为class类没有传形参的地方,所以需要constructor()来传递形参,来实现动态类;

简单demo

1
2
3
4
5
6
7
class Person{
// 创建一个构造器
constructor(){
console.log("构造器执行了") //构造器执行了
}
}
var person1 = new Person();

创建一个动态类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
// 创建一个构造器
constructor(name,age){
this.name=name;
this.age=age;
}
// 定义成员方法
say(){
console.log(`name is ${this.name},age is ${this.age}`) //name is 张三,age is 20
}
}
var person1 = new Person("张三",20);
person1.say();

继承

  • 一个类中存在另一个类的属性或方法,就是继承;
  • 继承使用关键字extends;
  • 继承的类称为子类,也叫派生类;
  • 被继承的类称为父类,也叫基类;

demo1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 父类
class Worker {
constructor(name, money) {
this.name = name;
this.money = money;
}
// 定义成员方法
kq() {
console.log(this.name + "的考勤")
};
jx() {
console.log(this.name + "的绩效" + this.money)
}
}

// 子类
class Boss extends Worker {
skq() {
console.log(this.name + "正在审核考勤")
}
}

var worker = new Worker("张三", 10000);
worker.kq(); //张三的考勤
worker.jx(); //张三的绩效10000

var boss = new Boss("李四", 15000);
boss.kq(); //李四的考勤
boss.jx(); //李四的绩效15000
boss.skq() //李四正在审核考勤

demo2:

有时候父类定义的方法不一定能满足子类的需求,可以通过在子类中重写该方法,会直接覆盖父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父类
class Worker {
constructor(name, money) {
this.name = name;
this.money = money;
}
// 定义成员方法
kq() {
console.log(this.name + "的考勤")
};
}

// 子类
class Boss extends Worker {
kq() {
console.log(this.name + "的考勤跟你不一样,俺的是年度的~~~")
}
}

var worker = new Worker("张三", 10000);
worker.kq(); //张三的考勤

var boss = new Boss("李四", 15000);
boss.kq(); //李四的考勤跟你不一样,俺的是年度的~~~

继承构造器

  • 我们可以通过刚才的方法来重写一个方法,但如果父类的构造器无法满足子类的需求,得需要继承构造器;
  • 关键字super;
  • 我们需要在子类中重新定义一个构造器,并继承父类所有参数,然后在子类的构造器中,使用super继承父类的构造器;
  • 其实就是子类用到的动态变量,父类没有,然后需要在子类中传形参;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 父类
class Worker {
constructor(name, money) {
this.name = name;
this.money = money;
}
// 定义成员方法
kq() {
console.log(this.name + "的工资为" + this.money)
};
}

// 子类
class Boss extends Worker {
// 1.子类中重新定义构造器 并继承父类所有参数
constructor(name, money, sex) {
// 2.在子类的构造器中,使用super继承父类的构造器
super(name, money);
this.sex = sex;
}
kq() {
console.log(this.name + "的工资为" + this.money+"性别为"+this.sex)
};
}

var worker = new Worker("张三", 10000);
worker.kq(); //张三的工资为10000

var boss = new Boss("李四", 15000,1);
boss.kq(); //李四的工资为15000性别为1

创建对象的方式

字面量方式

  • 优点:可以对命名空间进行划分,防止属性或方法冲突 (多个name也能区分开,因为属于对象私有属性或方法)
  • 缺点:属于手工作业模式,不能实现批量生产 (相同功能的代码冗余)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 优点:可以对命名空间进行划分,防止属性或方法冲突  (多个name也能区分开,因为属于对象私有属性或方法)
// 缺点:属于手工作业模式,不能实现批量生产 (相同功能的代码冗余)
var obj1 = {
name: "哈哈",
fn: function () {
console.log("obj1");
}
};
var obj2 = {
name: "呵呵",
fn: function () {
console.log("obj2");
}
};
console.log(obj1.name, obj2.name);
obj1.fn();
obj2.fn();

工厂函数模式

我们可以封装一个函数,这个函数用于帮助我们创建一个对象,我们只需要重复调用这个函数即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createPerson(name, age, height) {
//创建个空对象
const person = {};
person.name = name;
person.age = age;
person.height = height;
person.eat = function () {
console.log(this.name + "is eating");
}
//将对象作为函数的返回值返回
return person;
}
const p1 = createPerson("张三", 18, 188);
const p2 = createPerson("李四", 20, 177);
const p3 = createPerson("王五", 24, 178);

构造函数模式

ES6之前,我们都是通过function来声明一个构造函数(类)的,之后通过new关键字来对其进行调用;(如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;其实为了区别开,声明的时候首字母建议大写);

ES6之后,JavaScript可以像别的语言一样,通过class来声明一个类;

  • 优点:可以实现命名空间的划分,防止属性或方法冲突,可以实现批量生产,可以实例识别
  • 缺点:不能实现公共的属性或方法公有,都是自己私有的

new的五步骤:

其实这个结合工厂函数(普通函数共同属性封装)更好理解。构造函数调用封装的函数,他既没有声明一个新对象,也没将函数内部的变量return出去,而且构造函数内部使用的是this与对象的属性产生关联;所以实例化对象的时候,按道理是报错的,所以构造函数创建新对象,返回新对象,内部使用的this与创建的新对象关系绑定等操作,需要一个东西来完成,这个东西就是在调用构造函数前,使用的new关键字来完成的。

  1. 创建一个新的空对象(空实例)(obj)
  2. 将构造函数的显式原型赋值给这个新对象,作为新对象的隐式原型
  3. 构造函数内部的this,会指向创建出来的新对象(this指向第一步的空对象)
  4. 执行函数体的代码块
  5. 返回对象

简单实现一下new

1
2
3
4
5
6
7
8
9
10
function myNew(fn,...args){
//第一步:创建一个空对象
const obj={};
//第二步:将构造函数的显式原型赋值给这个新对象,作为新对象的隐式原型
obj.__proto__=fn.prototype;
//第三步:this指向obj,并调用构造函数
fn.apply(obj,args);
//第四步:返回对象
return obj;
}

instance of:检测当前这个实例(对象)是否属于某个类

1
2
3
4
//instance of 检测当前这个实例(对象)是否属于某个类
var arr = [1,2,3];
console.log(arr instanceof Array); //true
console.log(arr instanceof String); //false

简单案例:

1
2
3
4
5
6
7
8
9
//类是对象的抽象,对象是类的具体    
//这个类是我们自己创建的(类名须大写,和普通函数区别)
function Person(){
console.log(this); //当前实例 (person1,person2)
};

// new 类(); new了函数(类)就自己执行了,不用再去调用
var person1 = new Person(); //Person {}
var person2 = new Person(); //Person {}

案例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(`名字:${this.name},年龄:${this.age}`)
}
};

// new 类(); new了函数(类)就自己执行了,不用再去调用
var person1 = new Person("张三", 19);
var person2 = new Person("李四", 20);
console.log(person1); //Person {name: '张三', age: 19, say: ƒ}
console.log(person2); //Person {name: '李四', age: 20, say: ƒ}
console.log(person1.say === person2.say); //false 缺点:这个方法是每个函数私有的

原型模式

  • 优点:可以实现命名空间的划分,防止属性或方法冲突,可以实现批量生产,可以实例识别,可以实现公共属性或方法公有;
  • 原型模式的基于构造函数模式的;
  • 原型模式将当前实例(对象)公有的属性或方法写到prototype上;
  • prototype是当前函数(类)的属性;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原型模式的基于构造函数模式的;
// 原型模式将当前实例(对象)公有的属性或方法写到prototype上;
// prototype是当前函数(类)的属性

function Person(name, age) {
// 私有的属性或方法
this.name = name;
this.age = age;
}

// 共有的属性或方法
Person.prototype.say = function () {
console.log(`name:${this.name},age:${this.age}`)
}

Person.prototype.test = "我是测试用的~";

var person1 = new Person("zhangsan", 20);
var person2 = new Person("lisi", 24);
console.log(person1.say === person2.say); //true

详情可参考:

原型与原型链