深入面向对象
面向过程:
面向对象:模块化开发,按照功能来划分问题,而不是步骤。
面向对象编程(oop):每一个对象都是功能中心,具有明确的分工。具有灵活、代码可重用、容易维护和开发的优点。更适合多人合作的大型项目。
面向对象的特征:封装性、继承性、多态性。
前端面向过程更多!!!
构造函数
//构造函数 公共的属性和方法 封装到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指向的就是你创建的实例对象。
构造函数方法固然很好,但是存在内存浪费的问题
实质上都是同一个方法,但是每个对象中的function的指向又不相同,一旦对象过多,开辟了很多内存。就会造成内存浪费的问题(主要是复杂数据类型)。
原型对象
我们希望所有的对象都使用同一个函数,这样就比较节省内存,那么我们要怎么做呢?(属性可以不一样,但是希望方法在不同对象中,是一样的)
原型:
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
公共的属性写在构造函数中,公共的方法写在原型对象上。
构造函数和原型对象的this
- 构造函数
function Star(uname){
//this就相当于return
this.uname = uname
}
const ldh = new Star('刘德华')
在构造函数中:this就是实例对象,就是ldh。
- 原型对象
function Star(uname){
this.uname = uname
}
Star.prototype.sing = function(){
console.log('唱歌')
}
const ldh = new Star('刘德华')
ldh.sing()//实例对象ldh,调用原型对象方法
在原型对象:this指向的就是调用原型对象方法的【实例对象】,这里也就是ldh。
构造函数和原型里面的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属性:指回该原型对象的构造函数
对象原型
构造函数可以用来创建实例对象,每个构造函数中还有一个【原型对象】,一些公共的方法都存放在【原型对象】中,避免内存浪费。
但是,为什么实例对象可以访问【原型对象】里面的属性和方法呢?
通过【对象原型】来实现 “实例对象”访问【原型对象】
注:两个下划线__
对象都会有一个属性:__proto__,用来指向构造函数的prototype原型对象,之所以我们对象可以用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。
简而言之:【对象原型】-__proto__是属于【实例对象】的;用来指向【实例对象】和【prototype(对象原型)】之间的连接。
function Star(){
}
const ldh = new Star()
console.log(ldh)
这个[[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
原型继承
继承是面向对象的另一个特征,通过继承、进一步提升代码封装的程度,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
原型继承的核心:
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__,他就是原型。
基于原型对象的继承,使得不同构造函数的原型对象关联在一块。并且这种关联的关系,是一种链状的结构,我们将原型对象的链状结构,称为:原型链。
首先明确一点:__proto__是每个【对象】身上都会具有的属性,并且它是用来指向原型对象的。【原型对象】身上有constructor属性,用来指向创造我的构造函数。
Star构造函数,构造出一个实例对象,这个实例对象的原型__proto__属性,就指向Star构造函数的【原型对象】,是对象、就有__proto__属性,Star原型对象的__proto__属性指向顶层原型对象:Object.prototype,他也有一个__proto__属性,他也往上指,但是Object已经是最顶层了,因此返回null
原型链查找规则:
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
综合案例
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()
})