前序
关于对象,之前学过的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一个函数所执行的步骤时一样的
类构造函数在执行之后会返回一个this对象 作为实例化对象的this 如果没有东西引用实例对象 则该对象会被销毁
未完...