JS高级-3

127 阅读10分钟

深入面向对象

面向过程:

image.png

面向对象:模块化开发,按照功能来划分问题,而不是步骤。

image.png

面向对象编程(oop):每一个对象都是功能中心,具有明确的分工。具有灵活、代码可重用、容易维护和开发的优点。更适合多人合作的大型项目。

面向对象的特征:封装性、继承性、多态性。

image.png

前端面向过程更多!!!

构造函数

image.png

//构造函数 公共的属性和方法 封装到Star 构造函数中
function Star(uname,age){
    this.uname = uname
    this.age = age
    this.sing = function(){
        console.log('唱歌')
    }
}

const ldh = new Star('刘德华',55)
const zxy = new Star('张学友',58)

实例对象:ldh、zxy;this指向的就是你创建的实例对象。

构造函数方法固然很好,但是存在内存浪费的问题

image.png

实质上都是同一个方法,但是每个对象中的function的指向又不相同,一旦对象过多,开辟了很多内存。就会造成内存浪费的问题(主要是复杂数据类型)。

image.png

原型对象

我们希望所有的对象都使用同一个函数,这样就比较节省内存,那么我们要怎么做呢?(属性可以不一样,但是希望方法在不同对象中,是一样的)

原型:
1.构造函数通过原型分配的函数,是所有对象所共享的
2.JavaScript规定,每一个构造函数都有一个prototype属性,用来指向另一个对象,所以我们也称为:原型对象
3.这个对象可以挂载函数,对象实例化不会多次创建原型上的函数,节约内存。(是共用的对象,不会多次创建)
4.我么可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法了。
5.构造函数和原型对象中的this都指向:实例化的对象。

console.dir(Star.prototype):prototype虽然是一个属性(构造函数的属性),但是返回的是一个【对象】。

那些共享的方法、不希望每次实例对象时再创建的,就可以直接写在原型对象上,不用再写在构造函数上了,这样每次实例对象使用的方法,都是同一个,不存在内存浪费问题。

function Star(uname,age){
    this.uname = uname
    this.age = age
}

Star.prototype.sing = function(){
        console.log('唱歌')
}

const ldh = new Star('刘德华',55)
const zxy = new Star('张学友',58)

ldh.sing()
zxy.sing()

console.log(ldh.sing === zxy.sing)//true

公共的属性写在构造函数中,公共的方法写在原型对象上。

image.png

构造函数和原型对象的this

  1. 构造函数
function Star(uname){
    //this就相当于return
    this.uname = uname
}

const ldh = new Star('刘德华')

在构造函数中:this就是实例对象,就是ldh。

  1. 原型对象
function Star(uname){
    this.uname = uname
}
Star.prototype.sing = function(){
    console.log('唱歌')
}

const ldh = new Star('刘德华')
ldh.sing()//实例对象ldh,调用原型对象方法

在原型对象:this指向的就是调用原型对象方法的【实例对象】,这里也就是ldh。

image.png

构造函数和原型里面的this指向的都是【实例化的对象】

数组扩展方法

给数组扩展:求最大值和求和的方法;
比如:以前学过:

const arr = [1,2,3]
arr.reverse() //[3,2,1]

求和可以用reduce(),但是我希望我们可以自定义一个sum()方法,arr.sun()返回6。

通过原型对象来实现:

// 自定义数组的扩展方法:求和和最大值
// 我们定义的这个方法,任何一个数组实例对象都可以使用

const arr = [1,2,3]//arr就是一个实例对象
=》const arr = new Array(1,2,3)//等价

//求最大值,接收一个数组
Array.prototype.max = function(){
    //数组不能直接被max识别,而是需要将数组展开
    //展开后呈现数组的具体数据,进行max
    return Math.max(...this)//this就是实例对象arr
}
console.log(arr.max())

求和

const arr = [1,2,3]

Array.prototype.sum = function(){
    //...this = ...arr = 1,2,3
    return this.reduce((prev,item)=>prev+item,0)
}

arr.sum()

constructor属性

每个【原型对象】里面都有一个constructor属性(constructor构造函数)

作用:该属性指向该原型对象的构造函数,简单理解:指向我的爸爸(构造函数),我(原型对象)是有爸爸的孩子。

functino Star(uname){
    this.uname = uname
}
const ldh = new Star()
console.log(Star.prototype.contructor)//返回原型对象的构造函数:Star()

Star.prototype.constructor === Star //true

constructor应用

使用场景:如果有多个对象的方法,我们可以给原型对象采取对象形式赋值。
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象constructor就不再指向当前的构造函数了。
此时,我们可以对修改后的原型对象,添加一个constructor指向原来的构造函数。

Star.prototype.sing = function(){}
Star.prototype.dance = function(){}

上方这样写太麻烦了,我想直接写到一个对象里面,就会方便很多!如下:

console.log(Star.prototype)//这里面会存在constructor
function Star(){

}
//直接给构造函数的原型对象,再添加一个对象,里面用来挂载方法
Star.prototype = {
    sing:function(){
        console.log('唱歌')
    },
    dance:function(){
        console.log('跳舞')
    },
}

console.log(Star.prototype)//这里不再有constructor

此时就会分不清,这个原型对象到底属于哪个构造方法了。

需要我们手动指明constructor!!!

Star.prototype = {
    //重新指回 创造这个原型对象的构造函数
    constructor:Star,
    
    sing:function(){
        console.log('唱歌')
    },
    dance:function(){
        console.log('跳舞')
    },
}

constructor属性:指回该原型对象的构造函数

对象原型

构造函数可以用来创建实例对象,每个构造函数中还有一个【原型对象】,一些公共的方法都存放在【原型对象】中,避免内存浪费。

但是,为什么实例对象可以访问【原型对象】里面的属性和方法呢?

通过【对象原型】来实现 “实例对象”访问【原型对象】

image.png

注:两个下划线__

对象都会有一个属性:__proto__,用来指向构造函数的prototype原型对象,之所以我们对象可以用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。

image.png

简而言之:【对象原型】-__proto__是属于【实例对象】的;用来指向【实例对象】和【prototype(对象原型)】之间的连接。

function Star(){

}
const ldh = new Star()
console.log(ldh)

image.png

这个[[prototype]]就是__proto__(对象原型)

注意:
1.__proto__是JS非标准属性。
2.[[prototype]]__proto__意义相同。
3.用来表明当前实例对象指向哪个原型对象prototype。
4.__proto__对象原型里面也有一个constructor属性,指向创建该实例对象的构造函数。
5.只读的,只能获取、不能赋值。

简而言之:在控制台中查看:[[prototype]],书写代码:__proto__

ldh.__proto__ === Star.prototype对象原型属于(实例)对象,原型对象属于构造函数。

__proto__中的constructor指向构造函数
prototype中的constructor也指向构造函数\

__proto__.constructor === Star //true

总结:构造函数(Star)生了两个儿子:【大儿子--原型prototype】、【二儿子--实例对象】,但是二儿子不中用,有些时候想指望【大哥】,通过自身存在的属性:__proto__来指向大哥。还有一个__proto__.constructor来指向父亲(构造函数)。而大哥就不一样了,大哥很有本事,只需要指向父亲:prototype.constructor

image.png

image.png

原型继承

继承是面向对象的另一个特征,通过继承、进一步提升代码封装的程度,JavaScript中大多数借助原型对象(prototype),实现继承的特征。

//公共
const Person = {
    eyes: 2
    head: 1
}


//女人 构造函数
function Woman(){
    
}

//Woman通过 原型对象 来继承Person
Woman.prototype = Person

const red = new Woman()
red.eyes //返回2

prototype存放公共属性、方法,实例对象通过自身存在的__proto__来访问原型对象
这样我每次new出来的实例对象,都可以使用Person

但是此时,Woman的prototype中,已经没有constructor的属性了,(Person是一个对象的形式,直接进行了覆盖、再也指不回来了)需要我们手工指定构造函数。

Woman.prototype = Person

Woman.prototype.constructor = Woman

const red = new Woman()

原型继承遇到的问题

不仅想要继承父亲的属性、方法,我还要定义出属于我自己的属性、方法:

function Woman(){
    
}
Woman.prototype = Person
Woman.prototype.constructor = Woman

//女人独有的方法:生孩子
Woman.prototype.baby = function(){
    console.log('生孩子')
}

//此时我的red实例对象通过__proto__来实现使用原型对象的属性和方法
const red = new Woman()
red.baby()

----------------------------------------------

但是!!!
Man.prototype = Person
Man.prototype.constructor = Man
const pink = new Man()
console.log(pink)//pink里面也存在一个baby的方法

为什么会存在这种情况?我单独给Woman添加了baby方法,为什么Man里也会有这个方法?

答:因为他们都继承了Person,他们的原型对象是一模一样的。根据引用类型的特点,他们指向同一个对象,修改一个就会都影响。

如何解决?

//构造函数 new 出来的对象 结构一样,但是是不同对象
function Person(eyes,head){
    this.eyes = 2
    this.head = 1
}

function Woman(){
    
}
//创建实例对象,赋值给prototype原型对象
//以前我是在prototype里面书写公共的方法,现在直接将Person赋值给他
Woman.prototype = new Person()
Woman.prototype.constructor = Woman
-------------------------------------

function Man(){

}
Man.prototype = new Person()
Man.prototype.constructor = Man

image.png

原型继承的核心:
Woman.prototype = new Person()父类构造函数(父类)、子类构造函数(子类)

子类的原型 = new 父类

原型链

function Person(){

}

const ldh = new Person()

得到:ldh.__proto__ === Person.prototypetrue\

其实我们有一个最大的构造函数:function Object(){},意味着他也有一个原型对象:Object.prototype

在我们的【原型对象】中,也有一个属性:__proto__,他指向【原型对象】。实际上,他指向的是最顶层的原型对象:Object.prototype

Person.prototype.__proto__ === Object.prototype

Object.prototype原型对象,他是一个对象, 就意味着他有一个属性:Object.prototype.__proto__,然后再往顶层走,实际上Object是最顶层了,因此再也找不到了最顶层,返回一个【null】。

总结:只要是对象,就会有一个属性:__proto__,他就是原型。

image.png

基于原型对象的继承,使得不同构造函数的原型对象关联在一块。并且这种关联的关系,是一种链状的结构,我们将原型对象的链状结构,称为:原型链。

首先明确一点:__proto__是每个【对象】身上都会具有的属性,并且它是用来指向原型对象的。【原型对象】身上有constructor属性,用来指向创造我的构造函数。
Star构造函数,构造出一个实例对象,这个实例对象的原型__proto__属性,就指向Star构造函数的【原型对象】,是对象、就有__proto__属性,Star原型对象的__proto__属性指向顶层原型对象:Object.prototype,他也有一个__proto__属性,他也往上指,但是Object已经是最顶层了,因此返回null

image.png

原型链查找规则:
1.当访问一个对象的属性(包括方法)时,首先查找这个对象自身,有没有该属性
2.如果没有,就查找她的原型(也就是__proto__指向的prototype原型对象)
3.如果还没有,就查找原型对象的原型(Object的原型对象)
4.以此类推,一直找到Object为null
5.__proto__对象原型的意思:为对象成员查找机制、提供一个方向,或者说是一条路线
6.可以使用instanceof运算符,检测构造函数的prototype属性,是否出现在某个实例对象的原型链上。

instanceof

检测构造函数的prototype属性,是否出现在某个实例对象的原型链上。

function Person(){

}

const ldh = new Person()

console.log(ldh instanceof Person)//true
console.log(ldh instanceof Object)//true
console.log(ldh instanceof Array)//false 对象不在数组这条链路上走

console.log([1,2,3] instanceof Array)//true 数组就是在Array数组原型链上

console.log(Array instanceof Object)//true 万物皆对象

判断A instanceof(是否属于) B

image.png

综合案例

1.定义模态框Model构造函数,用来创建对象
2.模态框具备 打开功能open方法
3.模态框具备 关闭功能close方法

//构造函数
function Model(title='',message=''){
    //公共属性
    this.title = title
    this.message = message
    
    //创建盒子,谁用我,我就给谁创建盒子,一定要有this
    this.modelBox = document.createElement('div')
    
    //添加类名
    this.modelBox.className = 'model'
    
    //填充内容,更换数据,必须要加this,构造函数里必须加this
    this.modelBox.innerHTML = `
        <div class = "header">${this.title}<i>x</i></div> 
        <div class = "body">${this.message}</div>
    `
}

---------------------------------------------------------------

    //挂载方法到构造函数上
    Model.prototype.open = function(){
    
        if(!document.querySelector('.model')){
        //构造函数所有的 this 都指向实例对象
        document.body.appendChild(this.modelBox)
        
        //获取 x 调用关闭方法
        //因为打开后,删除功能和打开功能在一个盒子里,没办法单独调用删除功能
        this.modelBox.querySelector('i').addEventListener('click',()=>{
            //箭头函数的this是上一级的this指向,也就是指向了实例对象m
            this.close()
        })
        }
    }

    Model.prototype.close = function(){
        document.body.removeChild(this.modelBox)
    }

    //按钮点击
    document.querySelector('#delete').addEventListener('click', ()=>{
        const m = new Model('温馨提示','您没有权限删除')
        m.open()
    })

     document.querySelector('#login').addEventListener('click', ()=>{
        const m = new Model('温馨提示','您没有注册')
        m.open()
    })