基本类型
# 前言
JavaScript中有8种数据类型: Number、BigInt、String、Boolean、Null、Undefined、Object、Symbol
先来说说基本类型
# Number
JavaScript中
Number
类型包括:整数和小数
let num1= 3. // Number类型 整数
let num2 = 3.234 //Number 小数
2
数字可以进行+
、-
、*
、\
四则运算
let num1 = 3
let num2 = 4
let sum = num1 + num2 // 7 加法
let sub = num2 - sum1 // 1 减法
let time = num1 * num2 // 12 乘法
let mul = num1 / num2. // 0.75 除法
2
3
4
5
6
除了常规的数字外,还有Infinity
和NaN
两个特殊的值。Infinity
表示无穷大,NaN
表示一个不正确的或者一个未定义的数学操作所得到的结果。NaN是粘性的,任何对NaN的操作,其结果都将是NaN。
console.log( 1 / 0 ); // Infinity
console.log(Infinity); // Infinity
let "not a number" / 2 // NaN
2
3
4
同时,NaN是JavaScript中唯一一个不等于本身的值。判断一个值是不是NaN
,可以这样:
`
if (x != x) {
console.log("这个值是NaN值")
}
2
3
JavaScript中也提供了isNaN
方法,判断这个值是不是NaN
:
console.log(isNaN(m)) // NaN值返回true,否则返回false
Number
是number
的包装器,或者叫包装对象
// 看下这种应用场景
let num = 1;
console.log(num.toString()) // 1
2
3
看起来,调用了toString
这个方法,但是num
是一个简单类型,JavaScript中只有对象才具备方法。其实上述的代码相当于下面这段代码:
let num = 1;
num = new Number(num); // 此时num是一个Number包装对象
num.toString() // 1
num = null. // 执行完toString方法后直接销毁,避免副作用
2
3
4
既然Number
是一个包装对象,自然具备一些有用的方法,MDN (opens new window)可以在上看具体都有哪些方法。
# BigInt
BigInt的出现源于javaScript中Number只能表示 -9007199254740991 (-(2^53-1))和9007199254740991(2^53-1),超过这个范围的数字就会出现误差,而Binint可以表示任意大的数字
可以在一个数字后面加n的方式来表示这是一个BigInt
类型的数字
let bigIntNumber = 10n;
console.log( typeof bingIntnumber) // bigint
2
也可以使用包装对象BigInt
let bigIntNumber = BigInt(10)
console.log(typeof bigIntNumber) // binInt
2
但是当我们想使用new Number
一样使用new BigInt
的时候会报错
let bigIntNumber = new BigInt(10)
console.log(bigIntNumber) // 报错BigInt is not a constructor
2
那么new
关键字到底做了什么事情呢?
MDN上对new
这个关键字的解释是这样的:
- 以构造器的prototype属性为原型,创建新对象
- 将this(也就是第一步中的新对象)和调用参数传给构造器,执行
- 如果构造器没有手动返回对象,则返回第一步中创建的新对象,如果有,则舍弃掉第一步创建的新对象,返回手动return的对象
知道了new
关键字做了什么事情,我们可以自己试着实现一个简单的new
关键字
// 实现简单的new关键字
function myNew(ctor,...args) {
// 以构造器的prototype属性为原型,创建新对象
const newObj = Object.create(ctor.prototype);
// 将this(也就是第一步中的新对象)和调用参数传给构造器,执行
const result = ctor.apply(newObj,args)
//如果构造器没有手动返回对象,则返回第一步中创建的新对象,如果有,则舍弃掉第一步创建的新对象,返回手动return的对象
return (typeof result == 'object' && typeof result != 'null') ? result : newObj
}
2
3
4
5
6
7
8
9
10
11
我们可以这样测试
function Student (name,age) {
this.name = name;
this.age = age
}
Student.prototype.showName = function () {
console.log(this.name)
}
const testMyNew = myNew(Student,'马松松', 18);
console.log(testMyNew.name, testMyNew.age) // 马松松 18
console.log(testMyNew.__proto__ == Student.prototype); // true
2
3
4
5
6
7
8
9
10
11
12
我们实现的是一个很粗糙的new
,但是对于理解new
的原理来说这样就足够了。
回到new Number
和number
这个问题:
const num = Number(8)
const numObj = new Number(8)
console.log(typeof num) // number
console.log(typeof numObj) // object
2
3
4
使用了new
关键字的numObj
是object
类型, 所以可以使用toString
方法;num
也可以使用toString
方法。相当于这样:
const num = Number(8);
const numObj = new Number(num);
console.log( typeof numObj) // object
2
3
可以看到,当调用toString
方法时,都会经过包装对象的封装,这是解析器默认做的。
值得注意的是 BigInt类型不支持 new 关键字示例化对象;
并且虽然BigInt
可以表示任意大的数字,但是为了区别于Number
类型,有如下几个特点:
- 不能用于Math对象的方法
- 不能和Number对象实例混合计算
- 一定要同时计算时,要转换成同一种类型,不过转换时候极有可能丢失精度,转换时需特别注意
- 无符号右移(>>>)也是不允许的,因为BigInt都是带有符号的
# String
JavaScrip中没有类似强类型语言中表示单个字符的类型,如
char
,所有字符逗包裹在单引号或双引号中内部采用UTF-16 (opens new window)内部格式存贮
let name = '小新'
let age = "34"
2
UTF-16是unicode实现方式之一,unicode这就是一种采用16进制,想法是: 将全世界所有的字符包含在一个集合里,计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。
unicode只是定义了每个字符的码点,但是具体使用什么样的编码方式,不同的编码方法有不同的做法
在JavaScript中允许这样表示字符串
'好' === '\u597D' // true // 反斜杠+u+码点
所有的特使符号都需要使用\进行转义,所以这里的\也称作转义字符
console.log('I\'m a student') // I'm a student
字符串具有length属性
console.log("love".length) // 4
访问字符串的某个字符,可以通过类似数组下标的方式:
// 取字符串的第一个字符
let str = "love"
console.log(str[0]) // l
// 使用古老的方法charAt
console.log(str.charAt(0)) // l
// 可以使用for of遍历
for (let char of str) {
console.log(char) // l o v e依次打印
}
2
3
4
5
6
7
8
9
10
11
在字符串中查找子字符串有很多方法。
// substr是要查找的字符串 pos是从哪个位置开始查找
str.indexOf(substr, pos) // 从开头开始寻找给定字符串的索引
str.lastIndexOf(substr, pos) //从末尾开始查找
2
3
4
可以这样查找目标字符串在字符串中的每一个位置:
let sourceStr = "I hava a dream a hak jhgn a ljj!";
let targetStr = "a";
let index = 0;
while(true) {
index = sourceStr.indexOf(targetStr, index);
if (index == -1) break;
console.log(index)
index = index + 1
}
2
3
4
5
6
7
8
9
10
11
相同的算法可以这样写:
let sourceStr = "I hava a dream a hak jhgn a ljj!";
let targetStr = "a";
let index = -1;
while((index = sourceStr.indexOf(targetStr, index+1)) != -1) {
console.log(index)
}
2
3
4
5
6
7
上面的代码我们可以看出,我们使用不等于-1判断字字符串是否存在
let str = "Widget with id";
if (str.indexOf("Widget") != -1) {
console.log("We found it"); //像是这样判断
}
2
3
4
5
古老的判断子字符串是否存在的方法有一个按位NOT技巧 ,用符号~表示,它将数字转换为 32-bit 整数(如果存在小数部分,则删除小数部分),然后对其二进制表示形式中的所有位均取反。
按位取的规则其实很简单:~n 等于 ~(n + 1)
console.log( ~2 ); // -3,和 -(2+1) 相同
console.log( ~1 ); // -2,和 -(1+1) 相同
console.log( ~0 ); // -1,和 -(0+1) 相同
console.log( ~-1 ); // 0,和 -(-1+1) 相同
2
3
4
那这里我们思路就清晰了: sourceStr.indexOf(targetStr, index+1)为-1时, ~sourceStr.indexOf(targetStr, index+1) 为0
let str = "Widget with id";
if (~str.indexOf("Widget")) {
console.log("We found it"); //像是这样判断
}
2
3
4
5
不过不建议使用,当字符串的长度超过32位时,超过的位数会被置为0,这样就丢失了子字符串
判断子字符串是否存在的方式更流行的是使用includes、startWith、endWith
// 当我们只需要判断是子字符串是否存在,不需要知道准确位置,使用includes很方便
console.log("I love you".includes("o")) // true
console.log("I love you".includes("i")) // false includes是区分大小写的
// 判断是否以特定的字符开头或者结尾 使用starstWith和endsWith
console.log("I love you".startsWith("I")) // true
console.log("I love you".endsWith("you")) // true
2
3
4
5
6
7
includes有时需要我们polyfill,因为未必在所有环境中都能使用,可以这样做:
if (!String.prototype.includes) {
String.prototype.includes = function(searchStr, start) {
if (typeof start !== 'number') {
start = 0;
}
if (start + searchStr.length > this.length){
return false
} else {
return this.indexof(searchStr,start) != -1
}
}
}
2
3
4
5
6
7
8
9
10
11
12
在有的应用场景中我们需要切割字符串,substr、substring
、slice
方法就是做这样的事情
// slice(start,end) 切割start到end的字符串 不包括end为位置
let str = "stringify";
console.log( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5)
console.log(str.slice(1)); // 'stringify',从1位置一直到末尾
console.log(str.slice(-2,-1)) //f 负数表示它们的意思是起始位置从字符串结尾计算
console.log(str.slice(-1,-2)) // 没有返回。当start<end,没有返回值
2
3
4
5
6
三种方法用法基本相同,功能也相差不大(表格参照现代JavaScript教程)
slice(start, end)
从 start
到end
(不含end
)允许 substring(start, end)
start
与end
之间(包括start
,但不包括end
)负值代表 0
substr(start, length)
从 start
开始获取长为length
的字符串允许 start
为负数
字符串之间的比较是按字母顺序进行比较的
但是存在两种奇怪的情况
- 小写字母总是大于大写字母:
- 带变音符号的字母存在“乱序”的情况:
重新回到unicode,我们看看字符内部发生了什么
console.log('a' > "A") // true
**str.codePointAt(pos)**方法可以获取对应字符的unicode码点
console.log('a'.codePointAt(0)) // 97
console.log('A'codePointAt(0)) // 65
2
可以看到字母比较内部是按照unicode码点进行比较的
再来看看变音符号的乱序的情况
说明下什么是变音符号
变音符号,是指添加在字母上面的符号,以更改字母的发音或者以区分拼写相似词语
console.log('Ö' > 'z') // true
为了更直观的看到变音符号的乱序情况,我们可以遍历除65..220
的字符(拉丁字母和一些额外的字符)
可以使用fromCodePoint(i)
根据码点获取字符
let str = '';
for (let i = 65; i <= 220; i++) {
str += String.fromCodePoint(i);
}
console.log( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}...
// ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ
2
3
4
5
6
7
8
可以看到变音字符基本都是最后打印的,说白了一句话,以上两种情况都是根据内部unicode码点进行比较的
那如何正确的比较字符的大小呢?使用str.localeCompare(str2)
这个方法
- 如果
str
小于str2
则返回负数。- 如果
str
大于str2
则返回正数。- 如果它们相等则返回
0
。
比如:
let a = 'a';
let A = 'A';
console.log(a.localeCompare(A)) // -1 现在正常了
2
3
变音符号的组合特别多,如a
可以是 àáâäãåā
的基本字符,为了方便表示变音字符,unicode允许使用多个unicode字符进行组合,组合形式是这样:例如,如果我们 S
后跟有特殊的 “dot above” 字符(代码 \u0307
),则显示 Ṡ。
console.log('S\u0307') // Ṡ
如果我们想要给S
上下都加变音符号,再加一个unicode字符就可以了,像是这样:
consoe.log('S\u0307\u0323') // Ṩ
这样使得使用unicode表示字符十分灵活,但是也发现了一个有趣的现象:
console.log('S\u0307\u0323') // Ṩ
console.log('S\u0323\u0307') // Ṩ
2
3
发现由于Ṩ
的特殊性,先加上面的符号和先加下面的变音符号表示的字符都是Ṩ
,但是两者相等吗?
'S\u0307\u0323' == 'S\u0323\u0307' //false
两者并不相等,这就不符合常规的逻辑,为了解决这个问题:
str.normalize被提出并加入到标准中,这个方法会将表示相同的字符但是组合方式不一样的unicode字符转换成统一的格式
'S\u0307\u0323'.normalize() == 'S\u0323\u0307'.normalize() // true 符合我们的预期的输出
字符串中模版字符串
的用法
let name = "小新"
console.log(`my name is ${name}`) // my name is 小新
2
3
此外,+运算符在字符串中的使用
let name = "小新"
console.log("my name is " + name) // my name is 小新 is后面有个空格
2
3
# Boolean
布尔类型只有两个值true
和false
其实对于布尔类型,我们只需要明确几个原则,就可判断其值是true
还是false
- 值
0
,-0
,null
,false
,NaN
,undefined
),或空字符串(""
),值为false
- 任何对象,空数组(
[]
)或字符串"false"
,值都为true
但是对于包装对象得特别注意:
let booleanObj = new Boolean("false")
console.log(Boolean(booleanObj)) // 尽管传入构造器的值是false 但是结果是true
2
因为符合第二个原则, 这也可以说明不要用Boolean包装对象转换成布尔类型
推荐使用!!
:
let name = "小新"
console.log(!!name) // true
2
3
# Null
JavaScript中的
null
是空、无值的意思,别的语言的null
一般表示空指针。
let name = null. // 表示初始值是空
这里需要注意一点是:使用typeof
方法值检测null
的类型,并不是null
,而是object
console.log(typeof null) // object
这是一个历史bug,但是目前也是一直将错就错,其原因是:
javascript 里的数值由标记位和实际数值组成。标记位为 0 的类型是 object 类型,而 null 在底层对应的值其实是0x00,也就是0,其标记位是0。但是问题就在于,typeof 的逻辑是根据标记位来判断的,但是 typeof 的源码只判断 undefined, object, string, number 和 boolean 但是它没有一个 if 分支来专门判断 null(这也被很多人认为是一个 bug,但因为历史原因不做修复),所以在null 在 object 的 if 分支被判断成一个 object.
# Undefined
undefined
表示未被赋值
let name;
console.log(name) // undefined
2
值得注意一点null
和undefined
的区别
null == undefiend // true
null === undefiend // false
2