数据类型
数据类型
JavaScript 是一种弱类型或者说动态语言。
这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。
这也意味着你可以使用同一个变量保存不同类型的数据。
最新的 ECMAScript 标准定义了 8 种数据类型: 7+1
7 种原始类型,
Undefined
、Boolean
、Number
、String
、BigInt
、Symbol
、Null
Object
:任何
constructed
对象实例的特殊非数据结构类型;也用做数据结构:
new Object
,new Array
,new Map
,new Set
,new WeakMap
,new WeakSet
,new Date
,和几乎所有通过new
创建的东西。
除对象外的所有类型都定义了**不可变的值(即不能更改的值)**。例如字符串也是不可变的。我们将这些类型的值称为“原始类型”。
基本/原始类型,保存到了栈上。 Object
/引用类型,会被分配到了另一块区域,我们称之为堆(heap),其地址存到栈上。
其实类型指的是值的类型,不是变量的类型,这是动态语言和静态语言的差异。 对于静态语言来说,我们可以限定一个变量的类型。但是对于 JS 这种动态类型的语言来说, 我们无法给变量限定类型,变量的类型是可变的。举个例子:
1 | var a = 1; |
类型判断
typeof
typeof
操作符的唯一目的就是检查数据类型,如果我们希望检查任何从 Object 派生出来的结构类型,使用 typeof
是不起作用的,因为总是会得到 'object'
。
注意
typeof
返回值为string
格式:typeof(typeof(undefined)) -> 'string'
(类型都是小写)对于原始类型,除
null
(遗留已久的 bug)都可以正确判断;对于引用类型,除function
外,都会返回'object'
typeof(null) -> 'object'
typeof(()=>{})->'function'
typeof
未定义的变量不会报错,返回'undefined'
。尝试去读一个未定义的变量的值其实会直接Reference Error
。1
2
3
4typeof a
'undefined'
a
Uncaught ReferenceError: a is not defined
typeof是函数吗?
typeof
的返回值之一为'function'
,如果 typeof
为 function
,那么 typeof(typeof)
会返回'function'
,但是经测试,上述代码浏览器会抛出错误。
因此可以证明 typeof
并非函数。typeof
是操作符。
instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断
instanceof
判断对象的原型链上是否存在构造函数的原型。只能判断引用类型。instanceof
常用来判断A
是否为B
的实例。instanceof
返回的是一个布尔值。
instanceof
和多全局对象(例如:多个 frame 或多个 window 之间的交互)
在浏览器中,我们的脚本可能需要在多个窗口之间进行交互。
多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。
这可能会引发一些问题。
比如,表达式 [] instanceof window.frames[0].Array
会返回 false
,
因为 Array.prototype !== window.frames[0].Array.prototype
,并且数组从前者继承。
起初,你会认为这样并没有意义,但是当你在你的脚本中开始处理多个 frame 或多个 window 以及通过函数将对象从一个窗口传到另一个窗口时,这就是一个有效而强大的话题。
比如,实际上你可以通过使用Array.isArray(myObj)
或者Object.prototype.toString.call(myObj) === "[object Array]"
来安全的检测传过来的对象是否是一个数组。
Object.prototype.toString.call()
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
[2].toString()
调用的是数组的toString()
方法,而不是对象的toString()
方法。Array改写了Object的toString方法。
toString.call()
是Object.prototype.toString.call()
的简写形式。
调用该方法,统一返回格式'[object Xxx]'
的字符串。
1 | toString.call(()=>{}) // '[object Function]' |
类型转换
类型转换可以分为两种:隐式类型转换和显式类型转换。
显式类型强制转换是指当开发人员通过编写适当的代码用于在类型之间进行转换。
隐式类型转换是指在对不同类型的值使用运算符时,值可以在类型之间自动的转换。
在 JS 中只有 3 种类型的转换
- 转化为
Number
类型:Number()
/parseFloat()
/parseInt()
- 转化为
String
类型:String()
/toString()
- 转化为
Boolean
类型:Boolean()
类型转换的逻辑无论在原始类型和对象类型上,他们都只会转换成上面 3 种类型之一。所以只需要弄清在什么场景下应该转成哪种类型就可以了
转换为boolean
显式
:Boolean()
方法可以用来显式将值转换成布尔型。
隐式
:隐式类型转换通常在逻辑判断或者有逻辑运算符时被触发(|| && !)。
1 | Boolean(2) // 显示类型转换 |
逻辑运算符(比如 || 和 &&)是在内部做了 boolean 类型转换,但实际上返回的是原始操作数的值,即他们都不是 boolean 类型。
对于 ||
来说,如果条件判断结果为 true
就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
&&
则相反,如果条件判断结果为 true
就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果。
1 | // 返回 number 类型 123,而不是 boolean 型 true |
boolean 类型转换只会有 true 或者 false 两种结果。
除了“0/NaN/空字符串/null/undefined”五个值是false,其余都是true
转换为string
显式
:String()
方法可以用来显式将值转为字符串。
1 | String([1,2,3]) //"1,2,3" |
隐式
:隐式转换通常在有 +
运算符并且有一个操作数是 string
类型时被触发。
“+”代表的字符串拼接,如果下面的情况存在时会触发转换
- 有两边,一边是
字符串
,则会变成字符串拼接; - 有两边,一边是
对象
1 | 1 + '123' //"1123" |
转换为number
显式
:Number()
方法可以用来显式将值转换成数字类型。
- 字符串转换为数字:空字符串变为0,如果出现任何一个非有效数字字符,结果都是
NaN
1 | Number("") //0 |
- 布尔转换为数字
1 | Number(true) //1 |
- null和undefined转换成数字
1 | Number(null) //0 |
- Symbol无法转换为数字,会报错:Uncaught TypeError: Cannot convert a Symbol value to a number
1 | Number(Symbol()) //Uncaught TypeError: Cannot convert a Symbol value to a number |
- BigInt去除“n”
1 | Number(12312412321312312n) //12312412321312312 |
- 对象转换为数字,会按照下面的步骤去执行
- 先调用对象的
Symbol.toPrimitive
这个方法,如果不存在这个方法 - 再调用对象的
valueOf
获取原始值,如果获取的值不是原始值 - 再调用对象的
toString
把其变为字符串 - 最后再把字符串基于
Number()
方法转换为数字
1 | let obj ={ |
隐式
:number 的隐式类型转换是比较复杂的,因为它可以在下面多种情况下被触发。
- 比较操作(>, <, <=, >=)
- 按位操作(| & ^ ~)
- 算数操作(- + * / %), 注意:当 + 操作存在任意的操作数是 string 类型时,不会触发 number 类型的隐式转换
操作符==两边的隐式转换规则
如果两边数据类型不同,需要先转为相同类型,然后再进行比较,
- 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回
true
。 - 如果一个操作数是
null
,另一个操作数是undefined
,则返回true
。 - 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
- 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
- 如果操作数之一是
Boolean
,则将布尔操作数转换为1或0。- 如果是
true
,则转换为1
。 - 如果是
false
,则转换为0
。
- 如果是
- 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的
valueOf()
和toString()
方法将对象转换为原始值。
以下几种情况需要注意一下:
对象==字符串
将对象转换为字符串
1 | [1,2,3]=='1,2,3' //true |
null/undefined
未被赋值的变量类型为undefined
。如果正在求值的变量没有赋值,方法或语句也会返回undefined。如果函数没有返回值,则返回undefined。
null
是表示缺少的标识,指示变量未指向任何对象。把 null
作为尚未创建的对象,也许更好理解。在 API 中,null
常在返回类型应是一个对象,但没有关联的值的地方使用。
什么时候给变量赋值为null呢?
初始赋值, 表明将要赋值为对象,
可以用做约定俗成的占位符
结束前,
让对象成为垃圾对象
(被垃圾回收器回收)代码示例
1
2
3
4
5
6//起始,可以用做约定俗成的占位符
var b = null // 初始赋值为null, 表明将要赋值为对象
//确定对象就赋值
b = ['atguigu', 12]
//最后在不使用的时候,将其引用置空,就可以释放b这个对象占用的内存 ---当没有引用指向它的对象称为垃圾对象
b = null // 让b指向的对象成为垃圾对象(被垃圾回收器回收)
null/undefined和其他任何值都不相等
1 | null==undefined //true |
NaN
如果任一操作数为NaN
,则返回false
。
1 | NaN==NaN //false |
除了以上情况,只要两边类型不一致,剩下的都是转换为数字,然后再进行比较
需要注意的情况
1 | {} + [] === 0 // true |
===
是严格相等,要求数据类型和值都要相等;==
只需要值相等。==
会发生隐式类型转换,===
不会发生隐式类型转换。
看一道题
1 | let result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false; |
1 | 'foo' == new function(){ return String('foo'); }; // false |
String()作为普通函数使用时,将值转为字符串,不是对象,默认返回是一个空对象,原型为匿名函数的prototype。
1 | String(new function(){ return String('foo'); }) |
String()作为构造函数来用时,返回了一个字符串包装对象。
1 | String(new function(){ return new String('foo'); }) |
Array
[“1”,”2”,”3”].map(parseInt)的输出结果是多少?
这个网红题考察的就是 parseInt
有两个参数。 map
传入的函数可执行三个参数:
1 | // ele 遍历的元素 |
[‘1’,’2’,’3’].map(parseInt)相当于执行了以下三次过程:
1 | parseInt('1', 0, ['1','2','3']) |
parseInt('1', 0, ['1','2','3'])
: radix为0时,默认取10,最后返回1
parseInt('2', 1, ['1','2','3'])
: radix取值为2~36,如果该参数小于 2 或者大于 36,返回NaN
parseInt('3', 2, ['1','2','3'])
: radix取值为2,二进制只包括0,1,返回NaN
如何让上述代码返回[1,2,3],使用你能想到的最简单的方案(要求使用[].map())
1 | ["1","2","3"].map(Number) |
怎么判断数组
ES6
提供的新方法Array.isArray()
- 如果不存在
Array.isArray()
呢?可以借助Object.prototype.toString.call()
进行判断,此方式兼容性最好
1 | if (!Array.isArray) { |
instanceof
判断
1 | // 如果为true,则arr为数组 |
instanceof
判断数组类型如此之简单,为何不推荐使用?
当检测Array实例时, Array.isArray
优于 instanceof
,因为Array.isArray
能检测iframes
。
1 | var iframe = document.createElement('iframe'); |
Number
基于 IEEE 754 标准的双精度 64 位二进制格式的值(-(2^53 -1) 到 2^53 -1)。它并没有为整数给出一种特定的类型。
除了能够表示浮点数外,还有一些带符号的值:+Infinity
,-Infinity
和 NaN
(非数值,Not-a-Number)。
要检查值是否大于或小于 +/-Infinity
,你可以使用常量Number.MAX_VALUE
和 Number.MIN_VALUE
。
另外在 ECMAScript 6 中,你也可以通过 Number.isSafeInteger()
方法还有 Number.MAX_SAFE_INTEGER
和 Number.MIN_SAFE_INTEGER
来检查值是否在双精度浮点数的取值范围内。
超出这个范围,JavaScript 中的数字不再安全了。
1 | var a = 9007199254740995 |
0.1 + 0.2 !== 0.3?
导致这样的问题是因为 JavaScript
中使用基于IEEE 754标准的浮点数运算,所以会产生舍入误差。
也就是说所有遵循 IEEE 754
标准的语言进行浮点数运算的时候,都会有这个问题。
我们知道,JS中的Number类型使用的是双精度浮点型,也就是其他语言中的double类型。而双精度浮点数使用64 bit来进行存储,结构图如下:
也就是说一个Number类型的数字在内存中会被表示成:s x m x 2^e
这样的格式。
在ES规范中规定e的范围在-1074 ~ 971
,而m最大能表示的最大数是52个1
,最小能表示的是1
,这里需要注意:
二进制的第一位有效数字必定是1,因此这个1不会被存储,可以节省一个存储位,因此尾数部分可以存储的范围是1 ~ 2^(52+1)
也就是说Number能表示的最大数字绝对值范围是 2^-1074 ~ 2^(53+971)
前面提到,计算机中存储小数是先转换成二进制进行存储的,我们来看一下0.1和0.2转换成二进制的结果:
1 | (0.1)10 => (00011001100110011001(1001)...)2 |
可以发现,0.1和0.2转成二进制之后都是一个无限循环的数,前面提到尾数位只能存储最多53位有效数字,这时候就必须来进行四舍五入了,而这个取舍的规则就是在IEEE 754中定义的,0.1最终能被存储的有效数字是
1 | 0001(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)101 |
这里注意,53位的存储位指的是能存53位有效数字,因此前置的0不算,要往后再取到53位有效数字为止。
最终的这个二进制数转成十进制就是0.30000000000000004。
计算机中用二进制来存储小数,而大部分小数转成二进制之后都是无限循环的值,因此存在取舍问题,也就是精度丢失。
解决方案
- 使用
JavaScript
提供的最小精度值判断误差是否在该值范围内Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON
- 转为整数计算,计算后再转回小数
- 保留几位小数,比如金额,只需要精确到分即可
- 使用别人的轮子,例如:
math.js
- 转成字符串相加(效率较低)
不管是浮点数计算的计算结果错误和大整数的计算结果错误,最终都可以归结到JS的精度只有53位(尾数只能存储53位的有效数字)。
十进制与二进制转换
10的二进制:1010 。10除以2依次求余,然后从下往上取余数
0.3的二进制:0100110011…0011… 小数部分乘以2,如果大于1,取1,否则取0
小数点前或者整数要从右到左用二进制的每个数去乘以2的相应次方并递增,小数点后则是从左往右乘以二的相应负次方并递减。
例如:二进制数1101.01转化成十进制
1 | 1101.01(2)=1*2^0+0*2^1+1*2^2+1*2^3 +0*2^-1+1*2^-2=1+0+4+8+0+0.25=13.25(10) |
BigInt
BigInt
类型是 JavaScript 中的一个基础的数值类型,可以用任意精度表示整数。
使用 BigInt,您可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。
BigInt 是通过在整数末尾附加 n
或调用构造函数来创建的。
String
不同于类 C 语言,JavaScript 字符串是不可更改的。这意味着字符串一旦被创建,就不能被修改。但是,可以基于对原始字符串的操作来创建新的字符串。例如:
- 获取一个字符串的子串可通过选择个别字母或者使用
String.substr()
。 - 两个字符串的连接使用连接操作符 (
+
) 或者String.concat()
。
字符访问
有两种方法可以访问字符串中的单个字符。首先是 charAt()
方法:
1 | return 'cat'.charAt(1) // returns "a" |
另一种方法(在 ECMAScript 5 中引入)是将字符串视为类似数组的对象,其中单个字符对应于数字索引:
1 | return 'cat'[1] // returns "a" |
当使用括号表示法进行字符访问时,尝试删除这些属性或为这些属性赋值将不会成功。所涉及的属性既不可写也不可配置。
比较字符串
在 JavaScript 中,您只需使用小于和大于运算符:
1 | let a = 'a' |
请注意,它以通常区分大小写的方式比较 in和 for 中a == b
的字符串是否相等。如果您希望不考虑大小写字符进行比较,请使用与此类似的函数:
1 | function isEqual(str1, str2) |
由于某些 UTF-8 字符转换存在问题,此函数中使用大写而不是小写。
基本字符串和字符串对象的区别
请注意区分 JavaScript 字符串对象和基本字符串值 . ( 对于 Boolean
和Numbers
也同样如此.)
字符串字面量 (通过单引号或双引号定义) 和 直接调用 String 方法(没有通过 new 生成字符串对象实例)的字符串都是基本字符串。
JavaScript会自动将基本字符串转换为字符串对象,只有将基本字符串转化为字符串对象之后才可以使用字符串对象的方法。
当基本字符串需要调用一个字符串对象才有的方法或者查询值的时候(基本字符串是没有这些方法的),JavaScript 会自动将基本字符串转化为字符串对象并且调用相应的方法或者执行查询。
1 | var s_prim = "foo"; |
当使用 eval
时,基本字符串和字符串对象也会产生不同的结果。eval
会将基本字符串作为源代码处理; 而字符串对象则被看作对象处理, 返回对象。 例如:
1 | s1 = "2 + 2"; // creates a string primitive |
由于上述原因, 当一段代码在需要使用基本字符串的时候却使用了字符串对象就会导致执行失败(虽然一般情况下程序员们并不需要考虑这样的问题)。
利用 valueOf
方法,我们可以将字符串对象转换为其对应的基本字符串。
1 | console.log(eval(s2.valueOf())); // returns the number 4 |
长文字字符串
有时,您的代码将包含很长的字符串。您可能希望在源代码中专门将字符串分成多行而不影响实际的字符串内容,而不是让行无休止地继续下去,或者随编辑器的一时兴起而换行。有两种方法可以做到这一点。
方法一
您可以使用+
运算符将多个字符串附加在一起,如下所示:
1 | let longString = "This is a very long string which needs " + |
方法二
\
您可以在每行末尾 使用反斜杠字符 ( ) 来指示字符串将在下一行继续。确保反斜杠后没有空格或任何其他字符(换行符除外)或缩进;否则它将无法正常工作。
该表格如下所示:
1 | let longString = "This is a very long string which needs \ |
上述两种方法都会产生相同的字符串。
模板字面量
模板字面量是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。它们在ES2015规范的先前版本中被称为“模板字符串”。
模板字符串使用反引号来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression}
)的占位符。
占位符中的表达式和周围的文本会一起传递给一个默认函数,该函数负责将所有的部分连接起来,如果一个模板字符串由表达式开头,则该字符串被称为带标签的模板字符串,该表达式通常是一个函数,它会在模板字符串处理后被调用,在输出最终结果前,你都可以通过该函数来对模板字符串进行操作处理。
在模版字符串内使用反引号时,需要在它前面加转义符(\)。
常用方法
length
可以用来获取字符串的长度
charAt()
可以返回字符串中指定位置的字符
根据索引获取指定的字符
concat()
可以用来连接两个或多个字符串
作用和+
一样
indexOf()
该方法可以检索一个字符串中是否含有指定内容
如果字符串中含有该内容,则会返回其第一次出现的索引
如果没有找到指定的内容,则返回**-1**
可以指定一个第二个参数,指定开始查找的位置
slice()
可以从字符串中截取指定的内容
不会影响原字符串,而是将截取到内容返回
参数:[a,b)
- 第一个a,开始位置的索引(包括开始位置)
- 第二个b,结束位置的索引(不包括结束位置)
- 如果省略第二个参数,则会截取到后边所有的
- 也可以传递一个负数作为参数,负数的话将会从后边计算
substring()
可以用来截取一个字符串,和slice()类似
参数:
- 第一个:开始截取位置的索引(包括开始位置)
- 第二个:结束位置的索引(不包括结束位置)
- 不同的是这个方法不能接受负值作为参数,如果传递了一个负值,则默认使用0
- 而且他还自动调整参数的位置,如果第二个参数小于第一个,则自动交换
substr()
用来截取字符串
参数:
- 截取开始位置的索引
- 截取的长度
split()
可以将一个字符串拆分为一个数组
参数:
- 需要一个字符串作为参数,将会根据该字符串去拆分数组
- 如果传递一个空串作为参数,则会将每个字符都拆分为数组中的一个元素
toUpperCase()
将一个字符串转换为大写并返回
toLowerCase()
将一个字符串转换为小写并返回
Symbol
Symbol是 ECMAScript 第6版新定义的。符号类型是唯一的并且是不可修改的, 并且也可以用来作为 Object 的 key 的值。
对象
在计算机科学中, 对象是指内存中的可以被标识符引用的一块区域.
在 JavaScript 里,对象可以被看作是一组属性的集合。
用对象字面量语法来定义一个对象时,会自动初始化一组属性。(也就是说,你定义一个var a = “Hello”,那么a本身就会有a.substring这个方法,以及a.length这个属性,以及其它;如果你定义了一个对象,var a = {},那么 a 就会自动有 a.hasOwnProperty 及 a.constructor 等属性和方法。)而后,这些属性还可以被增减。
属性的值可以是任意类型,包括具有复杂数据结构的对象。
属性使用键来标识,它的键值可以是一个字符串或者符号值(Symbol)。
ECMAScript 定义的对象中有两种属性:数据属性和访问器属性。
数据属性是键值对,访问器属性有一个或两个访问器函数 (get 和 set) 来存取数值。
一个 JavaScript 对象就是键和值之间的映射。
键是一个字符串(或者 Symbol
),值可以是任意类型的值。 这使得对象非常符合哈希表。
函数是一个附带可被调用功能的常规对象。
查询某个对象是否有某个属性的方法
使用in关键字
该方法可以判断对象的自有属性和继承来的属性是否存在。
使用对象的hasOwnProperty()方法
该方法只能判断自有属性是否存在,对于继承属性会返回false。
使用undefined判断
自有属性和继承属性均可判断。
1 | var o={x:1}; |
该方法存在一个问题,如果属性的值就是undefined的话,该方法不能返回想要的结果,如下:
1 | var o={x:undefined}; |
在条件语句中判断
1 | var o={}; |
有序集: 数组和类型数组
数组是一种使用整数作为键属性和长度属性之间关联的常规对象。
此外,数组对象还继承了 Array.prototype 的一些操作数组的便捷方法。
例如, indexOf
(搜索数组中的一个值) or push
(向数组中添加一个元素),等等。 这使得数组是表示列表或集合的最优选择。
类型数组(Typed Arrays)是 ECMAScript Edition 6 中新定义的 JavaScript 内建对象,提供了一个基本的二进制数据缓冲区的类数组视图。
键控集: Maps, Sets, WeakMaps, WeakSets
这些数据结构把对象的引用当作键,其在 ECMAScript 第6版中有介绍。
Set和WeakSet表示一组对象,而Map和WeakMap将一个值关联到一个对象。
map和WeakMaps之间的区别在于前者对象键可以被枚举。这允许在后一种情况下进行垃圾收集优化。
我们可以在纯ECMAScript 5中实现映射和集合。但是,由于对象不能进行比较,因此查找性能必然是线性的。
它们的本地实现(包括WeakMaps)的查找性能大约为常数时间的对数。
通常,要将数据绑定到DOM节点,可以直接在对象上设置属性,或者使用data-*属性。
这样做的缺点是,在相同上下文中运行的任何脚本都可以使用这些数据。
Maps和WeakMaps使数据私下绑定到对象变得很容易。
结构化数据: JSON
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,来源于 JavaScript 同时也被多种语言所使用。
JSON 用于构建通用的数据结构。
**JSON
**对象包含两个方法: 用于解析 JavaScript Object Notation(JSON) 的 parse()
方法,以及将对象/值转换为 JSON字符串的 stringify()
方法。除了这两个方法, JSON这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。
JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null
。它基于 JavaScript 语法,但与之不同:
JavaScript不是JSON,JSON也不是JavaScript。