15.面向对象
面向对象
面向对象(Object Oriented Programming)几乎是目前所有主流语言所必备的特点。什么是面向对象呢?回答这个问题,需要先明白另一个问题:什么是对象?
什么是对象
程序的本质是对现实事物的抽象。所谓抽象就是用一些手段把现实事物表示出来。比如,画人像就是对人的抽象,写一本人物的传记就是对一个人一生的抽象,你的个人简历就是对你的抽象。
程序的作用就是用代码去表示现实的事物,由于现实的事物都是极其复杂的,不可能在代码中体现出事物的所有具体细节。所以只能采用抽象的形式,提取出事物的特点,然后通过代码呈现。
在现实的世界中,无论事物再复杂,一旦被抽象进入到程序之中,都会被转换为一段数据来存储,这些数据就被我们称为对象(Object)。换句话说,所有的事物到了程序中都会变成对象。
日常生活中使用的数字,到了程序中变成了Number对象。日常生活中说的语言文字,到了程序中变成了String对象。用来上网的浏览器,到了程序中变成了Window对象。日常中使用的日期时间,到了程序中变成了Date对象。总之,在程序中一切都是对象!
你也许会疑问,如何通过对象来表示出一个现实的事物呢?现实的事物是非常复杂的,但在程序之中,把每一个事物都分成了两个部分,一部分是数据,还有一部分是功能。
拿人举例子,人的身高、姓名、性别、年龄、攻击力、敏捷等等都属于是人的数据,这些数据在对象中被称为属性。人可以吃饭、睡觉、攻击、跑这些种种的都是人所具备的功能,这些功能在对象中被称为方法。
无论是再复杂的事物在程序中都由属性和方法两个部分组成,只需要这两个部分即可体现出事物的所有特征,不信你自己想一下,你随便说出你具备的某个特点,它一定不会超出属性和方法的范畴。
举个例子,现在我需要在JS中表示一个人的信息,这个人叫猪八戒、年龄28、地址高老庄、他会睡觉。很显然猪八戒、28、高老庄这些属于属性,睡觉属于方法:
const zbj = {
name:'猪八戒',
age:28,
address:'高老庄',
sleep:function () {
console.log(`${this.name}睡着了~~~`);
}
};
这样我们就将一个人的信息转换为了JS中的对象,以此类推所有的事物都可以转换为这样的对象。
面向对象呢?
知道什么是对象了,面向对象就简单了。所谓的面向对象指我们在编写程序时,所有的操作都是通过对象进行的。比如,表示数字,要先找到数字的对象Number。表示字符串,要找到字符串的对象String。进行数学运算,要找到数学运算的对象Math。刷新页面,要先找到表示浏览器地址栏的对象Location。也就是说所有的操作都要通过指定的对象进行。这样一来我们编写程序时大体上主要有两个步骤,步骤一:找对象,根据你要做的事情找到你需要的对象,当然有些时候没有满足你要求的对象,也许还要自己创建一个对象。步骤二:搞对象,搞对象即通过调用对象的属性或方法来完成你的需求。
面向对象本身的难点并不在于概念的理解,而是对象的定义,也就是我们如何能根据需要来定义一个对象。这就要求我们要具备两个能力,能力一:你得会定义对象。能力二:你得知道如何对事物进行抽象。抽象事物的能力需要一点一点锻炼,至于定义对象就相对简单了。
面向对象中的对象主要具有三个特点:封装、继承和多态。掌握了这三个特点即可轻松的创建一个你需要的对象。
类
使用Object创建对象的问题:
- 无法区分出不同类型的对象
- 不方便批量创建对象
在JS中可以通过(class)来解决这个问题
- 类是对象的模板,可以将对象中的属性和方法直接定义在类中
- 定义后,就可以直接通过类来创建对象
- 通过同一个类创建的对象,我们称为同类对象
- 可以通过instanceof来检查一个对象是否是由某个类创建
- 如果某个对象是由某个类创建,则我们称该对象是这个类的实例(实例化对象)
语法:
- class 类名{ } //类名要使用大驼峰命名
- const 类名 = class { } //不推荐使用
<script>
//人对象的模板,抽象类
class Proson {
}
//狗对象的模板,抽象类
class Dog {
}
//调用函数创建对象
const p1 = new Proson()
const d1 = new Dog()
console.log(p1); //Proson{}
console.log(d1); //Dog{}
console.log(p1 instanceof Proson); //true ,instanceof检查p1是否由Proson类创建
</script>
属性
类是创建对象的模板,要创建第一件事就是定义类
<script>
class Person {
/*
类的代码块,默认就是严格模式
类的代码块是用来设置对象的属性的,不是什么代码都能写
*/
name = "孙悟空" //Person的实例属性name 访问:p1.name
age = 18 //实例属性只能通过实例访问 p1.age
static test = "test静态属性" //使用static声明的属性,是静态属性(类属性)Person.test
static hh = "静态属性" //静态属性只能通过类去访问 Person.hh
}
const p1 = new Person() //Object { name: "孙悟空", age: 18 }
console.log(p1);
</script>

方法
<script>
class Person {
name = "孙悟空"
//添加方法一
test1 = function () {
console.log("我是方法一");
}
//添加方法二(实例方法)只能通过实例来调用 p1.test2()
//实例方法中this就是当前实例p1(谁调用就是谁)
test2() {
console.log("大家好我是:" + this.name);
}
//静态方法(类方法)只能通过类来调用 Person.test3()
//静态方法中this指向当前类Person(谁调用就是谁)
static test3() {
console.log("我是静态方法", this);
}
}
const p1 = new Person() //实例化Person对象
console.log(p1);
p1.test2() //大家好我是:孙悟空
Person.test3() //我是静态方法
</script>

构造方法(构造函数)
其使用方法和Java的构造方法一样,只不过写法有点区别。
<script>
class Person {
/* 可写可不写,应为在构造函数中this.xxx也相当于在Person中添加属性
name
age
gender
*/
//在类中可以添加一个特殊的方法construction
//该方法我们称之为构造函数(构造方法)
//构造函数会在我们调用类创建对象时执行
constructor(name, age, gender) {
// console.log("构造函数执行了~", name, age, gender);
//可以在构造函数中,为实例属性进行赋值
//在构造函数中,this表示当前所创建的对象
this.name = name;
this.age = age;
this.gender = gender;
}
}
const p1 = new Person("孙悟空", 18, "meal")
const p2 = new Person("猪八戒", 28, "meal")
console.log(p1); //Object { name: "孙悟空", age: 18, gender: "meal" }
console.log(p2); //Object { name: "猪八戒", age: 28, gender: "meal" }
</script>
对象的三大特征
封装
- 对象是一个用来存储不同属性的容器
- 对象不仅存储属性,还要负责数据的安全
- 直接添加到对象中的属性,并不安全,应为它们可以被任意修改
如何确保数据安全:
- 私有化数据
- 将需要保护的数据设置为私有,只能在类内部使用
- 提供给getter和setter方法来开放数据的操作
- 可以控制属性的读写权限
- 可以在方法中对属性值进行验证(在set中写判断语句)
封装主要用来保护数据的安全
- 实现封装的方式:
- 属性私有化 加#
- 通过getter和setter方法来操作属性
- get 属性名( ){
- return this.#属性
- }
- set 属性名( 参数 ){
- this.#xxx = 参数
- }
私有属性完成封锁

getter和setter方法提供数据操作(方法一)不建议使用
<script>
class Person {
// #号代表私有属性,只能在Person类内部使用,出了Person类就访问不了(封装)
#name
#age
#gender
constructor(name, age, gender) {
this.#name = name
this.#age = age
this.#gender = gender
}
getName() {
return this.#name;
}
setName(name) {
this.#name = name
}
getAge() {
return this.#name;
}
setAge(age) {
if (age>=0) { //对属性的值进行判断
this.#age = age
}
}
}
const p1 = new Person("孙悟空", 18, "meal")
console.log(p1.getName()); //孙悟空
p1.setAge(-11) //不做修改
</script>
getter和setter方法提供数据操作(方法二)
<script>
class Person {
// #号代表私有属性,只能在Person类内部使用,出了Person类就访问不了(封装)
#name
#age
#gender
constructor(name, age, gender) {
this.#name = name
this.#age = age
this.#gender = gender
}
get age() {
return this.#age;
}
set age(age) {
if (age >= 0) { //对属性的值进行判断
this.#age = age
}
}
}
const p1 = new Person("孙悟空", 18, "meal")
p1.age = 11 //相当于p1.setAge(11)
console.log(p1.age); //相当于p1.getAge(11)
console.log(p1); //Object { #name: "孙悟空", #age: 11, #gender: "meal" }
</script>
多态
- 在JS中不会检查参数类型,所以这就意味着任何数据都可以作为参数传递
- 要调用某个函数,无需指定类型,只要对象满足某些条件即可
- 如果一个东西走路像鸭子,叫起来像鸭子,那么它就是鸭子
- 多态为我们提供了灵活性
<script>
class Person {
constructor(name) {
this.name = name
}
}
class Dog {
constructor(name) {
this.name = name
}
}
const person = new Person("Tom")
const dog = new Dog("旺财")
// 定义一个函数,它以对象作为参数,它可以输出hello并打印对象的name属性
function sayHello(obj) {
console.log("hello" + obj.name);
}
sayHello(person)
sayHello(dog)
</script>
只要类里面有name属性都可以使用sayHello( )函数打印出各自的name。------多态
继承
- 可以通过extends关键字来完成继承
- 当一个类继承另一个类时,就相当于将另一个类中的代码复制到当前类中(简单理解)
- 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
- 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展
<script>
class Animal {
constructor(name) {
this.name = name;
}
test() {
console.log(this.name + "在叫");
}
}
class Dog extends Animal {
//继承相当于拥有父类的一切属性和方法
}
const dog = new Dog("旺财");
console.log(dog); //Object { name: "旺财" },还有一个test方法只不过被隐藏看不到(对象结构有讲到-原型)
dog.test() //旺财在叫
</script>
继承
- 通过继承可以在不修改一个类的情况下对其进行扩展
- OCP 开闭原则
- 程序应该对修改关闭,对扩展开放
<script>
class Animal {
constructor(name) {
this.name = name;
}
test() {
console.log(this.name + "在叫");
}
}
class Dog extends Animal {
//继承相当于用于父类的一切属性和方法
//重写父类方法
test() {
console.log(this.name + "汪~汪~叫");
}
}
class Cat extends Animal {
// 重写构造函数
constructor(name, age) {
// 重写构造函数时,构造函数的第一行代码必须是super()
super(name); //调用父类的构造函数
this.age = age;
}
test() {
super.test(); //在方法中可以使用super来引用父类方法
console.log("喵喵咪哦啊");
}
}
const dog = new Dog("旺财");
console.log(dog); //Object { name: "旺财" },还有一个test方法只不过被隐藏看不到
dog.test() //旺财汪~汪~叫
const cat = new Cat("汤姆", 3)
cat.test()
</script>
重写
重写是对继承的扩展,子类继承了父类的方法。
- 直接使用父类方法
- 在子类中重新书写父类的方法(重写)
<script>
class Animal {
constructor(name) {
this.name = name;
}
test() {
console.log(this.name + "在叫");
}
}
class Dog extends Animal {
//继承相当于用于父类的一切属性和方法
//重写父类方法
test() {
console.log(this.name + "汪~汪~叫");
}
}
const dog = new Dog("旺财");
console.log(dog); //Object { name: "旺财" },还有一个test方法只不过被隐藏看不到
dog.test() //旺财汪~汪~叫
</script>
总结
封装 —— 安全性
继承 —— 扩展性
多态 —— 灵活性
对象的结构
原型对象
对象中存储属性的区域实际有两个:
- 对象自身
- 直接通过对象所添加的属性,位于对象自身中
- 在类中通过 name = xxx 的形式添加的属性,位于对象自身中
- 原型对象(prototype)
- 对象中还有一些内容,会存储到其他的对象里(原型对象)
- 在对象中会有一个属性用来存储原型对象,这个属性叫做 _ _proto_ _
- 原型对象也负责为对象存储属性
- 当我们访问对象中的属性时,会优先访问自身的属性
- 对象自身不包含属性时,才会去原型对象中寻找
- 会添加到原型对象中的情况:
- 在类中通过xxx( ){. . .}方式添加的方法,位于原型中
- 主动向原型中添加的属性或方法

class Animal {
constructor(name) {
this.name = name;
}
test() { //位于原型对象中
console.log(this.name + "在叫");
}
}
访问原型对象
- 访问一个对象的原型对象
- 对象.__proto__(不建议使用,应为可以随时修改原型,下面修改原型就是用它)
- Object.getPrototypeOf(person)
- 原型对象中的数据:
- 1.对象中的数据(属性,方法等)
- 2.constructor(对象的构造函数)
- 注意:
- 原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同。
- person对象的原型链:person对象-->object--> Object原型--> null
- 原型链:
- 读取对象属性时,会优先对象自身属性,
- 如果对象中有,则使用,没有则去对象的原型中寻找
- 如果原型中有,则使用,没有则去原型的原型中寻找
- 直到找到Object对象的原型,Object的原型没有原型(为null)
- 如果依然没有找到返回undefined
- 读取对象属性时,会优先对象自身属性,
- 作用域链,是找变量的链,找不到会报错
- 原型链,是找属性的链,找不到会返回undefined
<script>
class Person {
name = "孙悟空"
age = 18
test() {
console.log("你好,我是:" + tthis.name);
}
}
const person = new Person()
console.log(Object.getPrototypeOf(person));
console.log(Object.getPrototypeOf(person) == person.__proto__); //true
console.log(person.__proto__);
console.log(person.__proto__.__proto__); //null
</script>

原型的一些补充
- 所有同类型对象,他们的原型都是同一个
- 也就意味着,同类型对象的原型链是一样的
- 原型的作用:
- 原型相当于一个公共的区域,可以被所有该类实例访问
- 可以将该类实例中,所有的公共属性(方法)统一存储到原型中
- 这样我们只需要创建一个属性,即可被所有实例访问
- 原型相当于一个公共的区域,可以被所有该类实例访问
- JS中继承就是通过原型来实现的,
- 当继承时,子类的原型就是一个父类的实例
- 在对象中有些值是队形独有的,像属性(name,age,gender)每个对象都应有自己的值
- 但是有些值对于每个对象都是一样的,像各种方法,对于一样的值没必要重复的创建
<script>
class Person {}
const person1 = new Person()
const person2 = new Person()
//所有同类型对象,他们的原型都是同一个
console.log(person1.__proto__ == person2.__proto__); //true
class Man extends Person { } //男人类继承人类
class XiaoMing extends Man { } //小明继承男人类
const xiaoming = new XiaoMing()
//xiaoming对象-->Man-->Person--object-->Object原型-->null
console.log(xiaoming);
</script>

修改原型
大部分情况下,我们是不需要修改原型对象(面试爱问的一些犄角旮旯的问题)
新版ES6写代码的时候基本用不上,除非你改一些老的项目要用ES5,ES3去写。
注意:
- 千万不要通过类的实例去修改原型
- 通过一个对象影响所有同类对象,这么做不合适
- 修改原型先得创建实例,麻烦
- 危险
- 除了通过_ _proto_ _能访问原型外,
- 还可以通过类的prototype属性,来访问实例的原型
- 修改原型时,最好通过类去修改
- 好处:
-
-
- 一修改就是修改所有的实例的原型
- 无需创建实例即可完成对类的修改
-
-
- 原则:
-
-
- 原型尽量不要手动改
- 要改也不要通过实例去修改
- 通过 类.prototype属性去修改
- 最好不要直接给prototype去赋值(prototype = xxx)但是可以添加属性 类.prototype.属性名 ="" 或 (){}
-
代码一,通过类的实例修改原型,相当于删除原有的基因把Dog的基因强加在Person身上,不伦不类。(不要这么做)
<script>
class Person {
name = "孙悟空"
age = 18
test() {
console.log("你好,我是:" + tthis.name);
}
}
class Dog { }
const p1 = new Person()
const p2 = new Person()
//通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法,不要这么做
p1.__proto__.run = () => {
console.log("我在跑~");
}
p1.__proto__ = new Dog() //直接为对象赋值了一个新的原型,不要这么做
console.log(p1);
console.log(p2);
</script>

代码二,通过类的prototype属性修改原型,相当于不修改原有基因的情况下添加新的基因,进化。(应用于所有所有依赖此原型的类)
<script>
class Person {
name = "孙悟空"
age = 18
test() {
console.log("你好,我是:" + tthis.name);
}
}
const p1 = new Person()
const p2 = new Person()
Person.prototype.fly = () => { //在Person实例的原型中添加属性
console.log("我在飞~");
}
console.log(Person.prototype); //访问Person实例的原型对象
p1.fly()
p2.fly()
</script>

instanceof和hasOwnProperty
instanceof
instanceof用来检查一个对象是否是一个类的实例
- instanceof检查的是对象的原型链上是否有该类的实例
- 只要原型链上有该类的实例,就会返回true
- dog -> Animal的实例 -> Object实例 -> Object原型
- Object是所有对象的原型,所任何对象和Object进行instanceof运算都会返回true
<script>
class Animal { }
class Dog extends Animal { }
const dog = new Dog()
//dog -> Animal的实例 -> Object实例 -> Object原型
//Object是所有对象的原型
console.log(dog instanceof Dog); //true
console.log(dog instanceof Animal); //true
console.log(dog instanceof Object); //true
// 如何访问Object?(最大的原型)
const obj = new Object();
console.log(obj.__proto__); //通过实例.__proto__,获取Object的原型
console.log(Object.prototype); //通过类的prototype属性,获取Object的原型
</script>
in
使用in运算检查属性时,无论对象在自身还是在原型中,都会返回true
<script>
class Person {
name = "孙悟空"
age = 18
test() {
console.log("你好,我是:" + tthis.name);
}
}
const p = new Person();
console.log("test" in p) //true
</script>
hasOwnProperty
对象.hasOwnProperty(属性名)--->已经是老方法了不建议再使用。
用来检查一个对象的自身是否含有某个属性
<script>
class Person {
name = "孙悟空"
age = 18
test() {
console.log("你好,我是:" + tthis.name);
}
}
const p = new Person();
// console.log("test" in p)
console.log(p.hasOwnProperty("name")); //true
console.log(p.hasOwnProperty("test")); //false
console.log(Person.prototype.hasOwnProperty("test")); //true
</script>
hasOwn
相比于hasOwnProperty这种实例方法,目前官方更推荐hasOwn这种静态方法。虽然效果都一样。
Object.hasOwn(对象,属性名)
用来检查一个对象的自身是否含有某个属性
<script>
class Person {
name = "孙悟空"
age = 18
test() {
console.log("你好,我是:" + tthis.name);
}
}
const p = new Person();
// console.log("test" in p)
// console.log(p.hasOwnProperty("name")); //true
// console.log(p.hasOwnProperty("test")); //false
// console.log(Person.prototype.hasOwnProperty("test")); //true
console.log(Object.hasOwn(p, "name")) //true
console.log(Object.hasOwn(p, "test")) //false
console.log(Object.hasOwn(Person.prototype, "test")) //true
</script>

浙公网安备 33010602011771号