面向对象
1. 什么是对象?
Everything is object (万物皆对象)。
- 对象是单个事物的抽象。
- 对象是一个容器,封装了属性(property)和方法(method)
- 注:属性——对象的状态;方法——对象的行为
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。
2. 什么是面向对象?
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
3. 面向对象编程介绍
(1)两大编程思想
- 面向过程
- 面向对象
(2)面向过程编程 POP(Process-oriented Programming)
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了
- 面向过程,就是按照我们分析好了的步骤,按照步骤解决问题
- 例如:将大象装进冰箱(分三步,打开冰箱——大象装进去——关闭冰箱)
(3)面向对象编程 OOP(Object Oriented Programming)
- 面向对象是把事物分解成为一个一个对象,然后由对象之间分工与合作
- 面向对象是以对象功能来划分问题,而不是步骤
- 例如:将大象装进冰箱,先找出对象,并写出这些对象的功能,然后使用大象和冰箱的功能(大象对象——进去,冰箱对象——打开、关闭)
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工
面向对象编程具有灵活,代码可复用,容易维护和开发的优点,更适合多人合作的大型软件项目
面向对象的特性:封装性、继承性、[多态性]抽象
(4)面向过程和面向对象的对比(根据不同的程序需要来选择)
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向过程:
- 优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如:单片机
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象:
- 优点:易维护,易复用,易扩展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活,更加易于维护
- 缺点:性能比面向过程低
比较简单的一些步骤明确的推荐面向过程;比较大、多人合作推荐面向对象
4. 总结
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目
案例:处理学生的成绩表,打印输出学生成绩
// 面向过程
// 定义学生的对象
var std1 = { name: "Bob", score: 89 };
var std2 = { name: "Mike", score: 98 };
// 封装一个打印学生成绩的函数
function printScroe(student) {
console.log("姓名:" + student.name + "成绩:" + student.score);
}
// 具体调用函数打印出对应成绩
printScroe(std1);
printScroe(std2);
// 面向对象
// 首先考虑的不是整个流程,而是将一个学生当成一个对象,对象有两个属性存储姓名和成绩,并且对象自己有打印成绩的功能,将所有跟学生有关的属性和行为都封装到对象身上
// 在这个过程,我们已知会有多个类似的对象,可以利用构造函数的方法先进行封装,然后创建单独的对象
// 抽象所有的数据行为成一个模板(Class)
function Student(name, score) {
this.name = name;
this.score = score;
this.printScroe = function () {
console.log("姓名:" + this.name + "成绩:" + this.score);
};
}
// 根据模板创建具体的实例对象(Instance)
var std1 = new Student("BOb", 90);
var std2 = new Student("Mike", 98);
// 调用 实例对象自己的方法
std1.printScroe();
std2.printScroe();
5. 面向对象的设计思想
- 抽象出 Class(构造函数)
- 根据 Class(构造函数)创建 Instance(实例)
- 指挥 Instance 得结果
创建对象的几种方式
1. 创建对象方式
(1)new Object() 构造函数
// new Object()
var person = new Object();
person.name = "BOb";
person.age = 18;
person.sayName = function () {
console.log(this.name);
};
person.sayName();
(2)对象字面量 {}
var person1 = {
name: "Bob",
age: 18,
sayName: function () {
console.log(this.name);
},
};
person.sayName();
(3)工厂函数
function createPerson(name, age) {
// 添加一个新对象
var person = new Object();
person.name = name;
person.age = age;
person.sayName = function () {
console.log(this.name);
};
// 必须有返回值
return person;
}
// 生成真正的对象
var person1 = createPerson("Bob", 17);
var person2 = createPerson("Mike", 20);
person1.sayName();
person2.sayName();
function createPerson(name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name);
},
};
}
// 生成真正的对象
var person1 = createPerson("Bob", 17);
person1.sayName();
var arr = [1, 3];
console.log(arr instanceof Array); // true
// createPerson 不是一个构造函数,它只是进行了一个封装
console.log(person1 instanceof createPerson); // false
console.log(person1 instanceof Object); // true
(4)自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
// 生成对象实例
// new 关键字的用途
// 1.创建一个新对象
// 2.将函数内部的 this 指向了这个新对象
// 3.执行构造函数内部的代码
// 4.将新对象作为返回值
var person1 = new Person("Bob", 18);
person1.sayName();
// 演示 new 的功能
function Person(name, age) {
// var instance = new Object();
// this = instance;
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
// return instance;
}
// 通过构造函数生成的实例是可以找到自己当初的构造函数的
var arr = new Array(1, 2);
// constructor 属性,构造器、构造函数的意思
// 每个对象的 constrictor 属性值就是生成这个对象的构造函数
console.log(arr.constructor);
console.log(person1.constructor);
// 判断一个对象的具体对象类型,需要使用 instanceof 进行判断
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Array); // false
console.log(person1 instanceof Object); // true
2. 构造函数和实例对象的关系
(1)构造函数是根据具体的事物抽象出来的抽象模板
(2)实例对象是根据抽象的构造函数模板得到的具体实例对象
(3)每一个实例对象都通过一个 constructor 属性,指向创建该实例的构造函数
- 注意:constructor 是实例的属性的说法不严谨,具体后面的原型会讲到
(4)可以通过 constructor 属性判断实例和构造函数之间的关系
- 注意:这种方式不严谨,推荐使用 instanceof 操作符,后面学原型会解释为什么
3. 静态成员和实例成员
(1)使用构造函数方法创建对象时,可以给构造函数和创建的实例对象添加属性和方法,这些属性和方法都叫做成员。
(2)实例成员:在构造函数内部添加给 this 的成员,属于实例对象的成员,在创建实例对象后必须由对象调用。
(3)静态成员:添加给构造函数自身的成员,只能使用构造函数调用,不能使用生成的实例对象调用。
function Person(name, age) {
// 实例成员,通过将来生成的实例对象进行调用的成员
// 创建时,是直接添加给函数内部的 this
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
// 静态成员:直接给构造函数添加的成员
Person.version = "1.0";
// 生成对象实例
var person1 = new Person("Bob", 18);
// 调用实例成员
console.log(person1.name); // Bob
// 使用构造函数调用实例成员会出错
console.log(Person.name); // Person
console.log(Person.version); // 1.0
Person.sayName(); // 报错
// 调用静态成员,只能通过构造函数进行调用
console.log(Person.version); // 1.0
console.log(person1.version); // undefined
4. 构造函数的问题
浪费内存
// 自定义构造函数
// 在生成中,每创建一个新的对象,重复的代码都要进行添加
function Person(name, age) {
this.name = name;
this.age = age;
// this 内部的 type 属性值是不变的
this.type = "human";
// 每个对象的 sayName 方法也是一样的
this.sayName = function () {
console.log(this.name);
};
}
原型
使用原型对象可以更好的解决构造函数的内存浪费问题。
1. prototype 原型对象
- 任何函数都具有一个 prototype 属性,该属性是一个对象。
- 可以在原型对象上添加属性和方法。
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 _proto_。
- 实例对象可以直接访问原型对象成员。
2. 构造函数、实例、原型对象三者之间的关系
Person 构造函数通过 new 方法生成一个实例对象 p1,p1通过_proto_ 属性能找到 原型对象
因为_proto_ 是一个非正规的属性,真正使用时会省略不写,会直接打点滴用原型对象上的属性和方法
3. 解决构造函数内存浪费问题的方法
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向构造函数的原型对象。
- 这个原型对象的所有属性和方法,都会被构造函数的实例对象所拥有。
- 因此,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
- 从而解决内存浪费问题
// 自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 更优的解决方法:将所有实例共享的属性和方法,都添加给原型对象
Person.prototype.type = "human";
Person.prototype.sayName = function () {
// 方法调用时,哪个对象调用,this 指向的就是谁
console.log(this.name);
};
Person.prototype.sayAge = function () {
// 方法调用时,哪个对象调用,this 指向的就是谁
console.log(this.age);
};
// 生成对象实例
var person1 = new Person("Bob", 18);
var person2 = new Person("Mike", 20);
// 调用原型对象上公用的属性和方法
person1.sayAge();
console.log(person1.sayAge === person2.sayAge); // true
原型链
1. 原型链
所以实例对象可以调用构造函数的 prototype 原型对象的属性和方法
2. 原型链查找机制
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性:
(1)搜索首先从对象实例本身开始
(2)如果在实例中找到了具有给定名字的属性,则返回该属性的值
(3)如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
(4)如果在原型对象中找到了这个属性,则返回该属性的值
(5)找到最后没找到,则返回 null
实例对象读写原型对象成员
1. 读取
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回 undefined
2. 值类型和引用类型写入
(1)值类型成员写入(实例对象.值类型成员 = xx):
- 当实例期望重写原型对象中的某个普通数据成员时,实际上会把该成员添加到自己身上
- 也就是说该行为实际上会屏蔽掉对原型对象成员的访问
- 即把该成员添加到自己身上 ,原型对象的值不会改变
(2)引用类型成员写入(实例对象.引用类型成员 = xx): 同上
3. 复杂类型成员修改
复杂类型成员修改(实例对象.成员.xx = xx):
- 同样会先在自己身上找该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)
4. 总结:
- 通过实例对象添加新成员,会直接添加给自己,会屏蔽掉对原型对象的访问
- 如果通过实例对象更改原型对象的属性和方法,会直接添加给自己,会屏蔽掉对原型对象的访问
- 通过实例对象更改原型对象中复杂类型数据中的内容,还是会进行原型链的查找
更简单的原型语法
1. 更简单的原型语法
前面在原型对象每添加一个属性和方法就要书写一遍 Person.prototype 。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,将 Person.prototype 重置到一个新的对象。
注意:原型对象会丢失 constructor 成员,所以需要手动将 constructor 指向正确的构造函数
// 自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 直接使用一个对象字面量对原型对象进行赋值
Person.prototype = {
constructor: Person, // 需要手动将 constructor 属性指向正确的构造函数
type: "human",
sayName: function () {
console.log(this.name);
}
};
2. 原型对象使用建议
在定义构造函数时,可以根据成员的功能不同,分别进行设置:
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototype 记得修正 constructor 的指向
原生构造函数的原型对象
1. JS 原生构造函数的原型对象
所有函数都有 prototype 属性对象。
JavaScript中的内置构造函数也有 prototype 原型对象属性:
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- …
2. 练习
为数组对象扩展原型方法
// 直接给原型对象添加一条新的属性
// 虽然这种方法可以给内置的原型对象添加新属性成功,但是不允许更改内置的原型对象
Array.prototype.getEvenSum = function () {
// 获取数组中每一项的方式
// this[i]
var sum = 0;
for (var i = 0; i < this.length; i++) {
if (i % 2 === 0) {
sum += this[i];
}
}
return sum;
};
内置构造函数也有 prototype 原型对象属性:
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- …
2. 练习
为数组对象扩展原型方法
// 直接给原型对象添加一条新的属性
// 虽然这种方法可以给内置的原型对象添加新属性成功,但是不允许更改内置的原型对象
Array.prototype.getEvenSum = function () {
// 获取数组中每一项的方式
// this[i]
var sum = 0;
for (var i = 0; i < this.length; i++) {
if (i % 2 === 0) {
sum += this[i];
}
}
return sum;
};