一. 面向对象
1. 什么是类?
类是对一类具有共同特征的事物的抽象,是对象的模板。类的内部封装了属性和方法,用于操作自身的成员。类是对某种对象的定义,具有行为,它描述一个对象能够做什么以及做的方法,它们是可以对这个对象进行操作的程序和过程。它包含有关对象行为方式的状态,包括它的名称、属性、方法和事件。
2. 什么是对象?
- 对象对于单个物体的简单抽象
- 对象是一个容器,封装了属性(对象的特征/状态)和方法(对象的行为)
3. 为什么要面向对象?
- 面向过程:是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用,即按照步骤编程 (函数和变量)。
- 面向对象:面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。将需求分析出一个一个的对象,然后在分析出对象中的属性和方法,最后按照步骤编程(方法和属性)。特点:逻辑迁移更加灵活、代码复用性高、高度模块化的体现
- 面向过程和面向对象对比
面向过程 | 面向对象 | |
---|---|---|
优点 | 性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。 | 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 |
缺点 | 不易维护、不易复用、不易扩展 | 性能比面向过程低 |
二. 创建对象
1. 对象字面量
使用花括号{ }包含表达这个对象的属性和方法。{ }里面采用键值对的方式表示。
缺点:创建具有同样接口的多个对象需要重复编写很多代码。且创建出来的对象是开放性的,无法管控,外部可以随意更改。
// 简单对象 - 本身开放, 外部可以随意更改
var person = {
name: '老王',
age: 30,
sayName: function() {
console.log(this.name);
}
}
2. 工厂模式
工厂模式是设计模式的一种,是一种用于抽象创建特定对象的过程
缺点:可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
function crearePerson(name, age) { // 创建对象的工厂
let o = new Object(); // 借助Object创建一个对象
o.name = name;
o.age = age;
o.sayName = function () {
console.log(this.name);
}
return o; // 返回创建完的对象
}
let person1 = crearePerson('老王', 30);
let person2 = crearePerson('小红', 20);
person1.sayName(); // 老王
person2.sayName(); // 小红
3. 构造函数 / new
是一种特殊的函数,主要用来初始化对象,为对象成员变量赋初始值。
构造函数可以是函数表达式,也可以是函数声明。
注:任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
- 构造函数名称首字母要大写
- 与new操作符组合使用创建实例
- 相比于工厂模式,构造函数可以确保实例被标识为特定类型,可以使用 instanceof 操作符确定对象类型
function Person(name, age) { // 与工厂模式对比,不显式创建对象
this.name = name; // 属性和方法直接赋值给this
this.age = age;
this.sayName = function () {
console.log(this.name);
}
// 没有 return 语句
}
const person = new Person('老王', 30);
- new 操作符干了什么事情?
- 结构上:在内存中创建一个新对象。
- 属性上:新对象的原型([[Prototype]]特性)指向构造函数的 prototype。
- 关系上:构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 生命周期上:执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
- 构造函数的缺点
构造函数定义的方法会在每个实例上都创建一遍,即使将共同的方法提取出来,可以解决相同逻辑的函数重复定义的问题,但全局作用域也因此变得混乱且会造成资源浪费,因为如果对象中需要多个方法,就要在全局作用域中定义多个函数。
function Person(name, age) {
this.name = name;
this.age = age;
// 每个实例的sayName方法逻辑等价,但不同实例上的sayName函数同名不相等
this.sayName = function () {
console.log(this.name);
}
}
// 优化: 利用this对象,可以把函数与对象的绑定推迟到运行时。
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() { // 引起全局作用域混乱和资源浪费
console.log(this.name);
}
4. 原型模式
每个函数都有一个 prototype 属性,这个属性是一个对象(原型对象)。这个对象就是通过调用构造函数创建的对象的原型。
每个原型对象(prototype)都有一个 constructor 属性,指回它的构造函数。
每个对象(函数也是对象)都有一个 _proto_属性,这个属性是一个对象,指向它的构造函数的原型。
原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
function Person() {}
Person.prototype.name = '老王'; // 所有实例共享的属性和方法
Person.prototype.age = 30;
Person.prototype.sayName = function() {
console.log(this.name);
}
var dan = new Person('丹丹', 24); // 传参没用的
5. Object.create()
Object.create() 方法,它创建一个对象,其中第一个参数就是这个对象的原型,Object.create()提供第二个可选参数,用以对对象的属性进行进一步描述。
var p1 = Object.create(null) // 相当于空对象,任何属性都没有
var p2 = Object.create(Person.prototype) // 相当于var p2 = {}
var p3 = Object.create({ name: '丹丹' }) // 相当于var p3 = {}; p3.prototype = { name: '丹丹' }
三. 原型和原型链
1. 构造函数,原型对象和实例
- 无论何时,只要创建一个函数,就会为这个函数创建一个 prototype 属性,指向这个函数的原型对象。因此定义构造函数的时候(构造函数可以是函数表达式,也可以是函数声明),构造函数就自动有了一个与之关联的原型对象。
- 所有的原型对象都自动获得一个 constructor 属性,指回与之关联的构造函数。
从上面的代码段不难看出,构造函数和原型对象通过 prototype 和 constructor 实现了循环引用,如下图所示function Person() {} // 函数声明构造函数 let Person = function() {}; // 函数表达式构造函数 // 构造函数 var person = new Person(); // 构造函数的原型对象 typeof Person.prototype; // 'object' // 原型对象的 constructor 属性,指回构造函数 Person.prototype; // { constructor: Person() }
- 每个通过构造函数创建的新实例内部的 [[Prototype]] 指针指向了其构造函数的原型对象(注意,不是prototype属性,实例的 prototype 属性为 undefined)。[[Prototype]] 指针对对象来说是隐藏的,但是在Firefox、Safari 和 Chrome中会暴露__proto__属性,通过这个属性可以访问到这个对象的原型。
- 实例通过__proto__ ([[Prototype]]特性)链接到原型对象,构造函数通过 prototype 属性链接到原型对象。实例与构造函数没有直接关联,与原型对象有直接关联。
如下图,① 的关系实际上是通过 ② 建立起来的
![](https://img-blog.csdnimg.cn/1534c2ae65504e9781184ccf9b9120e7.png)
由构造函数创建一个实例为例,每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。根据前文说明,对原型和原型链的关系绘制如下图:
function Person() {} // 相当于 var Person = new Function()
var person = new Person();
![](https://img-blog.csdnimg.cn/ad49bbb8e66e4fd1ba38a7942a9523e3.png)
function Person() {
this.name = '丹丹';
}
var person = new Person();
// 蓝色组 5
1. person.__proto__ === Person.prototype; // true
2. person.__proto__.__proto__ === Object.prototype; // true
3. person.__proto__.__proto__.__proto__ === null; // true
4. Person.prototype.constructor === Person; // true
5. Object.prototype.constructor === Object; // true
// 橙色组 4
6. Person.constructor === Function; // true
7. Object.constructor === Function; // true
8. Person.__proto__ === Function.prototype; // true
9. Object.__proto__ === Function.prototype; // true
// 紫色组 2
10. Function.prototype.constructor === Function; // true
11. Function.prototype.__proto__ === Object.prototype // true
2. 原型链
当一个对象实例的原型指向了另一个对象的实例,在实例和原型之间就构造成了一条原型链。
function Parent() {
this.parentMoney = 5000;
}
Parent.prototype.getParentMoney = function() {
return this.parentMoney;
}
function Child () {
this.childMoney = 5;
}
Child.prototype = new Parent(); // 原型指向了另一个对象的实例,实现了继承
Child.prototype.getChildMoney = function() {
return this.childMoney;
}
var peron = new Child();
console.log(peron.getParentMoney()); // 5000
console.log(peron.getChildMoney()); // 5
在读取实例上的属性时,首先会在实例上搜索这个属性,如果没找到,就会去这个实例的原型上找。再通过原型链实现继承后,搜索就可以继承向上,搜索原型的原型,这样层层向上就形成了一个链式结构,这就是原型链。
原型搜索机制
- 以搜索 getParentMoney() 方法为例
- 首先搜索
person
实例本身有没有这个方法,没有。 - 通过
__proto__
访问 person 实例的原型Child.prototype
,搜索原型上有没有这个方法,没有。 - person 的原型
Child.prototype
指向了 Parent 的实例new Parent()
,实现了继承。因此搜索可以继续向上。 - 搜索 Parent 的实例
new Parent()
上有没有这个方法,没有。 - 通过
__proto__
访问 new Parent() 这个实例的原型Parent.prototype
,搜索原型上有没有这个方法,找到啦。
- 首先搜索
搜索 getChildMoney() 方法的机制也是一样的道理,不过getChildMoney() 方法在 person 实例的原型上就找到了。
![](https://img-blog.csdnimg.cn/7d7c4f13777f40a28958ae87272e5e02.png)
原型链存在的问题
- 原型中包含的引用值会在所有实例之间共享,会导致意想不到的错误。且父类的属性一旦赋值给子类的原型,这些属性就属于子类共享属性了 (继承者的实例间篡改)
- 子类型在实例化时不能给父类型的构造函数传参。
四. 继承
为了解决原型链继承包含引用值导致的共享属性问题,不同的继承方法被提出和改进。
1. 盗用构造函数继承 / 经典继承 / 对象伪装
- 在子类的构造函数中调用父类的构造方法。
- 使用
apply()
或call()
以新创建的对象为上下文执行构造函数。
优点
- 可以在子类构造函数中向父类构造函数传参。
- 解决了原型的引用值属性的共享问题。
缺点
- 必须在构造函数中定义方法,否则函数不能被重用。
- 子类无法访问父类原型中的方法 ,如下面挂载在 Parent 原型上的 getName()。
function Parent(name, age) {
this.name = name;
this.age = age;
this.sex = '男';
}
Parent.prototype.getName = function () { // 这个函数无法被子类访问到
return this.name;
}
function Child(name, age) {
Parent.call(this, name, age); // 继承 Parent 并传参
this.friends = ['Mark']; // 自己的实例属性
}
var person = new Child('小明', 10); // { "name": "小明", "age": 10, "sex": "男" , friends: ['Mark']}
2. 组合继承 / 伪经典继承
组合继承综合了原型链继承和盗用构造函数继承的优点。
- 使用原型链继承原型上的属性和方法
- 使用盗用构造函数继承实例的属性。
这样既可以把公用方法挂载在原型上以实现重用,又可以让每个实例都有自己的属性。
优点
弥补了原型链和盗用构造函数的不足,并且保存了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。
缺点
为了实现原型链继承和盗用构造函数的继承,父类的构造函数会被执行两次,一次是在创建子类的时候,另一次是在子类构造函数中调用,造成资源的浪费。
function Parent(name, age) {
this.name = name;
this.age = age;
this.sex = '男';
}
// 挂载在父类原型上的公用方法
Parent.prototype.getName = function () {
return this.name;
}
function Child(name, age, frineds) {
Parent.call(this, name, age); // 继承属性, 执行了第二次 Parent 构造方法
this.friends = frineds; // 自定义属性
}
Child.prototype = new Parent(); // 实现继承, 执行了第一次 Parent 构造方法, 非必要
Child.prototype.constructor = Child; // constructor 丢失,指回构造函数
Child.prototype.getFriends = function() { // 挂载在子类原型上的公用方法
return this.friends;
}
var person1 = new Child('小明', 10, ['Mark']);
var person2 = new Child('小强', 8, ['Lisa'])
// 调用父类原型上的方法
console.log(person1.getName()); // 小明
// 调用子类原型上的方法
console.log(person1.getFriends()); // ['Mark']
person1.friends.push('Tom'); // 实例属性互不影响
console.log(person1.friends); // ['Mark', 'Tom']
console.log(person2.friends); // ['Lisa']
console.log(person1 instanceof Parent); // true
console.log(person1 instanceof Child); // true
3. 寄生组合继承
- 不直接通过调用父类构造函数给子类原型赋值,而是通过
Object.create()
取得父类原型的副本。 - 是引用类型继承的最佳模式。
function Parent(name, age) {
this.name = name;
this.age = age;
this.sex = '男';
}
Parent.prototype.getName = function () { // 挂载在父类原型上的公用方法
return this.name;
}
function Child(name, age, frineds) {
Parent.call(this, name, age); // 继承属性
this.friends = frineds;
}
// 使用 Object.create 继承 Parent
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child; // constructor 丢失,指回构造函数
Child.prototype.getFriends = function() { // 挂载在子类原型上的公用方法
return this.friends;
}
var person = new Child('小明', 10, ['Mark']);
怎么实现多重继承?
- 使用
Object.create()
实现单个继承 - 使用
Object.assign()
实现多重继承
function Father(name) {
this.eyeColor = 'blue'; // 蓝眼睛的爸爸
this.name = name;
}
Father.prototype.getEyeColor = function () {
return this.eyeColor;
}
function Mother(age) {
this.blood = 'A'; // A 血型的妈妈
this.age = age;
}
Mother.prototype.getAge = function () {
return this.age;
}
function Child(name, age) {
Father.call(this, name); // 继承爸爸的属性
Mother.call(this, age); // 继承妈妈的属性
}
// 继承爸爸的原型方法
Child.prototype = Object.create(Father.prototype);
// 继承妈妈的原型方法
Object.assign(Child.prototype, Mother.prototype);
// 原型的consturctor丢失,指回自己的构造函数
Child.prototype.constructor = Child;
var person = new Child('小明', 15);
console.log(person); // { "eyeColor": "blue", "name": "小明", "blood": "A", "age": 15 }
console.log(person.getEyeColor()); // 'blue'
console.log(person.getAge()); // 15