JavaScript 中的面向对象编程(OOP)是一种编程范式,它通过对象来模拟现实世界的事物和概念。对象可以包含属性(状态)和方法(行为)。JavaScript 支持构造函数、原型链、继承和类等 OOP 特性,使得代码组织更加结构化和模块化。通过这些特性,开发者可以创建可重用、易于维护和扩展的代码。



面向对象

>面向对象编程(OOP)

1. 程序是干嘛的?
 - 程序就是对现实世界的抽象(照片就是对人的抽象)

2. 对象是干嘛的?
 - 一个事物抽象到程序中后就变成了对象
 - 在程序的世界中,一切皆对象

例如:( )
心仪的女人:王老五
  - 一个事物通常由两部分组成:数据和功能
  - 一个对象由两部分组成:属性和方法
  - 事物的数据到了对象中,体现为属性
  - 事物的功能到了对象中,体现为方法

  - 数据:
        姓名
        年龄
        身高
        体重

  - 功能:
        睡
        吃  

const five = {
    //添加属性
    name:"王老五",
    age:48,
    height:180,
    weight:100,

    //添加方法
    eat(){
      console.log(this.name + "吃饭了")
    },
    
    sleep(){
      console.log(this.name + "睡觉了")
    }
    
}

3. 面向对象的编程
 - 面向对象的编程指,程序中的所有操作都是通过对象来完成
 - 做任何事情之前都需要先找到它的对象,然后通过对象来完成各种操作

>类

使用object创建的问题

  • 无法区分不同类型的对象

  • 不方便批量创建对象

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

    1. 类是对象模板,可以将对象中的属性和方法直接定义在类中,
       定义后,就可以直接通过类来创建对象

    2. 通过同一个类创建的对象,我们称为同类对象
       可以使用instanceof来检查一个对象是否是由某个类创建
       如果某个对象是由某个类所创建,则我们称该对象是这个类的实例

    语法:
        class 类名 {} // 类名要使用大驼峰命名
        const 类名 = class {}  
                    
    通过类创建对象
        new 类()

    // const Person = class {}

    // 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

    const five = {
        // 添加属性
        name: "王老五",
        age: 48,
        height: 180,
        weight: 100,

        // 添加方法
        sleep() {
            console.log(this.name + "睡觉了~")
        },

        eat() {
            console.log(this.name + "吃饭了~")
        },
    }

    const yellow = {
        name: "大黄",
        age: 3,
        sleep() {
            console.log(this.name + "睡觉了~")
        },

        eat() {
            console.log(this.name + "吃饭了~")
        },
    }

five.sleep(); // 输出: 王老五睡觉了~
five.eat();   // 输出: 王老五吃饭了~

yellow.sleep(); // 输出: 大黄睡觉了~
yellow.eat();   // 输出: 大黄吃饭了~

>属性

类是创建对象的模板,要创建第一件事就是定义类

class Person{
    /* 
        类的代码块,默认就是严格模式,
        类的代码块是用来设置对象的属性的,不是什么代码都能写
    */
    name = "孙悟空" // Person的实例属性name p1.name
    age = 18       // 实例属性只能通过实例访问 p1.age

    static test = "test静态属性" // 使用static声明的属性,是静态属性(类属性) Person.test
    static hh = "静态属性"   // 静态属性只能通过类去访问 Person.hh
}

// 创建Person类的实例
const p1 = new Person()
const p2 = new Person()

console.log(p1)
console.log(p2)

// 访问实例属性
console.log(p1.name); // 输出: 孙悟空
console.log(p1.age);  // 输出: 18

// 访问静态属性
console.log(Person.test); // 输出: test静态属性
console.log(Person.hh);   // 输出: 静态属性

>方法

class person {
    name = "孙悟空"

     sayHello == function(){

     }  添加方法的一种方式

    sayHello(){
        console.log(‘大家好,我是’ + this.name)
    } // 添加方法(实例方法),实例方法中的this就是当前实例! 谁调用了就是谁的this,例如P1调用,则this指向P1

    static test(){
      console.log('我是静态方法',this)
    } // 静态方法(类方法)通过类来调用,静态方法中this 指向的是当前类,例如Person.test调用,则指向Person
} 

const p1 = new Person()
//console.log(P1)

// 调用实例方法
p1.sayHello(); // 输出: 大家好,我是孙悟空

// 调用静态方法
Person.test(); // 输出: 我是静态方法 [class Person]

>构造函数

// class Person{
//     name="孙悟空" // 当我们在类中直接指定实例属性的值时,
//                 // 意味着我们创建的所有对象的属性都是这个值
//     age=18
//     gender="男"

//     sayHello(){
//         console.log(this.name)
//     }
// }

// 创建一个Person的实例
// const p1 = new Person("孙悟空", 18, "男")
// const p2 = new Person("猪八戒", 28, "男")
// const p3 = new Person("沙和尚", 38, "男")

// console.log(p1)
// console.log(p2)
// console.log(p3)

class Person{
            
    // 在类中可以添加一个特殊的方法constructor
    // 该方法我们称为构造函数(构造方法)
    // 构造函数会在我们调用类创建对象时执行

    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) //输出: Person { name: '孙悟空', age: 18, gender: '男' }
console.log(p2) //输出: Person { name: ' 猪八戒', age: 28, gender: '男' }
console.log(p3) //输出: Person { name: '沙和尚', age: 38, gender: '男' }

>封装

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

/* 

1、封装
  - 对象就是一个用来存储不同属性的容器
  - 对象不仅负责存储属性,还要负责数据的安全 例如 P1.name = -11 / haha 数据被修改,不安全
  - 直接添加到对象中的属性并不安全,因为它们可以被任意的修改

  - 如何确保数据的安全?
    ① 私有化数据 :  将需要保护的数据设置为私有,只能在类内部使用
    ② 提供 setter 和 getter 方法来开放对数据的操作 
       - 属性设置私有,通过 getter 和 setter 方法操作属性带来的好处?
         ?可以控制属性的读写权限
         ?可以在方法中对我们的属性值进行验证

  封装为了保护数据的安全,实现封装的方式:!私有化,!getter 及 setter 方法!
  
  get 属性名(){
    return this.#属性
  }
  
  set 属性名(参数){
    可以添加验证逻辑...
    this.#属性 = 参数
  
  }

*/ 

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){
    this.#age = age
  }

  //对属性进行验证
  setAge(age){
    if(age >= 0){
        this.#age = age
    }
  }
*/

  // js中的标准写法,使用属性来调用
  get gender(){
    console.log('getter执行了')
    return this.#gender
  }
  
  set gender(gender){
    this.#gender = gender 
  } // 此处调用则需要p1.gender = "女"
}
  
const p1 = new Person("孙悟空",18,”男“)

// p1.name = "hello"

// p1.getName()
p1.setAge(-11) // p1.age = 11 // p1.age
p1.setName('猪八戒')

console.log(p1.gender)

>多态

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

// console.log(dog)
// console.log(person)

/* 
    定义一个函数,这个函数将接收一个对象作为参数,他可以输出hello并打印对象的name属性


    多态
        - 在JS中不会检查参数的类型,所以这就意味着任何数据都可以作为参数传递
        - 要调用某个函数,无需指定的类型,只要对象满足某些条件即可
        - 如果一个东西走路像鸭子,叫起来像鸭子,那么它就是鸭子
        - 多态为我们提供了灵活性
*/
function sayHello(obj) {
    // if(obj instanceof Person){
    console.log("Hello," + obj.name)
    // }
}

sayHello(dog) //输出:你好,旺财!

>继承 (详解)

/* 
    继承
        - 可以通过extends关键来完成继承
        - 当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)
        - 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
        - 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展

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

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('汤姆')
dog.sayHello()
cat.sayHello()
console.log(dog)
console.log(cat)

>对象的结构

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

        1. 对象自身
            - 直接通过对象所添加的属性,位于对象自身中
            - 在类中通过 x = y 的形式添加的属性,位于对象自身中

        2. 原型对象(prototype)
            - 对象中还有一些内容,会存储到其他的对象里(原型对象)
            - 在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__

            - 原型对象也负责为对象存储属性,
                当我们访问对象中的属性时,会优先访问对象自身的属性,
                对象自身不包含该属性时,才会去原型对象中寻找

            - 会添加到原型对象中的情况:
                1. 在类中通过xxx(){}方式添加的方法,位于原型中
                2. 主动向原型中添加的属性或方法

*/

class Person {
    name = "孙悟空"
    age = 18

    // constructor(){
    //     this.gender = "男"
    // }

    sayHello() {
        console.log("Hello,我是", this.name)
    } //此处的方法添加在原型对象中
}

const p = new Person()

// p.address = "花果山"
// p.sayHello = "hello"

console.log(p.sayHello)

>原型对象

class Person {
    name = "孙悟空"
    age = 18

    sayHello() {
        console.log("Hello,我是", this.name)
    }
}

const p = new Person()

// console.log(p)
// console.log(p.constructor) 打印p的构造函数,

/* 
    访问一个对象的原型对象
        1.对象.__proto__  
        ==> console.log(p.__proto__)

        2.Object.getPrototypeOf(对象) 
        ==> console.log(Object.getPrototypeOf(p)) 

        ==> console.log(Object.getPrototypeOf(p) === p.__proto__) // 判断是否全等 输出:true
            
    原型对象中的数据:
        1. 对象中的数据(属性、方法等)
        2. constructor(对象的构造函数)

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

const obj = {} // obj.__proto__
console.log(p.__proto__.__proto__) 
console.log(p.__proto__.__proto__.__proto__) // 输出:null
class Person {
    name = "孙悟空"
    age = 18

    sayHello() {
        console.log("Hello,我是", this.name)
    }
}

const p = new Person()
const p2 = new Person()

// 所有的同类型对象它们的原型对象都是同一个,也就意味着,同类型对象的原型链是一样的.
console.log(p)
console.log(p2)
console.log(p.__proto__ === p2.__proto__) // 输出:ture


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

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

      p.sayHello = hello
      console.log(p.sayHello) // 输出:hello
      console.log(p2.sayHello) // 输出: sayHello() {console.log("Hello,我是", this.name)}

*/

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

class Cat extends Animal {

}
  
const cat = new Cat()

// cat --> Amimal --> object -->Object原型 --> null 
console.log(cat.__proto__) //输出: Animal {}

/* 
  原型链:
    - 读取对象属性时,会优先对象自身属性,
      如果对象中有,则使用,没有则去对象的原型中寻找
      如果原型中有,则使用,没有则去原型的原型中寻找
      直到找到Object对象的原型(Object的原型没有原型(为null))
      如果依然没有找到,则返回undefined

    - 作用域链,是找变量的链,找不到会报错
    - 原型链,是找属性的链,找不到会返回undefined
*/
尝试:
  - 函数的原型链是什么样子的?

  function Person2() {

}
  // Person2 --> 原型 --> Object原型 --> null
  console.log(Person2.__proto__) //输出:原型
  console.log(Person2.__proto__.__proto__) //输出:obj原型
  console.log(Person2.__proto__.__proto__.__proto__) //输出:null

  - Object的原型链是什么样子的?
  
  const Person3 = Object()
  
  // Person3 --> Object原型 --> null
  console.log(Person3.__proto__) //输出:原型
  console.log(Person3.__proto__.__proto__) //输出:obj原型
 

>修改原型

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

class Person {
    name = "孙悟空";
    age = 18;

    sayHello() {
        console.log("Hello,我是", this.name);
    }
}

const p = new Person();
const p2 = new Person();

//通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法 不要这么做
p.__proto__.run = () => { 
    console.log('我在跑~');
}

p.run(); // 输出: "我在跑~"
p2.run(); // 输出: "我在跑~"


/*   注意:千万不要通过类的实例去修改原型。
          1. 通过一个对象影响所有同类对象,这么做不合适
          2. 修改原型先得创建实例,麻烦
          3. 危险
*/

class Dog{

}

p.__proto__ = new Dog() //直接为对象赋值了一个新的原型  不要这么做
p.run(); //直接报错,因为原来的原型已经被覆盖
/* 
    处理通过__proto__能访问对象的原型外,还可以通过类的prototype属性,来访问实例的原型.
    修改原型时,最好通过通过类去修改.
    
    好处:
        1. 一修改就是修改所有实例的原型
        2. 无需创建实例即可完成对类的修改

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

console.log(Person.prototype) // 访问Person实例的原型对象
console.log(Person.prototype === p.__proto__) // 输出: true

Person.prototype.fly = () => {
  console.log("我在飞!")
}

p.fly()
p2.fly()

>_instanceof和hasOwn

  • instanceof检查的是对象的原型链上是否有该类实例

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

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

class Animal {}

class Dog extends Animal {}

const dog = new Dog()

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

console.log(dog instanceof Dog) // true 

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

console.log(dog instanceof Animal) // true

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

console.log(dog instanceof Object) // true
*/

//获取OBJ实例  
const obj = new Object()
console.log(obj.__proto__)  
console.log(Object.prototype)
dog.__proto__ / Dog.prototype


class Person {
    name = "孙悟空";
    age = 18;

    sayHello() {
        console.log("Hello,我是", this.name);
    }
}

// 使用in运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true
console.log("sayHello" in p)

// 对象.hasOwnProperty(属性名) (不推荐使用),用来检查一个对象的自身是否含有某个属性
console.log(p.hasOwnProperty("sayHello")) //输出:false

// Object.hasOwn(对象, 属性名)(推介的方法),用来检查一个对象的自身是否含有某个属性
 console.log(Object.hasOwn(p, "name"))

>旧类

  • 一个函数如果直接调用 xxx() 那么这个函数就是一个普通函数

  • 一个函数如果通过new调用 new xxx() 那么这个函数就是一个构造函数

/* 
    等价于:
        class Person{

        }
    
*/

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)


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)

>new运算符

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

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


function MyClass(){

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

2. 将构造函数的prototype属性设置为新对象的原型
    // newInstance.__proto__ = MyClass.prototype
}

3. 使用实参来执行构造函数,并且将新对象设置为函数中的this
 var mc = new MyClass()


4. 如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回(千万不要这么做)
   如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回
   通常不会为构造函数指定返回值

class Person{

    constructor(){

    }

}

new Person()

>总结

面向对象本质就是,编写代码时所有的操作都是通过对象来进行的。

    面向对象的编程的步骤:
        1. 找对象
        2. 搞对象
    
    学习对象:
        1. 明确这个对象代表什么,有什么用    
        2. 如何获取到这个对象
        3. 如何使用这个对象(对象中的属性和方法)

    对象的分类:
        内建对象
            - 由ES标准所定义的对象
            - 比如 Object Function String Number ....

        宿主对象
            - 由浏览器提供的对象
            - BOM、DOM

        自定义对象
            - 由开发人员自己创建的对象