这部分视频讲解了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.name
age = 18 // 实例属性只能通过实例访问 p1.age
// 使用static声明的属性,是静态属性(类属性) Person.test
static test = "test静态属性"
// 静态属性只能通过类去访问 Person.hh
static 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 = 18
gender = "男"
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 = name
this.age = age
this.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
#gender
constructor(name, age, gender) {
this.#name = name
this.#age = age
this.#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 = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log(p.__proto__) // 访问原型对象
console.log(Object.getPrototypeOf(p))
原型对象中的数据:
注意:原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同
原型链:
读取对象属性时,会优先对象自身属性,
如果对象中有,则使用,没有则去对象的原型中寻找
如果原型中有,则使用,没有则去原型的原型中寻找
直到找到Object对象的原型(Object的原型没有原型(为null))
如果依然没有找到,则返回undefined
作用域链和原型链的比较
所有的同类型对象它们的原型对象都是同一个,也就意味着,同类型对象的原型链是一样的
class Person {
name = "孙悟空"
age = 18
sayHello() {
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 = 18
sayHello() {
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 = 18
sayHello() {
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 = 18
sayHello() {
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 = name
this.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 = 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
})()
const p = new Person("孙悟空", 18)
console.log(p)
继承的实现方式
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运算符是创建对象时要使用的运算符
使用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
}