JavaScript 的数据类型,共有七种。
数值(Number):整数和小数(比如1和3.14)。
字符串(String):文本(比如Hello World)。
布尔值(Boolean):表示真伪的两个特殊值,即true(真)和false(假)。
Symbol:新的原始数据类型Symbol,表示独一无二的值。对象的属性名可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。
null:表示空值,即此处的值为空。
对象(Object):各种值组成的集合。
1.数值(Number)
1.1 整数和浮点数
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。
var a = 1
//undefined
var b = 1.0
//undefined
a === b
//true
由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
0.1+0.2 === 0.3
//false
0.3/0.1
//2.9999999999999996
(0.3-0.2) === (0.2-0.1)
//false
JavaScript 提供Number对象的MAX_VALUE和MIN_VALUE属性,返回可以表示的具体的最大值和最小值。
Number.MAX_VALUE
//1.7976931348623157e+308
Number.MIN_VALUE
//5e-324
1.2 数值的表示法
JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如35(十进制)和0xFF(十六进制)。
数值也可以采用科学计数法表示。
123e3 // 123000
123e-3 // 0.123
-3.1E+12
.1e-23
科学计数法允许字母e或E的后面,跟着一个整数,表示这个数值的指数部分。
1.3 数值的进制
JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。
-十进制:没有前导0的数值。
-八进制:有前缀0o或0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
-十六进制:有前缀0x或0X的数值。
-二进制:有前缀0b或0B的数值。
1.4 NaN
NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
10 - 'hello'
//NaN
0/0
//NaN
NaN不等于任何值,包括它本身。
NaN === NaN
//false
数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立。
[NaN].indexOf(NaN)
// -1
NaN在布尔运算时被当作false。
Boolean(NaN)
// false
NaN与任何数(包括它自己)的运算,得到的都是NaN。
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
2.字符串(String)
2.1 定义
字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。
'abc'
"abc"
单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。
'key = "value"'
"It's a long journey"
上面两个都是合法的字符串。
如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。
'Did she say \'Hello\'?'
// "Did she say 'Hello'?"
"Did she say \"Hello\"?"
// "Did she say "Hello"?"
字符串默认只能写在一行内,分成多行将会报错。
'a
b
c'
// SyntaxError: Unexpected token ILLEGAL
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
var longString = 'Long \
long \
long \
string';
longString
// "Long long long string"
注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。
连接运算符(+)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。
var longString = 'Long '
- 'long '
- 'long '
- 'string';
2.2 转义
常见转义字符
\0 :null(\u0000)
\b :后退键(\u0008)
\f :换页符(\u000C)
\n :换行符(\u000A)
\r :回车键(\u000D)
\t :制表符(\u0009)
\v :垂直制表符(\u000B)
' :单引号(\u0027)
" :双引号(\u0022)
\ :反斜杠(\u005C)
2.3 字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。
var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
// 直接对字符串使用方括号运算符
'hello'[1] // "e"
如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined。
'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined
但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
var s = 'hello';
delete s[0];
s // "hello"
s[1] = 'a';
s // "hello"
s[5] = '!';
s // "hello"
2.4 length属性
length属性返回字符串的长度,该属性也是无法改变的。
var hello = 'areyouok'
//undefined
hello.length
//8
hello.length = 5
//5
hello.length
//8
2.5 字符集
JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。
JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成\uxxxx的形式,其中xxxx代表该字符的 Unicode 码点。比如,\u00A9代表版权符号。
var s = '\u00A9';
s // "©"
解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示。输出给用户的时候,所有字符都会转成字面形式。
var f\u006F\u006F = 'abc';
foo // "abc"
上面代码中,第一行的变量名foo是 Unicode 形式表示,第二行是字面形式表示。JavaScript 会自动识别。
我们还需要知道,每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为16位长度,即2个字节。
但是,UTF-16 有两种长度:对于码点在U+0000到U+FFFF之间的字符,长度为16位(即2个字节);对于码点在U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。举例来说,码点U+1D306对应的字符为𝌆,它写成 UTF-16 就是0xD834 0xDF06。
JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。这是因为 JavaScript 第一版发布的时候,Unicode 的码点只编到U+FFFF,因此两字节足够表示了。后来,Unicode 纳入的字符越来越多,出现了四字节的编码。但是,JavaScript 的标准此时已经定型了,统一将字符长度限制在两字节,导致无法识别四字节的字符。上一节的那个四字节字符𝌆,浏览器会正确识别这是一个字符,但是 JavaScript 无法识别,会认为这是两个字符。
'𝌆'.length // 2
上面代码中,JavaScript 认为𝌆的长度为2,而不是1。
总结一下,对于码点在U+10000到U+10FFFF之间的字符,JavaScript 总是认为它们是两个字符(length属性为2)。所以处理的时候,必须把这一点考虑在内,也就是说,JavaScript 返回的字符串长度可能是不正确的。
2.6 Base64转码
有时,文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另一个场景是,有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码。
所谓 Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript 原生提供两个 Base64 相关的方法。
-btoa():任意值转为 Base64 编码
-atob():Base64 编码转为原来的值
var string = 'are you ok?'
//undefined
btoa(string)
//"YXJlIHlvdSBvaz8="
atob('YXJlIHlvdSBvaz8=')
//"are you ok?"
注意,这两个方法不适合非 ASCII 码的字符,会报错。
btoa('google大法好') // 报错
要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
//undefined
b64Encode('google大法好')
//"Z29vZ2xlJUU1JUE0JUE3JUU2JUIzJTk1JUU1JUE1JUJE"
b64Decode('Z29vZ2xlJUU1JUE0JUE3JUU2JUIzJTk1JUU1JUE1JUJE')
//"google大法好"
3.布尔值(Boolean)
布尔值代表“真”和“假”两个状态。“真”用关键字true表示,“假”用关键字false表示。布尔值只有这两个值。
boolean型的5个falsy值
Boolean(a)
//false
Boolean(0)
//false
Boolean(NaN)
//false
Boolean('')
//false
Boolean(null)
//false
Boolean(undefined)
//false
4.Symbol
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。
Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
let s = Symbol();
typeof s
// "symbol"
上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。
注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
5.null 和 undefined
null与undefined都可以表示“没有”,含义非常相似。将一个变量赋值为undefined或null,老实说,语法效果几乎没区别。
var a = undefined;
// 或者
var a = null;
上面代码中,变量a分别被赋值为undefined和null,这两种写法的效果几乎等价。
在if语句中,它们都会被自动转为false,相等运算符(==)甚至直接报告两者相等。
if (!undefined) {
console.log('undefined is false');
}
// undefined is false
if (!null) {
console.log('null is false');
}
// null is false
undefined == null
// true
从上面代码可见,两者的行为是何等相似!谷歌公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有null,没有undefined!
对于null和undefined,大致可以像下面这样理解。
null表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null,表示未发生错误。
undefined表示“未定义”,下面是返回undefined的典型场景。
// 变量声明了,但没有赋值
var i;
i // undefined
// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {
return x;
}
f() // undefined
// 对象没有赋值的属性
var o = new Object();
o.p // undefined
// 函数没有返回值时,默认返回 undefined
function f() {}
f() // undefined
6.对象(Object)
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
var obj ={
are: 'good',
you: 'morning'
};
上面代码中,大括号就定义了一个对象,它被赋值给变量obj,所以变量obj就指向一个对象。该对象内部包含两个键值对(又称为两个“成员”),第一个键值对是are: 'good',其中are是“键名”(成员的名称),字符串good是“键值”(成员的值)。键名与键值之间用冒号分隔。第二个键值对是you: 'morning',you是键名,morning是键值。两个键值对之间用逗号分隔。
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。如果键名是数值,会被自动转为字符串。如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。
// 报错
var obj = {
1p: 'Hello World'
};
// 不报错
var obj = {
'1p': 'Hello World',
'h w': 'Hello World',
'p+q': 'Hello World'
};
6.1 属性的读取
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
var obj ={
are: 'good',
you: 'morning'
};
obj.are
//"good"
obj['are']
//"good"
请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。
方括号运算符内部还可以使用表达式。
obj['hello' + ' world']
obj[3 + 3]
数字键可以不加引号,因为会自动转成字符串。
6.2 属性的赋值
点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。
var obj = {};
obj.foo = 'Hello';
obj['bar'] = 'World';
上面代码中,分别使用点运算符和方括号运算符,对属性赋值。
JavaScript 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。
var obj = { p: 1 };
// 等价于
var obj = {};
obj.p = 1;
6.3 属性的查看
查看一个对象本身的所有属性,可以使用Object.keys方法。
var obj = {
key1: 1,
key2: 2
};
Object.keys(obj);
// ['key1', 'key2']
6.4 delet命令
delete命令用于删除对象的属性,删除成功后返回true。
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
上面代码中,delete命令删除对象obj的p属性。删除后,再读取p属性就会返回undefined,而且Object.keys方法的返回值也不再包括该属性。
注意,删除一个不存在的属性,delete不报错,而且返回true。
6.5 属性是否存在: in 运算符
in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false。它的左边是一个字符串,表示属性名,右边是一个对象。
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj本身并没有toString属性,但是in运算符会返回true,因为这个属性是继承的。
这时,可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性。
var obj = {};
if ('toString' in obj) {
console.log(obj.hasOwnProperty('toString')) // false
}
6.6 属性的遍历: for...in 循环
for...in循环用来遍历一个对象的全部属性。
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
console.log('键名:', i);
console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3
for...in循环有两个使用注意点。
它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
它不仅遍历对象自身的属性,还遍历继承的属性。
举例来说,对象都继承了toString属性,但是for...in循环不会遍历到这个属性。
var obj = {};
// toString 属性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
console.log(p);
} // 没有任何输出
上面代码中,对象obj继承了toString属性,该属性不会被for...in循环遍历到,因为它默认是“不可遍历”的。关于对象属性的可遍历性,参见《标准库》章节中 Object 一章的介绍。
如果继承的属性是可遍历的,那么就会被for...in循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for...in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。
var person = { name: '老张' };
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name