七 JavaScript面向对象

七 面向对象(OOP)

7.1 面向对象的概念

程序: 程序就是对现实世界的抽象(照片就是对人的抽象)。

对象:一个事物抽象到程序中后就变成了对象。

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

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

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

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

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

数据:姓名、年龄、身高、体重… …

功能:学习、吃饭、睡觉… …

const people = {
    name:"Joe",
    age:18,
    height:180,
    weight:"70kg",

    study(){
        console.log("学习");
    },
    eat(){
        console.log("吃饭");
    },
    sleep(){
        console.log("睡觉");
    }
}

7.2 类

使用object创建对象的问题:

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

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

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

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

语法:

class 类名{} 同时类名要使用大驼峰命名,这是规范

const 类名 = class {}

class Person{
    
}
const person = new Person()
console.log(person);
console.log(typeof person);
/**
	运行结果:
		>Person
		object
*/

所以其实class关键字定义的东西根本上也是一个object,事实上单词object的含义除了有物体、实体以外,还有对象的含义

使用instanceof关键字来判断一个对象是否是类的实例(上面提到过什么是实例)

7.3 属性

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

  • 类的代码块默认是严格模式
  • 类的代码块是用来设置对象属性的并不是什么代码都能写
  • 可以定义实例属性,实例属性只能通过实例访问
  • 使用static关键字声明的属性,是静态属性(类属性),静态属性只能通过类去访问
class Person{
    name = "Joe" // 定义了一个实例属性
    age = 18 // 又定义了一个实例属性
    static test = "定义了一个静态属性"
}
const person = new Person() // 对象person是Person类的一个实例
console.log(person.name); // 使用实例调用实例属性name
console.log(Person.name);// 尝试使用类类调用类中定义的实例属性name
/**
	执行结果:
		Joe
		Person
	为什么会打印Person呢?原来啊,类里面自带一个name属性,使用Person.name获取到的是类自带的name属性,其值是类名	
*/

我们将name换成age

class Person{
    name = "Joe" // 定义了一个实例属性
    age = 18 // 又定义了一个实例属性
    static test = "定义了一个静态属性"
}
const person = new Person() // 对象person是Person类的一个实例
console.log(person.age); // 使用实例调用实例属性name
console.log(Person.age);// 尝试使用类类调用类中定义的实例属性name
/**
	执行结果:
		18
		undefined
	结论:使用实例正确获取了实例属性的值	
		  使用类却不能正确获取实例属性的值
*/

下面来看静态属性:

class Person{
    name = "Joe" // 定义了一个实例属性
    age = 18 // 又定义了一个实例属性
    static test = "定义了一个静态属性"
}
const person = new Person() // 对象person是Person类的一个实例
console.log(person.test); // 使用实例调用实例属性name
console.log(Person.test);// 尝试使用类类调用类中定义的实例属性name
/**
	执行结果:
		undefined
		定义了一个静态属性
*/

7.4 方法

方法就是对象里面定义的函数

class Person{
    hello(){ // 实例方法
        console.log("Hello world");
    }
    static hello(){ // 使用static修饰的静态方法
        console.log("Hello world");
    }
}
const person = new Person()
person.hello() // "Hello world"
Person.hello() // "Hello world"

定义了一个实例方法和一个静态方法,静态方法和静态变量一样,只能通过类来调用。

如果使用类去调用实例方法,会报is not a function错误

7.5 构造函数

在类中可以添加一个特殊的方法constructor

该方法我们称为构造函数(构造方法),构造函数会在我们调用类创建对象时执行。

class Person{
    constructor(){
        console.log("构造函数被执行");
    }
    hello(){ // 实例方法
        console.log("Hello world");
    }
    static hello(){ // 使用static修饰的静态方法
        console.log("Hello world");
    }
}
console.log("实例还未创建");
let person = new Person()
console.log("实例创建完成");

/**
	执行结果:
		实例还未创建
		构造函数被执行
		实例创建完成
*/

可以在构造函数中,为实例属性进行赋值

class Person{
    constructor(name,age){
        console.log(this); // >Person,即构造方法内的this指向的是Person对象 
        this.name = name // 将name添加入Person对象中
        this.age = age // 将age添加入Person对象中
    }
}
let person = new Person("张三",18)
console.log(person.age); // 18
console.log(Person.age); // 18

7.6 面向对象的特点

三大特点:继承、封装、多态

7.6.1 封装

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

如何确保数据的安全:

  1. 私有化数据:将需要保护的数据设置为私有,只能在类内部使用

  2. 提供setter和getter方法来开放对数据的操作

    属性设置私有,通过getter setter方法操作属性带来的好处:

    1. 可以控制属性的读写权限
    2. 可以在方法中对属性的值进行验证
7.6.1.1 私有化数据

#

加了#的属性,外部无法访问

class Person{
    #name="张三"
    #age = 18
    test = "非私有化"
}
const person = new Person()
console.log(person.age); // undefined ,所以 #age和age不是同一个属性
console.log(person.#age);
// SyntaxError: Private field '#age' must be declared in an enclosing class

加了#的属性,内部可以访问

class Person{
    #animal = "猫"
    changAnimal(){
        this.#animal = "狗" // 内部访问私有变量
        return this.#animal
    }
}
const person = new Person()
console.log(person.changAnimal()); // 狗
7.6.1.2 setter和getter方法

根据私有属性的特点:类内可以访问和修改,类外部无法访问和修改,则可以向外部提供一些方法来实现对私有属性的修改。虽然私有属性的操作增加了复杂性,但其提供了安全性和规范性的保障。

通过getter setter方法操作属性带来的好处

  1. 可以控制属性的读写权限
  2. 可以在方法中对属性的值进行验证
class Person{
    #animal = "猫"
    getAnimal(){
        return this.#animal
    }
    setAnimal(value){
        this.#animal = value
    }
}
const person = new Person()
console.log("修改前:",person.getAnimal()); 
person.setAnimal("狗")
console.log("修改后:",person.getAnimal());
/**
	执行结果:
		修改前:猫
		修改后:狗
*/

7.6.2 多态

多态感觉没什么好说的…就是要理解…就是太偏理论了…我也说不上来,可能就是定义一个函数,根据传入参数的不同会有不同的执行结果…

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

7.6.3 继承

可以通过extends关键字来完成继承

当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)

继承发生时,被继承的类称为 父类(超类),继承的类称为 子类

通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展

通过继承可以在不修改一个类的情况下对其进行扩展

原则:

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

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

  • 在方法中可以使用super来引用父类的方法

封装 —— 安全性

继承 —— 扩展性

多态 —— 灵活性

小试牛刀:

class Animal{
    type
    age
    call(){
        console.log(this.animal+"叫");
    }
}

class Cat extends Animal{
    animal = "猫"
}

const cat = new Cat()
cat.call()

重写构造函数

class Animal{
    type
    age
    constructor(type,age){
        this.type = type
        this.age = age
    }
    call(){
        console.log(this.animal+"叫");
    }
}

class Cat extends Animal{ 
    constructor(type,age,gender){
        super(type,age) //重写构造函数时,构造函数的第一行代码必须为super(),意思是调用父类的构造函数
        this.gender = gender
    }
    animal = "猫"
    Hello(){
        if(this.gender === "雄"){
            super.call() // 使用super关键字调用父类中的方法
        }
    }
}
const cat = new Cat("猫",2,"雄")
cat.Hello()
console.log("type:"+cat.type+"\n"+"age:"+cat.age+"\ngender:"+cat.gender);
/**
	输出:
		猫叫
		type:猫
        age:2
        gender:雄	
*/

7.7 对象的结构

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

  1. 对象自身

    • 直接通过对象所添加的属性,位于对象自身中
    • 在类中通过 x = y 的形式添加的属性,位于对象自身中
  2. 原型对象(prototype)

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

访问一个对象的原型对象:对象.__proto__ or Object.getPrototypeOf(对象)

原型对象中的数据:

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

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

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

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

原型链:读取对象属性时,会优先对象自身属性,

如果对象中有,则使用,没有则去对象的原型中寻找。直到找到Object对象的原型(Object的原型没有原型(为null))。

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

**作用域链:**是找变量的链,找不到会报错

**原型链:**是找属性的链,找不到会返回undefined

7.8 原型对象

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

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

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

在对象中有些值是对象独有的,像属性(name,age,gender)每个对象都应该有自己值, 但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没必要重复的创建,所以就可以将他保存在原型里。

class Person{
    name
    getInfo(){
        console.log("获取用户信息");
    }
}
console.log(Person.__proto__); 

上述代码让我们打印一下Person对象的原型

在这里插入图片描述

这是个啥…,不知道是啥,但知道这个就被称为Person类的原型对象

然后我创建一个Person对象的实例person(一个P大写一个不大写,要注意)再输出他的原型对象

在这里插入图片描述
我以为Person对象的实例person的原型对象应该是Person,但显然我的猜测是错误的。

我们再来比较:

console.log(person.__proto__ === Person.__proto__); // false

显然证明Person实例的原型和Person类的原型不是同一个原型

那么Person实例的原型的原型和Person类的原型的原型呢?

console.log(person.__proto__.__proto__ === Person.__proto__.__proto__); // true

将结果证明Person实例的原型的原型和Person类的原型的原型是同一个原型

继续找下去,我们发现原型链的尽头是null


我们再看如下代码:

class Animal{

}

class Cat extends Animal{

}

class TomCat extends Cat{

}
console.log(TomCat.__proto__);

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

故上述代码打印的结果就是

在这里插入图片描述

7.9 修改原型

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

注意:千万不要通过类的实例去修改原型

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

除了通过__proto__能访问对象的原型外,还可以通过类的prototype属性,来访问实例的原型。

即:

class Person{
    name
    getInfo(){
        console.log("获取用户信息");
    }
}
const person = new Person()
console.log(person.__proto__ === Person.prototype); // true

修改原型时,最好通过通过类去修改

好处:

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

原则:

  1. 原型尽量不要手动改
  2. 要改也不要通过实例对象去改
  3. 通过 类.prototype 属性去修改
  4. 最好不要直接给prototype去赋值
class Person{
    name
}
const person1 = new Person()
const person2 = new Person()
Person.prototype.getInfo = function(){
    console.log("获取用户信息");
}
person1.getInfo()
person1.getInfo()
console.log(person1.getInfo);

在这里插入图片描述

使用prototype给原型添加方法后,所有实例都可以访问该方法

7.10 instance of

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

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

    • 只要原型链上有该类实例,就会返回true
class Animal{

}

class Cat extends Animal{

}

class TomCat extends Cat{

}
const tomcat = new TomCat()
console.log(tomcat instanceof Cat);  // true
console.log(tomcat instanceof Animal); // true
console.log(tomcat instanceof TomCat); // true

7.11 in运算符

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

class Animal{
    age
}

class Cat extends Animal{

}

class TomCat extends Cat{

}
const tomcat = new TomCat()
console.log("age" in tomcat); // true

7.12 hasOwn

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

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

二者的区别:

const obj = Object.create(null) // 创建一个空对象
console.log(obj); // 打印一下创建的对象
// console.log(obj.hasOwnProperty("name")); // 使用hasOwnProperty报错:obj.hasOwnProperty is not a function
console.log(Object.hasOwn(obj,"name")); // false

7.13 旧类

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

  • 一个函数如果直接调用 xxx() 那么这个函数就是一个普通函数
  • 一个函数如果通过new调用 new xxx() 那么这个函数就是一个构造函数(当使用 new 关键字调用函数时,该函数将被用作构造函数。)

在这里插入图片描述

function Person(){
    this.name = "张三"
    this.age = 18
    this.getInfo = function(){
        return "name:"+this.name+",age:"+this.age
    }
}
// 将方法添加到原型中
Person.prototype.getAge = function(){
    return this.age
}
Person.test = "我是一个只能被类控制的静态变量"
Person.testMethod = function(){
    console.log("我是一个只能被类调用的静态方法");
}

Person() // 函数调用
const p = new Person() // 创建实例对象
console.log(p);
console.log(p.getInfo()); // 函数调用
console.log(p.name); // 获取参数
console.log(p.getAge()); // 函数调用,调用的是保存在原型中的函数
console.log(p.test);
console.log(Person.test);
Person.testMethod()
p.testMethod()

在这里插入图片描述

根据上述代码我们可以看到,Person类的声明、Person原型中添加属性和方法、向Person中添加静态属性和静态方法 三者的代码产生割裂,一个比较好的实践是使用立即执行函数来将类的构建工作汇总(独立作用域隔离)到一起。

var Person = (function(){
    function Person(){
        this.name = "张三"
        this.age = 18
        this.getInfo = function(){
            return "name:"+this.name+",age:"+this.age
        }
    }
    // 将方法添加到原型中
    Person.prototype.getAge = function(){
        return this.age
    }
    Person.test = "我是一个只能被类控制的静态变量"
    Person.testMethod = function(){
        console.log("我是一个只能被类调用的静态方法");
    }
    return Person
})()

Person() // 函数调用
const p = new Person() // 创建实例对象
console.log(p);
console.log(p.getInfo()); // 函数调用
console.log(p.name); // 获取参数
console.log(p.getAge()); // 函数调用,调用的是保存在原型中的函数
console.log(p.test);
console.log(Person.test);
Person.testMethod()
p.testMethod()

7.13.1 旧类继承

我们之前说过,**JS中继承就是通过原型来实现的,当继承时,子类的原型就是一个父类的实例。**所以旧类实现继承使用的就是将子类的原型设置成父类的实例。使用prototype关键字

const Animal = (function(){
    function Animal(){

    }
    return Animal
})()
const Cat = (function(){
    function Cat(){

    }
    Cat.prototype = new Animal()  // 这条语句实现继承
    return Cat
})()
var cat = new Cat()
var animal = new Animal()
console.log(cat.__proto__);

在这里插入图片描述

7.14 new运算符

new运算符是创建对象时要使用的运算符,使用new时,到底发生了哪些事情

当使用new去调用一个函数时,这个函数将会作为构造函数调用,

使用new调用函数时,将会发生这些事:

  1. 创建一个普通的JS对象(Object对象 {}), 为了方便,称其为新对象
  2. 将构造函数的prototype属性设置为新对象的原型
  3. 使用实参来执行构造函数,并且将新对象设置为函数中的this
  4. 如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回(千万不要这么做)如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回 通常不会为构造函数指定返回值。
// 旧类
function Test(){
    return {} // 返回一个非原始值(数字、字符串、布尔...)
}
function Test2(){
    return 1 // 返回一个原始值(数字、字符串、布尔...)
}

const test = new Test()
console.log(test); // {} 返回return的结果
const test2 = new Test2()
console.log(test2); // >test 返回一个test2实例

// 使用class定义的类,结果是一样的
class Person{
    constructor(){
        return {}
    }
}
const person = new Person() 
console.log(person);
debugger

7.15 总结

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

学习对象:

  1. 明确这个对象代表什么,有什么用
  2. 如何获取到这个对象
  3. 如何使用这个对象(对象中的属性和方法)

对象的分类:

  • 内建对象

    • 由ES标准所定义的对象

    • 比如 Object Function String Number …

  • 宿主对象

    • 宿主对象
    • BOM( Browser Object Model)、DOM(Document Object Model ,文档对象模型
  • 自定义对象

    • 由开发人员自己创建的对象
  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值