15.面向对象

面向对象

面向对象(Object Oriented Programming)几乎是目前所有主流语言所必备的特点。什么是面向对象呢?回答这个问题,需要先明白另一个问题:什么是对象?

什么是对象

程序的本质是对现实事物的抽象。所谓抽象就是用一些手段把现实事物表示出来。比如,画人像就是对人的抽象,写一本人物的传记就是对一个人一生的抽象,你的个人简历就是对你的抽象。

程序的作用就是用代码去表示现实的事物,由于现实的事物都是极其复杂的,不可能在代码中体现出事物的所有具体细节。所以只能采用抽象的形式,提取出事物的特点,然后通过代码呈现。

在现实的世界中,无论事物再复杂,一旦被抽象进入到程序之中,都会被转换为一段数据来存储,这些数据就被我们称为对象(Object)。换句话说,所有的事物到了程序中都会变成对象。

日常生活中使用的数字,到了程序中变成了Number对象。日常生活中说的语言文字,到了程序中变成了String对象。用来上网的浏览器,到了程序中变成了Window对象。日常中使用的日期时间,到了程序中变成了Date对象。总之,在程序中一切都是对象!

你也许会疑问,如何通过对象来表示出一个现实的事物呢?现实的事物是非常复杂的,但在程序之中,把每一个事物都分成了两个部分,一部分是数据,还有一部分是功能。

拿人举例子,人的身高、姓名、性别、年龄、攻击力、敏捷等等都属于是人的数据,这些数据在对象中被称为属性。人可以吃饭、睡觉、攻击、跑这些种种的都是人所具备的功能,这些功能在对象中被称为方法。

无论是再复杂的事物在程序中都由属性和方法两个部分组成,只需要这两个部分即可体现出事物的所有特征,不信你自己想一下,你随便说出你具备的某个特点,它一定不会超出属性和方法的范畴。

举个例子,现在我需要在JS中表示一个人的信息,这个人叫猪八戒、年龄28、地址高老庄、他会睡觉。很显然猪八戒、28、高老庄这些属于属性,睡觉属于方法:

const zbj = {
     name:'猪八戒',
     age:28,
     address:'高老庄',
     sleep:function () {
         console.log(`${this.name}睡着了~~~`);
     }
 };

这样我们就将一个人的信息转换为了JS中的对象,以此类推所有的事物都可以转换为这样的对象。

面向对象呢?

知道什么是对象了,面向对象就简单了。所谓的面向对象指我们在编写程序时,所有的操作都是通过对象进行的。比如,表示数字,要先找到数字的对象Number。表示字符串,要找到字符串的对象String。进行数学运算,要找到数学运算的对象Math。刷新页面,要先找到表示浏览器地址栏的对象Location。也就是说所有的操作都要通过指定的对象进行。这样一来我们编写程序时大体上主要有两个步骤,步骤一:找对象,根据你要做的事情找到你需要的对象,当然有些时候没有满足你要求的对象,也许还要自己创建一个对象。步骤二:搞对象,搞对象即通过调用对象的属性或方法来完成你的需求。

面向对象本身的难点并不在于概念的理解,而是对象的定义,也就是我们如何能根据需要来定义一个对象。这就要求我们要具备两个能力,能力一:你得会定义对象。能力二:你得知道如何对事物进行抽象。抽象事物的能力需要一点一点锻炼,至于定义对象就相对简单了。

面向对象中的对象主要具有三个特点:封装、继承和多态。掌握了这三个特点即可轻松的创建一个你需要的对象。

使用Object创建对象的问题:

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

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

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

语法:

  • class 类名{ } //类名要使用大驼峰命名
  • const 类名 = class { } //不推荐使用
<script>
        //人对象的模板,抽象类
        class Proson {

        }
        //狗对象的模板,抽象类
        class Dog {

        }

        //调用函数创建对象
        const p1 = new Proson()
        const d1 = new Dog()

        console.log(p1); //Proson{}
        console.log(d1); //Dog{}
        console.log(p1 instanceof Proson); //true ,instanceof检查p1是否由Proson类创建
</script>

属性

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

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

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

        const p1 = new Person() //Object { name: "孙悟空", age: 18 }

        console.log(p1);
</script>

方法

<script>
        class Person {
            name = "孙悟空"

            //添加方法一
            test1 = function () {
                console.log("我是方法一");
            }

            //添加方法二(实例方法)只能通过实例来调用 p1.test2()
            //实例方法中this就是当前实例p1(谁调用就是谁)
            test2() {
                console.log("大家好我是:" + this.name);
            }

            //静态方法(类方法)只能通过类来调用 Person.test3()
            //静态方法中this指向当前类Person(谁调用就是谁)
            static test3() {
                console.log("我是静态方法", this);
            }
        }

        const p1 = new Person() //实例化Person对象
        console.log(p1);
        p1.test2() //大家好我是:孙悟空
        Person.test3() //我是静态方法
</script>

 

构造方法(构造函数)

其使用方法和Java的构造方法一样,只不过写法有点区别。

<script>
        class Person {
            /* 可写可不写,应为在构造函数中this.xxx也相当于在Person中添加属性
             name
             age
             gender 
             */
            //在类中可以添加一个特殊的方法construction
            //该方法我们称之为构造函数(构造方法)
            //构造函数会在我们调用类创建对象时执行
            constructor(name, age, gender) {
                // console.log("构造函数执行了~", name, age, gender);
                //可以在构造函数中,为实例属性进行赋值
                //在构造函数中,this表示当前所创建的对象
                this.name = name;
                this.age = age;
                this.gender = gender;
            }
        }
        const p1 = new Person("孙悟空", 18, "meal")
        const p2 = new Person("猪八戒", 28, "meal")
        console.log(p1); //Object { name: "孙悟空", age: 18, gender: "meal" }
        console.log(p2); //Object { name: "猪八戒", age: 28, gender: "meal" }
</script>

对象的三大特征

封装

  • 对象是一个用来存储不同属性的容器
  • 对象不仅存储属性,还要负责数据的安全
  • 直接添加到对象中的属性,并不安全,应为它们可以被任意修改

如何确保数据安全:

  • 私有化数据
    • 将需要保护的数据设置为私有,只能在类内部使用
  • 提供给getter和setter方法来开放数据的操作
    • 可以控制属性的读写权限
    • 可以在方法中对属性值进行验证(在set中写判断语句)

封装主要用来保护数据的安全

  • 实现封装的方式:
  • 属性私有化  加#
  • 通过getter和setter方法来操作属性
    • get  属性名( ){
    •   return this.#属性
    • }
    • set  属性名( 参数 ){
    •       this.#xxx = 参数
    • }

私有属性完成封锁

getter和setter方法提供数据操作(方法一)不建议使用

<script>
        class Person {
            // #号代表私有属性,只能在Person类内部使用,出了Person类就访问不了(封装)
            #name
            #age
            #gender
            constructor(name, age, gender) {
                this.#name = name
                this.#age = age
                this.#gender = gender
            }
            getName() {
                return this.#name;
            }
            setName(name) {
                this.#name = name
            }
            getAge() {
                return this.#name;
            }
            setAge(age) {
                if (age>=0) { //对属性的值进行判断
                    this.#age = age
                }
            }
        }
        const p1 = new Person("孙悟空", 18, "meal")
        console.log(p1.getName()); //孙悟空
        p1.setAge(-11) //不做修改
</script>

 getter和setter方法提供数据操作(方法二)

<script>
        class Person {
            // #号代表私有属性,只能在Person类内部使用,出了Person类就访问不了(封装)
            #name
            #age
            #gender
            constructor(name, age, gender) {
                this.#name = name
                this.#age = age
                this.#gender = gender
            }

            get age() {
                return this.#age;
            }
            set age(age) {
                if (age >= 0) { //对属性的值进行判断
                    this.#age = age
                }
            }
        }
        const p1 = new Person("孙悟空", 18, "meal")
        p1.age = 11 //相当于p1.setAge(11) 
        console.log(p1.age); //相当于p1.getAge(11) 
        console.log(p1); //Object { #name: "孙悟空", #age: 11, #gender: "meal" }      
 </script>

多态

  • 在JS中不会检查参数类型,所以这就意味着任何数据都可以作为参数传递
  • 要调用某个函数,无需指定类型,只要对象满足某些条件即可
  • 如果一个东西走路像鸭子,叫起来像鸭子,那么它就是鸭子
  • 多态为我们提供了灵活性
<script>
        class Person {
            constructor(name) {
                this.name = name
            }
        }
        class Dog {
            constructor(name) {
                this.name = name
            }
        }

        const person = new Person("Tom")
        const dog = new Dog("旺财")

        // 定义一个函数,它以对象作为参数,它可以输出hello并打印对象的name属性
        function sayHello(obj) {
            console.log("hello" + obj.name);
        }

        sayHello(person)
        sayHello(dog)
</script>

只要类里面有name属性都可以使用sayHello( )函数打印出各自的name。------多态

继承

  •  可以通过extends关键字来完成继承
  • 当一个类继承另一个类时,就相当于将另一个类中的代码复制到当前类中(简单理解)
  • 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
  • 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展
<script>
        class Animal {
            constructor(name) {
                this.name = name;
            }
            test() {
                console.log(this.name + "在叫");
            }
        }
        class Dog extends Animal {
            //继承相当于拥有父类的一切属性和方法
        }
        const dog = new Dog("旺财");
        console.log(dog); //Object { name: "旺财" },还有一个test方法只不过被隐藏看不到(对象结构有讲到-原型)
        dog.test() //旺财在叫
</script>

继承

  • 通过继承可以在不修改一个类的情况下对其进行扩展
  • OCP 开闭原则
    • 程序应该对修改关闭,对扩展开放
<script>
        class Animal {
            constructor(name) {
                this.name = name;
            }
            test() {
                console.log(this.name + "在叫");
            }
        }
        class Dog extends Animal {
            //继承相当于用于父类的一切属性和方法
            //重写父类方法
            test() {
                console.log(this.name + "汪~汪~叫");
            }
        }
        class Cat extends Animal {
            // 重写构造函数
            constructor(name, age) {
                // 重写构造函数时,构造函数的第一行代码必须是super()
                super(name); //调用父类的构造函数
                this.age = age;
            }
            test() {
                super.test(); //在方法中可以使用super来引用父类方法
                console.log("喵喵咪哦啊");
            }
        }
        const dog = new Dog("旺财");
        console.log(dog); //Object { name: "旺财" },还有一个test方法只不过被隐藏看不到
        dog.test() //旺财汪~汪~叫

        const cat = new Cat("汤姆", 3)
        cat.test()
</script>

重写

重写是对继承的扩展,子类继承了父类的方法。

  • 直接使用父类方法
  • 在子类中重新书写父类的方法(重写)
<script>
        class Animal {
            constructor(name) {
                this.name = name;
            }
            test() {
                console.log(this.name + "在叫");
            }
        }
        class Dog extends Animal {
            //继承相当于用于父类的一切属性和方法
            //重写父类方法
            test() {
                console.log(this.name + "汪~汪~叫");
            }
        }
        const dog = new Dog("旺财");
        console.log(dog); //Object { name: "旺财" },还有一个test方法只不过被隐藏看不到
        dog.test() //旺财汪~汪~叫
</script>

 总结

封装 —— 安全性

继承 —— 扩展性

多态 —— 灵活性

对象的结构

原型对象

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

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

 

       class Animal {
            constructor(name) {
                this.name = name;
            }
            test() { //位于原型对象中
                console.log(this.name + "在叫");
            }
        }

访问原型对象

  • 访问一个对象的原型对象
    • 对象.__proto__(不建议使用,应为可以随时修改原型,下面修改原型就是用它)
    • Object.getPrototypeOf(person)
  • 原型对象中的数据:
    • 1.对象中的数据(属性,方法等)
    • 2.constructor(对象的构造函数)
  • 注意:
    • 原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同。
    • person对象的原型链:person对象-->object--> Object原型--> null
  • 原型链:
    • 读取对象属性时,会优先对象自身属性,
      • 如果对象中有,则使用,没有则去对象的原型中寻找
      • 如果原型中有,则使用,没有则去原型的原型中寻找
      • 直到找到Object对象的原型,Object的原型没有原型(为null)
        • 如果依然没有找到返回undefined        
  • 作用域链,是找变量的链,找不到会报错
  • 原型链,是找属性的链,找不到会返回undefined
    <script>
        class Person {
            name = "孙悟空"
            age = 18

            test() {
                console.log("你好,我是:" + tthis.name);
            }
        }
        const person = new Person()

        console.log(Object.getPrototypeOf(person));
        console.log(Object.getPrototypeOf(person) == person.__proto__); //true
        console.log(person.__proto__);
        console.log(person.__proto__.__proto__); //null
</script>

原型的一些补充

  • 所有同类型对象,他们的原型都是同一个
    • 也就意味着,同类型对象的原型链是一样的
  • 原型的作用:
    • 原型相当于一个公共的区域,可以被所有该类实例访问
      • 可以将该类实例中,所有的公共属性(方法)统一存储到原型中
      • 这样我们只需要创建一个属性,即可被所有实例访问
  • JS中继承就是通过原型来实现的,
    • 当继承时,子类的原型就是一个父类的实例
  • 在对象中有些值是队形独有的,像属性(name,age,gender)每个对象都应有自己的值
    • 但是有些值对于每个对象都是一样的,像各种方法,对于一样的值没必要重复的创建
<script>
        class Person {}
        const person1 = new Person()
        const person2 = new Person()

        //所有同类型对象,他们的原型都是同一个 
        console.log(person1.__proto__ == person2.__proto__); //true

        class Man extends Person { } //男人类继承人类
        class XiaoMing extends Man { } //小明继承男人类
        
        const xiaoming = new XiaoMing()
        //xiaoming对象-->Man-->Person--object-->Object原型-->null
        console.log(xiaoming);
</script>

修改原型

大部分情况下,我们是不需要修改原型对象(面试爱问的一些犄角旮旯的问题)

新版ES6写代码的时候基本用不上,除非你改一些老的项目要用ES5,ES3去写。

注意:

  • 千万不要通过类的实例去修改原型
    • 通过一个对象影响所有同类对象,这么做不合适
    • 修改原型先得创建实例,麻烦
    • 危险
  • 除了通过_ _proto_ _能访问原型外,
    • 还可以通过类的prototype属性,来访问实例的原型
  • 修改原型时,最好通过类去修改
    • 好处:
      1. 一修改就是修改所有的实例的原型
      2. 无需创建实例即可完成对类的修改
    • 原则:
      1. 原型尽量不要手动改
      2. 要改也不要通过实例去修改
      3. 通过 类.prototype属性去修改
      4. 最好不要直接给prototype去赋值(prototype = xxx)但是可以添加属性 类.prototype.属性名 ="" 或 (){}

代码一,通过类的实例修改原型,相当于删除原有的基因把Dog的基因强加在Person身上,不伦不类。(不要这么做)

    <script>
        class Person {
            name = "孙悟空"
            age = 18
            test() {
                console.log("你好,我是:" + tthis.name);
            }
        }

        class Dog { }

        const p1 = new Person()
        const p2 = new Person()

        //通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法,不要这么做
        p1.__proto__.run = () => {
            console.log("我在跑~");
        }
        p1.__proto__ = new Dog() //直接为对象赋值了一个新的原型,不要这么做

        console.log(p1);
        console.log(p2);
    </script>

代码二,通过类的prototype属性修改原型,相当于不修改原有基因的情况下添加新的基因,进化。(应用于所有所有依赖此原型的类)

<script>
        class Person {
            name = "孙悟空"
            age = 18
            test() {
                console.log("你好,我是:" + tthis.name);
            }
        }

        const p1 = new Person()
        const p2 = new Person()

        Person.prototype.fly = () => { //在Person实例的原型中添加属性
            console.log("我在飞~");
        }

        console.log(Person.prototype); //访问Person实例的原型对象

        p1.fly()
        p2.fly()
</script>

instanceof和hasOwnProperty

instanceof

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

  • instanceof检查的是对象的原型链上是否有该类的实例
  • 只要原型链上有该类的实例,就会返回true
    • dog -> Animal的实例 -> Object实例 -> Object原型
  • Object是所有对象的原型,所任何对象和Object进行instanceof运算都会返回true
<script>
        class Animal { }
        class Dog extends Animal { }

        const dog = new Dog()
        //dog -> Animal的实例 -> Object实例 -> Object原型
        //Object是所有对象的原型
        console.log(dog instanceof Dog); //true
        console.log(dog instanceof Animal); //true
        console.log(dog instanceof Object); //true

        // 如何访问Object?(最大的原型)
        const obj = new Object();
        console.log(obj.__proto__); //通过实例.__proto__,获取Object的原型
        console.log(Object.prototype); //通过类的prototype属性,获取Object的原型
</script>

in

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

<script>
        class Person {
            name = "孙悟空"
            age = 18
            test() {
                console.log("你好,我是:" + tthis.name);
            }
        }

        const p = new Person();

        console.log("test" in p) //true
</script>

hasOwnProperty

对象.hasOwnProperty(属性名)--->已经是老方法了不建议再使用。

  用来检查一个对象的自身是否含有某个属性

<script>
        class Person {
            name = "孙悟空"
            age = 18
            test() {
                console.log("你好,我是:" + tthis.name);
            }
        }
        const p = new Person();
        // console.log("test" in p)
        console.log(p.hasOwnProperty("name")); //true
        console.log(p.hasOwnProperty("test")); //false
        console.log(Person.prototype.hasOwnProperty("test")); //true
</script>

hasOwn

相比于hasOwnProperty这种实例方法,目前官方更推荐hasOwn这种静态方法。虽然效果都一样。

  Object.hasOwn(对象,属性名)

    用来检查一个对象的自身是否含有某个属性

<script>
        class Person {
            name = "孙悟空"
            age = 18
            test() {
                console.log("你好,我是:" + tthis.name);
            }
        }
        const p = new Person();
        // console.log("test" in p)
        // console.log(p.hasOwnProperty("name")); //true
        // console.log(p.hasOwnProperty("test")); //false
        // console.log(Person.prototype.hasOwnProperty("test")); //true
        console.log(Object.hasOwn(p, "name")) //true
        console.log(Object.hasOwn(p, "test")) //false
        console.log(Object.hasOwn(Person.prototype, "test")) //true
</script>

 

posted @ 2022-11-10 21:54  莫扎特03  阅读(31)  评论(0)    收藏  举报