面向对象
简介
面向对象编程(OOP)
-
程序是干嘛的?
程序是对现实世界的抽象(列如: 照片就是对人的抽象)
-
对象是干嘛的?
一个事物抽象到程序中后就变成了对象, 在程序中一切皆对象
一个事物通常有两部分组成: 数据和功能
一个对象有两部分组成: 属性和方法
面向对象编程:
程序中的所有操作都是通过对象来完成
做任何事之前都要先找到它的对象, 然后通过对象来完成各种操作
类
使用object
创建对象的问题:
- 无法区分出不同类型的对象
- 不方便批量创建对像
在JS
中可以通过类(class)来解决这个问题:
- 类是对象的模板, 可以将对象中的属性和方法直接定义在类中, 定义后就可以直接通过类来创建对象
- 通过同一个类创建的对象, 称为同类对象
可以使用instanceof
来检查一个对象是否有某个类创建, 如果某个对象是由某个类创建的, 则称该对象是这个类的实例
//创建类
class Person {
}
class Dog {
}
//调用构造函数创建对象
const p1 = new Person
const p2 = new Person
const d1 = new Dog
const d2 = new Dog
//使用instanceof检查是否为同类对象
console.log(p1 instanceof Person) //true
console.log(d2 instanceof Person) //false
类的属性
class Person {
//实例属性
name = "孙悟空" //Person的实例属性name, 实例属性只能通过实例访问-->p1.name p2.age
age = 18
//静态属性
static test = "test为静态属性"
static hh = "静态属性"
//使用static声明的的属性为静态属性(类属性)只能通过类去访问 Person.test Person.hh
}
const p1 = new Person()
const p2 = new Person()
console.log(p1, p2)
类是创建对象的模块, 要创建的第一件事就是定义类
类的代码块, 默认为严格模式, 类的代码块只能用来设置对象的属性, 不能什么代码都写
类的方法
class Person {
name = "猪八戒"
//实例方法
sayHello() {
//实例方法, 通过类的实例(p1)调用, this指向当前实例
console.log("我是猪八戒" + this.name)
}
//静态方法
static test() {
//静态方法, 通过类调用, this指向当前的类
console.log("text静态方法")
}
}
//类的实例赋值给 p1
const p1 = new Person()
p1.sayHello() //类实例调用, 实例方法
Person.test() //类调用, 静态方法
类的方法调用与类的属性调用方式一样,
实例属性(方法)通过类实例来调用
静态属性(方法)直接通过类调用
构造函数
class Person {
//在类中可以添加一个特殊的方法(constructor), 该方法我们称为构造函数(构造方法), 构造函数会在调用类创建对象时执行
constructor(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}
}
const p1 = new Person("孙悟空", 18, "男")// cosntructor执行
const p2 = new Person("猪八戒", 28, "男")
const p3 = new Person("白骨精", 38, "女")
console.log(p1)
console.log(p2)
console.log(p3)
构造函数会在我们调用类创建对象时执行
可以在构造函数中, 为实例属性进行赋值, 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, this.#address)
}
getName() {
return this.#name = name
}
setName(name) {
this.#name = name
}
// 控制数据的 读 权限
//getAge() {
//return this.#age = age
//}
setAge(age) {
//对属性的值进行验证
if(isNaN(age) && age >= 0) {
this.#age = age
}
}
//简便方式
get gender() {
console.log("getter(gender)执行了")
return this.#gender
}
set gender(gender) {
console.log("setter(gender)执行了")
this.#gender = gender
}
}
const p1 = new Person("孙悟空", 18 ,"男")
p1.setAge(18)
p1.setName("猪八戒")
p1.gender = "女" //实际上在调用set方法
console.log(p1)
console.log(p1.gender) //p1.gender实际上在调用get方法
封装:
(装) 对象就是一个用来存储不同属性的容器
对象不仅负责属性的存储, 还要负责属性的安全
(封) 保证数据的安全:
- 私有化数据: 将需要保护的数据设置为私有, 只能在类的内部使用
- 提供 setter 和 getter 方法来开放对数据的操作
- 属性设置私有, 通过getter 和 setter方法操作属性的优点
- 控制数据的 读, 写 权限
- 可以在方法中对属性的值进行验证
多态
class Person {
constructor(name) {
this.name = name
}
}
class Dog {
constructor(name) {
this.name = name
}
}
class text {
static name = '李四'
}
const dog = new Dog('旺财')
const person = new Person('张三')
console.log(dog, person)
//多态
function sayHello(obj) {
console.log('hello' + obj.name)
}
sayHello(text)//静态属性也能访问
多态:
- 在 js 中不会检查参数的类型, 因此任何数据都可以作为参数传递
- 要调用某个函数, 无需指定类型, 只要对象满足某些条件即可
- 多态为我们提供了灵活性
继承
class Animal {
constructor(name) {
this.name = name
}
sayHello() {
console.log("动物再叫")
}
}
//继承Animal
class Dog extends Animal {
}
class Cat extends Animal {
}
const dog = new Dog("旺财")
const cat = new Cat("汤姆")
dog.sayHello()
console.log(dog)
cat.sayHello()
console.log(cat)
继承:
-通过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()
console.log("喵喵喵")
}
}
const dog = new Dog("旺财")
const cat = new Cat("汤姆", 18)
dog.sayHello()
console.log(dog)
cat.sayHello()
console.log(cat)
通过继承可以在不修改一个类的情况下 对其进行扩展, 使用super关键字可以调用父类的方法
OCP原则:
程序应该对修改关闭, 对扩展开放
对象的结构
class Person {
age = 18 //对象自身
constructor(name) {
this.name = "张三" //对象自身
this.gender = "男" //对象自身
}
//对象自身
sayHello = () => {
console.log("hello, 我是" + this.name)
}
// //原型对象
// sayHello(name) {
// console.log("hello, 我是" + this.name)
// }
}
const p = new Person()
p.address = "花果山" // 对象自身
p.sayHello = "hello" // 对象自身(原型对象中也有,但是优先访问对象自身)
console.log(p)
console.log(p.sayHello)
对象在内存中的存储位置
1.对象自身(优先级高, 优先访问)
直接通过对象所添加的属性, 位于对象自身中
在类中通过 属性名 = 属性值(函数, 对象)
2.原型对象(prototype)
-对象中还有一些内容, 会存储到其他的对象里(原型对象)
-在对象中会有一个属性来存储原型对象并,链接对象自身, 这个属性叫做__proto__
-原型对象也负责对象的存储属性
-当我们访问对象中的属性时, 会优先访问自身的属性, 当该对象自身不包含该属性时, 则会去原型对象中寻找
-添加到原型对象的情况
-在类中通过xxx() {} 方法添加的方法, 为于原型中
-主动向原型中添加属性或者方法
原型链
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("我是" + this.name)
}
}
const p = new Person()
console.log(p)
//访问一个对象的原型
console.log(p.__proto__)
console.log(Object.getPrototypeOf(p))
console.log(p.constructor)
console.log(p.__proto__.__proto__.__proto__) //null
访问对象中的原型对象:
-对象.proto
-Object.getPrototypeOf
(对象)
原型对象中的数据:
-对象中的数据(属性, 方法, 等)
-constructor (对象的构造函数)
注意:
-原型对象也有对象, 以此类推就形成了一条原型链, 根据对象的复杂程度不同, 原型链的长度也会不同-原型链:
-读取对象属性时, 会优先使用对象自身属性, 如果对象自身没有则去原型对象中去寻找, 直到找不到返回null, 如果对象原型链中没有这个属性则返回 undefined-作用域链:
-是找变量, 找不到会报错-原型链:
-找属性, 找不到返回undefined
原型链的作用
class Person {
name = "张三"
age = 18
sayHello() {
console.log("我是" + this.name)
}
}
const p = new Person()
const p2 = new Person()
//所有的同类型对象, 他们的原型对象都是同一个
console.log(p === p2) //false
console.log(p.__proto__ === p2.__proto__)
//true, 虽然是两个不同的对象, 但他们都是通过Person()创建的, 因此他们的原型对象一样
console.log(p.sayHello) //原型对象
p.sayHello = "hello" // 添加与原型对象不同的方法
console.log(p.sayHello)//自身对象
console.log(p2.sayHello)//原型对象
class Animal {
}
class Cat extends Animal {
}
class Tom extends Cat {
}
原形型的作用:
-原型相当于一个公共区域, 可以被所有这个类的实例 访问
-可以将一个类的实例中, 存放所有公共属性(方法)进行统一存储
-这样只需要创建一个属性, 即可被所有实例访问
在对象中有些值时对象独有的, 如属性(name, age, gender)每个对象都应该有自己的值
但有些值对于每个对象来说都是一样的, 象各种方法, 对于一样的值没必要重复创建
继承就是通过原型链来实现的, 当继承时, 子类的原型就是一个父类的实例
修改原型
class Person {
name = "张三"
age = 48
sayHello(name) {
console.log("我是" + this.name)
}
}
class Dog {
}
const p = new Person()
const p2 = new Person()
//不建议使用
// p.__proto__.run = () => {
// console.log("跑")
// }
// console.log(p.run(), p2.run())
// //更不建议使用
// // p.__proto__ = new Dog()
// // console.log(p, p2)
Person.prototype.fly = () => {
console.log("飞")
}
// console.log(p, p2)
Person.prototype = {}
console.log(p.fly, p2)
一般情况下不需要修改原型
-注意:
-千万不要通过类的实例去修改原型
1.通过一个对象影响所有同类对象, 不合适
2.修改原型要先创建实例, 麻烦
3.危险
可以通过类的prototype属性, 来访问对象的原型, 也可以通过它来修改原型
-修改会对所有实例产生影响
-无需创建实例即可对类进行操作
-不建议直接给prototype直接赋值
instanceof, hasOwn
instanceof :
class Animal {}
class Dog extends Animal {}
const dog = new Dog()
console.log(dog.__proto__.__proto__.__proto__)
console.log(dog instanceof Dog) //true
console.log(dog instanceof Animal) //true
console.log(dog instanceof Object) //true
instanceof会检查一个对象是否是一个类的实例
-instanceof检查的是对象的原型链上是否有该类实例, 只要原型链上有就会返回true
-Object是所有对象的原型, 所以任何对象和Object进行instanceof运算都会返回true
hasOwn :
const obj = new Object()
console.log(obj.__proto__)
console.log(Object.prototype)
class Person {
name = "张三"
age = 18
sayHello() {
console.log("我是", this.name)
}
}
const p = new Person()
//"属性名" in 对象
// -使用in运算符检查属性时, 无论属性在原型中还是在对象自身中, 都会返回true, 除非没有这个属性
console.log("sayHello" in p) //true
//对象.hasOwnProperty("属性名"),(不推荐使用)
// -用来检查一个属性是否在对象自身中
console.log(p.hasOwnProperty("sayHello")) //false
//Object.hasOwn(对象, "属性名")
console.log(Object.hasOwn(p, "sayHello")) //false
旧类
var Person = (function() {
function Person(name, age) {
//在构造函数中, this就表示新建的对象
this.name = "张三"
this.age = 18
//旧的类添加方法, 会直接添加到对象本身
// this.sayHello = function() {
// console.log(this.name)
// }
}
//向函数中添加属性(方法)
Person.prototype.sayHello = function() {
console.log(this.name)
}
//添加静态属性
Person.staticProperty = "jjjj"
//添加静态方法
Person.staticMethod = function(){
console.log("张三")
}
return Person
})()
const p = new Person()
console.log(p)
console.log(Person)
var Cat = (function() {
function Cat() {
}
// 继承Person
Cat.prototype = new Person()
return Cat
})()
var cat = new Cat()
console.log(cat)
在早期的 js 中, 直接通过函数在定义类
- 一个函数如果直接调用: xxx(), 那么就是一个普通的函数
- 一个函数如果通过new调用 new xxx() 那么这个函数就是一个构造函数
new运算符
class Person {
constructor(name) {
// 3.使用实参来执行构造函数, 并且将新对象设置为函数中的this
this.naem = naem
//2.将构造函数的prototype属性设置为新对象的原型
let newInstance = {}
newInstance.__proto__ = Person.prototype
//4.如果构造函数返回的是一个非原始值, 则该值会作为new运算符的返回值返回(千万不要这么做)
//一旦指定了非原始值, 就会返回这个值, 就不会把新对象作为返回值返回
return {name: "12"} //非原始值 永远会返回 {name: "12"}
return 1 //原始值(五大基本数据类型: Undefined,Null,Boolean,Number,String)--原始值不能更改, 会返回新对象Person
}
}
//1.创建一个普通的js对象(Object 对象 {}), 为了方便, 称为新对象
let p = new Person()
console.log(p)
当时用new去调用一个函数时, 这个函数将会作为构造函数调用
- 创建一个普通的js对象(Object 对象 {}), 为了方便, 称为新对象
- 将构造函数的prototype属性设置为新对象的原型
- 使用实参来执行构造函数, 并且将新对象设置为函数中的this
- 如果构造函数返回的是一个非原始值, 则该值会作为new运算符的返回值返回(千万不要这么做)
如果构造函数的返回值是一个原始值, 或者不指定返回值, 则新对象会作为返回值返回, 通常不指定返回值
面向对象总结
面向对象, 即所有的操作都是通过对象来进行的
面向对象的编程步骤:
- 找对象
- 搞对象
学习对象:
- 明确这个对象代表这什么(如, window代表浏览器窗口对象, document代表文档对象), 与对象的具体作用, 操作这个对象能干些什么?
- 如何使用这个对象(对象中的属性和方法), 具体的操作步骤
对象的分类:
- 内建对象:
- 有Es标准所定义的对象
- 列如: String, Number, Boolean, Function, Object
- 宿主对象:
- 由浏览器提供的对象
- BOM, DOM
- 自定义对象:
- 有开发人员自己定义