js数据类型很简单,却也不简单
最近脑子里有冒出“多看点书”的想法,但我个人不是很喜欢翻阅纸质书籍,另一方面也是因为我能抽出来看书的时间比较琐碎,所以就干脆用app看电子书了(如果有比较完整的阅读时间,还是建议看纸质书籍,排版看起来更舒服点)。考虑到平时工作遇到的大部分问题还是javascript强相关的,于是我选择从《Javascript权威指南第6版》开始。
数据类型有哪些?
javascript
的数据类型分为两大类,一类是原始类型(primitive type),一类是对象类型(object type)。
原始类型
原始类型又称为基本类型,分为Number
, String
, Boolean
, Undefined
, Null
几类。比较特殊的是,undefined
是Undefined
类型中的唯一一个值;同样地,null
是Null
类型中的唯一一个值。
除此之外,ES6
引入了一个比较特殊的原始类型Symbol
,用于表示一个独一无二的值,具体使用方法可以看阮一峰老师的ECMAScript6入门,或者直接翻阅MDN,我平时看MDN比较多,感觉比较权威,API也很完善。
为什么说Symbol
是原始类型,而不是对象类型呢?因为我们知道,大部分程序员都是没有对象的,那么要想找到女朋友,最快的办法就是new
一个。
const options = {
'性格': '好',
'颜值': '高',
'对我': '好'
}
const gf = new GirlFriend(options) // new一个女朋友
好了,不皮了,回到正题,意思就是,Symbol
是没有构造函数constructor
的,不能通过new Symbol()
获得实例。
但是获取symbol
类型的值是通过调用Symbol
函数得到的。
const symbol1 = Symbol('Tusi')
Symbol
值是唯一的,所以下面的等式是不成立的。
Symbol(1) === Symbol(1) // false
对象类型
对象类型也叫引用类型,简单地理解呢,对象就是键值对key:value
的集合。常见的对象类型有Object
, Array
, Function
, Date
, RegExp
等。
除了这些,Javascript
还有蛮蛮多的全局对象,具体见JavaScript 标准内置对象。但是全局对象并不意味着它就是一种对象类型,就比如JSON
是一个全局对象,但是它不是一种类型,这一点要搞清楚。
前面说了,对象可以new
出来,所以对象类型都有构造函数,Object
类型对应的构造函数是Object()
,Array
类型对应的构造函数是Array()
,不再赘述。
var obj = new Object() // 不过我们一般也不会这么写一个普通对象
var arr1 = new Array(1) // 创建一个length是1的空数组
var arr2 = new Array(1, 2) // 创建数组[1, 2]
栈内存和堆内存
栈内存的优势是,存取速度比堆内存要快,充分考虑这一点,其实是可以优化代码性能的。
栈内存
原始类型是按值访问的,其值存储在栈内存中,所占内存大小是已知的或是有范围的;
对基本类型变量的重新赋值,其本质上是进行压栈操作,写入新的值,并让变量指向一块栈顶元素(大概意思是这样,但是v8
等引擎有没有做这方面的优化,就要细致去看了)
var a = 1; // 压栈,1成为栈顶元素,其值赋给变量a
a = 2; // 压栈,2成为栈顶元素,并赋值给变量a(内存地址变了)
堆内存
而对象类型是按引用访问的,通过指针访问对象。
指针是一个地址值,类似于基本类型,存储于栈内存中,是变量访问对象的中间媒介。
而对象本身存储在堆内存中,其占用内存大小是可变的,未知的。
举例如下:
var b = { name: 'Tusi' }
运行这行代码,会在堆内存中开辟一段内存空间,存储对象{name: 'Tusi'}
,同时声明一个指针,其值为上述对象的内存地址,指针赋值给引用变量b
,意味着b
引用了上述对象。
对象可以新增或删除属性,所以说对象类型占用的内存大小一般是未知的。
b.age = 18; // 对象新增了age属性
那么,按引用访问是什么意思呢?
我的理解是:对引用变量进行对象操作,其本质上改变的是引用变量所指向的堆内存地址中的对象本身。
这就意味着,如果有两个或两个以上的引用变量指向同一个对象,那么对其中一个引用变量的对象操作,会影响指向该对象的其他引用变量。
var b = { name: 'Tusi' }; // 创建对象,变量b指向该对象
var c = b; // 声明变量c,指向与b一致
b.age = 18; // 通过变量b修改对象
// 产生副作用,c受到影响
console.log(c); // {name: "Tusi", age: 18}
考虑到对象操作的副作用,我们会在业务代码中经常使用深拷贝来规避这个问题。
数据类型的判断
判断数据类型是非常重要的基础设施之一,那么如何判断数据类型呢?请接着往下看。
typeof
javascript
本身提供了typeof
运算符,可以辅助我们判断数据类型。
typeof
操作符返回一个字符串,表示未经计算的操作数的类型。
typeof
的运算结果如下,引用自MDN typeof
数据类型 | 运算结果 |
---|---|
Undefined | "undefined" |
Null | "object" |
Boolean | "boolean" |
Number | "number" |
String | "string" |
Symbol | "symbol" |
Function | "function" |
其他对象 | "object" |
宿主对象(由JS环境提供,如Nodejs有global,浏览器有window) | 取决于具体实现 |
可以看到,typeof
能帮我们判断出大部分的数据类型,但是要注意的是:
typeof null
的结果也是"object"
- 对象的种类很多,
typeof
得到的结果无法判断出数组,普通对象,其他特殊对象
那么如何准确地知道一个变量的数据类型呢?
结合instanceof
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
利用instanceof
,我们可以判断一个对象是不是某个构造函数的实例。那么结合typeof
,我们可以封装一个基本的判断数据类型的函数。
基本思想是:首先看typeof
是不是返回"object"
,如果不是,说明是普通数据类型,那么直接返回typeof
运算结果即可;如果是,则需要先把null
这个坑货摘出来,然后依次判断其他对象类型。
function getType(val) {
const type = typeof val;
if (type === 'object') {
if (val === null) {
// null不是对象,所以不能用instanceof判断
return 'null'
} else if (val instanceof Array) {
return 'array'
} else if (val instanceof Date) {
return 'date'
} else if (// 其他对象的instanceof判断) {
return 'xxx'
} else if (val instanceof Object) {
// 所有对象都是Object的实例,所以放最后
return 'object'
}
} else {
return type
}
}
// 测试下
getType(Symbol(1)) // "symbol"
getType(null) // "null"
getType(new Date()) // "date"
getType([1, 2, 3]) // "array"
getType({}) // "object"
但是,要把常用的对象类型都列举出来也是有点麻烦的,所以也不算一个优雅的方法。
终极神器toString
有没有终极解决方案?当然是有的。但是,不是标题中的toString
,而是Object.prototype.toString
。用上它,不仅上面的数据类型都能被判断出来,而且也可以判断ES6
引入的一些新的对象类型,比如Map
, Set
等。
// 利用了Object.prototype.toString和正则表达式的捕获组
function getType(val) {
return Object.prototype.toString.call(val).replace(/\[object\s(\w+)\]/, '$1').toLowerCase();
}
getType(new Map()) // "map"
getType(new Set()) // "set"
getType(new Promise((resolve, reject) => {})) // "promise"
为什么普通的调用toString
不能判断数据类型,而Object.prototype.toString
可以呢?
因为Object
是基类,而各个派生类,如Date
, Array
等在继承Object
的时候,一般都重写(overwrite
)了toString
方法,用以表达自身业务,从而失去了判断类型的能力。
装箱和拆箱
首先解释一下什么是装箱和拆箱,把原始类型转换为对应的对象类型的操作称为装箱,反之是拆箱。
装箱
我们知道,只有对象才可以拥有属性和方法,但是我们在使用一些基本类型数据的时候,却可以直接调用它们的一些属性或方法,这是怎么回事呢?
var a = 1;
a.toFixed(2); // "1.00"
var b = 'I love study';
b.length; // 12
b.substring(2, 6); // "love"
其实在读取一些基本类型数据的属性或方法时,javascript
会创建临时对象(也称为“包装对象”),通过这个临时对象来读取属性或方法。以上代码等价于:
var a = 1;
var aObj = new Number(a);
aObj.toFixed(2); // "1.00"
var b = 'I love study';
var bObj1 = new String(b);
bObj1.length; // 12
var bObj2 = new String(b);
bObj2.substring(2, 6); // "love"
临时对象是只读的,可以理解为它们在发生读操作后就销毁了,所以不能给它们定义新的属性,也不能修改它们现有的属性。
var c = '123';
c.name = 'jack'; // 给临时对象加新属性是无效的
c.name; // undefined
c.length; // 3
c.length = 2; // 修改临时对象的属性值,是无效的
c.length; // 3
我们也可以显示地进行装箱操作,即通过
String()
,Number()
,Boolean()
构造函数来显示地创建包装对象。
var b = 'I love study';
var bObj = new String(b);
拆箱
对象的拆箱操作是通过valueOf
和toString
完成的,且看下文。
类型的转换
javascript
在某些场景会自动执行类型转换操作,而我们也会根据业务的需要进行数据类型的转换。类型的转换规则如下:
对象到原始值的转换
toString
toString()
是默认的对象到字符串的转换方法。
var a = {};
a.toString(); // "[object Object]"
但是很多类都自定义了toString()
方法,举例如下:
- Array:将数组元素用逗号拼接成字符串作为返回值。
var a = [1, 2, 3];
a.toString(); // 1,2,3
- Function:返回一个字符串,字符串的内容是函数源代码。
- Date:返回一个日期时间字符串。
var a = new Date();
a.toString(); // "Sun May 10 2020 11:19:29 GMT+0800 (中国标准时间)"
- RegExp:返回表示正则表达式直接量的字符串。
var a = /\d+/;
a.toString(); // "/\d+/"
valueOf
valueOf()
会默认地返回对象本身,包括Object
, Array
, Function
, RegExp
。
日期类Date
重写了valueOf()
方法,返回一个1970年1月1日以来的毫秒数。
var a = new Date();
a.toString(); // 1589095600419
对象 --> 布尔值
从上表可见,对象(包括数组和函数)转换为布尔值都是true
。
对象 --> 字符串
对象转字符串的基本规则如下:
- 如果对象具有
toString()
方法,则调用这个方法。如果它返回字符串,则作为转换的结果;如果它返回其他原始值,则将原始值转为字符串,作为转换的结果。 - 如果对象没有
toString()
方法,或toString()
不返回原始值(不返回原始值这种情况好像没见过,一般是自定义类的toString()
方法吧),那么javascript
会调用valueOf()
方法。如果存在valueOf()
方法并且valueOf()
方法返回一个原始值,javascript
将这个值转换为字符串(如果这个原始值本身不是字符串),作为转换的结果。 - 否则,
javascript
无法从toString()
或valueOf()
获得一个原始值,会抛出异常。
对象 --> 数字
与对象转字符串的规则类似,只不过是优先调用valueOf()
。
- 如果对象具有
valueOf()
方法,且valueOf()
返回一个原始值,则javascript
将这个原始值转换为数字(如果原始值本身不是数字),作为转换结果。 - 否则,如果对象有
toString()
方法且返回一个原始值,javascript
将这个原始值转换为数字,作为转换结果。 - 否则,
javascript
将抛出一个类型错误异常。
显示转换
使用String()
, Number()
, Boolean()
函数强制转换类型。
var a = 1;
var b = String(a); // "1"
var c = Boolean(a); // true
隐式转换
在不同的使用场景中,javascript
会根据实际情况进行类型的隐式转换。举几个例子说明下。
加法运算符+
我们比较熟悉的运算符有算术运算符+
, -
, *
, /
,其中比较特殊的是+
。因为加法运算符+
可以用于数字加法,也可以用于字符串连接,所以加法运算符的两个操作数可能是类型不一致的。
当两个操作数类型不一致时,加法运算符+
会有如下的运算规则。
- 如果其中一个运算符是对象,则会遵循对象到原始值的转换规则,对于非日期对象来说,对象到原始值的转换基本上是对象到数字的转换,所以首先调用
valueOf()
,然而大部分对象的valueOf()
返回的值都是对象本身,不是一个原始值,所以最后也是调用toString()
去获得原始值。对于日期对象来说,会使用对象到字符串的转换,所以首先调用toString()
。
1 + {}; // "1[object Object]"
1 + new Date(); // "1Sun May 10 2020 22:53:24 GMT+0800 (中国标准时间)"
- 在进行了对象到原始值的转换后,如果加法运算符
+
的其中一个操作数是字符串的话,就将另一个操作数也转换为字符串,然后进行字符串连接。
var a = {} + false; // "[object Object]false"
var b = 1 + []; // "1"
- 否则,两个操作数都将转换为数字(或者NaN),然后进行加法操作。
var a = 1 + true; // 2
var b = 1 + undefined; // NaN
var c = 1 + null; // 1
[] == ![]
还有个很经典的例子,就是[] == ![]
,其结果是true
。一看,是不是觉得有点懵,一个值的求反竟然还等于这个值!其实仔细分析下过程,就能发现其中的奥秘了。
- 首先,我们要知道运算符的优先级是这样的,一元运算符
!
的优先级高于关系运算符==
。
- 所以,右侧的
![]
首先会执行,而逻辑非运算符!
会首先将其操作数转为布尔值,再进行求反。[]
转为布尔值是true
,所以![]
的结果是false
。此时的比较变成了[] == false
。 - 根据比较规则,如果
==
的其中一个值是false
,则将其转换为数字0
,再与另一个操作数比较。此时的比较变成了[] == 0
。 - 接着,再参考比较规则,如果一个值是对象,另一个值是数字或字符串,则将对象转为原始值,再进行比较。左侧的
[]
转为原始值是空字符串""
,所以此时的比较变成了"" == 0
。 - 最后,如果一个值是数字,另一个是字符串,先将字符串转换为数字,再进行比较。空字符串会转为数字
0
,0
与0
自然是相等的。
搞懂了这个问题,也可以分析下为什么{} == !{}
的结果是false
了,这个就比较简单了。
看到这里,你还觉得数据类型是简单的知识点吗?有兴趣深究的朋友可以翻阅下ES5的权威解释。
最后
数据类型是javascript
中非常重要的一部分,搞清楚数据类型的基本知识点,对于学习javascript
的后续知识点多有裨益。
另外,写笔记其实对思考问题很有帮助,就算只是总结很简单的基础知识,也是多有助益。
以上内容是个人笔记和总结,难免有错误或遗漏之处,欢迎留言交流。
js数据类型很简单,却也不简单的更多相关文章
- vue—你必须知道的 js数据类型 前端学习 CSS 居中 事件委托和this 让js调试更简单—console AMD && CMD 模式识别课程笔记(一) web攻击 web安全之XSS JSONP && CORS css 定位 react小结
vue—你必须知道的 目录 更多总结 猛戳这里 属性与方法 语法 计算属性 特殊属性 vue 样式绑定 vue事件处理器 表单控件绑定 父子组件通信 过渡效果 vue经验总结 javascript ...
- js便签笔记(13)——jsonp其实很简单【ajax跨域请求】
前两天被问到ajax跨域如何解决,还真被问住了,光知道有个什么jsonp,迷迷糊糊的没有说上来.抱着有问题必须解决的态度,我看了许多资料,原来如此... 为何一直知道jsonp,但一直迷迷糊糊的不明白 ...
- js数据类型简单介绍
JS数据类型 ECMAScript中有5种简单的数据类型:Undefined,Null,Boolean,Number,String.还有一种复杂的数据类型--Object(本质上是由一组无序的名值对组 ...
- JavaScript大厦之地基:js数据类型
一.数据和类型 俗话说物以类聚,人以群分:这里将人和物都按类别进行了区分.我们数据也一样,使用计算机我们能处理数值,也可以处理文本还可以处理图形.音频.视频等各种各样的数据,不同的数据有 ...
- 由js apply与call方法想到的js数据类型(原始类型和引用类型)
原文地址:由js apply与call方法想到的js数据类型(原始类型和引用类型) js的call方法与apply方法的区别在于第二个参数的不同,他们都有2个参数,第一个为对象(即需要用对象a继承b, ...
- 用JS做一个简单的电商产品放大镜功能
使用js制作一个简单的产品放大图 购物网站的产品页经常会放有一个产品展示图区.该图区有一个功能就是产品图的放大功能,移动左侧的焦点区域,可以放大细节部分观看,详情如下图.实现该功能的方法也非常简单. ...
- JavaScript学习10 JS数据类型、强制类型转换和对象属性
JavaScript学习10 JS数据类型.强制类型转换和对象属性 JavaScript数据类型 JavaScript中有五种原始数据类型:Undefined.Null.Boolean.Number以 ...
- 分享:计算机图形学期末作业!!利用WebGL的第三方库three.js写一个简单的网页版“我的世界小游戏”
这几天一直在忙着期末考试,所以一直没有更新我的博客,今天刚把我的期末作业完成了,心情澎湃,所以晚上不管怎么样,我也要写一篇博客纪念一下我上课都没有听,还是通过强大的度娘完成了我的作业的经历.(当然作业 ...
- HTTP真的很简单
原文:HTTP Made Really Easy因为我本身网络基础就很差,所以看到这篇文章一方面是学习网络知识,另一方面为了锻炼我蹩脚的英语水平,文中如有错误,欢迎浏览指正! 前言 在看这篇文章的时候 ...
随机推荐
- 用python代替人脑运算24点游戏
前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:老方玩编程 PS:如有需要Python学习资料的小伙伴可以加点击下方链 ...
- Postman:Pre-request Script
Pre-request Script:前置处理,会在发出请求前执行,主要用在生成一些动态参数. 例如:api接口都会有签名校验,这个校验在我们api测试的时候很不方便,这里可以利用 postman 前 ...
- idea 激活方法
转载自: https://www.jianshu.com/p/7d60ea5e51e9
- HTTPoxy漏洞(CVE-2016-5385)复现记录
漏洞介绍: httpoxy是cgi中的一个环境变量:而服务器和CGI程序之间通信,一般是通过进程的环境变量和管道. CGI介绍 CGI 目前由 NCSA 维护,NCSA 定义 CGI 如下:CGI(C ...
- 【题解】POJ3041 Asteroids - 图论 - 二分图匹配
声明:本博客所有题解都参照了网络资料或其他博客,仅为博主想加深理解而写,如有疑问欢迎与博主讨论✧。٩(ˊᗜˋ)و✧*。 POJ3041 Asteroids 题目描述 假如你现在正处在一个 \(N*N\ ...
- shiro:注解配置(五)
基于[shiro集成spring]项目改造 引入相关依赖环境 <!--AOP的jar包--> <dependency> <groupId>org.aspectj&l ...
- 云开发静态网站托管现已支持 Angular 应用
云开发静态托管是云开发提供的静态网站托管的能力,静态资源(HTML.CSS.JavaScript.字体等)的分发由腾讯云对象存储 COS 和拥有多个边缘网点的腾讯云 CDN 提供支持. 在云开发静态托 ...
- MVC-过滤器-Action
四个方法执行顺序是OnActionExecuting——>OnActionExecuted——>OnResultExecuting——>OnResultExecuted. demo代 ...
- Redis学习与应用-位图
什么是位图 位图bitmap是通过一个bit来表示某个元素对应的值或者状态,是由一组bit位组成,每个bit位对应0和1两个状态,虽然内部还是采用string类型进行存储,但是redis提供了直接操作 ...
- python 异步Web框架sanic
我们继续学习Python异步编程,这里将介绍异步Web框架sanic,为什么不是tornado?从框架的易用性来说,Flask要远远比tornado简单,可惜flask不支持异步,而sanic就是类似 ...