• JavaScript -- 07. 面向对象编程


    面向对象编程

    这部分视频讲解了JS中面向对象相关的知识。主要包括:类、属性、方法、构造函数、封装、多态、继承、对象的结构、原型、原型链、旧类、new运算符等内容。

    1 面向对象编程介绍

    面向对象编程(OOP, Object Oriented Programming)

    1. 程序是干嘛的?
    • 程序就是对现实世界的抽象(照片就是对人的抽象)
    1. 对象是干嘛的?
    • 一个事物抽象到程序中后就变成了对象
    • 在程序的世界中,一切皆对象
    1. 面向对象的编程
    • 面向对象的编程指,程序中的所有操作都是通过对象来完成
    • 做任何事情之前都需要先找到它的对象,然后通过对象来完成各种操作
    1. 事物和对象

      • 一个事物通常由两部分组成:数据和功能

      • 一个对象由两部分组成:属性和方法

      • 事物的数据到了对象中,体现为属性

      • 事物的功能到了对象中,体现为方法

    例如对于人来说:

    • 数据:

      • 姓名

      • 年龄

      • 身高

      • 体重

    • 功能:

    表现在代码中:

    const five = {
        // 添加属性
        name:"王老五",
        age:48,
        height:180,
        weight:100,
    
        // 添加方法
        sleep(){
            console.log(this.name + "睡觉了~")
        },
    
        eat(){
            console.log(this.name + "吃饭了~")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2 类

    使用Object创建对象的问题:

    1. 无法区分出不同类型的对象
    2. 不方便批量创建对象

    在JS中可以通过类(class)来解决这个问题:

    1. 类是对象模板,可以将对象中的属性和方法直接定义在类中
    2. 定义后,就可以直接通过类来创建对象
    3. 通过同一个类创建的对象,我们称为同类对象
    • 可以使用instanceof来检查一个对象是否是由某个类创建
    • 如果某个对象是由某个类所创建,则我们称该对象是这个类的实例

    语法:

    class 类名 {} // 类名要使用大驼峰命名
    const 类名 = class {}  
    
    • 1
    • 2

    通过类创建对象:

    const aa = new 类名()
    
    • 1

    例如:

    // Person类专门用来创建人的对象
    class Person{
    
    }
    
    // Dog类式专门用来创建狗的对象
    class Dog{
    
    }
    
    const p1 = new Person()  // 调用构造函数创建对象
    const p2 = new Person()
    
    const d1 = new Dog()
    const d2 = new Dog()
    
    console.log(p1 instanceof Person) // true
    console.log(d1 instanceof Person) // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3 属性

    • 类是创建对象的模板,要创建第一件事就是定义类
    • 类的代码块,默认就是严格模式,
    • 类的代码块是用来设置对象的属性的,不是什么代码都能写
    • 分为实例属性和静态属性
      • 示例属性只能通过实例化后的对象访问:实例化对象.属性名
      • 静态属性只能通过类名访问:类名.属性名
    class Person{
        name = "孙悟空" // Person的实例属性name p1.name
        age = 18       // 实例属性只能通过实例访问 p1.age
    
        // 使用static声明的属性,是静态属性(类属性) Person.test
        static test = "test静态属性" 
        // 静态属性只能通过类去访问 Person.hh
        static hh = "静态属性"   
    }
    
    const p1 = new Person()
    const p2 = new Person()
    
    console.log(p1)
    console.log(p2)
    console.log(p1.name, p1.test, Person.test)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20221202110721343

    4 方法

    定义方法和定义函数语法格式都是一样的,只不过可以省掉function关键字

    方法中的this和原来一样,谁调用this指向的就是谁

    class Person {
    
        name = "孙悟空"
    
        // 添加方法的一种方式,不推荐使用这种方式
        // sayHello1 = function () {
        //
        // } 
    
        // 添加方法(实例方法) 实例方法中this就是当前实例
        sayHello() {
            console.log('大家好,我是' + this.name)
        } 
    
        // 静态方法(类方法) 通过类来调用 静态方法中this指向的是当前类
        static test() {
            console.log("我是静态方法", this)
        } 
    }
    
    const p1 = new Person()
    
    // console.log(p1)
    Person.test()
    p1.sayHello()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    image-20221202111316892

    5 构造函数

    在前面的定义方法中,每个实例的属性都是我们预先定义好的,没法自定义去改变,唯一的改变方法就是通过下面这种方式,但是这种方式又不能提现类的便捷性

    class Person {
        name = "孙悟空" // 当我们在类中直接指定实例属性的值时,意味着我们创建的所有对象的属性都是这个值
        age = 18
        gender = "男"
    
        sayHello() {
            console.log(this.name)
        }
    }
    
    const p1 = new Person()
    p1.age = 19
    p1.name = "zhangsan"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    因此,我们介绍一种新的东西,叫做构造函数,使用方法如下:

    • 在类中可以添加一个特殊的方法constructor
    • 该方法我们称为构造函数(构造方法)
    • 构造函数会在我们调用类创建对象时执行
    • 我们通过new调用函数的时候调用的就是类中的构造函数
    • 默认会有一个无参构造函数
    class Person {
        constructor(name, age, gender) {
            // console.log("构造函数执行了~", name, age, gender)
            // 可以在构造函数中,为实例属性进行赋值
            // 在构造函数中,this表示当前所创建的对象
            this.name = name
            this.age = age
            this.gender = gender
        }
    }
    
    const p1 = new Person("孙悟空", 18, "男")
    const p2 = new Person("猪八戒", 28, "男")
    const p3 = new Person("沙和尚", 38, "男")
    
    console.log(p1)
    console.log(p2)
    console.log(p3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    6 封装

    面向对象的特点:封装、继承、多态

    封装 —— 安全性
    继承 —— 扩展性
    多态 —— 灵活性

    • 对象就是一个用来存储不同属性的容器

    • 对象不仅存储属性,还要负责数据的安全

    • 直接添加到对象中的属性,并不安全,因为它们可以被任意的修改

    • 如何确保数据的安全:

      1. 私有化数据
        • 将需要保护的数据设置为私有,只能在类内部使用
      2. 提供setter和getter方法来开放对数据的操作
        • 属性设置私有,通过getter setter方法操作属性带来的好处
          • 可以控制属性的读写权限
          • 可以在方法中对属性的值进行验证
    • 封装主要用来保证数据的安全

    • 实现封装的方式:

      1. 属性私有化在属性名前面加#
      2. 后面再使用的时候通过this.#name修改
      3. 私有化属性必须先声明再复制
      4. 通过gettersetter方法来操作属性
        1. 使用这种方式可以控制用户可以读写的属性
        2. 在修改属性的时候可以先对属性进行检查,如果不符合要求就不修改
      get 属性名(){
          return this.#属性
      }
      
      set 属性名(参数){
      	this.#属性 = 参数
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    例子:

    class Person {
        // #address = "花果山" // 实例使用#开头就变成了私有属性,私有属性只能在类内部访问
    
        #name
        #age
        #gender
    
        constructor(name, age, gender) {
            this.#name = name
            this.#age = age
            this.#gender = gender
        }
    
        sayHello() {
            console.log(this.#name)
        }
    
        // getter方法,用来读取属性
        getName(){
            return this.#name
        }
    
        // setter方法,用来设置属性
        setName(name){
            this.#name = name
        }
    
        getAge(){
            return this.#age
        }
    
        // 可以先检查要修改的值,如果不符合要求就不修改
        setAge(age){
            if(age >= 0){
                this.#age = age
            }
        }
    
        get gender(){
            return this.#gender
        }
    
        set gender(gender){
            this.#gender = gender
        }
    }
    
    const p1 = new Person("孙悟空", 18, "男")
    
    // p1.age = "hello"
    
    // p1.getName()
    p1.setAge(-11) // p1.age = 11  p1.age
    
    // p1.setName('猪八戒')
    
    p1.gender = "女" // 等同于p1.setGender("女")
    console.log(p1.gender)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    7 多态

    • 在JS中不会检查参数的类型,所以这就意味着任何数据都可以作为参数传递
    • 要调用某个函数,无需指定的类型,只要对象满足某些条件即可
    • 如果一个东西走路像鸭子,叫起来像鸭子,那么它就是鸭子
    • 多态为我们提供了灵活性
    class Person{
        constructor(name){
            this.name = name
        }
    }
    
    class Dog{
        constructor(name){
            this.name = name
        }
    }
    
    class Test{
    
    }
    
    const dog = new Dog('旺财')
    const person = new Person("孙悟空")
    const test = new Test()
    
    function sayHello(obj){
        // if(obj instanceof Person){
        console.log("Hello, "+obj.name)
        // }
    }
    
    sayHello(dog)
    sayHello(person)
    sayHello(test)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    image-20221202121524701

    8 继承

    • 可以通过extends关键来完成继承
    • 当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)
    • 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
    • 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展
    class Animal{
        constructor(name){
            this.name = name
        }
    
        sayHello(){
            console.log("动物在叫~")
        }
    }
    
    class Dog extends Animal{
        
    }
    
    class Cat extends Animal{
        
    }
    
    class Snake extends Animal{
    
    }
    
    const dog = new Dog("旺财")
    const cat = new Cat("汤姆")
    
    dog.sayHello()
    cat.sayHello()
    console.log(dog)
    console.log(cat)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    但是在上面的代码中,各个动物的sayHello()都是一个东西,但是每个动物的叫声是不一样的,所以可以通过重写父类方法来修改方法

    在子类中,可以通过创建同名方法来重写父类的方法

    重写构造函数时,构造函数的第一行代码必须为super()

    在方法中可以使用super来引用父类的方法,也就是说super指向的是父类对象

    OCP 开闭原则:程序应该对修改关闭,对扩展开放

    class Animal{
        constructor(name){
            this.name = name
        }
    
        sayHello(){
            console.log("动物在叫~")
        }
    }
    
    class Dog extends Animal{
        // 在子类中,可以通过创建同名方法来重写父类的方法
        sayHello(){
            console.log("汪汪汪")
        }
    }
    
    class Cat extends Animal{
        // 重写构造函数
        constructor(name, age){
            // 重写构造函数时,构造函数的第一行代码必须为super()
            super(name) // 调用父类的构造函数
            this.age = age
        }
        
        sayHello(){
            // 调用一下父类的sayHello, 也可以不调用
            super.sayHello() // 在方法中可以使用super来引用父类的方法
            console.log("喵喵喵")
        }
    }
    
    const dog = new Dog("旺财")
    const cat = new Cat("汤姆", 3)
    
    dog.sayHello()
    cat.sayHello()
    console.log(dog)
    console.log(cat)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    9 对象的内存结构

    对象中存储属性的区域实际有两个:

    1. 对象自身
      • 直接通过对象所添加的属性,位于对象自身中:this.gender = "男"
      • 在类中通过 x = y 的形式添加的属性,位于对象自身中:name = "zhansgan"
    2. 原型对象(prototype
      • 对象中还有一些内容,会存储到其他的对象里(原型对象)
      • 在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__
      • 原型对象也负责为对象存储属性,
        • 当我们访问对象中的属性时,会优先访问对象自身的属性
        • 对象自身不包含该属性时,才会去原型对象中寻找
      • 会添加到原型对象中的情况:
        1. 在类中通过xxx(){}方式添加的方法,位于原型中
        2. 主动向原型中添加的属性或方法

    image-20221202123950992

    10 原型

    访问一个对象的原型对象

    • 对象.__proto__
    • Object.getPrototypeOf(对象)
    class Person {
        name = "孙悟空"
        age = 18
    
        sayHello() {
            console.log("Hello,我是", this.name)
        }
    }
    
    const p = new Person()
    console.log(p.__proto__) // 访问原型对象
    console.log(Object.getPrototypeOf(p))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20221202124337168

    原型对象中的数据:

    1. 对象中的数据(属性、方法等)
    2. constructor (对象的构造函数)

    10.1 原型链

    注意:原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同

    • p对象的原型链:p对象 --> 原型 --> 原型 --> null

    • obj对象的原型链:obj对象 --> 原型 --> null

      image-20221202124937100

    原型链:

    • 读取对象属性时,会优先对象自身属性,

      • 如果对象中有,则使用,没有则去对象的原型中寻找

      • 如果原型中有,则使用,没有则去原型的原型中寻找

      • 直到找到Object对象的原型(Object的原型没有原型(为null))

      • 如果依然没有找到,则返回undefined

    • 作用域链和原型链的比较

      • 作用域链,是找变量的链,找不到会报错
      • 原型链,是找对象属性的链,找不到会返回undefined

    所有的同类型对象它们的原型对象都是同一个,也就意味着,同类型对象的原型链是一样的

    class Person {
        name = "孙悟空"
        age = 18
    
        sayHello() {
            console.log("Hello,我是", this.name)
        }
    }
    
    class Dog {}
    
    const p = new Person()
    const p2 = new Person()
    const d = new Dog()
    
    console.log(p == p2) // false
    console.log(p.__proto__ == p2.__proto__) // true
    console.log(d == p) // false
    console.log(d.__proto__ == p.__proto__) // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20221202180308792

    10.2 原型的作用:

    • 原型就相当于是一个公共的区域,可以被所有该类实例访问,
    • 可以将该类实例中,所有的公共属性(方法)统一存储到原型
    • 这样我们只需要创建一个属性,即可被所有实例访问

    JS中继承就是通过原型来实现的,当继承时,子类的原型就是一个父类的实例

    在对象中有些值是对象独有的,像属性(name,age,gender)每个对象都应该有自己值,但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没必要重复的创建

    尝试:
    函数的原型链是什么样子的?
    Object的原型链是什么样子的?

    11 修改原型

    大部分情况下,我们是不需要修改原型对象

    class Person {
        name = "孙悟空"
        age = 18
    
        sayHello() {
            console.log("Hello,我是", this.name)
        }
    }
    
    const p = new Person()
    const p2 = new Person()
    
    // 通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法 不要这么做
    p.__proto__.run = () => {
        console.log('我在跑~')
    }
    
    console.log(p)
    console.log(p2)
    
    p.run()
    p2.run()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    注意:千万不要通过类的实例去修改原型(类似上面这样)

    1. 通过一个对象影响所有同类对象,这么做不合适
    2. 修改原型先得创建实例,麻烦
    3. 危险

    处理通过__proto__能访问对象的原型外,还可以通过类的prototype属性,来访问实例的原型,修改原型时,最好通过类去修改

    Person.prototype === p.__proto__ //true
    
    • 1

    好处:

    1. 一修改就是修改所有实例的原型
    2. 无需创建实例即可完成对类的修改

    原则:

    1. 原型尽量不要手动改
    2. 要改也不要通过实例对象去改
    3. 通过 类.prototype 属性去修改
    4. 最好不要直接给prototype去赋值

    12 instanceof和hasOwn

    12.1 instanceof

    instanceof 用来检查一个对象是否是一个类的实例

    • instanceof检查的是对象的原型链上是否有该类实例,只要原型链上有该类实例,就会返回true

    • dog -> Animal的实例 -> Object实例 -> Object原型

    • Object是所有对象的原型,所以任何和对象和Object进行instanceof运算都会返回true

    12.2 in

    使用in运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true

    class Person {
        name = "孙悟空"
        age = 18
    
        sayHello() {
            console.log("Hello,我是", this.name)
        }
    }
    
    const p = new Person()
    console.log("sayHello" in p) // true
    console.log("name" in p) // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    12.3 hasOwnProperty

    对象.hasOwnProperty(属性名):用来检查一个对象的自身是否含有某个属性

    不推荐使用

    class Person {
        name = "孙悟空"
        age = 18
    
        sayHello() {
            console.log("Hello,我是", this.name)
        }
    }
    
    const p = new Person()
    console.log(p.hasOwnProperty("sayHello")) // false
    console.log(p.hasOwnProperty("name")) // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    12.4 hasOwn

    Object.hasOwn(对象, 属性名) :用来检查一个对象的自身是否含有某个属性

    13 旧类

    早期JS中,直接通过函数来定义类

    • 一个函数如果直接调用 xxx() 那么这个函数就是一个普通函数
    • 一个函数如果通过new调用 new xxx() 那么这个函数就是一个构造函数
    // 等价于:class Person{}
    function Person(name, age) {
        // 在构造函数中,this表示新建的对象
        this.name = name
        this.age = age
    }
    
    // 向原型中添加属性(方法)
    Person.prototype.sayHello = function () {
        console.log(this.name)
    }
    
    // 静态属性
    Person.staticProperty = "xxx"
    // 静态方法
    Person.staticMethod = function () {}
    
    const p = new Person("孙悟空", 18)
    console.log(p)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    上面这种方式定义类比较分散,可以使用立即执行函数把他们写在一起,等价于以下代码

    var Person = (function () {
        function Person(name, age) {
            // 在构造函数中,this表示新建的对象
            this.name = name
            this.age = age
    
            // this.sayHello = function(){
            //     console.log(this.name)
            // }
        }
    
        // 向原型中添加属性(方法)
        Person.prototype.sayHello = function () {
            console.log(this.name)
        }
    
        // 静态属性
        Person.staticProperty = "xxx"
        // 静态方法
        Person.staticMethod = function () {}
    
        return Person
    })()
    
    const p = new Person("孙悟空", 18)
    console.log(p)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    继承的实现方式

    var Animal = (function(){
        function Animal(){
    
        }
    
        return Animal
    })()
    
    
    var Cat = (function(){
        function Cat(){
    
        }
    
        // 继承Animal
        Cat.prototype = new Animal()
    
        return Cat
    })()
    
    var cat = new Cat()
    
    console.log(cat)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    14 new运算符

    new运算符是创建对象时要使用的运算符

    • 使用new时,到底发生了哪些事情:
      https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new

    • 当使用new去调用一个函数时,这个函数将会作为构造函数调用,使用new调用函数时,将会发生这些事:

      1. 创建一个普通的JS对象(Object对象 {}), 为了方便,称其为新对象

      2. 将构造函数的prototype属性设置为新对象的原型

      3. 使用实参来执行构造函数,并且将新对象设置为函数中的this

      4. 如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回(千万不要这么做)

      如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回

      所以一般不要写返回值

    function MyClass() {
     	// 第一步
        var newInstance = {}
        // 第二步
        newInstance.__proto__ = MyClass.prototype
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    当程序员后,才突然明白的21件事……
    SPI协议的verilog实现(spi master slave联合实现)
    Docker篇-(2)-Docker安装-centos
    [NSSRound#18 Basic]web解析
    关于 Cypress 的启动和 cy.visit 的调试
    如何维护知乎口碑?小马识途解析知乎口碑优化的技巧
    未在本地计算机上注册“Microsoft.ACE.OLEDB.12.0”提供程序
    ES6解构赋值与扩展运算符
    6.zigbee开发,zigbee组网原理
    java List
  • 原文地址:https://blog.csdn.net/qq_46311811/article/details/128168031