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

发布于 2021-05-10  233 次阅读


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)]


活的像诗一样