1、简介
1)引擎
-
JavaScript 不仅可以在浏览器中执行,也可以在服务端执行,甚至可以在任意搭载了 JavaScript 引擎 的设备中执行。
-
不同的引擎有不同的“代号”,V8 指 Chrome、Opera 和 Edge 中的 JavaScript 引擎。
2)限制性
-
为了用户信息安全,在浏览器中 JavaScript 的能力是受限的。目的是防止有人恶意利用网页获取用户私人信息或损害用户数据。
-
网页中的 JavaScript 不能读、写、复制和执行硬盘上的任意文件。它没有直接访问操作系统的功能。
2、基础
1)严格模式
- 当
"use strict"
处于脚本文件的顶部时,整个脚本文件都将以“严格模式”进行工作。
(1)限制
- 未声明的变量在赋值时会报错。
2)类型转换
(1)字符串转换
-
String(value)
可以将value
转换为字符串类型。let value = true; alert(typeof value); // boolean value = String(value); // 现在,值是一个字符串形式的 "true" alert(typeof value); // string
(2)数字型转换
-
在算术函数和表达式中,会自动进行 number 类型转换。比如,当把除法
/
用于非 number 类型:alert( "6" / "2" ); // 3, string 类型的值被自动转换成 number 类型后进行计算
-
number 类型转换规则:
值 变成…… undefined
NaN
null
0
true 和 false
1
and0
string
“按原样读取”字符串,两端的空白会被忽略。空字符串变成 0
。转换出错则输出NaN
。 -
例子
alert( Number(" 123 ") ); // 123 alert( Number("123z") ); // NaN(从字符串“读取”数字,读到 "z" 时出现错误) alert( Number(true) ); // 1
3)基础运算符
(1)运算符 + 连接字符串
-
如果加号
+
被应用于字符串,它将合并(连接)各个字符串。只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。alert( '1' + 2 ); // "12" alert( 2 + '1' ); // "21" alert(2 + 2 + '1' ); // "41",不是 "221"
(2)运算符 + 数字转化
-
加号
+
应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号+
则会将其转化为数字。// 对数字无效 let x = 1; alert( +x ); // 1 // 转化非数字 alert( +true ); // 1 alert( +"" ); // 0
-
效果和
Number(...)
相同,但是更加简短。let apples = "2"; let oranges = "3"; // 在二元运算符加号起作用之前,所有的值都被转化为了数字 alert( +apples + +oranges ); // 5 // 更长的写法 // alert( Number(apples) + Number(oranges) ); // 5
(3)运算符 ?比较
-
使用一系列问号
?
运算符可以返回一个取决于多个条件的值。let message = (age < 3) ? 'Hi, baby!' : (age < 18) ? 'Hello!' : (age < 100) ? 'Greetings!' : 'What an unusual age!'; --- 等价于 if (age < 3) { message = 'Hi, baby!'; } else if (age < 18) { message = 'Hello!'; } else if (age < 100) { message = 'Greetings!'; } else { message = 'What an unusual age!'; }
(4)运算符 &&
-
运算符 " && " 的优先级高于 " | | "
alert( null || 2 && 3 || 4 ); // 3
4)空值合并运算符 ??
(1)定义
-
a ?? b
的结果是:如果第一个参数不是null/undefined
,则返回第一个参数。否则返回第二个参数。 -
应用场景:提供默认值
let user; alert(user ?? "匿名"); // 匿名(user 未定义)
(2)与 || 比较
||
返回第一个 真 值,??
返回第一个 已定义的 值。- 换句话说,
||
无法区分false
、0
、空字符串""
和null/undefined
。
(3)注意
??
运算符的优先级非常低,仅略高于?
和=
,因此在表达式中使用它时请考虑添加括号。- 如果没有明确添加括号,不能将其与
||
或&&
一起使用。
5)值的比较
(1)类型间比较
-
当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。
alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2 alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1 alert( true == 1 ); // true alert( false == 0 ); // true
(2)严格相等
-
严格相等运算符
===
在进行比较时不会做任何的类型转换。 -
在非严格相等
==
下,null
和undefined
相等且各自不等于任何其他的值。
6)循环的结束
(1)单层
-
break
指令被激活,会立刻终止循环,将控制权传递给循环后的第一行。 -
continue
指令是break
的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。
(2)多层
-
break <labelName>
语句跳出循环至标签处,可以结束多层循环。outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { let input = prompt(`Value at coords (${i},${j})`, ''); // 如果是空字符串或被取消,则中断并跳出这两个循环。 if (!input) break outer; ... } } alert('Done!');
-
标签不允许我们跳到代码的任意位置。
break label; // 跳转至下面的 label 处(无效) label: for (...)
-
continue
指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。
3、数据类型
1)数字
(1)书写形式
-
可以使用下划线
_
作为分隔符:let billion = 1_000_000_000;
-
可以通过在数字后面附加字母
"e"
并指定零的个数来缩短数字:let billion = 1e9; // 10 亿,字面意思:数字 1 后面跟 9 个 0 1.23e6 === 1.23 * 1000000; // e6 表示 *1000000 let mcs = 1e-6; // 1 的左边有 6 个 0 1.23e-6 === 1.23 / 1000000; // 0.00000123
(2)进制形式
-
方法
num.toString(base)
返回在给定base
进制数字系统中num
的字符串表示形式。base
的范围可以从2
到36
。默认情况下是10
。let num = 255; alert( num.toString(16) ); // ff alert( num.toString(2) ); // 11111111 // JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。 alert( 123456..toString(36) ); // 2n9c (123456).toString(36);
(3)舍入函数
-
Math.floor
向下舍入:
3.1
变成3
,-1.1
变成-2
。 -
Math.ceil
向上舍入:
3.1
变成4
,-1.1
变成-1
。 -
Math.round
四舍五入:
3.1
变成3
,3.6
变成4
,中间值3.5
变成4
。 -
Math.trunc
移除小数点后的所有内容而不舍入:
3.1
变成3
,-1.1
变成-1
。 -
函数 toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果。
let num = 12.34; alert( num.toFixed(1) ); // "12.3" let num = 12.34; alert( num.toFixed(5) ); // "12.34000",在结尾添加了 0,以达到小数点后五位
(4)不精确计算
-
一个数字真的很大,则可能会溢出 64 位存储,变成一个特殊的数值
Infinity
:alert( 1e500 ); // Infinity
-
经常会发生的精度损失。相等性测试:
alert( 0.1 + 0.2 == 0.3 ); // false
(5)符号函数
-
parseInt
和parseFloat
可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。alert( parseInt('100px') ); // 100 alert( parseFloat('12.5em') ); // 12.5 alert( parseInt('12.3') ); // 12,只有整数部分被返回了 alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取
-
某些情况下,
parseInt/parseFloat
会返回NaN
。当没有数字可读时会发生这种情况:alert( parseInt('a123') ); // NaN,第一个符号停止了读取
-
parseInt()
函数具有可选的第二个参数。它指定了数字系统的基数,因此parseInt
还可以解析十六进制数字、二进制数字等的字符串:alert( parseInt('0xff', 16) ); // 255 alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效 alert( parseInt('2n9c', 36) ); // 123456
(6)特殊函数
isNaN(value)
将其参数转换为数字,然后测试它是否为NaN
:
alert( isNaN(NaN) ); // true
alert( isNaN("str") ); // true
-
值 “NaN” 是独一无二的,它不等于任何东西,包括它自身:
alert( NaN === NaN ); // false
-
isFinite(value)
将其参数转换为数字,如果是常规数字而不是NaN/Infinity/-Infinity
,则返回true
:alert( isFinite("15") ); // true alert( isFinite("str") ); // false,因为是一个特殊的值:NaN alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity
2)字符串
(1)查找子字符串
-
str.indexOf (): 从给定位置
pos
开始,在str
中查找substr
,没找到则返回-1
,否则返回匹配成功的位置。let str = 'Widget with id'; alert( str.indexOf('Widget') ); // 0,因为 'Widget' 一开始就被找到 alert( str.indexOf('Dget') ); // -1,没有找到,检索是大小写敏感的 alert( str.indexOf("id") ); // 1,"id" 在位置 1 处(……idget 和 id) alert( str.indexOf('id', 2) ) // 12
-
str.includes( substr , pos ): 根据
str
中是否包含substr
来返回true/false
。alert( "Midget".includes("id") ); // true alert( "Midget".includes("id", 3) ); // false, 从位置 3 开始没有 "id"
-
str.startsWith | endsWith:查找首尾。
alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始 alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束
(2)获取子字符串
-
str.slice(start [, end])
返回字符串从
start
到(但不包括)end
的部分。let str = "stringify"; alert( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5) alert( str.slice(0, 1) ); // 's',从 0 到 1,但不包括 1,所以只有在 0 处的字符
如果没有第二个参数,
slice
会一直运行到字符串末尾:let str = "stringify"; alert( str.slice(2) ); // 从第二个位置直到结束
start/end
也有可能是负值。它们的意思是起始位置从字符串结尾计算:let str = "stringify"; // 从右边的第四个位置开始,在右边的第一个位置结束 alert( str.slice(-4, -1) ); // 'gif'
-
str.substr(start [, length])
返回字符串从
start
开始的给定length
的部分。let str = "stringify"; alert( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符
第一个参数可能是负数,从结尾算起:
let str = "stringify"; alert( str.substr(-4, 2) ); // 'gi',从第 4 位获取 2 个字符
3)数组
(1)定义
-
数组可以存储任何类型的元素。
// 混合值 let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; // 获取索引为 1 的对象然后显示它的 name alert( arr[1].name ); // John // 获取索引为 3 的函数并执行 arr[3](); // hello
(2)执行机制
-
数组是一种特殊的对象。使用方括号来访问属性
arr[0]
实际上是来自于对象的语法。它其实与
obj[key]
相同,其中arr
是对象,而数字用作键(key) -
通过引用进行复制, 两个变量引用的是相同的数组 。
let fruits = ["Banana"] let arr = fruits; // 通过引用复制 (两个变量引用的是相同的数组) alert( arr === fruits ); // true arr.push("Pear"); // 通过引用修改数组 alert( fruits ); // Banana, Pear — 现在有 2 项了
-
数组真正特殊的是它们的内部实现。JavaScript 引擎尝试把这些元素一个接一个地存储在连续的内存区域,而且还有一些其它的优化,以使数组运行得非常快。
-
数组是基于对象的。我们可以给它们添加任何属性。但是 Javascript 引擎会发现,我们在像使用常规对象一样使用数组,那么针对数组的优化就不再适用了,然后对应的优化就会被关闭。
/* 数组误用的几种方式: 添加一个非数字的属性,比如 arr.test = 5。 制造空洞,比如:添加 arr[0],然后添加 arr[1000] (它们中间什么都没有)。 以倒序填充数组,比如 arr[1000],arr[999] 等等。 */ let fruits = []; // 创建一个数组 fruits[99999] = 5; // 分配索引远大于数组长度的属性 fruits.age = 25; // 创建一个具有任意名称的属性
(3)获取元素
-
arr.at(i)
返回数组元素,参数可以为负数。
let fruits = ["Apple", "Orange", "Plum"]; alert( fruits.at(-1) ); // Plum
(4)数组循环
-
**
for..in
循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。 ** -
for..of
不能获取当前元素的索引,只是获取元素值,但大多数情况是够用的。
let fruits = ["Apple", "Orange", "Plum"];
// 遍历数组元素
for (let fruit of fruits) {
alert( fruit );
}
(5)Length 属性
- **
length
属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。 **
let fruits = [];
fruits[123] = "Apple";
alert( fruits.length ); // 124
-
length
属性是可写的。手动增加不会发生任何事。但是减少它,数组就会被截断。该过程是不可逆的。let arr = [1, 2, 3, 4, 5]; arr.length = 2; // 截断到只剩 2 个元素 alert( arr ); // [1, 2] arr.length = 5; // 又把 length 加回来 alert( arr[3] ); // undefined:被截断的那些数值并没有回来
4、数组方法
1)修改:splice
-
arr.splice(start[, deleteCount, elem1, ..., elemN])
arr.splice 可以添加,删除和插入元素。
它从索引
start
开始修改arr
,删除deleteCount
个元素并在当前位置插入elem1, ..., elemN
。最后返回已被删除元素的数组。let arr = ["I", "study", "JavaScript", "right", "now"]; // 删除数组的前三项,并使用其他内容代替它们 arr.splice(0, 3, "Let's", "dance");
alert( arr ) // 现在 [“Let’s”, “dance”, “right”, “now”]
- 负向索引都是被允许的。
```javascript
let arr = [1, 2, 5];
// 从索引 -1(尾端前一位) 删除 0 个元素,然后插入 3 和 4
arr.splice(-1, 0, 3, 4);
alert( arr ); // 1,2,3,4,5
2)子数组副本:slice
-
arr.slice()
会创建一个arr
的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。arr.slice([start], [end])
-
arr.slice 返回一个新数组,将所有从索引
start
到end
(不包括end
)的数组项复制到一个新的数组。start
和end
都可以是负数,在这种情况下,从末尾计算索引。let arr = ["t", "e", "s", "t"]; alert( arr.slice(1, 3) ); // e,s(复制从位置 1 到位置 3 的元素) alert( arr.slice(-2) ); // s,t(复制从位置 -2 到尾端的元素)
3)合并:concat
-
arr.concat 创建一个新数组,其中包含来自于其他数组和其他项的值。
let arr = [1, 2]; alert( arr.concat([3, 4]) ); // 1,2,3,4 alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
4)遍历:foreach
-
arr.forEach 方法允许为数组的每个元素都运行一个函数。
arr.forEach(function(item, index, array) { // ... do something with item }); ["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { alert(`${item} is at index ${index} in ${array}`); });
5)查找特定对象:find
-
可以使用
arr.find
方法 查找对象数组中的特定对象let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; let user = users.find(item => item.id == 1); alert(user.name); // John
6)结果:map
-
arr.map 方法是最有用和经常使用的方法之一。它对数组的每个元素都调用函数,并返回结果数组。
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6
7)辨别:Array.isArray
-
数组是基于对象的,不构成单独的语言类型。所以
typeof
不能帮助从数组中区分出普通对象:alert(typeof {}); // object alert(typeof []); // object
-
如果
value
是一个数组,则返回true
;否则返回false
。alert(Array.isArray({a:b})); // false alert(Array.isArray([a,b])); // true
5、对象
1)属性
(0)访问属性
- 点符号:
obj.property
。 - 方括号
obj["property"]
,方括号允许从变量中获取键,例如obj[varWithKey]
。
(1)多词属性
- 多字词语来作为属性名,但必须给它们加上引号:
let user = {
name: "John",
age: 30,
"likes birds": true // 多词属性名必须加引号
};
// 这将提示有语法错误
user.likes birds = true
// 设置
user["likes birds"] = true;
let key = "likes birds";
// 跟 user["likes birds"] = true; 一样
user[key] = true;
(2)移除属性
- 可以用
delete
操作符移除属性:
let user = { // 一个对象
name: "John", // 键 "name",值 "John"
age: 30 // 键 "age",值 30
};
delete user.age;
(3)计算属性
-
当创建一个对象时,我们可以在对象字面量中使用方括号。
let fruit = prompt("Which fruit to buy?", "apple"); let bag = { [fruit]: 5, // 属性名是从 fruit 变量中得到的 }; alert( bag.apple ); // 5 如果 fruit="apple" ---本质上,这跟下面的语法效果相同: let fruit = prompt("Which fruit to buy?", "apple"); let bag = {}; // 从 fruit 变量中获取值 bag[fruit] = 5;
(4)遍历属性
-
遍历一个对象的所有键(key),可以使用一个特殊形式的循环:
for..in
let user = {
name: “John”,
age: 30,
isAdmin: true
};
for (let key in user) {
// keys
alert( key ); // name, age, isAdmin
// 属性键的值
alert( user[key] ); // John, 30, true
}
#### (5)属性存在性
- JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错! 读取不存在的属性只会得到 `undefined`。
```js
let user = { name: "John", age: 30 };
alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。
(6)属性排序
-
对象有 “特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示 。
let codes = { "49": "Germany", "41": "Switzerland", "44": "Great Britain", // .., "1": "USA" }; for(let code in codes) { alert(code); // 1, 41, 44, 49 }
2)引用
(1)定义
-
当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。
let user = { name: 'John' }; let admin = user; admin.name = 'Pete'; // 通过 "admin" 引用来修改 alert(user.name); // 'Pete',修改能通过 "user" 引用看到
(2)引用比较
- 仅当两个对象为同一对象时,两者才相等。
let a = {};
let b = a; // 复制引用
alert( a === b ); // true,都引用同一对象
- 而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):
let a = {};
let b = {}; // 两个独立的对象
alert( a == b ); // false
(3)合并与浅拷贝
-
对象合并可以使用
Object.assign(dest, [src1, src2, src3...])
- 第一个参数
dest
是指目标对象。 - 更后面的参数
src1, ..., srcN
(可按需传递多个参数)是源对象。 - 该方法将所有源对象的属性拷贝到目标对象
dest
中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
let user = { name: "John" }; let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; // 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中 Object.assign(user, permissions1, permissions2); // 现在 user = { name: "John", canView: true, canEdit: true }
- 第一个参数
-
如果被拷贝的属性的属性名已经存在,那么它会被覆盖:
let user = { name: "John" }; Object.assign(user, { name: "Pete" }); alert(user.name); // 现在 user = { name: "Pete" }
-
可以用
Object.assign
代替for..in
循环来进行浅拷贝:let user = { name: "John", age: 30 }; // 它将 `user` 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。 let clone = Object.assign({}, user);
(4)深拷贝
-
使用一个拷贝循环来检查
user[key]
的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。 -
为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)。
3)垃圾回收
(1)可达性
-
JavaScript 中主要的内存管理概念是可达性。简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。
-
全局变量和当前嵌套调用链上的局部变量不能被释放,这些值被称为根。
-
**如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。 如对象A的一个属性可以访问对象B,则对象A被认为是可达的, 而且它引用的内容也是可达的。 **
4)可选连 ?.
(1)定义
- 可选链
?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
let user = {}; // user 没有 address 属性
alert( user?.address?.street ); // undefined(不报错)
let html = document.querySelector('.elem')?.innerHTML; // 如果没有符合的元素,则为 undefined
(2)形式
obj?.prop
—— 如果obj
存在则返回obj.prop
,否则返回undefined
。obj?.[prop]
—— 如果obj
存在则返回obj[prop]
,否则返回undefined
。obj.method?.()
—— 如果obj.method
存在则调用obj.method()
,否则返回undefined
。
5)Symbol 类型
(1)定义
- symbol 保证是唯一的。即使我们创建了许多具有相同描述的 symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
- symbol 不会被自动转换为字符串 , 想显示一个 symbol,我们需要在它上面调用
.toString()
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
-
在对象字面量
{...}
中使用 symbol,需要方括号把它括起来。因为需要变量id
的值作为键,而不是字符串 “id”。let id = Symbol("id"); let user = { name: "John", [id]: 123 // 而不是 "id":123 };
(2)全局 symbol
-
想要名字相同的 symbol 具有相同的实体,需要 一个全局 symbol 注册表。
-
从注册表中读取(不存在则创建)symbol,使用
Symbol.for(key)
。
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 symbol
alert( id === idAgain ); // true
- 反向调用:
Symbol.keyFor(sym)
,它的作用完全反过来:通过全局 symbol 返回一个名字。 如果 symbol 不是全局的,它将无法找到它并返回undefined
。
// 通过 name 获取 symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
6、可迭代对象
1)自定义
- 一个
range
对象,它代表了一个数字区间:
let range = {
from: 1,
to: 5
};
// 我们希望 for..of 可以这样运行:
// for(let num of range) ... num=1,2,3,4,5
-
为了让
range
对象可迭代(也就让for..of
可以运行),我们需要为对象添加一个名为Symbol.iterator
的方法(一个专门用于使对象可迭代的内建 symbol)。- 当
for..of
循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有next
方法的对象。 - 当
for..of
循环希望取得下一个数值,它就调用这个对象的next()
方法。 next()
方法返回的结果的格式必须是{done: Boolean, value: any}
,当done=true
时,表示循环结束,否则value
是下一个值。
let range = { from: 1, to: 5 }; // 1. for..of 调用首先会调用这个: range[Symbol.iterator] = function() { // ……它返回迭代器对象(iterator object): // 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每一轮循环迭代中被调用 next() { // 4. 它将会返回 {done:.., value :...} 格式的对象 if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 现在它可以运行了! for (let num of range) { alert(num); // 1, 然后是 2, 3, 4, 5 }
请注意可迭代对象的核心功能:关注点分离。
range
自身没有next()
方法。- 相反,是通过调用
range[Symbol.iterator]()
创建了另一个对象,即所谓的“迭代器”对象,并且它的next
会为迭代生成值。
因此,迭代器对象和与其进行迭代的对象是分开的。
- 当
2)映射:Map
(1)定义
-
Map
允许任何类型的键(key),方法和属性如下: -
new Map()
—— 创建 map。 -
map.set(key, value)
—— 根据键存储值。 -
map.get(key)
—— 根据键来返回值,如果map
中不存在对应的key
,则返回undefined
。 -
map.has(key)
—— 如果key
存在则返回true
,否则返回false
。 -
map.delete(key)
—— 删除指定键的值。 -
map.clear()
—— 清空 map。 -
map.size
—— 返回当前元素个数。 -
map[key]
不是使用Map
的正确方式 -
虽然
map[key]
也有效,例如我们可以设置map[key] = 2
,这样会将map
视为 JavaScript 的 plain object,因此它暗含了所有相应的限制(仅支持 string/symbol 键等)。 -
应该使用
map
方法:set
和get
等。 -
链式调用:每一次
map.set
调用都会返回 map 本身,所以我们可以进行“链式”调用:map.set('1', 'str1') .set(1, 'num1') .set(true, 'bool1');
(2)迭代
map
里使用循环,可以使用以下三个方法:map.keys()
—— 遍历并返回一个包含所有键的可迭代对象,map.values()
—— 遍历并返回一个包含所有值的可迭代对象,map.entries()
—— 遍历并返回一个包含所有实体[key, value]
的可迭代对象,for..of
在默认情况下使用的就是这个。
(3)对象 转 Map
-
通过对象创建一个
Map
,可以使用内建方法Object.entries(obj)
,该方法返回对象的键/值对数组,该数组格式完全按照Map
所需的格式。let obj = { name: "John", age: 30 }; let map = new Map(Object.entries(obj)); alert( map.get('name') ); // John
(4)Map 转 对象
- 可以使用
Object.fromEntries
从Map
得到一个普通对象 。
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
let obj = Object.fromEntries(map);
// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }
3)弱映射:WeakMap
(1)定义
-
WeakMap
和Map
的第一个不同点就是,WeakMap
的键必须是对象,不能是原始值:let weakMap = new WeakMap(); let obj = {}; weakMap.set(obj, "ok"); // 正常工作(以对象作为键) // 不能使用字符串作为键 weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象
-
在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
let john = { name: "John" }; let weakMap = new WeakMap(); weakMap.set(john, "..."); john = null; // 覆盖引用 // john 被从内存中删除了!
(2)无迭代
- **
WeakMap
不支持迭代以及keys()
,values()
和entries()
方法。所以没有办法获取WeakMap
的所有键或值。原因是垃圾回收机制的时间是不确定的,当程序忙碌时,时间可能晚点。 ** WeakMap
只有以下的方法:weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
4)集合:Set
(1)定义
- 每一个值只能出现一次。它的主要方法如下:
new Set(iterable)
—— 创建一个set
,如果提供了一个iterable
对象(通常是数组),将会从数组里面复制值到set
中,并移除重复的元素。set.add(value)
—— 添加一个值,返回 set 本身。set.delete(value)
—— 删除值,如果value
在这个方法调用的时候存在则返回true
,否则返回false
。set.has(value)
—— 如果value
在 set 中,返回true
,否则返回false
。set.clear()
—— 清空 set。set.size
—— 返回元素个数。
(2)迭代
-
可以使用
for..of
或forEach
来遍历 Set:let set = new Set(["oranges", "apples", "bananas"]); for (let value of set) alert(value); // 与 forEach 相同: set.forEach((value, valueAgain, set) => { alert(value); });
(3)弱集合:WeakSet
- 与
Set
类似,但是我们只能向WeakSet
添加对象(而不能是原始值)。 - 对象只有在能被引用的时候,才能留在
WeakSet
中。 - 跟
Set
一样,WeakSet
支持add
,has
和delete
方法,但不支持size
和keys()
,并且不可迭代。
5)转换对象
-
对象缺少数组的许多方法,例如
map
和filter
等。可以使用Object.entries
,然后使用Object.fromEntries
:- 使用
Object.entries(obj)
从obj
获取由键/值对组成的数组。 - 对该数组使用数组方法,例如
map
,对这些键/值对进行转换。 - 对结果数组使用
Object.fromEntries(array)
方法,将结果转回成对象。
// 例如,我们有一个带有价格的对象,并想将它们加倍: let prices = { banana: 1, orange: 2, meat: 4, }; let doublePrices = Object.fromEntries( // 将价格转换为数组,将每个键/值对映射为另一对 // 然后通过 fromEntries 再将结果转换为对象 Object.entries(prices).map(entry => [entry[0], entry[1] * 2]) ); alert(doublePrices.meat); // 8
- 使用
7、函数
1)定义
- 函数是值。它们可以在代码的任何地方被分配,复制或声明。
- 函数总是返回一些东西。如果没有
return
语句,那么返回的结果是undefined
。
(1)函数声明
- 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。
- 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。
(2)函数表达式
- 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。
- 函数表达式在执行流程到达时创建。
(3)箭头函数
-
不带花括号:
(...args) => expression
—— 右侧是一个表达式:函数计算表达式并返回其结果。如果只有一个参数,则可以省略括号,例如n => n*2
。 -
带花括号:
(...args) => { body }
—— 花括号允许我们在函数中编写多个语句,但是我们需要显式地return
来返回一些内容。// 表达式在右侧 let sum = (a, b) => a + b; // 或带 {...} 的多行语法,此处需要 return: let sum = (a, b) => { // ... return a + b; } // 没有参数 let sayHi = () => alert("Hello"); // 有一个参数 let double = n => n * 2;
8、进阶
1)解构赋值
(1)数组解构
-
可以通过添加额外的逗号来丢弃数组中不想要的元素:
// 不需要第二个元素 let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert( title ); // Consul
-
等号左侧使用任何“可以被赋值的”东西。
let user = {}; [user.name, user.surname] = "John Smith".split(' '); alert(user.name); // John alert(user.surname); // Smith
-
收集其余参数,可以使用三个点
"..."
来再加一个参数以获取其余数组项:let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; // rest 是包含从第三项开始的其余数组项的数组 alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2
-
可以设置默认值:
// 默认值 let [name = "Guest", surname = "Anonymous"] = ["Julius"]; alert(name); // Julius(来自数组的值) alert(surname); // Anonymous
(2)对象解构
-
在最简单的情况下,等号左侧的就是
{...}
中的变量名列表。let options = { title: "Menu", width: 100, height: 200 }; let {title, width, height} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200
-
变量的顺序并不重要,下面这个代码也是等价的:
// 改变 let {...} 中元素的顺序 let {height, width, title} = { title: "Menu", height: 200, width: 100 }
-
可以使用冒号来设置变量名称:
let options = { title: "Menu", width: 100, height: 200 }; // { sourceProperty: targetVariable } let {width: w, height: h, title} = options; alert(title); // Menu alert(w); // 100 alert(h); // 200
-
收集其余参数,但形式与数组解构有所不同:
let options = { title: "Menu", height: 200, width: 100 }; let {title, ...rest} = options; // 现在 title="Menu", rest={height: 200, width: 100} alert(rest.height); // 200 alert(rest.width); // 100
(3)嵌套解构
- 如果一个对象或数组嵌套了其他的对象和数组,我们可以在等号左侧使用更复杂的模式来提取更深层的数据。
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
// 为了清晰起见,解构赋值语句被写成多行的形式
let {
size: { // 把 size 赋值到这里
width,
height
},
items: [item1, item2], // 把 items 赋值到这里
title = "Menu" // 在对象中不存在(使用默认值)
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
2)Spread 扩展语法
- **当我们在代码中看到
"..."
时,它要么是 rest 参数,要么就是 spread 语法。 ** - spread 把一个数组展开为列表。当在函数调用中使用
...arr
时,它会把可迭代对象arr
“展开”到参数列表中。
(1)定义
-
可以将 spread 语法与常规值结合使用:
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
(2)浅拷贝
- 这种方式比使用
let arrCopy = Object.assign([], arr)
来复制数组,或使用let objCopy = Object.assign({}, obj)
来复制对象写起来要短得多。
let obj = { a: 1, b: 2, c: 3 };
let objCopy = { ...obj }; // 将对象 spread 到参数列表中,然后将结果返回到一个新对象
// 两个对象中的内容相同吗?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
// 两个对象相等吗?
alert(obj === objCopy); // false (not same reference)
// 修改我们初始的对象不会修改副本:
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}
3)JSON 方法
(1) 对象转 JSON
【1】限制
-
JSON 是与语言无关的纯数据规范,因此一些特定于 JavaScript 的对象属性会被
JSON.stringify
跳过。- 函数属性(方法)。
- Symbol 类型的键和值。
- 存储
undefined
的属性。
let user = { sayHi() { // 被忽略 alert("Hello"); }, [Symbol("id")]: 123, // 被忽略 something: undefined // 被忽略 }; alert( JSON.stringify(user) ); // {}(空对象)
-
不得有循环引用。
let room = { number: 23 }; let meetup = { title: "Conference", participants: ["john", "ann"] }; meetup.place = room; // meetup 引用了 room room.occupiedBy = meetup; // room 引用了 meetup JSON.stringify(meetup); // Error: Converting circular structure to JSON
【2】排除和转换
-
JSON.stringify
的完整语法是:let json = JSON.stringify(value[, replacer, space])
-
可以为下文的
occupiedBy
以外的所有内容按原样返回value
。为了occupiedBy
,下面代码返回undefined
:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
return (key == 'occupiedBy') ? undefined : value;
}));
(2)JSON 转 对象
-
解码 JSON 字符串,需要另一个方法。
let value = JSON.parse(str, [reviver]);
-
将 reviver 函数传递给
JSON.parse
作为第二个参数,转换反序列为内建对象:
let schedule = `{
"meetups": [
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
]
}`;
schedule = JSON.parse(schedule, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( schedule.meetups[1].date.getDate() ); // 正常运行了!
4)浏览器调试
(1)断点查看
-
设置断点之后,打开控制台右侧的信息下拉列表。这里允许你查看当前的代码状态:
-
察看(Watch)
—— 显示任意表达式的当前值。你可以点击加号
+
然后输入一个表达式。调试器将显示它的值,并在执行过程中自动重新计算该表达式。 -
调用栈(Call Stack)
—— 显示嵌套的调用链。此时,调试器正在
hello()
的调用链中,被index.html
中的一个脚本调用(这里没有函数,因此显示 “anonymous”)如果你点击了一个堆栈项,调试器将跳到对应的代码处,并且还可以查看其所有变量。
-
作用域(Scope)
—— 显示当前的变量。Local
显示当前函数中的变量,你还可以在源代码中看到它们的值高亮显示了出来。Global
显示全局变量(不在任何函数中)。
-
(2)跟踪执行
- 继续执行,快捷键 F8**。**
- 运行下一条指令,快捷键 F9**。**