JavaScript中的面向对象编程

本文最后更新于:1 个月前

  1. 类是对象模版,可以将对象中的属性和方法直接定义在类中,定义后就可以直接通过类来创建对象
  2. 通过同一个类创建的对象,我们称之为同对象,可以通过instanceof来检查一个对象是否是某个类创建,则我们称该对象是这个类的实例
  3. 通过类创建对象的语法new 类()

示例:

1
2
3
4
5
6
7
class Person//person类专门用来创建人的对象
{

}
const p1 = new Person //p1是person的实例

console.log(p1 instanceof Person)//true

属性

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

使用static声明的属性为静态属性,不可以通过实例访问,只能通过类访问

1
2
3
4
5
6
7
class Person
{
name = 'Tom'//Person的实例属性name
age = 18//实例属性只能通过实例访问 p1.age
static test = 'test' //静态属性只能通过类访问 Person.test
}
const p1 = new Person()

构造函数

当我们在类中直接指定值的时候,意味着我们创建的所有对象都是这个值

1
2
3
4
5
6
7
8
9
class Perosn{
name = "Tom"
age = 18
gender = '男'

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

如果我们想自己给类的属性赋值怎么办?

此时我们可以使用一个特殊的方法——构造函数(constructor)

构造函数会在我们调用类创建对象时执行,我们可以在构造函数中为属性赋值,在构造函数中,this表示当前创建的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person2
{
name
age
gender

constructor(name,age,gender)
{
this.name = name
this.age = age
this.gender = gender
}
}

封装

  • 封装、继承、多态是面向对象编程三大特性

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

如何保证数据安全?

  1. 私有化数据

    • 将需要保护的数据设置为私有,只能在类内使用,私有属性必须事先说明
  2. 提供类内公有方法来间接读取类内私有属性,相当于提供一个接口,该方法的好处是不仅可以控制属性的读写权限还可以在方法中对属性值进行控制

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
class Person{

#address = 'Beijing' // 实例使用#开头就变成了私有属性 私有属性只能在类内部访问
#name;
#age;
#gender
constructor(name,age,gender){
this.#name = name
this.#age = age
this.#gender = gender
}
sayHello(){
console.log(this.#name)
}

getName(){//getter方法,用来读取属性
return this.#name
}
setAge(age){//setter方法,用来设置属性
if(age>=0){
this.#age = age
}
}
get gender(){
return this.#gender
}
}
const p1 = new Perosn('Tom',18,'男')

console.log(p1.gender())

多态

  • 在Js中不会检查参数的类型,这就意味着任何数据都可以作为参数传递

  • 要调用某个函数,无需指定的类型,只要对象满足某些条件即可

  • 多态为我们提供了灵活性

继承

通过extends关键字来完成继承

简单来说,当一个类继承另一个类时,就相当于将另一个类复制到了代码中

在继承中,被继承的类称为父类,继承的类称为子类

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

1
class 子类 extends 父类

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

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
class Animal{
constructor(name){
this.name = name
}
sayHello(){
console.log("动物在叫")
}
}

class Cat extends Animal{
constructor(name ,age){
//重新构造函数时,构造函数的第一行代码必须为super()
super(name)//this.name = name
this.age = age
}
sayHello(){
//调用父类的sayHello
// super.sayHello//动物在叫
console.log("喵喵喵")
}

}
const cat = new Cat("汤姆",5)

cat.sayHello()//喵喵喵

注意,如果你想调用父类中的方法活着构造函数,你需要用super()来调用父类的属性

对象的结构

1
2
3
4
5
6
7
8
9
10
class Person{
name = 'Tom'
age = 18

sayHello(){
console.log("Hello,",this.name)
}
}
const p = new Person
console.log(p)

如果你打印出这个对象,你会发现可以看见里面的name,age但是看不见sayHello方法

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

  1. 对象自身

  • 直接通过对象添加的属性,位于对象自身中

  • 在类中通过 x=y 添加的属性位于对象自身中

  1. 原型对象

  • 在对象中还有一些内容会存储到其他的位置

  • 在对象中会有一个属性来存储原型对象, 这个属性叫做__proto__,原型对象也负责为对象存储属性

  • 当我们访问对象中的属性时,会优先访问对象自身的属性,如果找不到,则去原型对象中去找

  • 会添加到原型对象中的情况

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

原型对象

原型对象中的数据结构:

  1. 对象中的数据(属性,方法等)

  2. constructor(对象的构造函数)

原型链

原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同,原型链最后指向NULL

读取对象属性时,优先在对象本身中寻找,如果对象中有,则使用,没有则去对象的原型中找,如果原型中有,则使用,没有则去原型的原型中去寻找,直到找到Object对象的原型,Object的原型没有原型(null),如果依然没有找到,返回Undefined。

区分作用域链和原型链

  • 作用域链,找变量的链,找不到会报错

  • 原型链,是找属性的链,找不到会返回Undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
name = 'Tom'
age = 18

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

const p = new Person
console.log(p)//可以看见name 和 age 看不见方法
console.log(p.__proto__)//原型对象,不建议这么访问

concole.log(Object.getPrototypeOf(p))//安全访问

原型的作用

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

  • 原型的作用

    • 相当于一个公共的区域,可以被所有的类访问。
    • 可以将一个该类实例中所有的公共属性(方法)统一存储到原型中,这样我们只需要创建一个属性,即可被所有实例访问
    • JS中继承就是通过原型实现的
    • 继承时,子类的原型就是一个父类的实例。
    • 在对象中有些属性是对象独有的,像属性,每个对象都应该有自己值,但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没有必要重复的创建

    修改原型

    大部分情况下不需要修改原型

    注意!

    千万不要通过类的实例去修改原型!

    1. 通过一个对象影响所有同类对象,这样不合适

    2. 修改原型需要先创建实例,繁琐

    3. 危险

  • 处理通过__proto__能访问对象的原型外,还可以通过类的prototype属性来访问实例的原型

好处

  1. 修改一个就是修改所有实例的原型

  2. 无需创建实例即可完成对类的修改

原则

  1. 尽量不要改

  2. 通过类.prototype来改

  3. 最好不要直接给prototype赋值

instanceof 和 hasOwn

1
2
3
4
5
6
7
8
class Animal{}

class Dog extends Animal{}

const dog = new Dog

console.log(dog instanceof Dog)//true
console.log(dog instanceof Animal)//true`

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person{
name = 'Tom'
age = 18

sayHello(){
console.log("Hello")
}
}

const p = new Person

console.log("name in p")// true
console.log("sayHello in p")//true
console.log(p.hasOwnProperty("sayHello"))//flase
console.log(Object.hasOwn(p,"sayHello"))//flase

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

  • 对象.hasOwnProperty(属性名)

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

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

new 运算符

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

使用new的时候发生呢干了什么?

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

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

  2. 指定新对象的原型指定为构造函数的prototype的属性

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

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


JavaScript中的面向对象编程
https://blog.seasalt-haiyan.top/2024/03/05/JavaScript中的面向对象编程/
作者
Xu Haoyang
发布于
2024年3月5日
更新于
2024年3月5日
许可协议