JS面向对象 (小记)

120 阅读12分钟

前序

关于对象,之前学过的java就是一门纯面向对象的语言,在更早之前JavaScript只能说是基于对象的语言,随着ES6的出现,JavaScript的面向对象的思想也变得更加明确

下面是对《JavaScript高级程序设计》关于面向对象编程章节的一个小总结!!

理解对象

ES6将对象定义为一组属性无序集合 内部属性没有特定顺序限制,每个属性和方法都是一组键值对(key/value)形式

创建对象的两种方式

1、创建Object实例

let person = new Object()
person.name = 'lwj'

2、字面量方式

let person = {
    name:'lwj'
    ...
}
属性的内部特征

ES6规定每个属性都是有内部特征的 我们可以通过Object.getOwnPropertyDescriptor()来查看属性的内部特征

//参数列表  参数1 对象名   参数2 属性名
console.log(Object.getOwnPropertyDescriptor(book,"year")) 
数据属性

属性的四个特性

1、[[Configurable]] 设置属性是否能被delet删除 默认true

2、[[Enumerable]] 设置属性是否能被for...in循环遍历 默认true

3、[[Writable]] 设置属性是否能被修改(重新复制) 默认true

4、[[value]] 存储属性的值 默认undefined

可以使用Object.defineProperty()来对改变对象的内部属性特性

Object.defineProperty()的参数列表:
参数1:对象  参数2:对象的属性  参数3:一个描述符对象{}   在里面设置属性的特性configurable、enumerable、writable、value

let person = {
    name:'lwj',
    age = '18'
}
 
//将name属性设置为不可更改
Object.defineProperty(person,'name',{
    writable:'false'
})
......

对于属性的特性修改为false之后,对其进行设置发现相应的特性丢失了 非严格模式下 会忽略对这个属性进行的对应操作 严格模式下 则会报错

当使用Object.Property()添加属性时,则configurable、enumerable、writable属性特性默认为false

为person对象添加一个job属性  value值为student
Object.Property(person,'job',{
    vaule:'student'
})

Object.getOwnPropertyDescriptor(person,'job')
//返回值
{
  value: 'student',
  writable: false,
  enumerable: false,
  configurable: false
}
访问器属性

理解:提供get和set方法 模拟面向对象编程中私有属性的特性

访问器属性不包含value值特性所以想对应writable 特性也没有

1、[[Configurable]] 设置属性是否能被delet删除 默认true

2、[[Enumerable]] 设置属性是否能被for...in循环遍历 默认true

3、[[Get]] 获取函数 读取属性是调用 默认undefined

4、[[Set]] 设置函数 写入属性读取 默认undefined

访问器属性必须使用Object.defineProperty()定义

let book = {
    year_:2017,
    edition:1
}
定义year_的私有访问器属性
Object.defineProperty(book,'year',{
    get(){
        return this.year_
    },
    set(newValue){
        if(newValue > 2017){
            this.year_ = newValue
            this.edition += newValue-2017
        }
    } 
})

Object.getOwnPropertyDescriptor(book,'year')
//返回值
{
  get: [Function: get],
  set: [Function: set],
  enumerable: false,
  configurable: false
}
定义多个属性

一次性定义多个属性 以及描述符 Object.defineProperties()

Object.defineProperities( book , {
    year_:{
          value:'2018'
    },
    edition:{
        value:1
    },
    year:{
       get(){
        return this.year_
       }
       set(){
       ...
       }
    }
   })

ES2017 通过对象名展示函数中每个属性的特性 Object.getOwnPropertyDescriptors(obj):传入对象名

合并/混入对象

Object.assign()方法

参数列表:
参数一 目标对象 参数2..n  1-n个原目标
合并规则:调用源对象的get方法  将返回值作为目标对象的set()方法的参数
let obj1 = {
	set a(value){
	  console.log(value)
	}	
}
let obj2 = {
	get b(){
		console.log('obj2 run')
		return 'aaa'
	}
}
Object.assign(obj1,obj2)
//源对象的get方法返回aaa作为set的参数  但是set方法中并没有对参数进行赋值操作  
//打印目标对象
console.log(obj1) //{ set a{} }

Object.assign()是对每个目标对象的浅复制 只复制对象的引用

当复制期间出错了 Object.assign()不会有回滚机制 只有可能部分进行复制

let dest,src;

dest = {}
src = {
	a:'foo',
	d:'kkk',
	get b (){
		throw new Error();
	},
	C:'bar'
}

try{
     Object.assign(dest,src)
}catch(err){
	
}
console.log(dest) //{ a: 'foo', d: 'kkk' }


对象增强语法

1、属性简写

let name = 'lwj'
let person = {
    //属性名和变量名一致
    name:name
}
//简写
let person = {
    name
}

2、可计算属性

//可计算属性  [js表达式]   可以将一个表达式的值做为对象的属性名
    const namekey = 'name'
    const agekey = 'age'

    const obj4 = {
            [namekey]:'lll',
            [agekey]:18
    }
    console.log(obj4)  {namekey:'name',agekey:'age'}

中括号[]告诉运行使将里面的内容作为JavaScript的表达式来求值

3、方法简写&可计算属性

const methodkey = 'sayName'
const obj5 = {
	[methodkey](name){
		console.log(`my name is ${name}`)
	}
	
}
console.log(obj5)
obj5.sayName('lisa') 

4、对象解构

    const obj6 = {
            cname:'lwj',
            cage:'18'
    }

    const { cname:reName,cage:reAge } = obj6
    //简写语法  可以在解构的同时为对象赋默认值
    const { cname , cage ,cjob = 'student'} = obj6
    
    console.log(`name:${reName},age:${reAge}`)  //name:lwj,age:18

    console.log(cname) //lwj
    console.log(cage)  //18
    console.log(cjob) //undefined

对初始数据类型进行解构,首先会使用ToObject()方法将初始数据类型转换为对象类型 ToObject()不能转换null和undefinednull和undefined不能被解构

将对象解构到事先声明好的属性中 需要将结构表达式包裹在一个()中

let rekname,rekage

let obj7 = {
	kname:'lwj',
	kage:55
}
//使用定时器避免错误
setTimeout(()=>{
	({kname:rekname,kage:rekage} = obj7)  //在node环境中运行时 ()中的代码会在先于let的声明之前执行  这时会报错
	console.log(rekname,rekage)//
},0)

嵌套结构当对象里面嵌套有对象的时候 对对象进行解构 会直接将源对象中对象的引用赋值出来

let person = {
    name:'lwj',
    job:{
        title:'abc'
    }
}
//声明title变量  将person.job.title赋值
let { job:{ title } } = person 

部分解构如果解构过程中报错了,则只会解构报错之前的属性 称为部分解构

参数解构在参数列表中进行解构时 不会影响到arguments对象 直接将结构出来的对象作为它的一项

创建对象的方法

前面起到 可以使用Object构造函数和字面量创建构造 但是他不能进行代码的复用

工厂方式
 let personFac = function(name,age,job){
     let o = ne Object()
     o.name = name
     o.age = age
     o.obj = job
     o.sayName = function(){
         console.log(this.name)
     }
     return o
 }
 //直接使用工厂函数将对象的参数传过去  工厂函数返回想要的对象
per1 = personFac('lwj',18,'teacher')  
per1.sayName()  //lwj
per2 = personFac('lmm',18,'teacher')
per2.sayName()  //lmm
构造方式
//自定义构造函数
    function Person(name,age,job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = function(){
                    console.log(this.name)
            }
         }
        //使用new 操作符创建实例
         let per1 = new Person('lwj',30,'teacher')
         let per2 = new Person('lmm',15,'stuedent')

new操作符

(1)在内存中创建新对象

(2)将实例对象的原型指针指向数构造函数的prototype属性 per1.proto == Person.prototype

(3)将构造函数的this指向新的实例对象

(4)执行构造中的代码

(5)返回值为null 则返回新创建的对象

构造函数也是函数: 任何函数使用new操作符调用就是构造函数 直接使用就是普通函数

普通函数调用时会 将 所有的属性添加到函数内部this指向的对象上面 当我们使用一个函数的时候没有对其进行任何call/apply/bind等关于this指向操作时 默认指向的都是全局对象

所以调用一个普通函数 默认将普通函数的所有属性添加到全局对象中 (通过call/apply将this指向改变 则所有属性会添加到this绑定的新对象中)

构造函数的问题 使用构造进行对象创建时 对于函数的定义

     function Person(){
         this.name = 'lwj'
         this.sayName = function(){
            console.log(this.name)
         }
     }
//this.sayName = new Function("console.log(this.name)")
//每一次new一个对象都会新建一个相同逻辑的函数  造成资源的浪费

可以将对象函数的定义提取到构造外面

 function Person(){
         this.name = 'lwj'
         this.sayName = sayName
     }
let sayName = function(){
            console.log(this.name)
         } 

这么做 虽然解决了相同逻辑代码重复定义的问题 但是造成的更严重的问题 导致全局作用域混乱

原型模式

每个函数都会创建一个prototype属性 这个原型上所有的属性和方法都可以被对象实例共享

我们利用原型的特性来定义对象

    function Person1(){}
    Person1.prototype.name = 'yuanxingmode'
    Person1.prototype.age = 18
    Person1.prototype.job = 'teacher'
    Person1.prototype.sayName = function(){
            console.log(this.name)
    }

    let p1 = new Person1()
    let p2 = new Person1()

    p1.sayName() //yuanxingmode
    p2.sayName() //yuanxingmode

这里就不详细说明原型了,之后再单独写一篇关于原型的总结

附上对学习原型的一些代码

//构造方法
let Stu = function(){}
//function Stu(){}
Stu.prototype.name = 'lwj'
Stu.prototype.job = 'cooke'
let stu1 = new Stu()
//实例与构造函数没有关系  与构造函数的原型有关系
console.log(Stu.prototype)
console.log(Stu.prototype.constructor)
console.log(stu1.__proto__)
console.log(stu1.__proto__.constructor)

//原型的API
//isPrototypeOf()  判断传入的参数是否使用它的原型对象
console.log(Stu.prototype.isPrototypeOf(stu1))

//Object.getPrototypeOf() 方便的取得一个实例对象的原型
console.log(Object.getPrototypeOf(stu1) == stu1.__proto__)
console.log(Object.getPrototypeOf(stu1) == Stu.prototype)
console.log(Object.getPrototypeOf(stu1))

//Object.setPrototypeOf()  向实例的原型对象写入一个新值(重写实例对象的继承关系)

let cat = {
	name:'kk',
	age:1
}

Object.setPrototypeOf(stu1 , cat)
console.log(stu1.name)
console.log(stu1.age)
console.log(stu1.__proto__)
console.log(Object.getPrototypeOf(stu1))
console.log(stu1.__proto__ === cat)
//不过使用Object.setPrototypeOf() 重写实例的原型指向  会严重影响代码性能

//我们还可以通过  Object.create() 来创建并指定一个新的原型对象

let dog = {
	name:'dd',
	age:4
}

stu1 = Object.create(dog)

console.log(stu1)
console.log(stu1.__proto__)


//in 操作符
//两种用法  1、单独使用  判断属性是否存在与实例或者原型上  2、for...in
console.log('------------------in操作符------------------')

let Stu1 = function(){}
Stu1.prototype.name = 'lwj'
Stu1.prototype.age = 18

let stu2 = new Stu1()
console.log('name' in stu2)

stu2.job = 'student'
console.log('job' in stu2)

console.log(stu2.hasOwnProperty('name'))
console.log(stu2.hasOwnProperty('job'))
console.log(stu2)
console.log(stu2.__proto__)

//写一个 判断属性是否存在实例的原型链上 的方法

function hasOwnPrototype(obj,property){
	// !obj.hasOwnProperty(property) 属性不能存在对象实例中 
	//property in obj 而且 属性存在在对象实例或者原型对象上

		if(!obj.hasOwnProperty(property) && property in obj){
			return true
			}
		else{
			return false
		}
}

//for in 循环  遍历实例对象及原型对象 中所有的可迭代属性
for(a in stu2){
	console.log(a)
}

console.log('---')
//屏蔽原型属性的for..in操作 enumerable:false
Object.defineProperty(Stu1.prototype,'name',{
	enumerable:false
})
console.log(Stu1)
//这里遍历不到name
for(a in stu2){
	console.log(a)
}

console.log('---')
//设置实例中的name属性
stu2.name ='ccc'

//覆盖掉 原型中的enumerable:false操作
//会遍历到实例对象中的 name属性
for(a in stu2){
	console.log(a)
}
Object.defineProperty(stu2,'job',{
	enumerable:true,
	value:'teacher'
})
console.log(stu2.job)
console.log(stu2)
console.log(Object.keys(stu2))
console.log(Object.getOwnPropertyNames(stu2))

//获取对象的值数组 Object.values
console.log(Object.values(stu2))
console.log(Object.entries(stu2))


//直接使用对象对原型赋值

console.log(Stu1.prototype)

console.log(Stu1.prototype.constructor)

//Stu1.prototype = {
//	name:'aaa',
//	job:'docotor'
	
//}
//console.log(Stu1.prototype)

//console.log(Stu1.prototype.constructor)//缺点 导致constructor不再指向原先的构造

//解决方法
Stu1.prototype = {
	//显示定义constructor
	//constructor:Stu1
	name:'aaa',
	job:'docotor'
}

//constructor默认是不可for..in遍历的(enumerable:false)  
//直接在对象中指定constructor  默认是enumerable:true

//可以直接使用Object.defineProperty() 修改enumerable属性

Object.defineProperty(Stu1.prototype,'constructor',{
	enumerable:false,
	value:Stu1
})
console.log(Stu1.prototype)
console.log(Stu1.prototype.constructor)
delete stu2.name
console.log(stu2.name)

重点强调 实例与构造函数没有关系 与构造函数的原型有关系

继承

原型链继承、盗用构造继承 、组合继承、原型式继承、寄生式继承、寄生式组合继承

原型链继承
    let FatherPro = function(){
            this.fname = 'father'
    }

     FatherPro.prototype.sayfName = function(){
            return this.fname
     }

    let SonPro = function(){
            this.sname = 'son'
    }
   
    SonPro.prototype = new FatherPro() //将FatherPro的实例对象 当做SonPro的原型对象
    //FatherPro构造函数中的属性会添加到SonPro的原型上
    
    SonPro.prototype.saysName = function(){
            return this.sname
    }

    let p1 = new SonPro()
    
    
    //由于SonPro的原型赋值为了FatherPro的实例对象
    //所以调用SonPro的constructor属性其实 是指向FatherPro
        console.log(SonPro.prototype.constructor) //FatherPro(){}
    //SonPro的实例对象 的constructor同样
        console.log(p1.__proto__.constructor) //FatherPro(){}

原型链继承的缺点:①共享性太强 对于引用对象类型 所有的实例都会共享同一个引用空间

②子类实例化时不能给父类构造传入参数

盗用构造继承
let Father = function(name = 'lwj'){
	this.name = name
	this.hobbies = ['game','ball']
	this.sayName = function(){
		console.log(this.name)
	}
}

let Son = function(){
        //在子类构造中调用父类构造的call方法 使其指向子类
        //将父类中的引用属性放到构造函数中  通过构造继承会复制引用属性的副本  解决原型继承属性的共享问题
	Father.call(this,'hmm')
	this.age = 19
}

优点:子类构造调用父类构造时可以传参

缺点:只能通过构造函数传递属性和方法 子类不能访问父类构造原型上的方法

组合继承

使用原型链继承原型上的属性和方法 使用盗用构造函数继承实例属性

let Father1 = function(name = 'lwj'){
	this.name = name
	this.age = 15
	this.hobbies = ['basketball','pingpang']
}
Father1.prototype.sayName = function(){
		console.log(this.name)
	}
	
let Son1 = function(){
	//继承父构造所有的实例属性和方法
	Father1.call(this)
}
    //继承父构造的原型
    Son1.prototype = new Father1()

缺点: 调用了两次父构造 造成效率的一定损耗

原型式继承

即便不定义类型也可以通过原型实现对象之间的信息传递

//定义一个函数  传入一个实例对象  返回一个子类实例对象
function object(o){
	function F(){}
	//将新构造的原型 o的构造的实例对象
	F.prototype = o
	return new F()
}
//在一个已有的实例对象的基础上 创建一个新对象  并且可以做一些修改
let Father3 = function(){
	this.name = 'lwj'
}

上面自定义的继承函数 ES6为我们提供了一个规范化方法Object.create()

Object.create()可以传入两个参数  第一个参数是 实例对象  第二个参数 一些额外的属性
第二个参数与 Object.getProperties()传参方式一样 每个属性都通过描述符描述

let son4 = Object.create(per,{
        name:{
             writable:false,
             value:'ccc'
        },
        age:{
            writable:'aaa',
            value:18
        }
    })

适合于不需要单独创建构造函数 但需要进行对象间信息的共享

寄生式继承

采用工厂的方式结合原型式继承 在定义的工厂中可以给对象添加额外的属性或方法 然后返回该对象

    function newObj(obj){
            let clone = Object.create(obj)
            clone.sayName = function(){
                    console.log(this.name)
            }
            return clone
    }

缺点 这种继承方式会导致继承难以重用 和构造继承类似

寄生式组合继承

采用盗用构造继承属性 混合式原型链继承方法

function inherit(per,stu){
	let protype = object(per.prototype)  //创建父类的实例对象
	protype.constructor = per //增强对象 重新将constructor属性指向父类实例
	stu.prototype = protype  //赋值对象
}

类的背后使用的仍是原型和构造函数的概念

定义类

类声明类表达式

类声明 class person {}

类表达式 let person = class {}

类和函数的区别:函数声明会提升 类声明不会提升(报错) 类定义块级作用域 函数声明定义函数作用域

默认情况 类中的代码都是严格模式下执行

类名称可以随意更改

let Person = class PersonName {}
Person.name  //PersonName
PersonName //ReferenceError
//可以通过Person.name 属性访问  但是不能直接访问PersonName
类构造函数

不定义构造会默认创建一个空构造

class Person { 
    constructor(){}
}
new Person()  //使用new 创建实例  会告诉类调用constructor构造方法

new一个类 跟new一个函数所执行的步骤时一样的

image.png

类构造函数在执行之后会返回一个this对象 作为实例化对象的this 如果没有东西引用实例对象 则该对象会被销毁


未完...