红宝书第三章部分笔记
1 var、let、const的区别
let
和const
都是ES6引入的,下面是它们和var
的区别。
- 使用
var
声明的变量只有全局作用域和函数作用域,使用let
和const
声明的变量存在局部作用域。 - 在全局作用域中使用
var
声明的变量会保存在window
对象中,使用let
和const
声明的变量不会。 - 使用
var
声明的变量具有变量提升,let
和const
没有。因此let
和const
必须先声明后使用(在let
和const
声明之前使用变量的这个瞬间称作暂时性死区,这时会抛出ReferenceError
),var
声明变量可以在使用变量之后。 - 同名变量可以使用
var
声明多次,在变量提升后这些声明会合并。由于let
和const
没有变量提升,因此无法判断此前是否声明过,所以在同一作用域使用let
和const
重复声明变量会报错。
此外,在for循环中使用var
和let
会有不同的行为。
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0);
}
// 5, 5, 5, 5, 5
for (let j = 0; j < 5; j++) {
setTimeout(() => console.log(j), 0);
}
// 0, 1, 2, 3, 4
出现这种情况的原因是var
声明的变量是存在于全局的,而setTimeout
是异步调用的,会在for
循环结束后调用,此时i
的值已经是5了,所以打印了5个5。而对于let
来说,由于for
循环的圆括号是一个局部作用域,每次循环时JavaScript引擎
会在后台声明一个新的j
,因此每次setTimeout
触发时都是在不同的作用域,引用的是不同的j
。为了证明是在不同作用域调用setTimeout
,且声明了不同的j
,我们可以将代码改造成这样:
for (let j = 0; j < 5; j++) {
let k = j;
setTimeout(() => console.log(k), 0);
}
// 0, 1, 2, 3, 4
上面代码输出同样的结果。这说明每次使用let
声明k
的时候都是在不同的作用域,否则根据let
不能同一作用域重复声明变量的规定,就会报错。并且,我们可以推断JavaScript引擎
会在后台记录下每次循环时j
的值,在下次进入循环体前声明新的j
,调用j++
做完判断后再进入循环体。下面的代码打印不同的结果。
for (let j = 0; j < 5;) {
setTimeout(() => console.log(j), 0);
j++;
}
// 1, 2, 3, 4, 5
这说明圆括号里面的j++
和上一次循环体是属于不同的作用域的。
const
和let
唯一的区别在于const
必须在声明的同时初始化,否则会报错。因为const
只能赋值一次。所以上面的代码如果改成const
会报错(j++
会改变j
的值)。
由于let
和const
存在块级作用域,因此下面的做法是无意义的。
if (typeof num === undefined) {
let num; // 本意是想确保 num 被声明后再赋值,但是由于块级作用域的存在使得 num 是一个局部变量
}
num = 3; // 报错
2 数据类型
JavaScript
的数据类型分为基本数据类型
和复杂数据类型
。基本数据类型
共有6种,分别是Number
、String
、Boolean
、Null
、Undefined
和Symbol
(ES6新增)。复杂数据类型
则指的对象(Object
)。
2.1 基本数据类型
1、Null
和Undefined
null
表示空指针,undefined
表示未定义。在ES3
时只有null
而没有undefined
,undefined
保存在window
对象中。
null == undefined; // true
typeof null === "object"; // true
typeof undefined === "undefined"; // true
所有声明但未赋值的变量都有一个默认值为undefined
,因此不必特意赋值为undefined
。此外,由于这一点也无法使用typeof
来判断一个变量是否被声明。
typeof num === "undefined"; // num 未声明,typeof也返回 undefined
因此,应该尽可能保证在赋值的时候初始化,这样可以使用typeof
判断变量是否被声明。初始化时,首先使用const
,如果确认值需要改变再使用let
,这样有利于纠错,防止不知不觉间改变了变量的值。如果确认变量的类型是对象,应该将初始值赋值为null
。
2、Boolean
Boolean
只有两个值:true
和false
。在一些判断语句中,会将表达式自动转化成Boolean
类型。比如if
语句。除了下面这些转化为false
,其余都为true
。
- null
- undefined
- false
- 0
- 空字符串
- NaN
也可以使用Boolean
强行转化其他类型的值:
Boolean('123'); // true
Boolean(NaN); // false
3、Number
类型
Number
常见的进制是十进制、二进制、八进制和十六进制。默认都是十进制,如果有特定前缀则可能为其余进制,如下:
123 // 十进制
0b0101 // 二进制
0o0037 // 八进制
0x0012 // 十六进制
这里面需要注意的是八进制,在浏览器中,如果开头为0
,分为两种情况:
- 如果后面的数字中有大于7的数,则表示十进制
- 如果后面的数字中没有大于7的数,则为八进制
如下:
034 // 八进制,在 chrome 控制台显示为十进制 28
078 // 十进制 78
注:上述写法只对整数有效,如果是浮点数则会报错。如 00.89会报错。
为了使得表达更明确,我们应该始终避免用047
这样的表达方式,而是使用0o47
。
还有一点需要注意的是,为了节省内存,JavaScript
会尽可能的省略掉无意义的0
。如下:
00000089 // 显示为 89,前面的 0 被省略
89.000 // 显示为 89, 后面的 0 被省略,内部当做整数保存
89.01000 // 显示为 89.01
JavaScript
内部所有数值都使用浮点数保存。在进行运算的时候再尝试转为整型。由于JavaScript
使用IEEE754
浮点数标准,因此会有精度问题:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
JavaScript
保存的数值的范围在Number.MIN_VALUE
和Number.MAX_VALUE
之间。如果超出Number.MAX_VALUE
则显示为Infinity
,最小值精度超出Number.MIN_VALUE
则显示为0。
Number.MIN_VALUE // 5e-324
5e-325 // 0
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_VALUE * 2 // Infinity
-Number.MAX_VALUE * 2 // -Infinity
JavaScript
最高精度为17位,超出部分会四舍五入。
0.12345678901234568 // 0.12345678901234568
0.123456789012345688 // 0.12345678901234569
0.123456789012345684 // 0.12345678901234568
0.123456789012345685 // 0.12345678901234569
1.12345678901234568 // 1.1234567890123457
123456789012345678 // 123456789012345680
我们可以使用Number()
来强行将其他类型的值转化为Number
类型。规则如下:
- Number类型的直接返回拷贝值(无意义的0会被忽略)
- Boolean类型的,true返回1,false返回0
- null返回0
- undefined返回NaN
- 字符串类型的,如果是空字符串则返回0,如果是符合数值格式的,则返回对应的十进制值,并忽略掉无意义的0(开头可以有正负号)。其余情况返回 NaN。
Number(23); // 23
Number(000023); // 19 八进制
Number(00099); // 99
Number(00098.8900); // 98.89
Number(true); // 1
Number(false); // 0
Number('0x0012'); // 18
Number('0x0012x'); // NaN
Number(''); // 0
- 如果是对象,则先调用对象的
valueOf()
方法,对返回值进行转化,如果无法转化为数值,再调用toString()
方法,对返回值进行转化。
parseInt
和parseFloat
用于将字符串转化为数值。它们按照一个字符一个字符进行判断。并且会忽略掉无意义的0.
parseInt
接受两个参数,第一个为需要转化的字符串,第二个为进制。默认为十进制。规则如下:
- 如果第一个字符为非数字且非正号、非负号,则直接返回NaN
- 空字符串返回NaN
- 如果第一个字符为数字或正号、负号,则依次判断之后的字符是否为数字,直到碰到第一个非数字字符或者碰到字符串结尾,结束判断并将结果返回
- 如果字符串符合进制,如
0x12
则当做对应进制转化为十进制。可以传第二个参数显式的指定进制。如果第一个参数不符合第二个参数指定的进制,则返回NaN
parseInt('a'); // NaN
parseInt('-9'); // -9
parseInt('+34'); // 34
parseInt('0098'); // 98
parseInt('23abc'); // 23
parseInt('0x0011'); // 17
parseInt('a', 16); // 10
parseFloat
只存在十进制,和parseInt
一样,依次判断每个字符。第一个字符可以为数字,正负号和小数点。第二个字符开始只能为数字和小数点,并且小数点只能出现一次。
parseFloat('8.3float'); // 8.3
parseFloat('.4'); // 0.4
parseFloat('0.44.5'); // 0.44
parseFloat('10.00'); // 10
parseFloat('00234.0'); // 234
这里有一道经典面试题
["1", "2", "3"].map(parseInt); // [1, NaN, NaN]
["1", "2", "3"].map(parseFloat); // [1, 2, 3]
这块考察3个知识点,一个是map
的传参,另两个是关于parseInt
和parseFloat
。首先,map
需要传入一个回调函数,这个函数可以接收三个参数,第一个参数是数组每一项的值,第二个参数为数组每项下标,第三个参数是数组本身。因此上面两个题可以简化为:
parseInt("1", 0); // 1
parseInt("2", 1); // NaN
parseInt("3", 2); // NaN
parseFloat("1"); // 1
parseFloat("2"); // 2
parseFloat("3"); // 3
4、String
类型
在JavaScript中,可以有三种方式表示一个字符串。分别是"
、'
、`。几种表达方式没有太大区别。
'abc' === "abc"; // true
'abc' === `abc`; // true
"abc" === `abc`; // true
这几种引号可以同时使用。
"'abc'"; // 'abc'
'"abc"'; // "abc"
`'abc'`; // 'abc'
`"abc"`; // "abc"
但是以什么引号开头必须以相同类型引号结尾。
"abc'; // 报错
"abc`; // 报错
如果在引号里面需要使用同一类型的引号需要使用\
转义。
"\"abc"; // "abc
'\'abc'; // 'abc
`\`abc`; // `abc
将其余类型转化为字符型主要有三种方法,分别是toString
、String
和使用+
号。
// 除了 null 和 undefined 以外都可以用 toString,toString方法是从 Object 上继承过来的
/*
* 1. 字符类型使用 toString 直接返回自身拷贝
* 2. 数值、布尔值使用 toString 则先自动转为基本包装类型,再调用 toString
* 3. 对象调用 toString 时,如果没有重写 toString 方法,则普通对象返回 [object Object],数组则将每个元素使用toString方法转成字符串,再使用 , 连接成最后的字符串。其余类型各自使用自己内部重写的 toString 方法。
*/
// 使用 String() 转字符串时,规则如下
/*
* 1. 如果是 null 或者 undefined,返回 'null' 或 'undefined'
* 2. 其余类型调用 toString 方法
*/
// 使用 + 号
/*
* 只要加号两边有一个是字符串,则会对另一个使用 String() 转成字符串,再进行拼接
*/
注:toString()里面还可以传入一个数字,用来表示进制。可以使用这个方法将数字转化为特定的进制。如果不传则默认转为十进制。
2.2 模板字符串
模板字符串可以插入变量
let str = '你好';
console.log(`${str}, 世界!`); // 你好, 世界!
模板字符串会保留空格和换行符
`<table>
<thead>
<tr>
<td>单元格</td>
</tr>
</thead>
</table>`
// 输出
/*
<table>
<thead>
<tr>
<td>单元格</td>
</tr>
</thead>
</table>
*/
字符串里面的\n
之类的转义字符如果需要原样打印,除了可以使用/
再次转义外还可以使用String.raw
`first line\nlast line`;
// 输出
/*
first line
last line
*/
String.raw`first line\nlast line`;
//输出
// first line\nlast line
模板字符串还可以使用标签函数。
function fn(strings, ...args) {
console.log(strings, args);
}
let str1 = "str1",
str2 = "str2",
str3 = "str3";
fn`字符串1${str1}字符串2${str2}字符串3${str3}`;
// ["字符串1", "字符串2", "字符串3", "", raw: Array(4)] ["str1", "str2", "str3"]