09-JavaScript面向对象
2018-11-02 本文已影响1944人
极客江南
面向对象基本概念
面向对象思想
- 面向对象(Object Oriented,OO)是软件开发方法
- 面向对象是一种对现实世界抽象的理解,是计算机编程技术发展到一定阶段后的产物
-
Object Oriented Programming-OOP ——面向对象编程
面向对象和面向过程区别
-
面向对象是相对面向过程而言
-
面向对象和面向过程都是一种思想
-
面向过程
- 强调的是功能行为
- 关注的是解决问题需要哪些步骤
-
回想下前面我们完成一个需求的步骤:
- 首先搞清楚我们要做什么
- 然后分析怎么做
- 最后我用代码体现
- 一步一步去实现,而具体的每一步都需要我们去实现和操作
-
在上面每一个具体步骤中我们都是参与者, 并且需要面对具体的每一个步骤和过程, 这就是面向过程最直接的体现
- 面向对象
- 将功能封装进对象,强调具备了功能的对象
- 关注的是解决问题需要哪些对象
- 当需求单一, 或者简单时, 我们一步一步去操作没问题, 并且效率也挺高。 可随着需求的更改, 功能的增加, 发现需要面对每一个步骤非常麻烦, 这时就开始思索, 能不能把这些步骤和功能再进行封装, 封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰多了, 用的时候, 找到对应的类就可以了, 这就是面向对象思想
- 示例
- 买电脑
-
面向过程
- 了解电脑
- 了解自己的需求
- 对比参数
- 去电脑城
- 砍价,付钱
- 买回电脑
- 被坑
-
面向对象
- 找班长
- 描述需求
- 班长把电脑买回来
-
- 吃饭
-
面向过程
- 去超市卖菜
- 摘菜
- 洗菜
- 切菜
- 炒菜
- 盛菜
- 吃
-
面向对象
- 去饭店
- 点菜
- 吃
-
- 洗衣服
- 面向过程
- 脱衣服
- 放进盆里
- 放洗衣液
- 加水
- 放衣服
- 搓一搓
- 清一清
- 拧一拧
- 晒起来
- 面向对象
- 脱衣服
- 打开洗衣机
- 丢进去
- 一键洗衣烘干
- 终极面向对象
- 买电脑/吃饭/洗衣服
- 找个对象
- 面向过程
- 现实生活中我们是如何应用面相对象思想的
- 包工头
- 汽车坏了
- 面试
面向对象的特点
- 是一种符合人们思考习惯的思想
- 可以将复杂的事情简单化
- 将程序员从执行者转换成了指挥者
- 完成需求时:
- 先要去找具有所需的功能的对象来用
- 如果该对象不存在,那么创建一个具有所需功能的对象
- 这样简化开发并提高复用
类与对象的关系
-
面向对象的核心就是对象,那怎么创建对象?
- 现实生活中可以根据模板创建对象,编程语言也一样,也必须先有一个模板,在这个模板中说清楚将来创建出来的对象有哪些
属性
和行为
- 现实生活中可以根据模板创建对象,编程语言也一样,也必须先有一个模板,在这个模板中说清楚将来创建出来的对象有哪些
-
JavaScript中的类相当于图纸,用来描述一类事物。
-
JavaScript利用类来创建对象,对象是类的具体存在, 因此面向对象解决问题应该是先考虑需要设计哪些类,再利用类创建多少个对象
-
JavaScript中可以自定义类, 但是也提供了一个默认的类叫做Object
使用默认类创建对象
- 通过 new Object() 创建对象
<script>
// 1.使用默认类创建一个空对象
var obj = new Object()
// 2.动态给空对象新增属性
obj.name = "lnj";
obj.age = 33;
// 3.动态给空对象新增方法
obj.say = function () {
console.log("hello");
}
// 4.使用对象的属性和方法
console.log(obj.name);
console.log(obj.age);
obj.say();
</script>
- 通过字面量创建对象
<script>
/*
// 1.使用字面量创建对象
var obj = {}; // 相当于var obj = new Object()
// 2.动态给空对象新增属性
obj.name = "lnj";
obj.age = 33;
// 3.动态给空对象新增方法
obj.say = function () {
console.log("hello");
}
*/
// 1.使用字面量创建对象
var obj = {
name : 'lnj',
age: 33,
say : function () {
console.log("hello");
}
}
// 2.使用对象的属性和方法
console.log(obj.name);
console.log(obj.age);
obj.say();
</script>
- 使用工厂函数创建对象
- 上面的创建方式, 没多创建一个人都需要将代码再写一遍, 冗余代码太多, 所以我们可以创建创建对象的代码封装到一个函数中
- 专门用于创建对象的函数我们称之为工厂函数
<script>
function createPerson(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.say = function () {
console.log("hello");
}
return obj;
}
var obj1 = createPerson("lnj", 33);
var obj2 = createPerson("zq", 18);
console.log(obj1);
console.log(obj2);
</script>
<script>
function createPerson(name, age) {
var obj = {
name: name,
age: age,
say: function () {
console.log("hello");
}
}
return obj;
}
var obj1 = createPerson("lnj", 33);
var obj2 = createPerson("zq", 18);
console.log(obj1);
console.log(obj2);
</script>
函数中的this关键字
- 每个函数中都有一个this关键字, 谁调用当前函数, this关键字就是谁
<script>
function test() {
console.log(this);
}
// 默认情况下直接调用的函数都是由window调用的
// 所以test函数中的this是window
test(); // 相当于window.test();
var obj = new Object()
obj.name = "lnj";
obj.age = 33;
obj.say = function () {
console.log(this.name, this.age);
}
// 这里的say是一个方法, 方法必须通过对象来调用
// 所以这里的say方法是由obj对象调用的, 所以say方法中的this是obj对象
obj.say();
</script>
如何设计一个类
- 生活中描述事物无非就是描述事物的
属性
和行为
。- 如:人有身高,体重等属性,有说话,打架等行为。
事物名称(类名):人(Person)
属性:身高(height)、年龄(age)
行为(功能):跑(run)、打架(fight)
- JavaScript中用类来描述事物也是如此
- 属性:对应类中的成员变量。
- 行为:对应类中的成员方法。
- 定义类其实在定义类中的成员(成员变量和成员方法)
- 拥有相同或者类似
属性
(状态特征)和行为
(能干什么事)的对象都可以抽像成为一个类
如何分析一个类
- 一般名词都是类(名词提炼法)
- 飞机发射两颗炮弹摧毁了8辆装甲车
飞机
炮弹
装甲车
- 隔壁老王在公车上牵着一条叼着热狗的草泥马
老王
热狗
草泥马
如何定义一个类
- 在JavaScript中可以通过构造函数来定义一个类
- 构造函数也是一个函数, 只不过函数的名称必须大写(帕斯卡命名)
- 构造函数也是一个函数, 只不过调用时必须通过new来调用
<script>
// 通过构造函数定义一个类
// 在构造函数中描述该类事物共性的属性和行为
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(this.name, this.age);
}
}
</script>
如何通过类创建一个对象
- 不过就是创建结构体的时候, 根据每个对象的特征赋值不同的属性罢了
<script>
// 通过构造函数定义一个类
// 在构造函数中描述该类事物共性的属性和行为
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(this.name, this.age);
}
}
// 通过构造函数创建对象
// new 的执行过程
// 1 在内存中创建了一个空的对象
// 2 让构造函数中的this指向刚刚创建的对象
// 3 执行构造函数,在构造函数中设置属性和方法(当然也可以做其它事情)
// 4 返回了当前对象
var p1 = new Person("lnj", 33);
var p2 = new Person("zq", 18);
p1.say();
p2.say();
</script>
与工厂函数的不同之处
- 不用我们自己手动创建对象
- 不同我们手动返回创建好的对象
- 构造函数名称首字母必须大写
- 构造函数必须通过new调用
构造函数作用
- 使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了
- 每个对象都可以访问一个名称叫做
constructor
的属性, 属性指向创建该实例的构造函数
<script>
var obj = new Object();
obj.name = "lnj";
console.log(obj); // Object
function Person(name) {
this.name = name;
}
var p = new Person("zs");
console.log(p); // Person
// 可以通过对象的constructor属性判断某个对象是否是某个类的实例
console.log(p.constructor === Person); // true
// 也可以通过instanceof判断某个对象是否是某个类的实例
// 企业开发推荐
console.log(p instanceof Person);
</script>
构造函数的内存优化问题
- 每当通过一个构造函数创建一个对象, 就会在内存中开辟一块存储空间, 该存储空间中保存了对象的所有属性和方法
- 如果构造函数中的某些属性或方法是需要变化的, 那么每份存储空间中都应该保存一份独有, 但是如果某些属性和方法是不变的, 那么每份存储空间中都保存一份则造成了内存浪费
<script>
function Person(name, age) {
this.name = name;
this.age = age;
this.type = "人";
this.say = function () {
console.log(this.name, this.age, this.type);
}
}
// 所有对象的type属性和say方法都是一样的
// 但是还会在每个对象的存储空间中都存储一份
var p1 = new Person("lnj", 33);
var p2 = new Person("zs", 33);
console.log(p1.say === p2.say); // false
</script>
- 对于这种问题我们可以把需要共享的函数定义到构造函数外部
<script>
function say() {
console.log(this.name, this.age, this.type);
}
function Person(name, age) {
this.name = name;
this.age = age;
this.type = "人";
this.say = say;
}
var p1 = new Person("lnj", 33);
var p2 = new Person("zs", 33);
console.log(p1.say === p2.say); // true
</script>
- 但是如果有多个需要共享的函数, 就会造成全局命名空间冲突的问题(同一作用域不能出现同名的标识符)
<script>
// 将共享函数封装到一个对象中, 与外界隔绝, 这样就不会污染全局命名空间了
var fns = {
say: function () {
console.log(this.name, this.age, this.type);
},
setName: function (name) {
this.name = name;
},
setAge: function (age) {
this.age = age;
}
}
function Person(name, age) {
this.name = name;
this.age = age;
this.type = "人";
this.say = fns.say;
this.setName = fns.setName;
this.setAge = fns.setAge;
}
var p1 = new Person("lnj", 33);
var p2 = new Person("zs", 33);
console.log(p1.say === p2.say); // true
</script>
构造函数的内存优化问题(更好的方案)
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。
- 这个对象的所有属性和方法,都会被构造函数的所拥有
- 也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
// 给原型对象添加新的属性和方法
Person.prototype.say = function () {
console.log(this.name, this.age, this.type);
};
Person.prototype.type = "人";
var p1 = new Person("lnj", 33);
var p2 = new Person("zs", 33);
console.log(p1.say === p2.say); // true
console.log(p1.type === p2.type); // true
// 当调用对象的属性或者方法的时候,先去找对象本身的属性/方法
// 如果对象没有该属性或者方法。此时去原型中查找对应的属性/方法
// 如果对象本身没有该属性/方法,原型中也没有该属性或者方法,此时会报错
p1.say();
console.log(p1.type);
</script>
- 构造函数、实例、原型三者之间的关系
- 任何函数都具有一个 prototype 属性,该属性是一个对象
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto
原型链
<script>
function Person(name, age) {
this.name = name;
this.age = age;
this.type = "超人";
}
// 给原型对象添加新的属性和方法
Person.prototype.say = function () {
console.log(this.name, this.age, this.type);
};
Person.prototype.type = "人";
var p = new Person("lnj", 33);
console.log(p.type);
console.log(p.__proto__);
console.log(p.__proto__.__proto__);
console.log(p.__proto__.__proto__.__proto__);
</script>
- 方法查找规则
- 先查找当前对象, 当前对象有就使用当前对象的方法
- 当前对象没有再逐层在原型链上查找, 最先找到那个就使用哪个
- 如果找到null都没找到就报错
<script>
function Person(name, age) {
this.name = name;
this.age = age;
// this.say = function () {
// console.log("自己的", this.name, this.age, this.type);
// };
}
// 给原型对象添加新方法
// Person.prototype.say = function () {
// console.log("原型链上的", this.name, this.age, this.type);
// };
var p = new Person("lnj", 33);
// 自己有吗? 有, 使用自己的
// 自己有吗? 没有, 去原型链查找
// 都没有吗? 报错
p.say();
</script>
- 属性查找规则
- 先查找当前对象, 当前对象有就使用当前对象的方法
- 当前对象没有再逐层在原型链上查找, 最先找到那个就使用哪个
- 如果找到null都没找到就输出undefined
<script>
function Person(name, age) {
this.name = name;
this.age = age;
// this.type = "超人";
}
// 给原型对象添加新的属性和方法
// Person.prototype.type = "人";
var p = new Person("lnj", 33);
// 自己有吗? 有, 使用自己的
// 自己有吗? 没有, 去原型链查找
// 都没有吗? undefined
console.log(p.type);
</script>
- 属性的设置规则
- 不会修改原型链上的属性, 会给当前对象新增一个属性
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
// 给原型对象添加新的属性和方法
Person.prototype.type = "人";
var p = new Person("lnj", 33);
console.log(p.__proto__.type);
// p.__proto__.type = "超人";
// 自己有这个属性吗? 没有, 新增
p.type = "超人";
console.log(p.__proto__.type);
</script>
自定义原型对象
- 原型对象是构造函数的一个属性, 所以我们可以通过修改属性值的方式来自定义原型对象
- 需要注意的是, 自定义原型对象不能破坏原有的三角恋关系
<script>
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person, // 手动还原constructor属性, 保持三角恋关系
type: "人",
say: function () {
console.log(this.name, this.age, this.type);
}
}
var p = new Person("lnj", 33);
p.say();
</script>
原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototype 记得修正 constructor 的指向
面向对象三大特性
- 封装性
- 封装性就是隐藏实现细节,仅对外公开接口
- 类是数据与功能的封装,数据就是成员变量,功能就是方法
- 为什么要封装?
- 不封装的缺点:当一个类把自己的成员变量暴露给外部的时候,那么该类就失去对该成员变量的管理权,别人可以任意的修改你的成员变量
- 封装就是将数据隐藏起来,只能用此类的方法才可以读取或者设置数据,不可被外部任意修改是面向对象设计本质(
将变化隔离
)。这样降低了数据被误用的可能 (提高安全性
和灵活性
)
<script>
function Person(name) {
this.name = name; // 公有属性
// 只能在内部使用, 不能在外部使用
var age = 666; // 私有属性
// 公有方法
this.say = function () {
console.log(this.name, age);
test();
}
this.setAge = function (num) {
age = num;
}
this.getAge = function () {
return age;
}
// 私有方法
function test() {
console.log("test");
}
}
var p = new Person("lnj");
console.log(p.name);
p.say();
console.log(p.age); // undefined
p.setAge(123);
console.log(p.getAge());
p.test(); // 报错
</script>
- 封装原则
- 将不需要对外提供的内容都隐藏起来,把属性都隐藏,提供公共的方法对其访问
实例属性和实例方法/静态属性和静态方法
- 通过构造函数创建出来的对象访问的属性和方法,我们称之为实例属性和实例方法
- 实例属性和实例方法都是绑定在构造函数创建出来的对象上的
<script>
function Person(name) {
this.name = name; // 实例属性
this.eat = function () { // 实例方法
console.log("eat");
}
}
Person.prototype.age = "0"; // 实例属性
Person.prototype.say = function () { // 实例方法
console.log("hello");
}
var p = new Person("lnj");
console.log(p.name); // 通过对象访问
console.log(p.age); // 通过对象访问
p.eat(); // 通过对象访问
p.say(); // 通过对象访问
</script>
- 通过构造函数直接调用的属性和方法,我们称之为静态属性和静态方法
- 静态属性和静态方法都是绑定在构造函数上的
<script>
function Person() {
Person.name = "lnj"; // 静态属性
Person.eat = function () { // 静态方法
console.log("eat");
}
}
Person.count = 0; // 静态属性
Person.say = function () { // 静态方法
console.log("hello");
};
console.log(Person.name); // 通过构造函数访问
console.log(Person.count); // 通过构造函数访问
Person.eat(); // 通过构造函数访问
Person.say(); // 通过构造函数访问
</script>
- 继承性
- 儿子继承父亲的物品就是继承最好的体现
-
js中继承目的: 把子类型中共同的成员提取到父类型中,代码重用
- 借用原型链实现继承
- 直接将子类的原型对象修改为父类对象, 这样就能使用原型链上的属性和方法
<script>
// 父类
function Person() {
this.name = "lnj";
this.age = 33;
this.gender = "male";
}
// 子类
function Student(score) {
this.score = score;
}
// 由于是直接将子类原型对象修改为了父类对象
// 所以继承的属性值很难自定义
Student.prototype = new Person();
Student.prototype.constructor = Student;
var stu1 = new Student(99);
console.log(stu.name, stu.age, stu.gender, stu.score);
var stu2 = new Student(66);
console.log(stu.name, stu.age, stu.gender, stu.score);
</script>
- 借用构造函数实现继承
- 在子类中调用父类构造函数, 并且将父类构造函数的this修改为子类对象
<script>
// 父类
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 借用构造函数只是调用了父类的构造函数, 借用了构造函数中的代码
// 相当于动态的给子类添加了许多属性, 但是并没有修改子类的原型
// 所以子类无法继承父类的方法
Person.prototype.say = function () {
console.log(this.name, this.age, this.gender);
}
// 子类
function Student(score, name, age, gender) {
Person.call(this, name, age, gender);
this.score = score;
}
var stu1 = new Student(99, "lnj", 33, "male");
var stu2 = new Student(66, "zq", 18, "female");
console.log(stu1.name, stu1.age, stu1.gender, stu1.score);
console.log(stu2.name, stu2.age, stu2.gender, stu2.score);
stu1.say(); // 报错
stu2.say(); // 报错
</script>
- 借用构造函数+借用原型链组合继承
- 通过借用构造函数实现属性继承
- 通过借用原型链实现方法继承
<script>
// 父类
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
Person.prototype.say = function () {
console.log(this.name, this.age, this.gender);
}
// 子类
function Student(score, name, age, gender) {
Person.call(this, name, age, gender);
this.score = score;
}
Student.prototype = Person.prototype;
Student.prototype.constructor = Student;
// 由于子类的原型指向了父类的原型, 所以操作的都是同一个原型对象
// 给子类的原型新增方法或者属性, 父类也会受到影响
Student.prototype.study = function () {
console.log("好好学习天天向上");
};
Student.prototype.type = "学生";
var stu = new Student(99, "lnj", 33, "male");
stu.say();
stu.study();
console.log(stu.type);
var p = new Person("zq", 18, "female");
p.say();
p.study();
console.log(p.type);
</script>
- 终极方案
<script>
// 父类
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
Person.prototype.say = function () {
console.log(this.name, this.age, this.gender);
}
// 子类
function Student(score, name, age, gender) {
Person.call(this, name, age, gender);
this.score = score;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
// 由于子类的原型指向一个全新的对象
// 所以给子类的原型新增方法或者属性, 父类不会受到影响
Student.prototype.study = function () {
console.log("好好学习天天向上");
};
Student.prototype.type = "学生";
var stu = new Student(99, "lnj", 33, "male");
stu.say();
stu.study();
console.log(stu.type);
var p = new Person("zq", 18, "female");
p.say();
p.study(); // 报错
console.log(p.type); // 报错
</script>
对象的增删改查
<script>
// 1.定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.type = "人";
// 2.利用构造函数创建对象
var p1 = new Person("lnj", 33);
var p2 = new Person("zq", 18);
// 3.动态增加属性
p1.score = 99;
console.log(p1);
console.log(p2);
// 4.删除某个属性
delete p1.name;
console.log(p1);
console.log(p2);
// 5.修改某个属性的值
p1.age = 66;
console.log(p1);
console.log(p2);
// 6.查询是否包含某个属性
// in 会到原型对象里面查找
console.log("type" in p1);
// hasOwnProperty: 只在对象自身查找
console.log(p1.hasOwnProperty("type"));
</script>
Object对象的bind-call-apply方法
- 默认情况下所有对象都有bind-call-apply方法
- 这三个方法的作用是用于修改指定函数中this的指向
<script>
function sum(a, b) {
// 默认情况下函数中的this谁调用就是谁
console.log(this);
console.log(a, b);
}
// test();
var obj = {
name: "zq"
};
// bind方法可以修改指定函数中this的指向
// 但是bind方法并不会调用函数, 如果需要手动调用
// sum.bind(obj)(10, 20);
// call方法也可以修改指定函数中this的指向
// 并且call方法会自动调用函数, 不用手动调用
// sum.call(obj, 10, 20);
// apply方法也可以修改指定函数中this的指向
// 并且apply方法会自动调用函数, 不用手动调用
// 和call方法的区别是传递参数的形式不同而已
sum.apply(obj,[10, 20]);
</script>
JavaScript中继承远不止如何, 更多惊喜尽在后续ES6中
对象的拷贝
- 浅拷贝
- 对于基本类型属性无论是深浅拷贝,都会复制一份
- 对于引用类型属性浅拷贝拷贝的是引用类型的地址
- 简而言之, 浅拷贝修改引用类型属性, 拷贝前拷贝后的对象都会受到影响
<script>
var obj1 = {
name: "lnj", // 基本类型属性
age: 33, // 基本类型属性
dog: { // 引用类型属性
name: "wc",
age: 1,
}
};
var obj2 = {};
/*
for(var key in obj1){
obj2[key] = obj1[key];
}
*/
function copy(o1, o2){
for(var key in o1){
o2[key] = o1[key];
}
}
copy(obj1, obj2);
console.log(obj1);
console.log(obj2);
// 修改基本类型属性, 不会影响原有对象
obj2.name = "zs";
console.log(obj1.name);
console.log(obj2.name);
// 修改引用类型属性, 会影响原有对象
obj2.dog.name = "xq";
console.log(obj1.dog.name);
console.log(obj2.dog.name);
</script>
- 深拷贝
- 对于基本类型属性无论是深浅拷贝,都会复制一份
- 对于引用类型属性深拷贝会将引用类型复制一份
- 简而言之, 深拷贝修改引用类型属性, 只会影响当前修改对象
<script>
var obj1 = {
name: "lnj", // 基本类型属性
age: 33, // 基本类型属性
dog: { // 引用类型属性
name: "wc",
age: 1,
}
};
var obj2 = {};
function copy(o1, o2){
for(var key in o1){
var item = o1[key];
// 判断当前属性是否是引用类型
if(item instanceof Object){
// 创建一个新引用类型属性
var o = {};
o2[key] = o;
// 进一步拷贝引用类型属性
copy(item, o);
}
// 判断当前属性是否数组
else if(item instanceof Array){
// 创建一个数组属性
var arr = [];
o2[key] = arr;
// 进一步拷贝数组属性
copy(item, arr);
}
// 如果不是引用类型, 直接拷贝即可
else{
o2[key] = item;
}
}
}
copy(obj1, obj2);
console.log(obj1);
console.log(obj2);
// 修改基本类型属性, 不会影响原有对象
obj2.name = "zs";
console.log(obj1.name);
console.log(obj2.name);
// 修改引用类型属性, 会影响原有对象
obj2.dog.name = "xq";
console.log(obj1.dog.name);
console.log(obj2.dog.name);
</script>