js数据类型很简单,却也不简单

最近脑子里有冒出“多看点书”的想法,但我我的不是很喜欢翻阅纸质书籍,另外一方面也是由于我能抽出来看书的时间比较琐碎,因此就干脆用app看电子书了(若是有比较完整的阅读时间,仍是建议看纸质书籍,排版看起来更舒服点)。考虑到平时工做遇到的大部分问题仍是javascript强相关的,因而我选择从《Javascript权威指南第6版》开始。javascript

Javascript权威指南第6版

数据类型有哪些?

javascript的数据类型分为两大类,一类是原始类型(primitive type),一类是对象类型(object type)。java

原始类型

原始类型又称为基本类型,分为Number, String, Boolean, Undefined, Null几类。比较特殊的是,undefinedUndefined类型中的惟一一个值;一样地,nullNull类型中的惟一一个值。git

除此以外,ES6引入了一个比较特殊的原始类型Symbol,用于表示一个独一无二的值,具体使用方法能够看阮一峰老师的ECMAScript6入门,或者直接翻阅MDN,我平时看MDN比较多,感受比较权威,API也很完善。程序员

为何说Symbol是原始类型,而不是对象类型呢?由于咱们知道,大部分程序员都是没有对象的,那么要想找到女友,最快的办法就是new一个。es6

const options = {
    '性格': '好',
    '颜值': '高',
    '对我': '好'
}
const gf = new GirlFriend(options) // new一个女友
复制代码

皮一下

好了,不皮了,回到正题,意思就是,Symbol是没有构造函数constructor的,不能经过new Symbol()得到实例。github

可是获取symbol类型的值是经过调用Symbol函数获得的。正则表达式

const symbol1 = Symbol('Tusi')
复制代码

Symbol值是惟一的,因此下面的等式是不成立的。数组

Symbol(1) === Symbol(1) // false
复制代码

对象类型

对象类型也叫引用类型,简单地理解呢,对象就是键值对key:value的集合。常见的对象类型有Object, Array, Function, Date, RegExp等。promise

除了这些,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能帮咱们判断出大部分的数据类型,可是要注意的是:

  1. typeof null的结果也是"object"
  2. 对象的种类不少,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是基类,而各个派生类,如DateArray等在继承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);
复制代码

拆箱

对象的拆箱操做是经过valueOftoString完成的,且看下文。

类型的转换

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。一看,是否是以为有点懵,一个值的求反居然还等于这个值!其实仔细分析下过程,就能发现其中的奥秘了。

  1. 首先,咱们要知道运算符的优先级是这样的,一元运算符!的优先级高于关系运算符==

js运算符优先级

  1. 因此,右侧的![]首先会执行,而逻辑非运算符!会首先将其操做数转为布尔值,再进行求反。[]转为布尔值是true,因此![]的结果是false。此时的比较变成了[] == false
  2. 根据比较规则,若是==的其中一个值是false,则将其转换为数字0,再与另外一个操做数比较。此时的比较变成了[] == 0
  3. 接着,再参考比较规则,若是一个值是对象,另外一个值是数字或字符串,则将对象转为原始值,再进行比较。左侧的[]转为原始值是空字符串"",因此此时的比较变成了"" == 0
  4. 最后,若是一个值是数字,另外一个是字符串,先将字符串转换为数字,再进行比较。空字符串会转为数字000天然是相等的。

搞懂了这个问题,也能够分析下为何{} == !{}的结果是false了,这个就比较简单了。

看到这里,你还以为数据类型是简单的知识点吗?有兴趣深究的朋友能够翻阅下ES5的权威解释

最后

数据类型是javascript中很是重要的一部分,搞清楚数据类型的基本知识点,对于学习javascript的后续知识点多有裨益。

另外,写笔记其实对思考问题颇有帮助,就算只是总结很简单的基础知识,也是多有助益。

以上内容是我的笔记和总结,不免有错误或遗漏之处,欢迎留言交流。

欢迎交流
相关文章
相关标签/搜索