目录
一、面向对象编程(OOP) 基本概念
二、类
1、语法
2、通过类创建对象
3、类的属性
4、类的方法
5、构造函数
三、面向对象的三个特点
1、封装
如何确保数据的安全(实现封装的方式):
2、继承
在子类中,可以通过创建同名方法来重写父类的方法。
在子类中,可以重写构造函数。
OCP 开闭原则
3、多态
四、对象的内存结构
1、对象自身
2、原型对象(prototype)
五、原型对象
1、访问一个对象的原型对象
2、原型对象中的数据
3、原型链
4、原型的作用
作用:
对比函数的原型链和Object的原型链
5、修改原型
6、检查对象自身或原型上是否有某属性的方法
6.1、instanceof
6.2、in运算符
6.3、hasOwnProperty(不推荐)
6.4、hasOwn
六、旧类
七、new运算符
八、总结
一、面向对象编程(OOP) 基本概念
1. 程序是干嘛的? - 程序就是对现实世界的抽象(照片就是对人的抽象)
2. 对象是干嘛的?- 一个事物抽象到程序中后就变成了对象。 在程序的世界中,一切皆对象
3. 面向对象的编程 - 面向对象的编程指,程序中的所有操作都是通过对象来完成。做任何事情之前都需要先找到它的对象,然后通过对象来完成各种操作。
- 一个事物通常由两部分组成:数据和功能
- 一个对象由两部分组成:属性和方法
- 事物的数据到了对象中,体现为属性
- 事物的功能到了对象中,体现为方法
二、类
使用Object创建对象的问题: 1. 无法区分出不同类型的对象; 2. 不方便批量创建对象
在JS中可以通过类(class)来解决这个问题:类是对象模板,可以将对象中的属性和方法直接定义在类中。定义后,就可以直接通过类来创建对象。
可以说,创建对象的第一步,就是为这个对象定义一个类。
1、语法
方式一:
class 类名 {} // 类名要使用大驼峰命名
方式二:
const 类名 = class {}
const Person = class {}
// Person类专门用来创建人的对象
class Person{
}
// Dog类式专门用来创建狗的对象
class Dog{
}
2、通过类创建对象
语法:new 类()
const p1 = new Person() // 调用构造函数创建对象
console.log(p1) // Person{}
Person()相当于把类当做一个函数在调用,加上new就是构造函数
通过同一个类创建的对象,我们称为同类对象。可以使用instanceof来检查一个对象是否是由某个类创建。
const p1 = new Person() // 调用构造函数创建对象
const p2 = new Person()
const d1 = new Dog()
const d2 = new Dog()
console.log(p1 instanceof Person) // true
console.log(d1 instanceof Person) // false
如果某个对象是由某个类所创建,则我们称该对象是这个类的实例。以上代码中p1、p2是Person类的实例,d1、d2是Dog类的实例。
3、类的属性
类是创建对象的模板,要创建第一件事就是定义类。
类的代码块,默认就是严格模式,类的代码块是用来设置对象的属性的,不是什么代码都能写的。
- 实例属性只能由实例对象访问的
- 用static声明的属性,只能通过类访问,实例对象是无法访问的
class Person{
name = "孙悟空" // Person的实例属性name, p1.name、p2.name
age = 18 // 实例属性只能通过实例访问, p1.age、p2.age
static test = "test静态属性" // 使用static声明的属性,是静态属性(类属性) Person.test
static hh = "静态属性" // 静态属性只能通过类去访问, Person.hh 实例对象中是没有的
}
4、类的方法
添加方法的方式有2种。
class Person{
name = "孙悟空"
// 添加方法的一种方式(不推荐使用,{}内有无内容,打印p1时都会显示该方法)
// sayHello = function(){
// }
// 添加方法(实例方法),方法中也就是{}中没写内容,打印p1时不会显示sayHello,但是方法已经加入到Person中了
sayHello(){
console.log('大家好,我是' + this.name) // 实例方法中this就是当前实例p1
}
// 静态方法(类方法) 通过类来调用
static test(){
console.log("我是静态方法", this) // 静态方法中this指向的是当前类Person
}
}
const p1 = new Person()
console.log(p1)
Person.test()
p1.sayHello() // 哈哈哈
5、构造函数
当我们在类中直接指定实例属性的值时,意味着我们创建的所有对象的属性都是这个值。因此,希望每次new类对象时,每个实例对象的属性的值都是不一样的。
在类中可以添加一个特殊的方法constructor,该方法我们称为构造函数(构造方法),构造函数会在我们调用类创建对象时执行。
class Person{
constructor(name, age, gender){
console.log("构造函数执行了~") // 构造函数会在我们调用类创建对象时执行
}
}
const p1 = new Person()
console.log(p1) // 构造函数执行了~
class Person{
constructor(name, age, gender){
// 可以在构造函数中,为实例属性进行赋值,用this
// 在构造函数中,this表示当前所创建的对象
this.name = name // 将参数的name赋值给Person对象的name实例属性
this.age = age
this.gender = gender
}
}
const p1 = new Person("孙悟空", 18, "男") // 向Person里传的参数,实际上就是传给构造函数
const p2 = new Person("猪八戒", 28, "男")
const p3 = new Person("沙和尚", 38, "男")
console.log(p1) // {name:"孙悟空", age:18,gender:"男"}
console.log(p2) // {}
console.log(p3) // {}
三、面向对象的三个特点
面向对象的特点:封装(安全性)、继承(扩展性)和多态(灵活性)
1、封装
装:对象就是一个用来存储不同属性的容器。
封:对象不仅存储属性,还要负责数据的安全,保证数据的合法性(比如年龄不能为负数)。直接添加到对象中的属性,并不安全,因为它们可以被任意的修改。
如何确保数据的安全(实现封装的方式):
1.私有化数据
将需要保护的数据设置为私有,只能在类内部使用
class Person {
#address = "花果山" // 实例使用#开头就变成了私有属性,私有属性只能在类内部访问
sayHello() {
console.log(this.#address)
}
}
const p1 = new Person()
console.log(p1.#address) // 报错,私有属性不能访问
console.log(p1.sayHello()) // 花果山
私有化属性一定要先声明再使用,否则会报错
class Person {
#name
#age
#gender
constructor(name, age, gender) {
this.#name = name
this.#age = age
this.#gender = gender
}
const p1 = new Person("孙悟空", 18, "男")
console.log(p1) // Person{#name: "孙悟空",#age: 18,#gender: "男"}
p1.#age = 11 // 报错,私有属性无法访问,更无法更改
2.提供setter和getter方法来开放对数据的操作
属性设置私有,通过getter setter方法操作属性带来的好处:
- ①可以控制属性的读写权限:数据只读,就只有getter方法;数据只写,只有setter方法;数据可读写,有getter和getter方法;数据不可读也不可写,getter和getter方法都无;
- ②可以在方法中对属性的值进行验证:在方法中添加一些数据的条件,如年龄不能为负数
class Person {
#name
#age
#gender
constructor(name, age, gender) {
this.#name = name
this.#age = age
this.#gender = gender
}
// getter方法,用来读取属性
getName(){
return this.#name
}
// setter方法,用来设置属性
setName(name){
this.#name = name
}
getAge(){
return this.#age
}
setAge(age){
if(age >= 0){
this.#age = age
}
}
}
const p1 = new Person("孙悟空", 18, "男")
p1.getName()
p1.setName('猪八戒')
console.log(p1) // Person{#name: "猪八戒",#age: 18,#gender: "男"}
为了访问属性时不用像上面的getter方法一样麻烦,可以这么写访问函数:
class Person {
#gender
constructor(gender) {
this.#gender = gender
}
get gender(){
return this.#gender
}
set gender(gender){
this.#gender = gender
}
}
const p1 = new Person("孙悟空", 18, "男")
// 读取属性的值,调用get方法
console.log(p1.gender) // 男
// 修改属性的值,调用set方法
p1.gender = "女"
console.log(p1.gender) // 女
2、继承
通过extends关键来完成继承。当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)。继承发生时,被继承的类称为 父类(超类),继承的类称为 子类。通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展。
class Animal{
constructor(name){
this.name = name
}
sayHello(){
console.log("动物在叫~")
}
}
class Dog extends Animal{
}
class Cat extends Animal{
}
const dog = new Dog("旺财")
const cat = new Cat("汤姆")
dog.sayHello() // 动物在叫~
cat.sayHello() // 动物在叫~
不用继承,以上代码就会重复冗余,写成如下形式:
class Dog extends Animal{
constructor(name){
this.name = name
}
sayHello(){
console.log("动物在叫~")
}
}
class Cat extends Animal{
constructor(name){
this.name = name
}
sayHello(){
console.log("动物在叫~")
}
}
const dog = new Dog("旺财")
const cat = new Cat("汤姆")
dog.sayHello() // 动物在叫~
cat.sayHello() // 动物在叫~
在子类中,可以通过创建同名方法来重写父类的方法。
class Animal{
constructor(name){
this.name = name
}
sayHello(){
console.log("动物在叫~")
}
}
class Dog extends Animal{
sayHello(){
console.log("汪汪汪")
}
}
class Cat extends Animal{
sayHello(){
console.log("喵喵喵")
}
}
const dog = new Dog("旺财")
const cat = new Cat("汤姆")
dog.sayHello() // 汪汪汪
cat.sayHello() // 喵喵喵
在子类中,可以重写构造函数。
重写构造函数时,构造函数的第一行代码必须为super(),表示继承父类的构造函数。
class Animal{
constructor(name){
this.name = name
}
sayHello(){
console.log("动物在叫~")
}
}
class Cat extends Animal{
// 重写构造函数
constructor(name, age){ // 一定要写传入的参数,否则属性值为undefined
// 重写构造函数时,构造函数的第一行代码必须为super()
super(name) // 调用父类的构造函数,继承了父类的name属性,所以一定要写name参数
//如果不写,那就自己重写name属性=》this.name = name
this.age = age
}
sayHello(){
super.sayHello() // 在方法中使用super来继承父类的方法
console.log("喵喵喵")
}
}
const cat = new Cat("汤姆", 3)
console.log(cat) // Cat{name:"汤姆", age: 3}
cat.sayHello() // 动物在叫~ 喵喵喵
OCP 开闭原则
程序应该对修改关闭,对扩展开放。尽量不修改父类,而是用一个子类继承父类,父类的方法、属性中子类需要的就保留,不需要的就不要,然后在子类再扩展一些子类需要的属性、方法。
3、多态
在JS中不会检查参数的类型,所以这就意味着任何数据都可以作为参数传递。要调用某个函数,无需指定的类型,只要对象满足某些条件即可。多态为我们提供了灵活性
class Person{
constructor(name){
this.name = name
}
}
class Dog{
constructor(name){
this.name = name
}
}
const dog = new Dog('旺财')
const person = new Person("孙悟空")
function sayHello(obj){
console.log("Hello,"+obj.name)
}
sayHello(dog) // Hello, 旺财
sayHello(person) // Hello, 孙悟空
四、对象的内存结构
对象中存储属性的区域实际有两个:对象自身和原型对象
1、对象自身
直接通过对象所添加的属性,位于对象自身中。
在类中通过 属性名 = 属性值 的形式添加的属性,位于对象自身中。
2、原型对象(prototype)
对象中还有一些内容,会存储到其他的对象里(原型对象)。
在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__。
原型对象也负责为对象存储属性,当我们访问对象中的属性时,会优先访问对象自身的属性,对象自身不包含该属性时,才会去原型对象中寻找。
会添加到原型对象中的情况:
- 在类中通过xxx(){}方式添加的方法,位于原型中
- 主动向原型中添加的属性或方法
class Person {
name = "孙悟空"
age = 18
constructor(){
this.gender = "男"
}
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(p.sayHello) // Hello,我是孙悟空
p.sayHello = "hello"
console.log(p.sayHello) // hello 先从自身属性找sayHello
在内存中,上述代码的表现:
五、原型对象
1、访问一个对象的原型对象
语法:对象.__proto__ or Object.getPrototypeOf(对象)
PS:浏览器不建议使用对象.__proto__去访问对象的原型对象,所以提供了Object.getPrototypeOf(对象)去访问对象的原型对象
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(p) // {name:"孙悟空", age: 18}
console.log(p.__proto__) // {constructor:f, sayHello: f}
console.log(Object.getPrototypeOf(p)) // {constructor:f, sayHello: f}
2、原型对象中的数据
- 1. 对象中的数据(属性、方法等)
- 2. constructor (对象的构造函数),类里面没有就找原型的构造函数
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
// Person类里面没有constructor,所以去原型里找
console.log(p.__proto__.constructor) // 打印整个class Person
console.log(p.constructor) // 打印整个class Person
原型对象也有原型。这样就构成了一条原型链。
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(p.__proto__.__proto__) // 原型对象也有原型
console.log(p.__proto__.__proto__.__proto__) // null
根据对象的复杂程度不同,原型链的长度也不同。空对象的原型存在、原型的原型不存在。
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(p.__proto__.__proto__) // 原型对象也有原型
console.log(p.__proto__.__proto__.__proto__) // null
const obj = {}
console.log(obj.__proto__) // 存在
console.log(obj.__proto__.__proto__) // null
3、原型链
读取对象属性时,会优先对象自身属性,如果对象中有,则使用,没有则去对象的原型中寻找如果原型中有,则使用,没有则去原型的原型中寻找,直到找到Object对象的原型(Object的原型没有原型(为null))。如果依然没有找到,则返回undefined。
作用域链与原型链的区别:
- 作用域链,是找变量的链,找不到会报错
- 原型链,是找属性的链,找不到会返回undefined
4、原型的作用
所有的同类型对象它们的原型对象都是同一个,也就意味着,同类型对象的原型链是一样的
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
const p2 = new Person()
console.log(p === p2) // false
console.log(p.__proto__ === p2.__proto__) // true
const d = new Dog()
console.log(d === p) // false
console.log(d.__proto__ === p.__proto__) // false,因为d和p不同类
作用:
原型就相当于是一个公共的区域,可以被所有该类实例访问,可以将该类实例中,所有的公共属性(方法)统一存储到原型中。这样我们只需要创建一个属性,即可被所有实例访问。
在对象中有些值是对象独有的,像属性(name,age,gender)每个对象都应该有自己值,但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没必要重复的创建。
如果只有少数对象的方法与其他的不一样,那么就实例.方法 = 新方法内容进行修改即可。
JS中继承就是通过原型来实现的,当继承时,子类的原型就是一个父类的实例。
class Animal{
}
class Cat extends Animal{
}
class TomCat extends Cat{
}
const cat = new Cat()
console.log(cat) // Cat{}
console.log(cat.__proto__) // Animal{}
cat --> Animal实例 --> object --> Object原型 --> null
TomCat --> cat --> Animal实例 --> object --> Object原型 --> null
所有类都是object的子类
对比函数的原型链和Object的原型链
- 函数的原型链:
function A(){ }; var f1 = new A(); f1.proto ==>A.prototype ==> A.prototype.proto ==> Object.prototype ==> Object.prototype.proto ==> null --------------------------------------------------------- function A(){ }; A.proto ==>Function.prototype ==>Function.prototype.proto ==> Object.prototype ==> Object.prototype.proto ==> null
- Object的原型链:
5、修改原型
通过类的prototype属性,修改应在类定义好之后。好处:①一修改就是修改所有实例的原型,②无需创建实例即可完成对类的修改
语法:类.prototype
通过类的实例去修改原型,原则上不太好,因为是通过一个对象影响所有同类对象,这么做不合适。
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
Person.prototype.fly = () => {
console.log("我在飞!")
}
最好不要直接给prototype去赋值,因为会把原来的东西覆盖掉
6、检查对象自身或原型上是否有某属性的方法
6.1、instanceof
作用:检查一个对象是否是一个类的实例。检查的是对象的原型链上是否有该类实例,只要原型链上有该类实例,就会返回true。
Object是所有对象的原型,所以任何和对象和Object进行instanceof运算都会返回true。
class Animal {}
class Dog extends Animal {}
const dog = new Dog()
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
console.log(dog instanceof Object) // true
dog(Dog的实例) -> Animal的实例 -> Object实例 -> Object原型
6.2、in运算符
使用in运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log("name" in p) // true
console.log("sayHello" in p) // true
6.3、hasOwnProperty(不推荐)
作用:用来检查一个对象的自身是否含有某个属性,有则返回true。原型上的属性返回false。
语法:对象.hasOwnProperty(属性名)
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(p.hasOwnProperty("name")) // true
console.log(p.hasOwnProperty("sayHello")) // false
console.log(p.__proto__.__proto__.hasOwnProperty("hasOwnProperty")) // true
6.4、hasOwn
作用:新增的静态方法,用来检查一个对象的自身是否含有某个属性。
语法:Object.hasOwn(对象, 属性名)
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(Object.hasOwn(p, "name")) // true
console.log(Object.hasOwn(p, "sayHello")) // false
六、旧类
早期JS中,直接通过函数来定义类。
一个函数如果直接调用 xxx() 那么这个函数就是一个普通函数。一个函数如果通过new调用 new xxx() 那么这个函数就是一个构造函数。
// 用立即执行函数将对象的所有代码包裹起来
var Person = (function() {
function Person(name, age) {
// 在构造函数中,this表示新建的对象
this.name = name
this.age = age
// this.sayHello = function(){
// console.log(this.name)
// }
}
// 向原型中添加属性(方法)
Person.prototype.sayHello = function () {
console.log(this.name)
}
// 静态属性
Person.staticProperty = "xxx"
// 静态方法
Person.staticMethod = function () {}
return Person
})()
旧类的继承:
var Animal = (function(){
function Animal(){
}
return Animal
})()
var Cat = (function(){
function Cat(){
}
// 继承Animal
Cat.prototype = new Animal()
return Cat
})()
var cat = new Cat()
console.log(cat)
七、new运算符
官网:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
当使用new去调用一个函数时,这个函数将会作为构造函数调用,使用new调用函数时,将会发生这些事:
- 1. 创建一个普通的JS对象(Object对象 {}), 为了方便,称其为新对象
- 2. 将构造函数的prototype属性设置为新对象的原型
- 3. 使用实参来执行构造函数,并且将新对象设置为函数中的this
- 4. 如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回(千万不要这么做)。如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回。通常不会为构造函数指定返回值。
八、总结
面向对象本质就是,编写代码时所有的操作都是通过对象来进行的。
面向对象的编程的步骤:
1. 找对象
2. 搞对象:调用属性、修改属性、调用方法、修改方法
学习对象:
1. 明确这个对象代表什么,有什么用
2. 如何获取到这个对象
3. 如何使用这个对象(对象中的属性和方法)
对象的分类:
内建对象
- 由ES标准所定义的对象
- 比如 Object Function String Number ....
宿主对象
- 由浏览器提供的对象
- BOM、DOM
自定义对象
- 由开发人员自己创建的对象