七 面向对象(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
创建对象的问题:
- 无法区分出不同类型的对象
- 不方便批量创建对象
在JS中可以通过类(class)来解决这个问题:
-
类是对象模板,可以将对象中的属性和方法直接定义在类中。定义后,就可以直接通过类来创建对象。
-
通过同一个类创建的对象,我们称为同类对象,可以使用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 封装
对象就是一个用来存储不同属性的容器,对象不仅存储属性,还要负责数据的安全。直接添加到对象中的属性,并不安全,因为它们可以被任意的修改。
如何确保数据的安全:
-
私有化数据:将需要保护的数据设置为私有,只能在类内部使用
-
提供setter和getter方法来开放对数据的操作
属性设置私有,通过getter setter方法操作属性带来的好处:
- 可以控制属性的读写权限
- 可以在方法中对属性的值进行验证
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方法操作属性带来的好处
- 可以控制属性的读写权限
- 可以在方法中对属性的值进行验证
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 对象的结构
对象中存储属性的区域实际有两个:
-
对象自身
- 直接通过对象所添加的属性,位于对象自身中
- 在类中通过 x = y 的形式添加的属性,位于对象自身中
-
原型对象(prototype)
- 对象中还有一些内容,会存储到其他的对象里(原型对象)
- 在对象中会有一个属性用来存储原型对象,这个属性叫做
__proto__
- 原型对象也负责为对象存储属性,
- 当我们访问对象中的属性时,会优先访问对象自身的属性,
- 对象自身不包含该属性时,才会去原型对象中寻找
- 会添加到原型对象中的情况:
- 在类中通过xxx(){}方式添加的方法,位于原型中
- 主动向原型中添加的属性或方法
访问一个对象的原型对象:对象.__proto__
or Object.getPrototypeOf(对象)
原型对象中的数据:
- 对象中的数据(属性、方法等)
- 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 修改原型
大部分情况下,我们是不需要修改原型对象
注意:千万不要通过类的实例去修改原型
- 通过一个对象影响所有同类对象,这么做不合适
- 修改原型先得创建实例,麻烦
- 危险
除了通过__proto__能访问对象的原型外,还可以通过类的prototype属性,来访问实例的原型。
即:
class Person{
name
getInfo(){
console.log("获取用户信息");
}
}
const person = new Person()
console.log(person.__proto__ === Person.prototype); // true
修改原型时,最好通过通过类去修改
好处:
- 一修改就是修改所有实例的原型
- 无需创建实例即可完成对类的修改
原则:
- 原型尽量不要手动改
- 要改也不要通过实例对象去改
- 通过 类.prototype 属性去修改
- 最好不要直接给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调用函数时,将会发生这些事:
- 创建一个普通的JS对象(Object对象 {}), 为了方便,称其为新对象
- 将构造函数的prototype属性设置为新对象的原型
- 使用实参来执行构造函数,并且将新对象设置为函数中的this
- 如果构造函数返回的是一个非原始值,则该值会作为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 总结
面向对象本质就是,编写代码时所有的操作都是通过对象来进行的。
学习对象:
- 明确这个对象代表什么,有什么用
- 如何获取到这个对象
- 如何使用这个对象(对象中的属性和方法)
对象的分类:
-
内建对象
-
由ES标准所定义的对象
-
比如 Object Function String Number …
-
-
宿主对象
- 宿主对象
- BOM( Browser Object Model)、DOM(Document Object Model ,文档对象模型)
-
自定义对象
- 由开发人员自己创建的对象