面向对象
1. 概念
首先面向对象不是一种新的语法,而是一种编程思维,是一种编程范式(方式)
一般我们可以把编程思想分为两种面向过程
1.1 面向过程:
面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。 以过程为核心,强调事件的流程、顺序面向对象
1.2 面向对象:
面向对象是一种以对象为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。 以对象为核心,强调事件的角色、主体
1.3 举个栗子
小明每天早上起床上班的事情包含:起床、穿衣、洗漱、开车上班
从面向过程的角度看就是:
- 1、起床;2、穿衣服;3、刷牙洗脸;4、开车上班。
从面向对象的角度看就是:
主人公:小明;
拥有的财产:床、衣服、洗漱用品、车;
小明要做的事:从床上起来、把衣服穿上、去洗漱,开车上班。
在日常生活或编程中,简单的问题可以用面向过程的思路来解决,直接有效 ,但是当问题的规模变得更大时,用面向过程的思想是远远不够的。所以慢慢就出现了面向对象的编程思想
1.4 优缺点比较
面向过程:
优点:
流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。
效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。
缺点:
- 需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大。面向对象
面向对象:
优点: 结构清晰,程序是模块化和结构化,更加符合人类的思维方式;
缺点:开销大,当要修改对象内部时,对象的 直接存取,消耗的内存会加大
2. 创建对象的方式
因为面向对象就是一个找到对象的过程
所以我们先要了解如何创建一个对象
2.1 Object 构造函数创建
js 给我们内置了一个 Object 构造函数
这个构造函数就是用来创造对象的
当 构造函数 和 new 关键字连用的时候,就可以为我们创造出一个对象
因为 js 是一个动态的语言,那么我们就可以动态的向对象中添加成员了
var o1 = new Object()
// 正常操作对象
o1.name = 'Jack'
o1.age = 18
o1.gender = '男'2.2 字面量创建
直接使用字面量的形式,也就是直接写
可以在写的时候就添加好成员,也可以动态的添加
// 添加默认成员
var o1 = {
name: 'Jack',
age: 18,
gender: '男'
}
// 动态添加成员
var o2 = {}
o2.name = 'Rose'
o2.age = 20
o2.gender = '女'2.4 工厂函数创建
先书写一个工厂函数
这个工厂函数里面可以创造出一个对象,并且给对象添加一些属性,还能把对象返回
使用这个工厂函数创造对象
// 1. 先创建一个工厂函数
function createObj() {
// 手动创建一个对象
var obj = new Object()
// 手动的向对象中添加成员
obj.name = 'Jack'
obj.age = 18
obj.gender = '男'
// 手动返回一个对象
return obj
}
// 2. 使用这个工厂函数创建对象
var o1 = createObj()
var o2 = createObj()2.5 自定义构造函数创建
工厂函数需要经历三个步骤
手动创建对象
手动添加成员
手动返回对象
构造函数会比工厂函数简单一下
- 自动创建对象
- 手动添加成员
- 自动返回对象
构造函数创建对象过程
- 先书写一个构造函数
- 在构造函数内向对象添加一些成员
- 使用这个构造函数创造一个对象(和 new 连用)
- 构造函数可以创建对象,并且创建一个带有属性和方法的对象
- 面向对象就是要想办法找到一个有属性和方法的对象
- 面向对象就是我们自己制造
构造函数 的过程
// 1. 先创造一个构造函数
function Person(name, gender) {
this.age = 18
this.name = name
this.gender = gender
}
// 2. 使用构造函数创建对象
var p1 = new Person('Jack', 'man')
var p2 = new Person('Rose', 'woman')3.关于原型
我们从一个例子展开
一个构造函数可以有多个属性,每次实例化都会创造一个新的引用对象,属性也是一样的
function Person() {
this.name = '55开'
this.age = 25
this.hobby = "吃鸡"
}
let p1 = new Person;
let p2 = new Person;
let p3 = new Person;
p2.sayHi = function (){
console.log('大家好 我是55开');
}
p3.nickname = "我开哥没有开挂"
console.log(p1);
console.log(p2);
// console.log(p1==p2);
console.log(p3);
//问题:
//3个实例有着相同的属性,但是这些相同的属性分别在不同的内存区域中,造成了大量的内存消耗
//为何不将这些共享的属性放到一个对象中?放到一个内存中?
//原型:
// 原型的本质:原型就是一个对象,里面放着各个实例共有的属性(想象成它就是一个仓库)
// 原型的作用:减少内存消耗3.1 原型的本质
就是一个对象,我们可以理解为它就是一个仓库,所有的实例都可以去这个仓库中拿属性
3.2 原型的作用
用来存放实例中共有的那部份属性、方法,可以大大减少内存消耗。
3.3 prototype
每一个函数自带一个成员,叫做 prototype,是一个对象空间
即然每一个函数都有,构造函数也是函数,构造函数也有这个对象空间
function Person() {}
console.log(Person.prototype) // 是一个对象即然是个对象,那么我们就可以向里面添加数据
function Person() {}
Person.prototype.name = 'prototype'
Person.prototype.sayHi = function () {}重点: 在函数的 prototype 里面存储的内容,不是给函数使用的,是给函数的每一个实例化对象使用的
3.4 proto
每一个对象都天生自带一个成员,叫做
__proto__,是一个对象空间__proto__是对构造函数prototype的引用,也就是说__proto__和prototype指向同一个地址访问对象成员的时候,先查找自身对象成员,如果没有,就查找__proto__
__proto__只能通过实例访问
function Person() {}
var p1 = new Person()
// __proto__和prototype 地址相同
console.log(p1.__proto__ === Person.prototype) // true- 实例化对象的
__proto__和所属的构造函数的prototype是同一个对象空间 - 可以通过构造函数名称来向 prototype 中添加成员
注意:当通过obj.key使用对象的属性或方法时,若对象本身没有,则会在实例化对象的proto中找
function Person() {}
// 向prototype中添加成员
Person.prototype.sayHi = function () {
console.log('hello Person')
}
// 实例化对象
var p1 = new Person()
p1.sayHi()- 以上代码中,p1的构造函数Person自己没有 sayHi 方法,就会去自己的 proto 中查找
- p1.__proto__就是 Person.prototype
- 我们又向 Person.prototype 中添加了 sayHi 方法
- 所以 p1.sayHi 就可以执行了
每个实例化对象的__proto__都指向一个其构造函数的prototype
function Person() {}
Person.prototype.sayHi = function () {
console.log('hello')
}
var p1 = new Person()
var p2 = new Person()
console.log(p1.sayHi === p2.sayHi)- p1是 Person 的一个实例
- p2是 Person 的一个实例
- 也就是说 p1.proto 和 p2.proto 指向的都是 Person.prototype
- 当 p1 去调用 sayHi 方法的时候是去 Person.prototype 中找
- 当 p2 去调用 sayHi 方法的时候是去 Person.prototype 中找
- 那么两个实例化对象就是找到的一个方法,也是执行的一个方法
所以:当我们写构造函数的时候,属性我们直接写在构造函数体内,方法我们写在原型上
4.原型链
我们说构造函数的 prototype 是一个对象 又说了每一个对象都天生自带一个 __proto__ 属性,那么构造函数的 prototype 里面的 __proto__ 属性又指向哪里呢?
首先我们要清楚明白两个概念:
js分为函数对象和普通对象,每个对象都有
__proto__属性,但是只有函数对象才有prototype属性Object、Function都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String
全局对象有哪些?navigator history location window
那么__proto__和prototype到底是什么,两个概念理解它们
函数都有一个属性叫做prototype 里面有一个constructor属性指向构造函数, 记录了实例的创建者是谁
对象都有一个属性叫做
__proto__指向构造函数的prototypeprototype本质是一个对象
function本质也是一个对象
4.2 举个栗子
有以下构造函数Person,他的原型上有所属国属性motherland='china'
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.motherland = 'china'通过new Person()创建的person01实例
let person01 = new Person('小明', 18);js之父在设计js原型、原型链的时候遵从以下两个准则
// 准则1:原型对象(即Person.prototype)的constructor指向构造函数本身
Person.prototype.constructor == Person
// 准则2:实例(即person01)的__proto__和原型对象指向同一个地方
person01.__proto__ == Person.prototype4.3 一个对象所属的构造函数
每一个对象都有一个自己所属的构造函数
比如: 数组
// 数组本身也是一个对象
var arr = []
var arr2 = new Array()以上两种方式都是创造一个数组
我们就说数组所属的构造函数就是 Array
比如: 函数
// 函数本身也是一个对象
var fn = function () {}
var fun = new Function()以上两种方式都是创造一个函数
我们就说函数所属的构造函数就是 Function
4.4 constructor
对象的
__proto__里面也有一个成员叫做 constructor这个属性就是指向当前这个对象所属的构造函数
4.5 链状结构
当一个对象我们不知道准确的是谁构造的时候,我们呢就把它看成 Object 的实例化对象
也就是说,我们的 构造函数 的 prototype 的 proto 指向的是 Object.prototype
那么 Object.prototype 也是个对象,那么它的 proto 又指向谁呢?
因为 Object 的 js 中的顶级构造函数,我们有一句话叫 万物皆对象
所以 Object.prototype 就到顶了,Object.prototype 的 proto 就是 null
4.6 顶级构造函数
我们 js内置了一个构造函数Object用来创建对象
作为一个函数,就会拥有 prototype成员
console.log(Object.prototype);
// 通过 Object构造函数 实例的对象 的 __proto__ 指向的就是 Object.prototype
let o1 = new Object();
console.log(o1.__proto__);
console.log(o1.__proto__ === Object.prototype); // true我们 自定义的构造函数的原型对象的原型对象 指向的是 内置构造函数Object的原型对象
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(p1.__proto__.__proto__ === Object.prototype); // true
console.log(p2.__proto__.__proto__ === Object.prototype); // true在 js中 Object构造函数 是用于创建对象的 顶级构造函数
所以我们 可以认为 所有的对象都是 Object构造函数创建
当一个对象我们不知道准确的是谁构造的时候,我们呢就把它看成 Object 的实例化对象
也就是说,我们的 构造函数 的 prototype 的 proto 指向的是 Object.prototype
那么 Object.prototype 也是个对象,那么它的 __proto__ 又指向谁呢?
因为 Object 的 js 中的顶级构造函数,我们有一句话叫 万物皆对象
所以 Object.prototype 就到顶了,Object.prototype 的__proto__ 就是 null
console.log(Object.prototype.__proto__); // null
//在正常情况下,非Object构造函数创建的对象的 __proto__ 的 __proto__ 都是指向 Object.prototype
// 沿着 一个对象的 __proto__ 一直向上 去查找 就最终构成了 原型链4.7 原型链的访问原则
我们之前说过,访问一个对象的成员的时候,自己没有就会去 proto 中找
接下来就是,如果
__proto__里面没有就再去__proto__里面找一直找到
Object.prototype里面都没有,那么就会返回 undefiend
注意:
如果 针对一个对象的属性进行赋值,是 不会 按照原型链查找赋值的
执行的逻辑是
如果对象 本身就有这个属性 直接 修改
如果对象 本上就没有这个属性 直接 添加
5. 关于原型的一些方法
5.1 Object.create(obj)
该方法接受一个对象作为参数, 返回一个全新的对象, 新对象会将参数中的对象作为自己的原型链(作为自己原型对象)
语法:
obj2=Object.create(obj)obj为obj2的原型对象
let obj1={
name:'对象1',
say:function(){
console.log(`我是${name}`);
}
}
let obj2=Object.create(obj1)
obj2.name='对象2'
// obj1时obj2的原型对象
console.log(obj2.__proto__); //obj1
console.log(obj2.__proto__===obj1); //true
console.log(obj2.__proto__.__proto__); //Object.prototype5.2 Object.getPrototypeOf()
该方法接受一个对象作为参数
ES5, 返回参数对象的原型链对象(原型对象)
此方法和使用 对象.proto 返回的结果是一样的
let abc = Object.getPrototypeOf(obj1);
console.log(abc.__proto__); // obj
console.log(abc===obj1.__proto__); // true
console.log(Object.getPrototypeOf(abc)===abc.__proto__); //true5.3 isPrototypeOf()
语法:
obj1.isPrototypeOf(obj2)该方法用于 检测 ob1 是否 存在于 ob2 的 原型链上,返回布尔值
let test = function () { } //构造函数
let testObject = new test(); //实例对象
test.prototype.isPrototypeOf(testObject) // true test.prototype在testObject的原型链上
Object.prototype.isPrototypeOf(testObject)// true Object.prototype在testObject的原型链上5.4 instanceof
用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置
1.检查某个实例对象是不是由某个构造函数创建的
let arr=[1,2]
let o=22
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
console.log(o instanceof Array); // false2.用于检查某个数据是不是一个数组类型
语法:obj instanceof constructor
let arr=[1,2]
let o=22
console.log(arr instanceof Array); // true
console.log(o instanceof Array); // false5.5 hasOwnProperty()
指示对象自身属性中是否具有指定的属性,但是不会查找原型链上的属性
语法:obj.hasOwnProperty(prop)
参数: prop 要查找的属性
返回值: 用来判断某个对象是否含有指定的属性的Boolean。
let test ={ 'food': '嗯呢' }
test.hasOwnProperty('food'); // true
test.hasOwnProperty('toString'); // false test本身没查找到toString6.new一个对象发生了什么
new一个构造函数,发生了什么
创建一个对象(开辟一个内存区域)
绑定this(构造函数内的this指向创建的对象实例)
会执行构造函数中的代码(为这个新对象添加属性)
实例constructor(构造器)指向构造函数
创建出来的对象,它的__proto__会指向构造函数的prototype
当函数没有返回新对象的时候,那么会自动获得通过new构建出来的对象
7.构造函数
7.1 构造函数基本使用
7.1.1 创建构造函数
- 和普通函数创建一样,区别是调用的方式不同
- 一名合格的程序员构造函数名首字母应该大写
function Test(){
// ***
}7.1.2 实例化构造函数
- 不同于普通函数,调用的时候必须要和 new 连用
- 在实例化new一个对象时,如果不需要传递参数,那么可以不写 (),如果传递参数就必须写
var t = new Test()- 每次 new 的时候,函数内部的 this 都是指向当前这次的实例化对象
- 每次new一个新对象,都会创建一个内存空间,所以每个实例化对象的数据互不影响
- 构造函数会自动返回一个对象,所以构造函数内部不要写 return
- 你如果 return 一个基本数据类型,那么写了没有意义
- 如果你 return 一个引用数据类型,那么构造函数本身的意义就没有了
function Person() {
console.log(this)
}
var o1 = new Person() // 本次调用的时候,this => o1
var o2 = new Person() // 本次调用的时候,this => o27.2 构造函数的普通属性
一个构造函数可以有普通属性,原型属性,静态属性
通过在构造函数中使用this.属性名=值来定义数据
function Person() {
// 定义普通属性
this.name = 'Jack'
// 定义普通方法
this.sayHi = function () {
console.log('hello')
}
}
// 创建实例对象
var p = new Person()
// 使用普通属性
console.log(p.name) // 'Jack'
// 使用普通方法
p.sayHi()普通属性占内存,因为每当我们new一个实例对象,都会开辟一个新的内存控件,但这些空间的属性和方法都是一样的,占用了两个空间地址,因此就会引起不必要的内存消耗
7.3 构造函数的原型属性
7.3.1 定义
通过构造函数.prototype.属性来添加
function Person() {}
// 给构造函数添加原型属性
Person.prototype.name='xiazhi'
// 给构造函数添加原型方法
Person.prototype.sayHi=function(){
console.log('hello')
}TIP
原型属性占用内存较少
为了避免普通属性占内存的问题,我们可以把属性和方法放在一个公共的空间里,那就是构造函数的prototype
原型属性是属于一个类,而不是属于每个实例对象
每次实例化一个对象时,对象身上的属性都需要开辟空间进行存储,而原型属性只需要开辟一次空间存储
7.3.2 使用
依然是直接通过.操作符进行访问
// 创建实例对象
var o1 = new Person()
// 使用原型数据
console.log(o1.name) // 'xiazhi'
o1.sayHi() // 'hello'TIP
在访问一个对象的属性时,会先在对象本身上查找,找不到则会去对象的原型上找,一直往上找到Object对象的原型中,若一直没找到则返回undefined
class Person {
age = 18
}
const person = new Person()
// person对象身上有age属性,值为18
console.log(person.age) // 18
// person对象身上没有score属性,为undefined
console.log(person.score) // undefined
Object.prototype.score = 100
// 给所有对象添加score属性,值为100
console.log(person.score) // 1007.4 构造函数的静态属性
我们给构造函数添加原型方法虽然不占内存,但每次使用都需要创建实例对象
使用静态属性不用创建实例对象,比如Array.isArray()
7.4.1 定义
需要通过构造函数名来添加
function Person() {}
Person.age=18
Person.sayHi=function(){
console.log("hello")
}7.4.2 使用
通过构造函数.属性使用
console.log(Person.age) //18
Person.sayHi() // "hello"8.class类
8.1 概述
在ES6之前都没有类的概念, 其实 ES6 的 类 只是一个 语法糖
从底层来说,还是构造函数
定义类: class 名称{ }
8.2 动态成员
类实例化才能够使用的成员,就是动态成员,也就是普通属性
什么是动态方法:在类实例化之后,内存中已经开辟了空间,用于保存当前的对象,那么这个方法就会添加到实例上,也就是说,只有实例化类之后,才能调用的方法,就是动态方法
8.2.1 constructor内添加
constructor构造器 === 和构造函数是一样的
给 实例对象 添加的**普通属性(动态成员)**书写在 constructor ( ) { } 构造器 中,( ) 可以写参数
在
constructor构造器 的this指向 实例对象和构造函数一样,给实例对象添加成员是通过 this添加
在 constructor构造器中 给 实例对象 添加成员 都是 动态成员
class Test {
constructor(num) {
this.num = num;
this.name = "xiazhi";
this.speak = function () {
console.log("hello");
};
}
}
// 实例化
const t = new Test(66);
// 使用普通属性
console.log(t.num); //66
console.log(t.name); // xiazhi
t.speak(); // hello8.2.2 constructor外添加
如果 在 constructor构造器外 定义 动态成员就直接书写,不需要使用 this
在 constructor构造器外定义动态属性不能使用参数赋值,如果使用就会报错,不建议将 动态属性 定义在 constructor构造器外
在constructor外用this.属性访问对象成员
constructor外定义动态方法用 fn=function(){}或箭头函数fn=()=>{}
fn ( ) 是给原型添加方法,相当于 Person.prototype.fn = function(){}
只能在class里添加原型方法,不能添加原型属性,原型属性需要在class外面添加
class Person {
constructor() {
this.name = '张三';
}
//动态属性
age=18
//动态方法,普通函数
hello=function(){
console.log(`我是${this.name},今年${this.age}`);
}
//动态方法,箭头函数
hello1=()=>{
console.log(`我是${this.name},今年${this.age}`);
}
}
// 实例化
let person1=new Person('男',22)
person1.hello()
person1.hello1()8.3 类的原型属性
8.3.1 class里定义原型属性
在constructor外定义,不需要=号
class Test {
speak() {
console.log('hello');
}
}
const t = new Test();
t.speak(); // hello8.3.2 class外定义原型属性
通过类名.prototype.函数名定义
class Test {}
Test.prototype.speak = () => {
console.log('hello')
}
// 实例化
const t = new Test();
t.speak(); // hello8.3 类的静态成员
静态成员 是和类一起生成的 如果实例化,那么会把这个成员标记为'封闭',也就意味着,实例化之后是无法通过 实例对象 访问到静态成员
给类定义静态成员需要通过 static关键字
定义静态方法:static fn ( ) =function ( ) { }
定义静态属性:static name='张三'
静态成员 只能通过
类名.键实现访问,不能通过实例对象调用访问静态方法:Person.fn ( ) Person为类名,fn()为静态方法,name为静态属性
访问静态属性:Person.name
在静态方法中,不好使用this,因为this指向的是当前的类,而不是当前的实例
静态方法中可以通过this访问静态成员,无法访问动态成员
class Person {
constructor() {
this.name = '小明';
}
hello() {
console.log('我是' + this.name);
}
// 静态成员 -- 方法
static fn = function () {
console.log('我是类的静态方法');
// 在静态方法中,不好使用this,因为this指向的是当前的类,而不是当前的实例
console.log(this);
// 在静态方法中可以访问静态成员
console.log(this.age); // 18
//无法访问 动态成员
console.log(this.name); // undefined
}
// 静态成员 -- 属性
static age= 18;
}
let p1=new Person()
Person1.fn()在class外添加静态成员
class Person{
constructor(){
}
}
// 静态成员 -- 方法
Person.sayHi=function(){
console.log("hello")
}
// 静态成员 -- 属性
Person.age=188.4 类的继承
继承,就是将父类的成员,继承到子类,如果子类没有这些成员,可以通过访问父类来获取这些成员,减少重复性逻辑的编写,复用代码逻辑
实现类的继承,需要子类使用到extends关键字
super关键字用于访问和调用一个对象的父对象上的
constructor函数。
class Father{
constructor(arr1,arr2){
...
}
}
class Son extends Father{
constructor(arr1,arr2){
super(arr1,arr2)
}
}当子类和父类中有相同的属性,则 优先使用子类的这个属性
如果子类中没有,才会按照父类中的执行
实例:
如果有需要传参的属性,也需要在super的()中传入参数
class Father {
constructor(name) {
this.name = name;
this.house='别墅'
this.say=function(){
console.log("你好,我是张三");
}
}
}
class Son extends Father{
constructor(name){
super(name)
this.name=name
this.say=function(){
console.log("你好,我是李四");
}
}
}
let f1=new Father('张三')
f1.say() //你好,我是张三
let son=new Son('李四') //你好,我是李四
son.say()
console.log(son.name); //李四
console.log(son.house); //别墅,子类没有定义属性,继承父亲的属性8.5 属性访问器
属性访问器是js类中,当我们访问一个属性时,可以做一些其他操作
属性访问器是两个函数,分别是set()和get()
- set()函数在属性被修改时被处触发
- get()函数在属性被访问时被处触发
语法如下
class Person {
get name() {
return 'John'
}
set name(value) {
console.log('Name cannot be changed')
}
}
const p = new Person()
console.log(p.name) // John上述代码并没有在类中定义name属性,所以不可修改,开发中可以作为实现私有属性的一个办法
若要实现修改访问器,需要单独定义一个方法
class Person {
constructor() {
this._name = 'John'
}
get name() {
return this._name
}
set name(value) {
this._name = value
}
}
const p = new Person()
console.log(p.name) // John
p.name = 'Mary'
console.log(p.name) // Mary注意:上述代码中属性访问器的名称和属性名并不相同,这是因为
8.6 私有成员
通过#关键字定义一个私有成员,并且私有属性只能在类内,construct外定义
class Person {
#_name = ''
#fn = () => {
console.log('Hello')
}
}私有成员可以在类内访问,类外不可访问,并且访问时也要带上#
class Person {
#_name
#fn = () => {
console.log('Hello')
}
constructor() {
this.#_name = 'John'
this.#fn()
}
}
const p=new Person()
console.log(p.#_name) // 报错若想在类外访问类内的私有成员,可以单独写一个方法进行”间接访问“
class Person {
#_name='John'
#fn = () => {
console.log('Hello')
}
get name() {
return this.#_name
}
myFn = () => {
this.#fn()
}
}
const p = new Person()
console.log(p.name) //John
p.myFn()