什么是面向过程

  • 分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。

  • 是一种以过程为中心的编程思想 代码没有任何封装,按照编码的逻辑 自上而下 平铺直叙。

  • 面向过程是一种自顶而下的编程思想,将要实现的功能划分为小的模块,再将小的模块继续细分,当所有模块都写完,功能也就实现。

  • 优点

  • 1.运行效率高,因为与 CPU 的工作方式接近,CPU 就是按照顺序来一步一步执行的。

  • 2.编程效率高,不需要对功能进行复杂的抽象,直接进行划分就好,只不过模块划分的粒度和划分原则需要把控好,这也是架构师的重要职责之一

  • 缺点

  • 1.程序扩展性和灵活性比较差,如果功能有改动,对程序会进行较大的改动,而且由于很多地方会改动,不能快速适应需求的变化

  • 2.数据存在很多工程共享数据,安全性不好

什么是面向对象

  • 是把构成问题的事务分解成各个对象,每个对象都有自己独立的属性和行为, 对象可以将整个问题事务进行分工, 不同的对象做不同的事情。
  • 是一类以对象作为基本程序结构单位的程序设计语言,指用于描述的设计是以对象为核心。

面向对象的编程思想是先根据要实现的功能,抽象出对象,或者说类,然后赋予对象相应的数据和操作,功能的实现依靠对象的方法调用。

  • 优点
  • 1.安全,面向对象的封装特性会将数据进行隐藏,保证数据的安全。
  • 2.扩展性好,需求的更改会体现在某个对象的或某些对象的修改上,因此只需要做局部的修改就好,不会对全局造成影响。
  • 3.复用,代码冗余小。由于继承的特性,代码量得到了很大的缩减,重写的特性又保证了多态,即灵活性
  • 缺点
  • 1.抽象相对复杂,不如直接写功能模块方便

概念

面向对象也即是 OOP,Object Oriented Programming,是计算机的一种编程架构,OOP 的基本原则是计算机是由子程序作用的单个或者多个对象组合而成,包含属性和方法的对象是类的实例,但是 JavaScript 中没有类 class 的概念(但是 es6 中新增的 class 的概念),而是直接使用对象来实现编程。

特征:

  • 封装
  • 继承
  • 多态

封装:

封装的主要目的就是为了隐藏数据信息,包括属性和方法的私有化。封装可以使对象内部的变化对其他对象而言是透明的,对象只对自己的行为负责。对象之间通过暴露 API 接口来进行通信,其他对象和用户不需要关心 API 的实现细节,是对象之间的耦合变松散。(解耦)
公有属性,公有方法:属性在构造函数中声明,方法定义在原型上
私有属性,私有方法 : js 中没有私有属性的概念我们一般使用_约定的形式表达私有概念
静态属性,静态方法:我们一般吧静态属性静态方法添加到类上直接通过类来调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function User(name, age, id) {
// 定义用户信息
var _id = id; // 私有属性
var _sayHi = function(){console.log('hi');} // 私有方法
this.name = name; // 公有属性
this.age = age; // 公有属性
this.getId = function() {return _id;} // 特权方法
this.sayHi = function(){return _sayHi;} // 特权方法
}
User.name = 'User'; // 静态属性
User.sortByAge = function (...arguments){
return arguments.sort((a, b)=>{
return a.age - b.age;
})
} // 静态方法
// 定义用户行为
User.prototype.sayWords = function(words){console.log(words);} // 公有方法

重载:

多态是同一个行为具有多个不同表现形式或形态的能力,简单点就是一个 api 方法有多种用途,类似 jquery 中,不同的方法传递参数个数,参数类型等,都可以实现不同的效果

继承

既然要实现继承,那么首先我们得有一个父类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个动物类
function Animal(name) {
// 属性
this.name = name || "Animal";
// 实例方法
this.sleep = function () {
console.log(this.name + "正在睡觉!");
};
}
// 原型方法
Animal.prototype.eat = function (food) {
console.log(this.name + "正在吃:" + food);
};

原型链继承 (**)

利用 js 原型链的特点将子类的原型等于父类的实例,那么子类在原型链访问过程中就可以访问到父类的属性和方法,问题,多个实例共享一个原型, 如果父类中有引用类型数据的话,多个子类其中一个改变其他的都会改变,因为 js 引用类型的问题会有属性共享的问题
核心: 将父类的实例作为子类的原型

1
2
3
4
5
6
7
8
9
10
11
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = "cat";

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat("fish"));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现

缺点:

  1. 要想为子类新增属性和方法,必须要在 new Animal()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例 1)
  4. 创建子类实例时,无法向父类构造函数传参

构造函数继承

相当于吧父类当成一个函数在子类中运行,并且改变 this 为子类实例,这样就相当于拷贝了父类中构造函数中的属性和方法,实现构造函数继承,但是无法继承原型中的方法和属性

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

1
2
3
4
5
6
7
8
9
10
11
function Cat(name) {
Animal.call(this);
this.name = name || "Tom";
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

  1. 解决了 1 中,子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call 多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

实例继承

核心:为父类实例添加新特性,作为子类实例返回

1
2
3
4
5
6
7
8
9
10
11
12
function Cat(name) {
var instance = new Animal();
instance.name = name || "Tom";
return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

特点:

  1. 不限制调用方式,不管是 new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

拷贝继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Cat(name) {
var animal = new Animal();
for (var p in animal) {
Cat.prototype[p] = animal[p];
}
// 2020年10月10日21点36分:感谢 @baclt 的指出,如下实现修改了原型对象,会导致单个实例修改name,会影响所有实例的name值
// Cat.prototype.name = name || 'Tom'; 错误的语句,下一句为正确的实现
this.name = name || "Tom";
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

  1. 支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性、方法)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用 for in 访问到)
  3. 无法获取父类的静态属性、静态方法

组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Cat(name) {
Animal.call(this);
this.name = name || "Tom";
}
Cat.prototype = new Animal();

//组合继承也是需要修复构造函数指向的。
Cat.prototype.constructor = Cat;

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:

  1. 弥补了方式 2 的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

寄生组合继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Cat(name) {
Animal.call(this);
this.name = name || "Tom";
}
(function (Cat) {
// 创建一个没有实例方法的类
var Super = function () {};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})(Cat);

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

特点:

  1. 堪称完美

缺点:

  1. 实现较为复杂

测试一下

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
31
32
33
34
35
36
37
38
39
40
41
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
//实例引用属性
this.features = [];
}
function Cat(name){
}
Cat.prototype = new Animal();

var tom = new Cat('Tom');
var kissy = new Cat('Kissy');

console.log(tom.name); // "Animal"
console.log(kissy.name); // "Animal"
console.log(tom.features); // []
console.log(kissy.features); // []

tom.name = 'Tom-New Name';
tom.features.push('eat');

//针对父类实例值类型成员的更改,不影响
console.log(tom.name); // "Tom-New Name"
console.log(kissy.name); // "Animal"
//针对父类实例引用类型成员的更改,会通过影响其他子类实例
console.log(tom.features); // ['eat']
console.log(kissy.features); // ['eat']

原因分析:

关键点:属性查找过程

执行tom.features.push,首先找tom对象的实例属性(找不到),
那么去原型对象中找,也就是Animal的实例。发现有,那么就直接在这个对象的
features属性中插入值。
console.log(kissy.features); 的时候。同上,kissy实例上没有,那么去原型上找。
刚好原型上有,就直接返回,但是注意,这个原型对象中features属性值已经变化了。