这部分视频讲解了JS中面向对象相关的知识。主要包括:类、属性、方法、构造函数、封装、多态、继承、对象的结构、原型、原型链、旧类、new运算符等内容。
面向对象编程(OOP, Object Oriented Programming)
事物和对象
一个事物通常由两部分组成:数据和功能
一个对象由两部分组成:属性和方法
事物的数据到了对象中,体现为属性
事物的功能到了对象中,体现为方法
例如对于人来说:
数据:
姓名
年龄
身高
体重
功能:
睡
吃
表现在代码中:
const five = {// 添加属性name:"王老五",age:48,height:180,weight:100,// 添加方法sleep(){console.log(this.name + "睡觉了~")},eat(){console.log(this.name + "吃饭了~")}
}
使用Object创建对象的问题:
在JS中可以通过类(class)来解决这个问题:
instanceof
来检查一个对象是否是由某个类创建语法:
class 类名 {} // 类名要使用大驼峰命名
const 类名 = class {}
通过类创建对象:
const aa = new 类名()
例如:
// Person类专门用来创建人的对象
class Person{}// Dog类式专门用来创建狗的对象
class Dog{}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
实例化对象.属性名
类名.属性名
class Person{name = "孙悟空" // Person的实例属性name p1.nameage = 18 // 实例属性只能通过实例访问 p1.age// 使用static声明的属性,是静态属性(类属性) Person.teststatic test = "test静态属性" // 静态属性只能通过类去访问 Person.hhstatic hh = "静态属性"
}const p1 = new Person()
const p2 = new Person()console.log(p1)
console.log(p2)
console.log(p1.name, p1.test, Person.test)
定义方法和定义函数语法格式都是一样的,只不过可以省掉function
关键字
方法中的this和原来一样,谁调用this指向的就是谁
class Person {name = "孙悟空"// 添加方法的一种方式,不推荐使用这种方式// sayHello1 = function () {//// } // 添加方法(实例方法) 实例方法中this就是当前实例sayHello() {console.log('大家好,我是' + this.name)} // 静态方法(类方法) 通过类来调用 静态方法中this指向的是当前类static test() {console.log("我是静态方法", this)}
}const p1 = new Person()// console.log(p1)
Person.test()
p1.sayHello()
在前面的定义方法中,每个实例的属性都是我们预先定义好的,没法自定义去改变,唯一的改变方法就是通过下面这种方式,但是这种方式又不能提现类的便捷性
class Person {name = "孙悟空" // 当我们在类中直接指定实例属性的值时,意味着我们创建的所有对象的属性都是这个值age = 18gender = "男"sayHello() {console.log(this.name)}
}const p1 = new Person()
p1.age = 19
p1.name = "zhangsan"
因此,我们介绍一种新的东西,叫做构造函数,使用方法如下:
new
调用函数的时候调用的就是类中的构造函数class Person {constructor(name, age, gender) {// console.log("构造函数执行了~", name, age, gender)// 可以在构造函数中,为实例属性进行赋值// 在构造函数中,this表示当前所创建的对象this.name = namethis.age = agethis.gender = gender}
}const p1 = new Person("孙悟空", 18, "男")
const p2 = new Person("猪八戒", 28, "男")
const p3 = new Person("沙和尚", 38, "男")console.log(p1)
console.log(p2)
console.log(p3)
面向对象的特点:封装、继承、多态
封装 —— 安全性
继承 —— 扩展性
多态 —— 灵活性
对象就是一个用来存储不同属性的容器
对象不仅存储属性,还要负责数据的安全
直接添加到对象中的属性,并不安全,因为它们可以被任意的修改
如何确保数据的安全:
封装主要用来保证数据的安全
实现封装的方式:
#
this.#name
修改getter
和setter
方法来操作属性 get 属性名(){return this.#属性
}set 属性名(参数){this.#属性 = 参数
}
例子:
class Person {// #address = "花果山" // 实例使用#开头就变成了私有属性,私有属性只能在类内部访问#name#age#genderconstructor(name, age, gender) {this.#name = namethis.#age = agethis.#gender = gender}sayHello() {console.log(this.#name)}// getter方法,用来读取属性getName(){return this.#name}// setter方法,用来设置属性setName(name){this.#name = name}getAge(){return this.#age}// 可以先检查要修改的值,如果不符合要求就不修改setAge(age){if(age >= 0){this.#age = age}}get gender(){return this.#gender}set gender(gender){this.#gender = gender}
}const p1 = new Person("孙悟空", 18, "男")// p1.age = "hello"// p1.getName()
p1.setAge(-11) // p1.age = 11 p1.age// p1.setName('猪八戒')p1.gender = "女" // 等同于p1.setGender("女")
console.log(p1.gender)
class Person{constructor(name){this.name = name}
}class Dog{constructor(name){this.name = name}
}class Test{}const dog = new Dog('旺财')
const person = new Person("孙悟空")
const test = new Test()function sayHello(obj){// if(obj instanceof Person){console.log("Hello, "+obj.name)// }
}sayHello(dog)
sayHello(person)
sayHello(test)
extends
关键来完成继承class Animal{constructor(name){this.name = name}sayHello(){console.log("动物在叫~")}
}class Dog extends Animal{}class Cat extends Animal{}class Snake extends Animal{}const dog = new Dog("旺财")
const cat = new Cat("汤姆")dog.sayHello()
cat.sayHello()
console.log(dog)
console.log(cat)
但是在上面的代码中,各个动物的sayHello()
都是一个东西,但是每个动物的叫声是不一样的,所以可以通过重写父类方法来修改方法
在子类中,可以通过创建同名方法来重写父类的方法
重写构造函数时,构造函数的第一行代码必须为
super()
在方法中可以使用
super
来引用父类的方法,也就是说super指向的是父类对象
OCP 开闭原则:程序应该对修改关闭,对扩展开放
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() // 在方法中可以使用super来引用父类的方法console.log("喵喵喵")}
}const dog = new Dog("旺财")
const cat = new Cat("汤姆", 3)dog.sayHello()
cat.sayHello()
console.log(dog)
console.log(cat)
对象中存储属性的区域实际有两个:
this.gender = "男"
name = "zhansgan"
__proto__
xxx(){}
方式添加的方法,位于原型中访问一个对象的原型对象
对象.__proto__
Object.getPrototypeOf(对象)
class Person {name = "孙悟空"age = 18sayHello() {console.log("Hello,我是", this.name)}
}const p = new Person()
console.log(p.__proto__) // 访问原型对象
console.log(Object.getPrototypeOf(p))
原型对象中的数据:
注意:原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同
p对象的原型链:p对象 --> 原型 --> 原型 --> null
obj对象的原型链:obj对象 --> 原型 --> null
原型链:
读取对象属性时,会优先对象自身属性,
如果对象中有,则使用,没有则去对象的原型中寻找
如果原型中有,则使用,没有则去原型的原型中寻找
直到找到Object对象的原型(Object的原型没有原型(为null))
如果依然没有找到,则返回undefined
作用域链和原型链的比较
所有的同类型对象它们的原型对象都是同一个,也就意味着,同类型对象的原型链是一样的
class Person {name = "孙悟空"age = 18sayHello() {console.log("Hello,我是", this.name)}
}class Dog {}const p = new Person()
const p2 = new Person()
const d = new Dog()console.log(p == p2) // false
console.log(p.__proto__ == p2.__proto__) // true
console.log(d == p) // false
console.log(d.__proto__ == p.__proto__) // false
JS中继承就是通过原型来实现的,当继承时,子类的原型就是一个父类的实例
在对象中有些值是对象独有的,像属性(name,age,gender)每个对象都应该有自己值,但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没必要重复的创建
尝试:
函数的原型链是什么样子的?
Object的原型链是什么样子的?
大部分情况下,我们是不需要修改原型对象
class Person {name = "孙悟空"age = 18sayHello() {console.log("Hello,我是", this.name)}
}const p = new Person()
const p2 = new Person()// 通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法 不要这么做
p.__proto__.run = () => {console.log('我在跑~')
}console.log(p)
console.log(p2)p.run()
p2.run()
注意:千万不要通过类的实例去修改原型(类似上面这样)
处理通过__proto__
能访问对象的原型外,还可以通过类的prototype
属性,来访问实例的原型,修改原型时,最好通过类去修改
Person.prototype === p.__proto__ //true
好处:
原则:
类.prototype
属性去修改instanceof 用来检查一个对象是否是一个类的实例
instanceof检查的是对象的原型链上是否有该类实例,只要原型链上有该类实例,就会返回true
dog -> Animal的实例 -> Object实例 -> Object原型
Object是所有对象的原型,所以任何和对象和Object进行instanceof运算都会返回true
使用in运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true
class Person {name = "孙悟空"age = 18sayHello() {console.log("Hello,我是", this.name)}
}const p = new Person()
console.log("sayHello" in p) // true
console.log("name" in p) // true
对象.hasOwnProperty(属性名)
:用来检查一个对象的自身是否含有某个属性
不推荐使用
class Person {name = "孙悟空"age = 18sayHello() {console.log("Hello,我是", this.name)}
}const p = new Person()
console.log(p.hasOwnProperty("sayHello")) // false
console.log(p.hasOwnProperty("name")) // true
Object.hasOwn(对象, 属性名)
:用来检查一个对象的自身是否含有某个属性
早期JS中,直接通过函数来定义类
xxx()
那么这个函数就是一个普通函数new
调用 new xxx()
那么这个函数就是一个构造函数// 等价于:class Person{}
function Person(name, age) {// 在构造函数中,this表示新建的对象this.name = namethis.age = age
}// 向原型中添加属性(方法)
Person.prototype.sayHello = function () {console.log(this.name)
}// 静态属性
Person.staticProperty = "xxx"
// 静态方法
Person.staticMethod = function () {}const p = new Person("孙悟空", 18)
console.log(p)
上面这种方式定义类比较分散,可以使用立即执行函数把他们写在一起,等价于以下代码
var Person = (function () {function Person(name, age) {// 在构造函数中,this表示新建的对象this.name = namethis.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
})()const p = new Person("孙悟空", 18)
console.log(p)
继承的实现方式
var Animal = (function(){function Animal(){}return Animal
})()var Cat = (function(){function Cat(){}// 继承AnimalCat.prototype = new Animal()return Cat
})()var cat = new Cat()console.log(cat)
new运算符是创建对象时要使用的运算符
使用new时,到底发生了哪些事情:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
当使用new去调用一个函数时,这个函数将会作为构造函数调用,使用new调用函数时,将会发生这些事:
创建一个普通的JS对象(Object对象 {}), 为了方便,称其为新对象
将构造函数的prototype属性设置为新对象的原型
使用实参来执行构造函数,并且将新对象设置为函数中的this
如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回(千万不要这么做)
如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回
所以一般不要写返回值
function MyClass() {// 第一步var newInstance = {}// 第二步newInstance.__proto__ = MyClass.prototype
}