JS 学习笔记 (七) 面向对象编程OOP

2022-12-11,,,,

1、前言

创建对象有很多种方法,最常见的是字面量创建和new Object()创建。但是在需要创建多个相同结构的对象时,这两种方法就不太方便了。

如:创建多个学生信息的对象

let tom = {
name: "Tom",
age: 20,
sex: "boy",
height: 175
}; let marry = {
name: "Marry",
age: 22,
sex: "girl",
height: 165
}

2、对象工厂

2.1 实例

使用对象工厂改进上述代码,如:

function person(name, age, sex, height) {
return {
name, age, sex, height
}
}
let tom = new person("Tom", 20, "boy", 175)
let marry = new person("Marry", 22, "girl", 165)
console.log(tom);
console.log(marry);

打印出结果:
{ name: 'Tom', age: 20, sex: 'boy', height: 175 }
{ name: 'Marry', age: 22, sex: 'girl', height: 165 }

对象工厂函数创建返回的是一个新对象。

2.2 缺陷以及解决方法

对象工厂本身是一个普通函数,用于表达对象结构时,描述性不强
对象工厂没有解决对象标识的问题,即创建的对象是什么类型。
利用构造函数可以解决这些问题

3、构造函数

3.1 实例详解

更改上面的代码,并添加一个say函数:

function person(name, age, sex, height) {
this.name = name;
this.age = age;
this.sex = sex;
this.height = height;
this.say = function(){
console.log(`你好,我是${this.name}`);
}
}
let tom = new person("Tom", 20, "boy", 175)
let marry = new person("Marry", 22, "girl", 165)
tom.say()
marry.say()

打印出结果:
你好,我是Tom
你好,我是Marry

其中,代码中的this是对new Person的空对象进行扩展。
每个对象都具有constructor属性,用于标识对象的“类型”,如:

console.log(Tom.constructor == Person); // true
console.log(Tom.constructor == Object); // false

若要判tom和marry对象的类型,推荐使用instanceof方法。如:

// 使用instanceof方法判断对象类型
console.log(Tom instanceof Person); // true
console.log(Tom instanceof Object); // true

但是这个解决方法还是有一些缺陷。

// 判断实例所对应的方法是否相同
console.log(Tom.say == Jerry.say); // false

输出是false的原因是因为同一个构造函数的实例都会创建一个自己的方法。这样可能会极大的增加的内存的负荷。而且,同一个方法应该是完成相同的任务,没有必要创建多个相同的方法。

我们可能会想到的解决方案是将它的say()方法提取出来,如:

function Person(name, age, sex, height) {
this.name = name;
this.age = age;
this.sex = sex;
this.height = height;
}
function say() {
console.log(`你好,我是${this.name}`);
}
let tom = new person("Tom", 20, "boy", 175)
let marry = new person("Marry", 22, "girl", 165)
tom.say()
marry.say()

输出结果:
你好,我是Tom
你好,我是Marry

这样处理的好处就是say()方法不会被多次创建,但会产生一定的问题。即:
say() 为全局函数,会导致作用域混乱。而且只有Person创建的对象才能调用该方法,由于该方法是放在全局的,可能会产生内存泄漏,会让全局的函数更加臃肿。

解决方法:
利用原型模式,将方法定义在构造函数的原型对象上,可以解决这个问题

每个函数都有一个prototype属性,指向一个对象。
该对象包含应该由特定引用类型的实例共享的属性和方法。
该对象就是通过调用构造函数创建的对象的原型。

看下列代码:

function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function () {
console.log(`你好,我是${this.name}`);
} let Tom = new Person("Tom", 12)
Tom.say()
let Marry= new Person("Jerry", 10)
Marry.say()
console.log(Tom.say == Marry.say); // true

输出结果:
你好,我是Tom
你好,我是Marry

若要对person的原型进行属性扩展,可直接使用Person.prototype。因为当在构造函数原型上创建属性(或方法)时,会被改构造函数的额所有对象所共享。如:

Person.prototype.from = "China"
console.log(Tom.from); // China
console.log(Marry.from); // China

若Marry对象本身有from属性,则继承自Person的from就不起作用了(原型的from属性被屏蔽掉了),起作用的是Marry自身的from属性,如:

Marry.from = "America"
console.log(Tom.from); // China
console.log(Marry.from); // America

若要判断对象的原型,对象原型的构造函数,可枚举的属性:

console.log(Object.keys(Marry)); // 可枚举的自身的属性 [ 'name', 'age' ]
console.log("from" in Marry); // 可枚举的自身的属性和继承的属性 true
console.log(Object.getOwnPropertyNames(Marry)); // 可枚举的自身的属性 [ 'name', 'age' ]
console.log(Object.getPrototypeOf(Marry).constructor == Person); // true

3.2 基本原理总结

在内存中创建一个新对象。
这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
构造函数内部的this被赋值为这个新对象(即this指向新对象)。
执行构造函数内部的代码(给新对象添加属性)。
如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

4、原型继承

4.1 概要

利用构造函数构建对象结构(模板,近似于类),从语义上较为清晰的表达对象结构。
利用构造函数原型扩展,能方便的为该构造函数所创建的对象进行基于原型的扩展。
利用构造函数还可以进行基于原型对象的继承

4.2 构造函数、原型和实例的关系

每个构造函数都有一个原型对象。
原型有一个属性指回构造函数
实例有一个内部指针[[Prototype]]指向原型。

4.3 原型链

当对象原型是另一个构造函数的实例,如此迭代,形成了一连串的继承关系,即为原型链。原型链表达了对象与对象之间的继承关系。

举个例子:

function Person(name, age) {
this.name = name
this.age = age
}
let Marry = new Person("Marry", 10)
console.log(Marry instanceof Person); // true
console.log(Marry instanceof Object); // true
console.log(Object.getPrototypeOf(Marry)); // {}
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Marry))); // [Object: null prototype] {}

4.4 原型链的问题

function Animal() {
this.colors = ["white", "black"];
}
function Mouse(name, age) {
this.name = name;
this.age = age;
}
Mouse.prototype = new Animal();
let m1 = new Mouse("Mickey", 10);
console.log(m1.name, m1.colors);
m1.colors.push("red");
let m2 = new Mouse("Miney", 9);
console.log(m2.colors);

输出结果:
Mickey [ 'white', 'black' ]
[ 'white', 'black', 'red' ]

这是因为当原型中包含引用值,在实例件共享的是该引用值的引用,当修改实例中的该属性时,会影响全部实例。

存在的问题:子类型在实例化时不能给父类型传递参数。
解决方案:盗用构造函数

4.5 盗用构造函数

在子类构造函数中调用父类构造函数,并将子类当前实例只定为构造函数的上下文。

function Animal(type) {
this.colors = ["white", "black"];
this.type = type
}
function Mouse(name, age, type = "Mouse") {
Animal.call(this, type) // 父类构造函数"盗用"(仅仅把Animal当做普通函数调用)
this.name = name;
this.age = age;
}
Mouse.prototype = new Animal()
let m1 = new Mouse("Mickey", 20)
m1.colors.push("red")
console.log(m1.name, m1.colors);
let m2 = new Mouse("Miney", 18)
console.log(m2.name, m2.colors); console.log(m1 instanceof Mouse);
console.log(m1 instanceof Animal);

输出结果:
Mickey [ 'white', 'black', 'red' ]
Miney [ 'white', 'black' ]
true
true

存在的问题:
无法访问父类原型上的方法,或者说是没有父类
解决方法:
将原型链与盗用构造函数结合起来

4.6 原型链与盗用构造函数的组合

将两者优点集中起来,如:

function Animal(type) {
this.colors = ["white", "black"];
this.type = type
}
Animal.prototype.show = function () {
console.log(this.type, this.colors); // Mouse [ 'white', 'black', 'red' ]
}
function Mouse(name, age, type = "Mouse") {
Animal.call(this, type) // 父类构造函数“盗用”,解决传参问题
this.name = name;
this.age = age;
}
Mouse.prototype = new Animal() // 强制指定原型对象,表达继承关系
let m1 = new Mouse("Mickey", 20)
m1.colors.push("red")
console.log(m1.name, m1.colors); // Mickey [ 'white', 'black', 'red' ]
m1.show() // 通过原型继承获取 let m2 = new Mouse("Miney", 18)
console.log(m2.name, m2.colors); // Miney [ 'white', 'black' ]
m2.show()
console.log(Object.keys(m1));// [ 'colors', 'type', 'name', 'age' ]

存在的问题:

console.log(m1 instanceof Mouse); // false
console.log(m1 instanceof Animal);// true

构造函数的指向不正确,问题在于Mouse.prototype = new Animal(),将构造函数指向了Animal

解决方法:强制指定构造函数的指向为原型构造函数

Mouse.prototype.constructor = Mouse
console.log(m1.constructor == Mouse); // true
console.log(m1 instanceof Mouse); // true

4、类

4.1 概述

ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。
类( class)是ECMAScript 中新的基础性语法糖结构
虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念

4.2 实例一

[ ] 要求:
使用ES5实现
创建一个Person类,其中实例属性包括:姓名name,年龄age。
创建一个数组people,向该列表中存入4个Person的实例 按照年龄升序对people列表进行排序,显示排序后的姓名和年龄。
为Person类添加一个方法setAttr(attr, value),可以动态的为Person类的实例添加属性和属性值。**

function Person(name, age) {
this.name = name;
this.age = age
}
// 在Person的原型上复写toString方法
Person.prototype.toString = function () {
return `${this.name}:${this.age}`
}
// 在Person的原型上定义setAttr方法
Person.prototype.setAttr = function (attr, value) {
this[attr] = value
}
let people = [];
// 定义people空数组,实例化4个Person对象,并添加到people数组中
let p1 = new Person("tom", 20)
let p2 = new Person("henry", 19)
let p3 = new Person("mark", 21)
let p4 = new Person("jeorge", 23)
people.push(p1, p2, p3, p4)
// 对people数组的age属性的值从大到小的排序
people.sort((a, b) => a.age - b.age)
console.log(people);
// 输出每个实例的name和age组成的语句
people.forEach((item) => console.log(item.toString()))
// 给第一个实例添加gender属性并赋值
people[0].setAttr("gender", "Male")
console.log(people[0]);
// 输出第二个实例化对象与第一个对象进行比较,看添加gender属性是否成功
console.log(people[1]);

输出结果:
[
Person { name: 'henry', age: 19 },
Person { name: 'tom', age: 20 },

Person { name: 'mark', age: 21 },
Person { name: 'jeorge', age: 23 }
]
henry:19
tom:20
mark:21
jeorge:23
Person { name: 'henry', age: 19, gender: 'Male' }
Person { name: 'tom', age: 20 }

4.2 实例二

[ ] 要求:
使用ES6实现实例一的功能

// 定义一个Person类
class Person {
// 在Person类里面定义的constructor构造函数,并传入参数name,age
constructor(name, age) {
// 定义name属性和age属性,并把传入的值赋给它
this.name = name;
this.age = age
};
// 定义的toString方法
toString() {
// 直接返回name和age的属性值
return `${this.name}:${this.age}`
};
// 定义setAttr方法,传入参数attr和value,表示属性和属性值
setAttr(attr, value) {
// 给当前的attr属性赋value值
this[attr] = value
}
}
let people = [
new Person("tom", 20),
new Person("henry", 19),
new Person("mark", 21),
new Person("jeorge", 23)
];
// 从大到小排序
people.sort((a, b) => a.age - b.age)
console.log(people);
// 返回每个实例的toString方法return的值
people.forEach((item) => console.log(item.toString()))
// 给第一个person实例对象添加gender属性并赋Male值
people[0].setAttr("gender", "Male")
// 打印出添加gender属性后的实例和未添加的实例进行比较
console.log(people[0]);
console.log(people[1]);

输出结果:
[
Person { name: 'henry', age: 19 },
Person { name: 'tom', age: 20 },
Person { name: 'mark', age: 21 },
Person { name: 'jeorge', age: 23 }
]
henry:19
tom:20
mark:21
jeorge:23
Person { name: 'henry', age: 19, gender: 'Male' }
Person { name: 'tom', age: 20 }

4.3 实例三

[ ] 要求:
使用ES6实现,在 实例二 Person类的基础上,编写以下两个类继承自Person。
Teacher类除了具有Person类的姓名和性别,还具有一个“课程”course属性。
Student类除了具有Person类的姓名和性别,还具有一个“分数”score属性
通过代码分析一个Teacher实例与Person类、Teacher类、Student类以及object之间的关系

class Person {
constructor(name, age) {
this.name = name;
this.age = age
};
toString() {
return `${this.name}:${this.age}`
}
} // 这里实现Teacher子类继承Person父类
class Teacher extends Person {
constructor(name, age, course) {
// super 子类Teacher调用父类Person的属性
super(name,age)
// 添加course属性
this.course = course
}
}
// 实例化一个Teacher对象
let Liming = new Teacher("Liming",22,"语文")
console.log(Liming);
// 这里实现Student子类继承Person父类
class Student extends Person{
constructor(name, age, score) {
// super 子类Student调用父类Person的属性
super(name,age)
// 添加score属性
this.score = score
}
}
// 实例化一个Student对象
let Tom = new Student("Tom",22,100)
console.log(Tom); // 判断Tom 是否是Student的实例
console.log(Tom instanceof Student);
console.log(Tom instanceof Person);
console.log(Tom instanceof Object); // 判断实例化对象Liming和Tom的原型对象
console.log(Object.getPrototypeOf(Liming));
console.log(Object.getPrototypeOf(Tom)); // 判断Student类和Teacher类的原型对象
console.log(Object.getPrototypeOf(Student));
console.log(Object.getPrototypeOf(Teacher));

输出结果:
Teacher { name: 'Liming', age: 22, course: '语文' }
Student { name: 'Tom', age: 22, score: 100 }
true
true
true
Person {}
Person {}
[class Person]
[class Person]

4.4 实例四

[ ] 要求
使用ES6实现,在 实例3 中Student类的基础上进行修改。
为Student类添加属性grade,用于表示该学生实例的年级(如:Grade One等)。
为学生实例的grade属性赋值时,能自动的将输入的信息转换为所有字母大写进行保存在对象中。
读取学生实例的grade属性时,如果该属性没有信息,则返回“NO GRADE”。

解法一:

class Person {
constructor(name, age) {
this.name = name;
this.age = age
}
} class Student extends Person {
constructor(name, age, score) {
super(name, age)
this.score = score;
};
// 存取器属性取出数据,若没有则显示NO GRADE
get grade() {
return this.__level || "NO GRADE"
};
// 存取器属性存入数据,并转为大写
set grade(value) {
this.__level = value.toUpperCase()
}
}
let Tom = new Student("Tom", 22, 100)
let Jerry = new Student("Jerry", 22, 100)
Tom.grade = "grade one"
console.log(Tom.grade);
console.log(Jerry.grade);

输出结果:
GRADE ONE
NO GRADE

解法二:

// 在es5构造函数中定义存储器方法
let Person = function(){
return function(name,age){
this.name = name;
this.age = age;
}
}
class Students extends Person(){
#gd; //实例私有字段
constructor(name,age,score){
super(name,age)
this.score = score
}
get grade(){
return this.#gd || "No GRADES"
}
set grade(value){
this.#gd = value.toUpperCase().trim()
}
}
let p1 = new Person("Jerry",20)
let p2 = new Person("Jerry")
p1.grade = "abc"
console.log(p1);
console.log(p2);

输出:
[Function (anonymous)] { grade: 'abc' }
[Function (anonymous)]

JS 学习笔记 (七) 面向对象编程OOP的相关教程结束。

《JS 学习笔记 (七) 面向对象编程OOP.doc》

下载本文的Word格式文档,以方便收藏与打印。