深入理解JavaScript(一):从数据类型说起-在JavaScript中,数据类型可以分为基本数据类型和引用数据类型:基本数据类型:Undefined、Null、Boole
在Javascript中,数据类型可以分为基本数据类型和引用数据类型:
- 基本数据类型:Undefined、Null、Boolean、Number、String、Symbol
- 引用数据类型:Object、Function、Array、Date等
一、基本数据类型
1、Undefined和Null的异同点:
?相同点:
- 它们都只有一个字面量值,分别为undefined和null;
- 转换为Boolean类型的值时,都是false;
- 如果一个对象是Undefined或Null,访问属性时会出现引用错误:
let a
let b = null
console.log(a.name)
// Uncaught TypeError: Cannot read property 'name' of undefined
console.log(b.name)
// Uncaught TypeError: Cannot read property 'name' of null
?️不同点:
- typeof返回的类型不同:
let a
let b = null
typeof a // 'undefined'
typeof b // 'object'
- 通过call调用Object.prototype.toString函数时返回结果不同:
let a
let b = null
Object.prototype.toString.call(a)
// '[object Undefined]'
Object.prototype.toString.call(b)
// '[object Null]'
- 转换为字符串类型时,null会转换为字符串'null',而undefined会转换为字符串'undefined';
- 转换为数值类型时,undefined会转换为NaN,无法参与计算;null会转换为0,可以参与计算。
PS:不要将一个变量显式设为undefined!如果需要定义某个变量来保存将来要使用的对象,应该将其初始化为null,这样不仅能将null作为空对象指针的惯例,还有助于区分null和undefined。
2、“幻假值”都有哪些
所谓“幻假值”,指的是非布尔值但经过Boolean()转换之后为false的值,在js中,属于“幻假值”的有:
- 空字符串
- 0和NaN
- null (注意空对象{}并不是幻假值哦)
- undefined
3、关于Number类型需要知道的几点
1、藏在map()函数与parseInt()函数中的隐形坑
设想这样一个场景:存在一个数组,数组中的每个元素都是Number类型的字符串['1','2', '3', '4'],如果我们想要将数组中的元素全部转换为整数,我们该怎么做呢?我们可能会想到在Array的map()函数中调用parseInt()函数:
let arr = ['1','2', '3', '4']
let result = arr.map(parseInt)
console.log(result)
// [1, NaN, NaN, NaN]
并不是我们期望的结果[1, 2, 3, 4]啊,这是为什么呢?
这就是一个藏在map()函数与parseInt()函数中的隐形坑!上面的代码其实和下面的代码等效:
let result = arr.map(function(value, index) {
return parseInt(value, index)
})
parseInt()函数接收的第二个参数实际为数组的索引值,但它本身是将第二个参数作为转换Number类型的进制基数,所以实际处理的过程是这样的:
parseInt('1', 0) // 1
// 任何整数以0为基数取整时,都会返回本身
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN
parseInt('4', 3) // NaN
// parseInt()函数对应的基数只能为2~36
// 且数值不能比进制基数大,所以类型转换失败
所以我们可以改动一下以达到我们的目的:
let result = arr.map(function(value) {
return parseInt(value, 10)
})
2、isNaN()和Number.isNaN()有啥区别
判断NaN时,ES5提供了isNaN()函数,ES6为Number类型增加了静态函数isNaN(),既然isNaN()能提供判断NaN的功能,ES6为何要新增一个呢?它俩有啥区别呢?
我们来看看isNaN的作用,它用来确定一个变量是不是NaN。如果传递的参数是Number类型数据,可以很容易判断是不是NaN。如果传递的参数是非Number类型,它返回的结果往往会让人费解。比如:
isNaN({}) // true
这里把空对象判断为NaN的原因是isNaN会进行数据的类型转换,它在处理的时候会去判断传入的变量值能否转换为数字,如果能转换成数字则会返回“false”,如果无法转换则会返回“true”。
既然在全局环境中有isNaN函数,为什么在ES6中会专门针对Number类型增加一个isNaN函数呢?这是因为之前的isNaN函数本身存在误导性,而ES6中的Number.isNaN()函数会在真正意义上去判断变量是否为NaN,不会做数据类型转换。只有在传入的值为NaN时,才会返回“true”,传入其他任何类型的值时会返回“false”。
如果在非ES6环境中想用ES6中的Number.isNaN()函数,有以下兼容性处理方案:
if(!Number.isNaN){
Number.isNaN = function(n) {
// 只有在变量值为NaN时才会返回false
return n!==n
}
}
3、0.1+0.2还能不等于0.3?
我们知道,一个浮点型数在计算机中的表示总共长度是64位,其中最高位为符号位,接下来的11位为指数位,最后的52位为小数位,即有效数字。
因为浮点型数使用64位存储时,最多只能存储52位的小数位,对于一些存在无限循环的小数位浮点数,会截取前52位,从而丢失精度,所以会出现0.1+0.2===0.3为false的结果,那这个结果具体是怎么得到的呢?
首先将各个浮点数的小数位按照“乘2取整,顺序排列”的方法转换成二进制表示。具体做法是用2乘以十进制小数,得到积,将积的整数部分取出;然后再用2乘以余下的小数部分,又得到一个积;再将积的整数部分取出,如此推进,直到积中的小数部分为零为止。然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位,得到最终结果。
举个?,0.1的二进制表示过程为:
0.1*2=0.2 // 取整数部分0
0.2*2=0.4 // 取整数部分0
0.4*2=0.8 // 取整数部分0
0.8*2=1.6 // 取整数部分1
0.6*2=1.2 // 取整数部分1
0.2*2=0.4 // 取整数部分0
0.4*2=0.8 // 取整数部分0
0.8*2=1.6 // 取整数部分1
0.6*2=1.2 // 取整数部分1
...... // 无限循环
因此0.1转换成二进制表示为0.0 0011 0011 0011 0011 0011 0011……(无限循环)。
同理对0.2进行二进制的转换,计算过程与0.1类似,直接从0.2开始,相比于0.1,少了第一位的0,其余位数完全相同,结果为0.0011 0011 0011 0011 0011 0011……(无限循环)。
将0.1与0.2相加,然后转换成52位精度的浮点型表示,得到的结果为0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 11001100,转换成十进制值为0.30000000000000004。
我们该如何解决这种精度丢失的问题呢?一种可行的思路是将浮点数先乘以一定的数值(比如浮点数的小数位后的长度)转换为整数,通过整数进行运算,然后将结果除以相同的数值转换成浮点数后返回。具体实现可以参考《Javascript重难点实例精讲》。
4、String类型常见算法
1、字符串逆序输出
思路1:借助数组的reverse()
function reverseStr(str) {
return str.split('').reverse().join('')
}
思路2:利用字符串本身的charAt()
function reverseStr(str) {
let res = ''
let len = str.length
for(let i=len-1;i > -1;i--){
res += str.charAt(i)
}
return res
}
思路3:利用栈的先进后出
// 代码略,自己写着玩玩
2、统计字符串中出现次数最多的字符及出现的次数
3、去除字符串中重复的字符
4、判断一个字符串是否为回文字符串
leetcode都有,去leetcode慢慢刷~
5、使用typeof运算符时需要考虑的问题
1、typeof运算符区分对待Object类型和Function类型
《Javascript高级程序设计》一书中讲到,从技术角度讲,函数在ES中是对象,不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过typeof运算符来区分函数和其他对象是有必要的。
2、typeof运算符对null的处理
typeof null // 'object'
这是一个在Javascript设计之初就存在的问题。在Javascript中,每种数据类型都会使用3bit表示:
- 000表示Object类型的数据
- 001表示Int类型的数据
- 010表示Double类型的数据
- 100表示String类型的数据
- 110表示Boolean类型的数据
由于null代表的是空指针,大多数平台中值为0x00,因此null的类型标签就成了0,所以使用typeof运算符时会判断为object类型,返回“object”,虽然在后来的提案中有提出修复方案,但是因为影响面太大,所以并没有被采纳,从而导致这个问题一直存在。
二、JS中的引用数据类型
1、Object类型及其实例和静态函数
1、new操作符都干了点什么
function Cat(name, age) {
this.name = name
this.age = age
}
let cat = new Cat()
从表面上看,new的主要作用是创建一个Cat对象的实例,并将这个实例值赋予cat变量,cat变量就会包含Cat对象的属性和函数。
其实new操作符做了4件事情:
- 首先创建一个空对象,这个对象将会作为执行 new 构造函数() 之后,返回的对象实例:
let cat = {}
- 将上面创建的空对象的原型(proto)指向构造函数的 prototype 属性:
cat.__proto__ = Cat.prototype
- 将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑:
Cat.call(cat)
- 根据构造函数执行逻辑,返回第一步创建的对象或者构造函数的显式返回值:
return cat
2、Object类型的静态函数
1、Object.create()
该函数的主要作用是创建并返回一个指定原型和指定属性的对象:
let obj = Object.create(prototype, property)
// prototype会作为obj的原型,property用于制定obj的属性
// 举个?
let obj = Object.create(null, {name: 'Hah'})
如果我们要自己实现一个Object.create(),核心部分在于:
Object.create = function(proto, property){
function F(){}
// 中间部分省略
F.prototype = proto
return new F()
}
2、Array类型
1、判断一个变量arr是不是数组
Array.isArray(arr)
Object.prototype.toString.call(arr) //'[object Array]'
2、reduce函数
reduce()函数最主要的作用是做累加处理,即接收一个函数作为累加器,将数组中的每一个元素从左到右依次执行累加器,返回最终的处理结果,基本用法:
array.reduce(callback[,initialValue])
initialValue用作callback的第一个参数值,如果没有设置,则会使用数组的第一个元素值。
callback会接收4个参数(accumulator、currentValue、currentIndex、array)。
· accumulator表示上一次调用累加器的返回值,或设置的initialValue值。如果设置了initialValue,则accumulator=initialValue;否则accumulator=数组的第一个元素值。
· currentValue表示数组正在处理的值。
· currentIndex表示当前正在处理值的索引。如果设置了initialValue,则currentIndex从0开始,否则从1开始。
· array表示数组本身。
用法举例?:
求数组每个元素相加的和
let arr = [1,2,3,4,5]
arr.reduce(function(accumulator,currentValue){
return accumulator + currentValue
})
// 15
统计数组中每个元素出现的次数
let arr = [1,2,2,3,4,4,4,4,5]
arr.reduce(function(accumulator,currentValue){
accumulator[currentValue] ? accumulator[currentValue]++ : accumulator[currentValue] = 1
return accumulator
},{})
// {1: 1, 2: 2, 3: 1, 4: 4, 5: 1}
3、Date类型
1、比较日期大小
在实际开发中,经常会碰到需要判断开始时间是否在结束之间之前的需求,怎么实现呢?
大致思想:以斜线(/)作为分隔符的时间类型字符串,可以直接转换为Date类型对象并直接进行比较。
function compareDate(start, end){
// 将传入的带有“-”分隔符的时间字符串通过正则表达式匹配替换为“/”
// 转换原因主要是为了兼容各个浏览器
let date1 = start.replace('/-/g', '\/')
let date2 = end.replace('/-/g', '\/')
return new Date(date1)
2、计算当前日期前后N天的日期
获取前后N天的日期的主要思想是对date值的设置,在Date对象的实例函数中提供setDate函数,用于设置日期值。
function getDate(count){
// count参数表示前后N天的具体值 eg: 30为一个月后,-90为三个月前
let date = new Date() //当然这里可以传参获取制定日期的前后N天
date.setDate(date.getDate() + count)
let y = date.getFullYear()
let m = date.getMonth() + 1
let d = date.getDate()
return y + '-' + m + '-' + 'd'
}
注:参考《Javascript重难点实例精讲》