或许在我们平时的项目中,js的面向对象使用的并不多。但是一旦项目变得庞大,我们可能会发现定义了越来越多结构功能相似的对象,这时候如果能引入面向对象的思想,会对项目的代码结构进行非常大的优化。追根溯源,既然说到面向对象,就先从js中的object这个对象数据类型说起。
文章目录
- object数据类型
- 内存清理机制
- this关键字
- 构造函数和new关键字
- Json格式转换
- 总结
- 参考
object数据类型
和另外几种单一类型的基本数据不同,object类型用来存储一系列键值对。
声明
有如下两种方式去声明一个object对象
let obj1={};
let obj2=new Object();
第一种大括号的方式适合直接带值创建少量对象,第二种new
关键字创建类实例就是面向对象的方式,适合批量创建大量对象。当然平时使用第一种的比较多。
new关键字后面还会详细讲
let obj1={
'name': 'xiaofu',
'age': 18, //这个逗号不写也没错,写着便于阅读
}
console.log(obj1); //{name: "xiaofu", age: 18}
用冒号连接key和对应的value,key必须是字符类型,value可以实任意类型。注意这里key的引号不加也是可以的。
赋值和取值
可以通过点号或者中括号的方式去赋值和取值。
console.log(obj1.name); //xiaofu
console.log(obj1.age); //18
obj1.age=99;
console.log(obj1.age); //99
但是点号的限制比较多,例如key包含空格,或者读取变量的内容做为key就只能用方括号
let test='name';
console.log(obj1.test); //error
console.log(obj1[test]); //xiaofu
同时需要强调的是,因为在初始化的时候,key是可以不加引号的,所以如果想要用某变量的值做为key,或者是经过某种计算得到的值做为key,都应该放在中括号内
let var1='name';
let obj2={
[var1]: 'xiaofu',
[var1+'2']: 'joker',
};
console.log(obj2); //{name: "xiaofu", name2: "joker"}
console.log(obj2[var1+2]); //joker
作为对比,python中字典的key就必须要带引号,就不会有类似需求
删除
通过delete
关键字去删除某个键值对
delete obj1.age;
console.log(obj1); //{name: "xiaofu"}
变量传递
在另一篇讲python的博客《python3中各类变量的内存堆栈分配和函数传参区别实例详解》中,我也用javascript做了类比。python中所有的变量值都在堆中,而变量名在栈中,所以变量的传递都是指针传递并不是值传递。而javascript的几种基础类型都是直接保存在栈中,变量传递就是值传递,只有object这种复杂类型是保存在堆中,变量传递则是指针传递。
let obj1={
'name': 'xiaofu',
'age': 18,
}
temp=obj1;
temp['name']='joker';
console.log(obj1['name']); //joker
而且用const
关键字声明的常量object,虽然栈内的内容不能被修改,也就是不能指向一块新的堆地址,即不能被整体赋值,但是其中的键值对是可以被修改的
const obj3={name:'xiaofu'};
obj3={}; //error
obj3.name='joker';
console.log(obj3); //{name: "joker"}
判断key是否存在
通过in
关键字来判断object里面有没有某个key
console.log('name' in obj3); //true
console.log('age' in obj3); //false
但是和很多其他语言不同的是,即使没有key,js中依然可以通过点号或者中括号去获取key的值,并不会报错,而是会返回undefined
console.log(obj3.age); //undefined
console.log(obj3['age']); //undefined
做为对比,python中key不存在会报错,所以推荐使用get方法去获取key对应的value
遍历所有key
通过for...in
语法来遍历所有的key
let obj4={'name':'xiaofu','age':99};
for(let prop in obj4){
console.log(prop+': '+obj4[prop]);
}
注意,即使用来遍历的对象是空的,for循环也不会报错,只是不会执行而已。
整体内容克隆
大多数时候,指针传递就够用了,但是如果只是想克隆object对象里面的值也是可以办到的。除了遍历对象一个个key去复制,还可以用下面的方法
obj5={'name':'xiaofu'};
obj6={'age':99,'hobby':'music'};
Object.assign(obj5,obj6);
console.log(obj5); //{name: "xiaofu", age: 99, hobby: "music"}
但是如果一个object对象的某个key的value也是个object对象,这个方法就又失效了,那就必须得手动去解决了。
内存清理机制
这个值得好好说一说,毕竟内存的合理使用一直是程序开发中的一个关键因素。
首先要明确,程序运行的时候下列变量是会一直存在的:
- 当前运行函数的参数以及局部变量
- 当然函数调用的其他函数的参数以及局部变量
- 全局变量
这些变量被称为roots。要看内存中的值需不需要被回收主要是看该值有没有被roots直接或间接引用。
let info={name: 'xiaofu'};
上面这句话定义了一个全局变量info
,并且在内存的堆中创建一个object对象,内容为name: 'xiaofu'
,然后全局变量引用这个object对象,如下
此时认为object对象name: 'xiaofu'
从roots
可达(reachable),所以该对象不会被当作垃圾清理。而之后执行如下操作
info=null;
从info到该object对象的引用消失
此时该object对象从roots不可达(unreachable),所以该object对象就被清理了。
下面两点需要强调一下
- 只要有至少一条从roots的引用链到达object对象,该对象都不会被清除
- 引用链必须是到达的,也就是从roots被引用
例如下面的操作
let info={name: 'xiaofu'};
let student={name:'james'};
info.friends=student;
info=null;
内存的引用关系如下
此时name: 'xiaofu'
这个对象虽然引用了别的object对象,但是自己本身没有被引用,所以也会被回收。
js对于内存的垃圾清理是自动进行的,不需要我们参与,我们也感知不到,其最基础的算法就是“标记然后清理”(mark-and-sweep),其基本步骤如下
- 垃圾清理程序从roots出发,标记被直接引用的所有对象,并标记它们
- 然后再从被标记的对象沿着引用链往下继续标记下一级被引用的对象,如果是已经被标记的对象就不会再被标记
- 重复这个过程一直到所有从roots可达的对象都被标记
- 最后将没有被标记的对象清理掉
这个过程遍历了所有的对象,可以想象当程序庞大以后任务量是很繁重的,会占用相当多的cpu。于是有很多的改进算法被提出来,例如针对变量被创建的先后顺序分级对待,或者将整个内存进行分区批次处理,又或者设定只在cpu空闲的时候进行垃圾清理。关于垃圾清理的优化又是一个很大的话题了,涉及到更底层的数据结构和算法的知识,这里就不展开说明了。
this关键字
object对象是用来模拟现实世界中的实体的,例如学生,订单等等。前面的object对象只包含了这些实体的属性(attribute),也就是一个个变量。而现实世界中的实体还可以做一些行为,反映到程序世界中就是一个个函数。
例如
let employee={name: 'xiaofu'};
employee.work=function(){alert('Writing code like crazy.')}
employee.work(); // Writing code like crazy.
如果一个函数变成了某个object对象的属性,该函数就被称为该object的方法(method)。上面的employee对象就具有work这个方法。
可以直接在声明object对象的时候就定义方法,下面这两种格式都是可以的
let employee={
name: 'xiaofu',
work: function(){alert('Writing code like crazy.')},
};
employee.work();
let employee={
name: 'xiaofu',
work(){alert('Writing code like crazy.')},
};
employee.work();
第一种方式比较好理解,第二种方式更简洁。
有的时候object的方法需要访问object的属性,这个时候就需要用到this
关键字
let employee={
name: 'xiaofu',
sayHello(){alert('Hello, my name is '+this.name)},
};
employee.sayHello();// Hello, my name is xiaofu
this
关键字在这里指代的就是employee
这个对象本身。
那么为什么不直接用employee
呢?因为不具有通用性。例如
let student=employee;
employee=null;
student.sayHello();
假如使用的是this
这里就可以成功执行,否则就会报错。
在js中,任意函数都可以使用this,函数被调用的时候this会指向调用该函数的对象。这个特点在前面的DOM元素事件的回调函数的时候我们使用过,this会指向产生事件的元素。例如
let calcButton=document.querySelector('#calc');
let accumButton=document.querySelector('#accum');
function showInfo(){
console.log(this.id+' is clicked');
}
calcButton.onclick=showInfo; // calc is clicked
accumButton.onclick=showInfo; // accum is clicked
注意,箭头函数的this关键字无效,所以需要使用this关键字的函数不要用箭头的格式书写
let employee={
name: 'xiaofu',
sayHello:()=>{alert('Hello, my name is '+this.name)},
};
employee.sayHello(); //Hello, my name is
xiaofu没有显示出来。
同时,直接执行函数,而不是通过对象的方法去调用,this的内容为undefined
function makeUser() {
return {
name: "xiaofu",
ref: this
};
};
let user = makeUser();
alert( user.ref.name );// what is the result?
上面执行之后会报错,因为函数是直接被执行,而不是做为某个对象的方法去调用,所以this的值为undefined,而undefined是没有name这个属性的。
像下面这样改写就能达到目的
function makeUser() {
return {
name: "xiaofu",
ref(){
return this;
}
};
};
let user = makeUser();
alert( user.ref().name );// what is the result?
此时就会返回xiaofu
了。
构造函数和new关键字
铺垫了这么多,下面要进入本小节的重点了。
前面我们都是通过{...}
的方法去直接生成一个object对象,但是大多数时候我们需要批量创建很多类似的对象,这时候就需要借助构造函数和new关键字了。
构造函数(constructor function)就是普通函数,不过一般约定俗成的使用首字母大写做为函数名。例如
function Employee(name){
this.name=name;
};
构造函数只能用new关键字去执行,例如
let xiaofu=new Employee('xiaofu');
console.log(xiaofu.name); //xiaofu
在这个过程中函数完成了如下操作
- 创建一个空的对象
{}
,并赋值给this
- 执行函数体,通常是对
this
的一系列操作 - 返回
this
所以上面的new Employee('xiaofu');
其实是返回了如下的一个对象
{name: 'xiaofu'}
如果我们带入不同的参数,就可以返回不同的对象,这就是构造函数的意义。
再次强调,任何函数都可以看作构造函数,都可以用
new
关键字去执行,并且执行上述的步骤,不要觉得构造函数是一种特殊的函数
构造函数是没有return
的,默认会将this
进行返回。一般不会有人在构造函数进行返回,但是如果有return
语句,分为下面两种情况
- 如果返回的是object对象,则执行return语句,而不返回this
- 如果返回的不是object,则return语句被忽略,返回this
这里只是了解就好,就不举例了。
除了属性,还可以通过构造函数添加方法,例如
function Employee(name){
this.name=name;
this.sayHello=function(){
console.log('Hello, my name is '+this.name);
}
};
let xiaofu=new Employee('xiaofu');
xiaofu.sayHello(); //Hello, my name is xiaofu
Json格式转换
json是一个在网络传输中非常常见的格式,如果要和后端进行通讯,往往是以json格式发送内容。json和object数据很相似,不过是字符串形式来表示。
将object对象转换成json数据
let a={name:'xiaofu'};
let b=JSON.stringify(a);
console.log(b); //{"name":"xiaofu"}
console.log(typeof b); //string
将json数据转换为object对象
let c=JSON.parse(b);
console.log(c); //{name: "xiaofu"}
console.log(typeof c); //object
总结
总结下这一节的知识点
- object的内容存储在堆中,栈里面的变量名存放的是指向堆中地址的指针
- 通过判断堆中的数据是否有roots的直接或间接的引用,来判断是否要回收这一块内存
- 任何函数都可以包含this关键字,如果是在object对象的方法中调用,表示的是object对象本身。如果是函数直接调用,this的内容为undefined
- 通过构造函数去批量创建类似结构的object对象,构造函数必须用new关键字去执行
这一节算是对js中的面向对象进行了基础知识的铺垫,知道了如何用构造函数去创建object,下一节我们继续深入,去了解prototype和继承,让我们创建出来的object更灵活更强大。
参考
- Objects: the basics