基础

1 数据类型

1.1 基本数据类型

1.1.1 number,数字类型

1.1.2 string,字符串类型

1.1.3 boolean,布尔类型

布尔值会被转换为数字,true 为 1,false 为 0

1.1.4 undefined.

undefined 是一个特殊的值,表示变量未被赋值或未定义

1.1.5 null

null 是一个特殊的值,表示“无”或“空”,通常用于表示变量未指向任何对象

1.1.7 Symbol

symbol 是 ES6 引入的一种新的原始数据类型,表示独一无二的值

number, string, boolean, null, undefined, symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 基本数据类型 numbser, string, boolean, null, undefined, symbol

let a = 1; // number,数字类型
// console.log(a + 1); // 数字相加,结果为 2
// console.log(a + "1"); // 字符串和数字相加,数字会被转换为字符串
// console.log(a + "hello"); // 字符串和数字相加,数字会被转换为字符串
let b = "hello"; // string,字符串类型
// console.log(a + b); // 字符串和数字相加,数字会被转换为字符串
// console.log(a + "1"); // 字符串和数字相加,数字会被转换为字符串
let c = true; // boolean // 布尔类型
// console.log(a + c); // 布尔值会被转换为数字,true 为 1,false 为 0
// console.log(a + "true"); // 字符串和布尔值相加,布尔值会被转换为字符串
// console.log(a + "false"); // 字符串和布尔值相加,布尔值会被转换为字符串
let d = null; // null // null 是一个特殊的值,表示“无”或“空”,通常用于表示变量未指向任何对象
// console.log(a + d); // null 会被转换为 0,数字和 null 相加,null 会被转换为 0
// console.log(a + "null"); // 字符串和 null 相加,null 会被转换为字符串 "null"
let e = undefined; // undefined undefined 是一个特殊的值,表示变量未被赋值或未定义
let test;

console.log(test); // 输出 undefined,因为 test 没有被赋值
// console.log(a + e); // undefined 会被转换为 NaN,数字和 undefined 相加,undefined 会被转换为 NaN
// console.log(a + "undefined"); // 字符串和 undefined 相加,undefined 会被转换为字符串 "undefined"
let f = Symbol("symbol"); // symbol symbol 是 ES6 引入的一种新的原始数据类型,表示独一无二的值
// console.log(a + f); // Symbol 不能与其他类型相加,会抛出错误
// console.log(a + "symbol"); // 字符串和 symbol 相加,symbol 会被转换为字符串 "Symbol(symbol)"

console.log(a, b, c, d, e, f);

console.log(a);
console.log(b);
console.log(c);
console.log(d);
console.log(e);
console.log(f);

let my_name = "我";
let my_age = 18;
console.log(my_name + my_age); // 字符串和数字相加,数字会被转换为字符串

变量预算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let a = 100;
let b = a;
b = 200;
console.log(a); // 输出 100,因为 b 的修改不会影响 a 的值

let a = 100;
a = a + 20;
console.log(a); // 输出 120,因为 a 的值被修改了

let my_name = "王刚";
full_name = my_name + "同学";
console.log(full_name); // 输出 "王刚同学",字符串可以与其他字符串相加

let my_age = 18;
my_age = my_age + 2;
let my_old_age = my_age;
console.log(my_old_age); // 输出 20,my_old_age 是 my_age 的值的副本

数据类型

1
2
3
4
5
6
7
8
9
10
typeof my_name; // 输出 "string",my_name 是一个字符串类型
typeof a; // 输出 "number",a 是一个数字类型

typeof 1; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof null; // "object" (这是一个历史遗留问题,null 实际上是一个基本数据类型)

console.log(typeof my_name); // 输出 "string"
console.log(typeof a); // 输出 "number"

1.2 复合数据类型

1.3 初识对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// object 对象
// key value 键值对
// 对象的属性可以通过点操作符或方括号操作符访问

user_name = "王刚";
user_age = 18;
user_height = 1.75;
user_hobby = "编程, 阅读, 运动";

let user_object = {
user_name: "王刚",
user_age: 18,
user_height: 1.75,
user_hobby: "编程, 阅读, 运动",
};
console.log(user_object.user_name); // 输出 "王刚"
console.log(user_object.user_age); // 输出 18
console.log(user_object.user_height); // 输出 1.75
console.log(user_object.user_hobby); // 输出 "编程, 阅读, 运动"
console.log(user_object); // 输出整个对象

// 对象的属性可以通过点操作符或方括号操作符访问
console.log(user_object["user_name"]); // 输出 "王刚"

// 对象的属性可以通过点操作符或方括号操作符访问
console.log(user_object["user_name"]); // 输出 "王刚"

user_object.user_name = "李四"; // 修改对象的属性
console.log(user_object.user_name); // 输出 "李四"

user_object["user_age"] = 20; // 修改对象的属性
console.log(user_object["user_age"]); // 输出 20

user_object.parent = "mother"; // 添加新的属性
console.log(user_object.parent); // 输出 "mother"

console.log(user_object); // 输出整个对象,包含新添加的属性

user_object.new_user = {
user_name: "张三",
user_age: 22,
user_height: 1.80,
user_hobby: "旅行, 摄影",
};
console.log(user_object); // 输出整个对象,包含新添加的对象属性

// 链式访问对象属性
console.log(user_object.new_user); // 输出新添加的对象属性
console.log(user_object.new_user.user_name); // 输出 "张三"
console.log(user_object.new_ user.user_age); // 输出 22
console.log(user_object.new_user.user_height); // 输出 1.80
console.log(user_object.new_user.user_hobby); // 输出 "旅行, 摄影"

console.log(user_object.abc); // 输出 undefined,因为 user_object 中没有 abc 属性

// 检查对象是否包含某个属性
console.log("user_name" in user_object); // 输出 true
console.log("user_address" in user_object); // 输出 false

// 删除对象的属性
delete user_object.user_hobby;
console.log(user_object.user_hobby); // 输出 undefined,因为 user_hobby 属性已被删除

// 对象的属性可以是任意类型,包括函数
user_object.greet = function () {
console.log("你好,我是 " + this.user_name);
};

复制对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 定义一个 baby 对象,包含姓名、年龄和父母信息
let baby = {
name: "Baby", // 姓名
age: 3, // 年龄
parent: {
// 父母信息
mom: "Mom", // 妈妈
dad: "Dad", // 爸爸
},
};

// 输出 baby 的姓名
console.log(baby.name); // Baby
// 输出 baby 的年龄
console.log(baby.age); // 3
// 输出 baby 的妈妈姓名
console.log(baby.parent.mom);

// 定义一个 babyfake 对象,复制了 baby 的部分属性
let babyfake = {
name: baby.name, // 复制姓名属性
age: baby.age, // 复制年龄属性
parent: baby.parent, // 复制父母信息
};


// 输出 babyfake 的姓名
console.log(babyfake.name); // Baby
// 输出 babyfake 的年龄
console.log(babyfake.age); // 3
// 输出 babyfake 的妈妈姓名
console.log(babyfake.parent); // { mom: 'Mom', dad: 'Dad' }
// 输出 babyfake 的爸爸姓名
console.log(babyfake.parent.dad); // Da

复制后修改原对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
let a = 100;
let b = a;
console.log("a", a, "b", b); // a 100 b 100
b = 200;
console.log("a", a, "b", b); // a 100 b 200

let little_boy = {
name: "小明",
gender: "男",
age: 18
};
// 复制对象
let big_boy = little_boy;
// 复制后,big_boy和little_boy指向同一个对象
console.log("littly_bogy的name", little_boy.name)
console.log("big_boy的name",big_boy.name) // littly_bogy的name 小明

little_boy.name = "小红"; // 修改了little_boy的name属性
// 由于big_boy和little_boy指向同一个对象,所以big_boy的name属性也被修改了
console.log("littly_bogy的name", little_boy.name) // littly_bogy的name 小红
console.log("big_boy的name",big_boy.name) // littly_bogy的name 小红

let c = {
name: "小明",
age: 18,
}
let d = c; // 复制对象

c = null; // 将c设置为null
// 此时d仍然指向原来的对象
console.log(d.name); // 小明
console.log(d.age); // 18
console.log(c); // null

// 任意声明一个变量,然后创建一个兑现和a中的对象数据一样,赋值给这个变量
// 但是修改这个变量中的对象数据不会影响到a中的对象数据
// 这可以用于创建一个独立的对象副本,避免对原始对象的修改影响到副本

// 定义用户对象a
let a = {
user_name: "JohnDoe", // 用户名
user_age: 30, // 用户年龄
chile:{ // 子对象
child_name: "JaneDoe", // 孩子姓名
child_age: 5 // 孩子年龄
}
}
console.log( a.user_name );

// 定义用户对象b,基于对象a的属性
let b = {
user_name:a.user_name, // 从a复制用户名
user_age:a.user_age, // 从a复制用户年龄
child:{ // 子对象
child_name:a.chile.child_name, // 从a复制孩子姓名
child_age:a.chile.child_age // 从a复制孩子年龄
}
}
console.log( b.user_name );
// 覆盖对象b中的user_name属性
b.user_name = "xxx";
console.log( b.user_name ); // 打印对象b的user_name属性,应该是"xxx"
// 打印对象a的user_name属性,应该仍然是"JohnDoe"

console.log( a.user_name ); // 这里仍然会打印"JohnDoe",因为b是一个独立的对象

运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//运算符 =,+,-,*,/
// = 赋值运算符
let abc = 10;
// 运算符的使用
// abc = 20; // 将变量abc的值改为20
// + 加法运算符
// abc = abc + 10; // 将变量abc的值加10
// - 减法运算符
// abc = abc - 5; // 将变量abc的值减5
// * 乘法运算符
// abc = abc * 2; // 将变量abc的值乘以2
// / 除法运算符
// abc = abc / 2; // 将变量abc的值除以2
// ** 幂运算符(ES2016及以上版本支持)
// abc = abc ** 2; // 将变量abc的值平方
// 以上运算符可以组合使用
abc = (abc + 10) * 2 - 5 / 2; // 先加10,再乘以2,最后减去5除以2的结果
console.log(abc); // 输出最终结果 37.5
// 注意:在实际使用中,运算符的优先级会影响结果
// 例如,乘法和除法的优先级高于加法和减法,所以在没有括号的情况下,先进行乘除运算
// 如果需要改变运算顺序,可以使用括号来明确优先级

// 复合赋值运算符
// +=, -=, *=, /=, **=
// 这些运算符是对变量进行赋值的同时进行运算
abc += 10; // 相当于 abc = abc + 10;
abc -= 5; // 相当于 abc = abc - 5;
abc *= 2; // 相当于 abc = abc * 2;
abc /= 2; // 相当于 abc = abc / 2;
abc **= 2; // 相当于 abc = abc ** 2;

// 自增和自减运算符
// ++ 自增运算符
// -- 自减运算符
let x = 5; // 定义一个变量x并赋值为5
x++; // 将变量x的值加1
x--; // 将变量x的值减1
y = x;
x += 2; // 将变量x的值加2
console.log(x); // 输出最终结果
console.log(y); // 输出y的值,应该是5
// 注意:自增和自减运算符可以直接作用于变量
// 也可以在表达式中使用
// 例如:console.log(++x); // 输出x加1后的值
// 例如:console.log(x++); // 输出x当前的值,然后再加1
// 注意:自增和自减运算符可以放在变量前面或后面
// 前缀自增/自减:++x 或 --x
// 后缀自增/自减:x++ 或 x--
// 前缀会先进行运算再返回值,后缀会先返回值再进行运算
// 例如:
let a = 5;
console.log(++a); // 输出6,a变为6
console.log(a++); // 输出6,a变为7
console.log(a); // 输出7
console.log(--a); // 输出6,a变为6
console.log(a--); // 输出6,a变为5
console.log(a); // 输出5
// 注意:自增和自减运算符在不同的上下文中可能会有不同的行为
// 在循环中使用自增和自减运算符可以简化代码
for (let i = 0; i < 5; i++) {
console.log(i);
}

// 逗号运算符
// 逗号运算符可以在一个表达式中执行多个操作
// 例如:
let b = (1, 2, 3);
console.log(b); // 输出3
// 逗号运算符会返回最后一个表达式的值
// 逗号运算符通常用于需要多个表达式的地方
// 例如在for循环中,可以使用逗号运算符来初始化多个变量
for (let i = 0, j = 10; i < j; i++, j--) {
console.log(i, j); // 输出i和j的值
}
// 注意:逗号运算符的使用场景较少,通常在需要同时执行多个表达式时使用
// 例如在函数调用中,可以使用逗号运算符来传递多个参数
function example(a, b) {
console.log(a, b);
}

// 可选链运算符
// 可选链运算符(?.)用于访问对象的属性或方法时,如果对象为null或undefined,则返回undefined,而不会抛出错误
let obj = {
a: {
b: {
c: 42
}
}
};
console.log(obj.a?.b?.c); // 输出42
console.log(obj.a?.b?.d); // 输出undefined
// 如果obj.a或obj.a.b为null或undefined,则不会抛出错误,而是返回undefined
// 可选链运算符可以用于访问嵌套对象的属性,避免了在访问时需要进行多层判断
// 例如:
let user = {
profile: {
name: "Alice",
age: 30
}
};
console.log(user.profile?.name); // 输出"Alice"
console.log(user.profile?.gender); // 输出undefined
// 可选链运算符也可以用于调用方法
// 如果方法不存在,则返回undefined,而不会抛出错误
let obj2 = {
method: function() {
return "Hello, world!";
}
};
console.log(obj2.method?.()); // 输出"Hello, world!"
console.log(obj2.nonExistentMethod?.()); // 输出undefined
// 注意:可选链运算符只能用于访问对象的属性或方法,不能用于访问数组元素或函数参数
// 如果需要访问数组元素,可以使用数组的length属性来判断是否存在
let arr = [1, 2, 3];
console.log(arr[0]); // 输出1
console.log(arr[3]?.toString()); // 输出undefined,因为arr[3]不存在
// 可选链运算符在处理复杂对象时非常有用,可以减少代码的复杂性
// 例如在处理API响应时,可能会遇到嵌套的对象结构
let apiResponse = { data: {
user: {
id: 1,
name: "Alice",
age: 30
}
}
};
// 扩展可选链运算符的使用
console.log(apiResponse.data?.user?.name); // 输出"Alice"
console.log(apiResponse.data?.user?.gender); // 输出undefined
// 如果apiResponse.data或apiResponse.data.user为null或undefined,则不会抛出错误,而是返回undefined
// 可选链运算符可以大大简化代码,避免了多层嵌套的判断
// 例如在处理深层嵌套的对象时,可以使用可选链运算符来安全地访问属性
/**
* 求和函数
* @param {...number} numbers - 需要求和的数字参数
* @returns {number} 所有参数的和
*/
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
// 使用可选链运算符调用函数
let result = sum?.(1, 2, 3, 4, 5);
console.log(result); // 输出15
// 如果sum函数不存在,则result为undefined,而不会抛出错误
// 可选链运算符在处理函数调用时也非常有用,尤其是在动态加载模块或函数时
// 例如在某些情况下,可能会根据条件加载不同的模块或函数

// 扩展运算符
// 扩展运算符(...)用于将数组或对象展开为单独的元素或属性
// 在函数调用时,可以使用扩展运算符将数组作为参数传递
function multiply(x, y, z) {
return x * y * z;
}
let numbers = [2, 3, 4];
console.log(multiply(...numbers)); // 输出24
// 扩展运算符也可以用于数组字面量,将一个数组的元素添加到另一个数组中
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5];
console.log(arr2); // 输出[1, 2, 3, 4, 5]
// 扩展运算符还可以用于对象字面量,将一个对象的属性添加到另一个对象中
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3 };
console.log(obj2); // 输出{ a: 1, b: 2, c: 3 }
// 注意:扩展运算符只能用于可迭代对象(如数组、字符串等)和对象字面量
// 在函数参数中使用扩展运算符,可以将多个参数合并为一个数组
function collectArgs(...args) {
return args;
}

// 课后练习1:
let a = 100; // 初始化变量a为100
a += 10; // 加等于:a = a + 10
console.log(a); // 输出加法后的值:110
a -= 10; // 减等于:a = a - 10
console.log(a); // 输出减法后的值:100
a *= 10; // 乘等于:a = a * 10
console.log(a); // 输出乘法后的值:1000
a /= 10; // 除等于:a = a / 10
console.log(a); // 输出除法后的值:100
a %= 10; // 取模等于:a = a % 10(取余数)
console.log(a); // 输出取模后的值:0
a **= 2; // 幂等于:a = a ** 2(平方)
console.log(a); // 输出平方后的值:0
a <<= 2; // 左移等于:a = a << 2(二进制左移2位)
console.log(a); // 输出左移后的值:0
a >>= 2; // 右移等于:a = a >> 2(二进制右移2位)
console.log(a); // 输出右移后的值:0
a >>>= 2; // 无符号右移等于:a = a >>> 2(无符号右移2位)
console.log(a); // 输出无符号右移后的值:0
a &= 5; // 按位与等于:a = a & 5(按位与运算)
console.log(a); // 输出按位与后的值:0
a |= 3; // 按位或等于:a = a | 3(按位或运算)
console.log(a); // 输出按位或后的值:3
a ^= 1; // 按位异或等于:a = a ^ 1(按位异或运算)
console.log(a); // 输出按位异或后的值:2
a = 100; // 重置a为100,便于观察
console.log(a); // 输出重置后的值:100

// 课后练习2:
// 声明变量x并赋值为3
let x = 3;
// 后置递增:先使用x的值(3)参与运算,然后x自增1
// 计算:y = 3 + 3 = 6,之后x变为4
y = x++ + 3;
// 输出结果:x=4, y=6
console.log(x, y); // 输出: 4, 6
// 声明变量a并赋值为5
let a = 5;
// 前置递增:先将a自增1变为6,然后参与运算
// 计算:b = 6 + 2 = 8
b = ++a + 2;
// 输出结果:a=6, b=8
console.log(a, b); // 输出: 6, 8

// 课后练习3:
// 声明变量x并赋值为100
let x = 100;
// 前置递减:先将x减1变为99,然后参与运算
// 计算:y = 99 + 1 = 100
y = --x + 1;
// 输出结果:x=99, y=100
console.log(x, y); // 输出: 99 100
// 重新将x赋值为100
x = 100;
// 后置递减:先使用x的值(100)参与运算,然后x减1
// 计算:y = 100 + 1 = 101,之后x变为99
y = x-- + 1;
// 输出结果:x=99, y=101
console.log(x, y); // 输出: 99 101

// 课后练习4:
// 声明变量x并赋值为100
let x = 100;
// 声明变量z并赋值为20
let z = 20;
// 前置递减:先将x减1变为99,然后加上z的值20
// 计算:y = 99 + 20 = 119
y = --x + z;
// 输出结果:x=99, y=119, z=20
console.log(x, y, z); // 输出: 99 119 20
// 重新将x赋值为100
x = 100;
// 重新将z赋值为20
z = 20;
// 后置递减:先使用x的值(100)加上z的值20,然后x减1
// 计算:y = 100 + 20 = 120,之后x变为99
y = x-- + z;
// 输出结果:x=99, y=120, z=20
console.log(x, y, z); // 输出: 99 120 20

// 课后练习5:
// 声明变量x并赋值为100
let x = 100;
// 声明变量z并赋值为20
let z = 20;
// 后置递增:先使用x的值(100)减去z的值(20),然后x自增1
// 计算:y = 100 - 20 = 80,之后x变为101
y = x++ - z;
// 输出结果:x=101, y=80, z=20
console.log(x, y, z); // 输出: 101 80 20
// 重新声明变量x并赋值为100
let x = 100;
// 重新声明变量z并赋值为20
let z = 20;
// 前置递增:先将x自增1变为101,然后减去z的值20
// 计算:y = 101 - 20 = 81
y = ++x - z;
// 输出结果:x=101, y=81, z=20
console.log(x, y, z); // 输出: 101 81 20

比较运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 比较运算
// > 大于
// < 小于
// >= 大于等于
// <= 小于等于
// = 赋值(不是比较运算符)
// == 相等(会进行类型转换)
// != 不相等(会进行类型转换)
// === 全等(类型和值都相等,不进行类型转换)
// !== 严格不相等(类型或值不相等就返回true,不进行类型转换)

function compareValues(a, b) {
console.log(`Comparing ${a} and ${b}:`);
console.log(`a > b: ${a > b}`);
console.log(`a < b: ${a < b}`);
console.log(`a >= b: ${a >= b}`);
console.log(`a <= b: ${a <= b}`);
console.log(`a == b: ${a == b}`);
console.log(`a != b: ${a != b}`);
console.log(`a === b: ${a === b}`);
console.log(`a !== b: ${a !== b}`);
}

// 示例调用
compareValues(5, 10);
compareValues("5", 5);
compareValues(10, 10);
compareValues(true, false);
compareValues(null, undefined);
compareValues("hello", "world");
compareValues(3.14, 3.14);

// 比较运算的结果会根据数据类型和具体值而有所不同
// 注意:在 JavaScript 中,== 和 === 的区别在于前者会进行类型转换,而后者不会
// 因此,"5" == 5 会返回 true,但 "5" === 5 会返回 false
// 这使得全等运算符 === 更加严格和安全
// 使用全等运算符可以避免一些潜在的错误和混淆
// 在实际开发中,建议尽量使用 === 和 !== 来进行比较运算
// 以确保类型和值都匹配
// 这样可以提高代码的可读性和可维护性

// 另外,比较运算符也可以用于条件语句中,如 if、while 等
// 例如:if (a > b) { ... } 可以根据比较结果执行不同的代码块
// 这使得比较运算在控制流中非常有用
// 还可以使用比较运算符来对数组或对象进行排序
// 例如:array.sort((a, b) => a - b) 可以对数组进行升序排序

// 总之,比较运算是 JavaScript 中非常重要的操作之一
// 它们在条件判断、循环、排序等方面都发挥着重要作用
// 理解和正确使用比较运算符可以帮助我们编写更高效和可靠的代码

// 另外,比较运算符还可以用于字符串的比较
// 在 JavaScript 中,字符串的比较是基于字符的 Unicode 编码进行的
// 例如:'apple' < 'banana' 返回 true,因为 'a' 的 Unicode 编码小于 'b'
// 这使得字符串的比较也可以使用相同的比较运算符
// 需要注意的是,字符串比较是区分大小写的
// 例如:'Apple' < 'apple' 返回 true,因为大写字母 'A' 的 Unicode 编码小于小写字母 'a'
// 因此,在进行字符串比较时,需要考虑大小写的影响
// 如果需要进行不区分大小写的字符串比较,可以将字符串转换为统一的大小写形式
// 例如:'apple'.toLowerCase() < 'Banana'.toLowerCase() 返回 true,因为"apple"按字母顺序小于"banana"

// 总之,比较运算符在 JavaScript 中具有广泛的应用场景
// 理解它们的工作原理和使用方法对于编写高质量的代码至关重要
// 通过比较运算符,我们可以实现各种逻辑判断和条件控制
// 以及对数据进行排序和筛选等操作
// 这使得比较运算符成为 JavaScript 编程中不可或缺的一部分
//
// 总结:
// 1. 优先使用 === 和 !== 进行严格比较
// 2. 注意类型转换带来的潜在问题
// 3. 字符串比较基于Unicode编码
// 4. NaN不等于任何值,包括自身
// 5. 对象和数组比较的是引用而非内容

// 特殊值的比较
// NaN(Not a Number)是一个特殊的值,用于表示非数字值
// NaN 在 JavaScript 中有一些特殊的比较规则
// 例如,NaN 不等于任何值,包括自身
// 这使得 NaN 的比较操作与其他值有所不同
console.log("=== 基本值比较 ===");
compareValues(5, 10);
compareValues("5", 5);
compareValues(10, 10);
compareValues(true, false);
compareValues(null, undefined);
compareValues("hello", "world");
compareValues(3.14, 3.14);

// 字符串比较
console.log("=== 字符串比较 ===");
console.log("apple" < "banana"); // true
console.log("apple" > "banana"); // false
console.log("Apple" < "apple"); // true
console.log("Apple" > "apple"); // false
console.log("apple" === "apple"); // true

// 字符串比较(区分大小写)
console.log("=== 字符串比较(区分大小写) ===");
console.log("apple" < "Banana"); // true
console.log("apple" > "Banana"); // false
console.log("apple".toLowerCase() < "Banana".toLowerCase()); // true,因为"apple"按字母顺序小于"banana"
console.log("apple".toLowerCase() > "Banana".toLowerCase()); // false,因为"apple"按字母顺序小于"banana"
console.log("apple".toUpperCase() < "Banana".toUpperCase()); // true,因为"APPLE"按字母顺序小于"BANANA"
console.log("apple".toUpperCase() > "Banana".toUpperCase()); // false,因为"APPLE"按字母顺序小于"BANANA"

// 特殊值比较
console.log("=== 特殊值比较 ===");
console.log(NaN > 0); // false,因为 NaN 不是一个有效的数字
console.log(NaN < 0); // false,因为 NaN 不是一个有效的数字
console.log(NaN == 0); // false,因为 NaN 不是一个有效的数字
console.log(NaN == NaN); // false,这是JavaScript的特殊规则:NaN不等于任何值,包括自身
console.log(NaN === NaN); // false,NaN是唯一不等于自身的值
console.log("123s" == 123); // false,因为 "123s" 无法转换为数字 123,结果是 NaN
console.log("123s" === 123); // false,因为类型不同
console.log("5" == 5); // true,因为 "5" 被转换为数字 5
console.log("5" === 5); // false,因为类型不同
console.log(null == undefined); // true,因为 null 和 undefined 被认为相等
console.log(null === undefined); // false,因为类型不同
console.log(true == 1); // true,因为 true 被转换为数字 1
console.log(true === 1); // false,因为类型不同
console.log(false == 0); // true,因为 false 被转换为数字 0
console.log(false === 0); // false,因为类型不同
console.log("hello" == "hello"); // true,因为两个字符串相等
console.log("hello" === "hello"); // true,因为两个字符串相等且类型相同

// 对象和数组的比较(引用比较)
console.log("=== 对象和数组的比较 ===");
console.log({} == {}); // false,两个不同的对象引用
console.log({} === {}); // false,两个不同的对象引用
console.log([] == []); // false,两个不同的数组引用
console.log([] === []); // false,两个不同的数组引用

const obj1 = { a: 1 };
const obj2 = obj1;
console.log(obj1 == obj2); // true,同一个对象引用
console.log(obj1 === obj2); // true,同一个对象引用

数据类型转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* parseInt() 函数解析字符串并返回整数
* 语法:parseInt(string, radix)
* - string: 要解析的字符串
* - radix: 2到36之间的整数,表示字符串的基数(进制)
*
* 特性:
* 1. 从字符串开头解析数字,直到遇到非数字字符
* 2. 忽略前导和尾随空格
* 3. 如果第一个字符不能转换为数字,返回NaN
*/

// 1. 不同进制下的解析示例
parseInt("10", 10); // 十进制 → 10
parseInt("10", 2); // 二进制 → 2
parseInt("10", 8); // 八进制 → 8
parseInt("10", 16); // 十六进制 → 16
parseInt("10", 36); // 三十六进制 → 36

// 2. 无效进制处理
parseInt("10", 1); // 进制无效(1 < 2) → NaN
parseInt("10", 37); // 进制无效(37 > 36) → NaN
parseInt("10", -1); // 进制无效(负数) → NaN
parseInt("10", 0); // 0表示自动检测进制(0x开头为16进制,0开头为8或10进制) → 10

// 3. 特殊字符串处理
parseInt("s123"); // 非数字开头 → NaN
parseInt("123s"); // 解析到第一个非数字字符 → 123
parseInt("123abc"); // 解析到第一个非数字字符 → 123
parseInt("abc123"); // 非数字开头 → NaN

// 4. 前缀处理(ES6+)
parseInt("0x10"); // 0x前缀 → 十六进制 → 16
parseInt("0b10"); // 0b前缀 → 二进制 → 2
parseInt("0o10"); // 0o前缀 → 八进制 → 8

// 5. 其他特殊情况
parseInt("10.5"); // 忽略小数部分 → 10
parseInt("10e2"); // 不支持科学计数法 → 10
parseInt("10.0"); // 整数部分 → 10
parseInt(" 10 "); // 忽略前后空格 → 10
parseInt("10 ", 10); // 忽略尾部空格 → 10

/**
* parseFloat() 函数解析字符串并返回浮点数
* 语法:parseFloat(string)
*
* 特性:
* 1. 从字符串开头解析数字,直到遇到非数字字符
* 2. 支持小数点和科学计数法
* 3. 忽略前导和尾随空格
* 4. 如果第一个字符不能转换为数字,返回NaN
* 5. 不支持进制参数,总是解析为十进制
*/

// 1. 基本浮点数解析
parseFloat("10.5"); // → 10.5
parseFloat("10.5abc"); // → 10.5 (解析到第一个非数字字符)
parseFloat("abc10.5"); // → NaN (非数字开头)

// 2. 科学计数法支持
parseFloat("10.5e2"); // → 1050
parseFloat("10.5e-2"); // → 0.105
parseFloat("10.5e+2"); // → 1050

// 3. 空格处理
parseFloat(" 10.5 "); // → 10.5 (忽略前后空格)
parseFloat("10.5 "); // → 10.5 (忽略尾部空格)

// 4. 无效用法示例(注释掉)
// parseFloat("10", 10); // 错误: parseFloat不接受第二个参数
// parseFloat("10", 2); // 错误: parseFloat不接受第二个参数
// parseFloat("10", 16); // 错误: parseFloat不接受第二个参数

数字和布尔类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// ==================== Number类型转换测试 ====================

// 基本类型转换
console.log(Number(null)); // → 0
console.log(Number(undefined)); // → NaN
console.log(Number(false)); // → 0
console.log(Number(true)); // → 1
console.log(Number("")); // → 0

// 字符串转换
console.log(Number("123")); // → 123 (纯数字字符串)
console.log(Number("123.456")); // → 123.456 (浮点数字符串)
console.log(Number("123abc")); // → NaN (包含非数字字符)
console.log(Number("abc123")); // → NaN (以非数字开头)

// 数组转换
console.log(Number([123])); // → 123 (单元素数组转换为元素值)
console.log(Number([1, 2])); // → NaN (多元素数组)
console.log(Number([1, 2, 3])); // → NaN (多元素数组)
console.log(Number([true])); // → 1 (布尔值数组)
console.log(Number([false])); // → 0 (布尔值数组)
console.log(Number([null])); // → 0 (null数组)
console.log(Number([undefined])); // → NaN (undefined数组)

// 对象转换
console.log(Number({})); // → NaN (空对象)
console.log(Number({ a: 1 })); // → NaN (非空对象)
console.log(Number(new Date("2023-10-01"))); // → 1738358400000 (日期对象转换为时间戳)
console.log(Number(new Date("Invalid Date"))); // → NaN (无效日期)

// 函数转换
console.log(Number(function() {})); // → NaN (函数)
console.log(Number(function(a) { return a; })); // → NaN (带参数函数)
console.log(Number(() => {})); // → NaN (箭头函数)
console.log(Number(() => 42)); // → NaN (返回值的箭头函数)

// 特殊值转换
console.log(Number(Infinity)); // → Infinity
console.log(Number(-Infinity)); // → -Infinity
console.log(Number(NaN)); // → NaN

// BigInt转换
console.log(Number(BigInt(123))); // → 123 (小BigInt)
console.log(Number(BigInt(12345678901234567890))); // → 12345678901234568000 (大BigInt会丢失精度)
console.log(Number(BigInt("12345678901234567890"))); // → 12345678901234568000 (同上)
console.log(Number(BigInt("123456789012345678901234567890"))); // → Infinity (超出安全范围)
console.log(Number(BigInt("-123456789012345678901234567890"))); // → -Infinity (负超大值)


// ==================== Boolean类型转换测试 ====================

// 转换为false的值
console.log(Boolean(null)); // → false
console.log(Boolean(undefined)); // → false
console.log(Boolean(false)); // → false
console.log(Boolean(0)); // → false
console.log(Boolean(NaN)); // → false
console.log(Boolean("")); // → false (空字符串)
console.log(Boolean(BigInt(0))); // → false (零BigInt)

// 转换为true的值
console.log(Boolean("abc")); // → true (非空字符串)
console.log(Boolean(123)); // → true (非零数字)
console.log(Boolean(-123)); // → true (负数)
console.log(Boolean(Infinity)); // → true
console.log(Boolean(-Infinity)); // → true

// 数组和对象
console.log(Boolean([1, 2, 3])); // → true (非空数组)
console.log(Boolean([])); // → true (空数组也是对象)
console.log(Boolean({})); // → true (空对象)
console.log(Boolean({ a: 1 })); // → true (非空对象)

// 函数
console.log(Boolean(function() {})); // → true
console.log(Boolean(() => {})); // → true
console.log(Boolean(function(a) { return a; })); // → true

// BigInt
console.log(Boolean(BigInt(123))); // → true (非零BigInt)
console.log(Boolean(BigInt("12345678901234567890"))); // → true (大BigInt)

/*
* JavaScript类型转换总结:
*
* Number转换规则:
* 1. null → 0
* 2. undefined → NaN
* 3. true → 1, false → 0
* 4. 空字符串 → 0
* 5. 纯数字字符串 → 对应数字
* 6. 非数字字符串 → NaN
* 7. 单元素数组 → 元素值
* 8. 多元素数组 → NaN
* 9. 对象 → NaN (Date对象除外)
* 10. BigInt → 数字(可能丢失精度)
*
* Boolean转换规则:
* 1. false值: null, undefined, false, 0, NaN, "", BigInt(0)
* 2. true值: 其他所有值,包括空数组/对象
*/

// 课后练习
// JavaScript代码演示各种相等性和比较操作
console.log("相等性和比较操作");
console.log(1 === 1); // 严格相等(===):值和类型都必须相等,1(数字)=== 1(数字)→ true
console.log("1" === 1); // 严格相等(===):类型不同,字符串"1" !== 数字1 → false
console.log(true == "true"); // 宽松相等(==):类型转换后比较,true转换为1,"true"转换为NaN → false
console.log(parseInt("123s") === 123); // 严格相等:parseInt("123s")返回123(数字)=== 123(数字)→ true
console.log(false == 0); // 宽松相等(==):false转换为0,0 == 0 → true
console.log(true > false); // 大于比较:true转换为1,false转换为0,1 > 0 → true
console.log(NaN !== NaN); // 不等于(!==):NaN与任何值都不相等,包括自身 → true(这是NaN的特殊性质)
console.log(true == 12231); // 宽松相等:true转换为1,1 == 12231 → false
console.log(NaN < 1000); // 小于比较:NaN与任何数值比较都为false → false
console.log(NaN >= 1000); // 大于或等于比较:NaN与任何数值比较都为false → false

// 补充说明:
// 1. === 严格相等:不进行类型转换,值和类型都必须相同
// 2. == 宽松相等:会进行类型转换后再比较
// 3. NaN是唯一一个不等于自身的值
// 4. 布尔值在比较时会转换为数字:true→1,false→0
// 5. 字符串与数字比较时,字符串会尝试转换为数字

逻辑运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* JavaScript逻辑运算符演示
* 逻辑运算符用于对布尔值进行操作或进行布尔转换
* 包括:与(&&)、或(||)、非(!)和三元条件运算符(? :)
*/

console.log("逻辑运算符演示");

// 逻辑与(&&) - 当所有操作数都为true时返回true,否则返回false
// 如果第一个操作数为false,则不会计算第二个操作数(短路求值)
console.log(true && true); // true && true → true (两个操作数都为true)
console.log(true && false); // true && false → false (第二个操作数为false)
console.log(false && true); // false && true → false (第一个操作数为false,短路)
console.log(false && false); // false && false → false (两个操作数都为false)

// 逻辑或(||) - 当任一操作数为true时返回true,否则返回false
// 如果第一个操作数为true,则不会计算第二个操作数(短路求值)
console.log(true || true); // true || true → true (第一个操作数已为true,短路)
console.log(true || false); // true || false → true (第一个操作数为true,短路)
console.log(false || true); // false || true → true (第二个操作数为true)
console.log(false || false); // false || false → false (两个操作数都为false)

// 逻辑非(!) - 对操作数取反
console.log(!true); // !true → false (true的取反)
console.log(!false); // !false → true (false的取反)

/**
* 三元条件运算符(? :)演示
* 语法:条件 ? 表达式1 : 表达式2
* 如果条件为true,返回表达式1的值,否则返回表达式2的值
*/
console.log(true ? "是" : "否"); // true ? "是" : "否" → "是" (条件为true)
console.log(false ? "是" : "否"); // false ? "是" : "否" → "否" (条件为false)
console.log(1 > 2 ? "大于" : "不大于"); // 1 > 2为false → "不大于"
console.log(2 > 1 ? "大于" : "不大于"); // 2 > 1为true → "大于"
console.log(1 === 1 ? "相等" : "不相等"); // 1 === 1为true → "相等"
console.log(1 !== 2 ? "不相等" : "相等"); // 1 !== 2为true → "不相等"
// 三元运算符与类型转换示例
// 演示条件运算符(?:)在处理字符串和数字转换时的行为

let e = '0'; // 初始值:字符串'0'

// 三元条件表达式:条件 ? 真值表达式 : 假值表达式
// 条件判断:e为字符串'0',在布尔上下文中,非空字符串为真值
e = e ? parseInt(e) : e + 100; // 执行过程:
// 1. 条件判断:e = '0' → 布尔值true (非空字符串)
// 2. 执行真值分支:parseInt('0') → 0 (字符串转整数)
// 3. 结果赋值:e = 0 (类型从string变为number)
// 注意:假值分支(e + 100)不会执行,但如果执行会是字符串拼接


/**
* JavaScript中的真值(truthy)和假值(falsy)演示
* 假值包括:false, 0, "", null, undefined, NaN
* 其他所有值都是真值,包括对象、数组、函数等
*/

// 单次取反(!)将值转换为布尔值并取反
console.log(!1); // !1 → false (1是真值,取反为false)
console.log(!0); // !0 → true (0是假值,取反为true)

// 双重取反(!!)是将任意值转换为布尔值的常用方法
console.log(!!1); // !!1 → true (1是真值)
console.log(!!0); // !!0 → false (0是假值)

// 字符串的真假值判断
console.log(!!""); // !!"" → false (空字符串是假值)
console.log(!!"非空字符串"); // !!"非空字符串" → true (非空字符串是真值)

// 特殊值的真假值判断
console.log(!!null); // !!null → false (null是假值)
console.log(!!undefined); // !!undefined → false (undefined是假值)
console.log(!!NaN); // !!NaN → false (NaN是假值)

// 数字特殊值的真假值判断
console.log(!!Infinity); // !!Infinity → true (Infinity是真值)
console.log(!!-Infinity); // !!-Infinity → true (-Infinity是真值)

// 对象和数组的真假值判断
console.log(!!{}); // !!{} → true (对象是真值,即使是空对象)
console.log(!![]); // !![] → true (数组是真值,即使是空数组)

// 函数的真假值判断
console.log(!!function () {}); // !!function(){} → true (函数是真值)

// Symbol的真假值判断
console.log(!!Symbol()); // !!Symbol() → true (Symbol是真值)

// BigInt的真假值判断
console.log(!!BigInt(0)); // !!0n → false (0n是假值)
console.log(!!BigInt(1)); // !!1n → true (非零BigInt是真值)
console.log(!!BigInt(-1)); // !!-1n → true (非零BigInt是真值)
console.log(!!BigInt("12345678901234567890")); // !!123...n → true (非零BigInt是真值)
console.log(!!BigInt("0")); // !!0n → false (0n是假值)
console.log(!!BigInt("1")); // !!1n → true (非零BigInt是真值)
console.log(!!BigInt("-1")); // !!-1n → true (非零BigInt是真值)

/**
* 逻辑运算符的短路求值和返回值演示
* 逻辑运算符返回的是操作数本身,而不是布尔值
*/

// 逻辑或(||) - 返回第一个真值操作数或最后一个操作数
console.log(1 || null); // 1 || null → 1 (1是真值,短路返回)
console.log(null || 1); // null || 1 → 1 (null是假值,检查下一个)
console.log(0 || "默认值"); // 0 || "默认值" → "默认值" (0是假值,返回第二个操作数)

// 逻辑与(&&) - 返回第一个假值操作数或最后一个操作数
console.log(1 && null); // 1 && null → null (1是真值,检查下一个)
console.log(null && 1); // null && 1 → null (null是假值,短路返回)
console.log(2 || null); // 2 || null → 2 (2是真值,短路返回)
console.log(null || 2); // null || 2 → 2 (null是假值,检查下一个)
console.log(2 && null); // 2 && null → null (2是真值,检查下一个)
console.log(null && 2); // null && 2 → null (null是假值,短路返回)

// 混合使用逻辑运算符
console.log(2 && true); // 2 && true → true (2是真值,返回第二个操作数)
console.log(true && 2); // true && 2 → 2 (true是真值,返回第二个操作数)
console.log(2 || false); // 2 || false → 2 (2是真值,短路返回)
console.log(false || 2); // false || 2 → 2 (false是假值,检查下一个)
console.log(2 && true); // 2 && true → true (同上)
console.log(true && 2); // true && 2 → 2 (同上)
console.log(2 && false); // 2 && false → false (2是真值,返回第二个操作数)
console.log(false && 2); // false && 2 → false (false是假值,短路返回)

// 总结
// 逻辑或(||)返回第一个真值操作数或最后一个操作数
// 逻辑与(&&)返回第一个假值操作数或最后一个操作数
// 逻辑非(!)用于取反布尔值
// 三元条件运算符(? :)用于根据条件返回不同的值
// 真值和假值的判断在JavaScript中非常重要,影响逻辑运算和条件判断
// 使用逻辑运算符时要注意短路求值的特性,避免不必要的计算
// 逻辑运算符的返回值可以是任意类型的值,而不仅仅是布尔值
// 了解这些运算符的行为可以帮助我们编写更高效和可读的代码
// 逻辑运算符的使用场景非常广泛,包括条件判断、默认值设置等
// 在实际开发中,合理使用逻辑运算符可以提高代码的可读性和性能
// 逻辑运算符的组合使用可以实现复杂的逻辑判断和条件控制
// 注意在使用逻辑运算符时,操作数的类型和值会影响最终结果
// 通过理解真值和假值,可以更好地掌握JavaScript中的逻辑运算
// 逻辑运算符的使用可以简化代码逻辑,减少冗余
// 在编写条件语句时,合理使用逻辑运算符可以提高代码的清晰度
// 逻辑运算符在函数参数默认值、条件渲染等场景中非常有用
// 掌握逻辑运算符的用法是JavaScript编程的基础技能之一
// 通过实践和练习,可以更熟练地使用逻辑运算符进行编程
// 逻辑运算符的使用可以帮助我们更好地控制程序的执行流程
// 在编写复杂逻辑时,可以将多个逻辑运算符组合使用
// 注意逻辑运算符的优先级,避免出现意外的计算结果
// 逻辑运算符的使用可以提高代码的表达力和可维护性
// 返回的是能决定整体是true还是false的那个表达式的值

// 一元正负运算符示例
// 一元+运算符会尝试将其操作数转换为数字
// 一元-运算符会先转换操作数为数字,然后对结果取负
+true; // +true → 1 (true在数字上下文中被转换为1,一元+返回1)
-true; // -true → -1 (true转换为1后,一元-运算符对结果取负)
+false; // +false → 0 (false在数字上下文中被转换为0,一元+返回0)
-false; // -false → -0 (false转换为0后,一元-运算符对结果取负,得到-0)
+undefined; // +undefined → NaN (undefined无法转换为有效数字,返回NaN)
-undefined; // -undefined → NaN (undefined转换为NaN后取负,仍然是NaN)
+"123"; // +'123' → 123 (字符串'123'可以完全转换为数字123)
-"123"; // -'123' → -123 (字符串'123'转换为123后取负)

// += 运算符示例:复合赋值运算符,执行加法后将结果赋给左操作数
// 注意:当操作数类型不同时会发生隐式类型转换
let a = 2;

// 数字与布尔值相加:布尔值true转换为数字1
a += true; // a = a + true → 2 + Number(true) → 2 + 1 → 3 (结果类型:number)

// 数字与字符串相加:数字转换为字符串后进行拼接
a += "true"; // a = a + 'true' → String(3) + 'true' → '3' + 'true' → '3true' (结果类型:string)

// 数字与空字符串相加:数字转换为字符串
100 + ""; // 100 = 100 + '' → String(100) + '' → '100' + '' → '100' (结果类型:string)

// 布尔值与字符串相加:布尔值转换为字符串
true + ""; // true = true + '' → String(true) + '' → 'true' + '' → 'true' (结果类型:string)

// JavaScript减法运算符(-)的隐式类型转换行为
// 与加法不同,减法运算符始终尝试将操作数转换为数字

// 示例1:两个可转换为数字的字符串相减
"123" - "3"; // 详细转换过程:
// 1. 左操作数"123" → Number("123") → 123 (成功转换)
// 2. 右操作数"3" → Number("3") → 3 (成功转换)
// 3. 执行算术运算:123 - 3 = 120
// 结果:120 (类型:number)

// 示例2:包含无效数字字符串的减法
"123" - "a"; // 详细转换过程:
// 1. 左操作数'123' → Number('123') → 123 (成功转换)
// 2. 右操作数'a' → Number('a') → NaN (转换失败,'a'不是有效数字)
// 3. 执行算术运算:123 - NaN → NaN (任何与NaN的运算结果都是NaN)
// 结果:NaN (类型:number,NaN是number类型的特殊值)

2. 流程控制语句

1.1 if else, if else if … else 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 通勤方式决策系统 - 双重实现对比
// 本文件展示了两种条件判断方式的等价实现:if-else链 vs switch(true)

// 系统参数:剩余时间(分钟)
let leftTime = 60; // 测试值:60分钟(边界值)

// ===== 实现方式1:传统的if-else链 =====
// 优点:直观易懂,适合简单条件判断
// 缺点:条件复杂时可能冗长

// 注意:存在拼写错误 leftTTime 应为 leftTime
if (leftTime >= 60) {
// 条件1:时间充裕(≥60分钟)
// 选择步行,可能距离较远但时间充足
console.log("走路去公司");
} else if (leftTime >= 30 && leftTime < 60) {
// 条件2:时间适中(30-60分钟)
// 注意:leftTTime是拼写错误,会导致ReferenceError
console.log("骑车去公司");
} else {
// 条件3:时间紧张(<30分钟)
// 选择开车,最快但可能堵车
console.log("开车去公司");
}

// 定义一个学生对象,包含基本信息
let student = {
name: "张三", // 学生姓名
age: 290, // 学生年龄(注意:290岁明显不合理,可能是测试数据)
gender: "男", // 学生性别
language: "中文", // 学生使用的语言
height: 160, // 学生身高(单位:厘米)
};

// 根据学生年龄的不同范围,动态添加不同的属性
if (student.age < 18) {
// 如果年龄小于18岁,添加空字符串的女朋友属性(表示没有女朋友)
student.girlfriend = "";
} else if (student.age >= 18 && student.age < 25) {
// 如果年龄在18-24岁之间,添加女朋友属性,值为"小丽"
student.girlfriend = "小丽";
} else if (student.age >= 25 && student.age < 30) {
// 如果年龄在25-29岁之间
if ("girlfriend" in student) {
// 如果之前有女朋友属性,删除它(因为现在要结婚了)
delete student.girlfriend;
}
// 添加妻子属性,值为"小丽"
student.wife = "小丽";
} else {
// 如果年龄大于等于30岁,添加孩子对象
student.children = {
name: "小红", // 孩子姓名
age: 3, // 孩子年龄
gender: "女", // 孩子性别
};
}

// 打印最终的学生对象到控制台,查看所有属性
console.log(student);

1.2 switch case 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ===== 实现方式2:switch(true)技巧 =====
// 原理:switch对表达式进行严格比较(===),利用true的布尔特性
// 优点:条件清晰,易于扩展多个分支
// 缺点:初学者可能不易理解

switch (true) {
case leftTime >= 60:
// 当leftTime >= 60为true时匹配
console.log("走路去公司");
break; // 必须break,否则会继续执行后续case

case leftTime >= 30 && leftTime < 60:
// 复合条件表达式,当整个表达式为true时匹配
console.log("骑车去公司");
break;

default:
// 兜底条件,相当于else
// 当所有case都不匹配时执行
console.log("开车去公司");
}

3 循环语句

3.1 for 循环语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 基础循环结构示例 - for循环详解
// 本示例展示了JavaScript中最常用的循环结构

// ===== for循环标准语法 =====
// for (初始化; 条件判断; 迭代更新) { 循环体 }

// 示例:打印数字0到9
// 这是一个经典的计数器循环模式
for (let i = 0; i < 10; i++) {
// 循环变量i的生命周期:
// 1. 初始化:let i = 0 → 创建块级变量i并赋值为0
// 2. 条件检查:i < 10 → 每次迭代前检查是否继续
// 3. 循环体:console.log(i) → 打印当前i的值
// 4. 迭代更新:i++ → 每次循环后i自增1

console.log(i); // 输出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
}

// ===== 循环执行流程分析 =====
// 第1次:i=0, 0<10=true, 输出0, i变为1
// 第2次:i=1, 1<10=true, 输出1, i变为2
// ...
// 第10次:i=9, 9<10=true, 输出9, i变为10
// 第11次:i=10, 10<10=false, 循环结束

// ===== 扩展知识 =====
// 1. let vs let:使用let创建块级作用域变量,避免污染全局
// 2. i++ vs ++i:后置递增先返回值后递增,前置递增先递增后返回值
// 3. 循环复杂度:O(n),n=10,执行10次console.log
// 4. 内存占用:常数级别,仅存储循环变量i

// ===== 常见变体示例 =====
// 倒序循环:for (let i = 9; i >= 0; i--) { console.log(i); }
// 步长为2:for (let i = 0; i < 10; i += 2) { console.log(i); } // 0,2,4,6,8
// 无限循环:for (;;) { console.log('无限循环'); break; } // 需要手动break

3.2 for 循环跳出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// ===== 循环控制流详解:break vs continue =====
// 本示例展示循环控制语句在不同位置的效果差异

// ===== 示例1:break在循环体前部 =====
// 场景:当i等于5时立即终止整个循环
// 预期输出:0, 1, 2, 3, 4(不会输出5及以后的数字)
for (let i = 0; i < 10; i++) {
if (i === 5) {
break; // 立即终止整个循环,跳到循环后的第一条语句
}
console.log(i); // 当i=5时,这行不会执行
}

// ===== 示例2:break在循环体后部 =====
// 场景:先输出当前值,然后判断是否终止
// 预期输出:0, 1, 2, 3, 4, 5(输出5后终止)
for (let i = 0; i < 10; i++) {
console.log(i); // 先输出当前i值,包括5
if (i === 5) {
break; // 输出5后立即终止,不会继续到6
}
}

// ===== 示例3:continue跳过特定迭代 =====
// 场景:跳过i=5的迭代,继续后续循环
// 预期输出:0, 1, 2, 3, 4, 6, 7, 8, 9(跳过5)
for (let i = 0; i < 10; i++) {
if (i === 5) {
continue; // 跳过本次循环的剩余语句,直接进入下一次迭代
}
console.log(i); // 当i=5时,这行不会执行
}

// ===== 示例4:continue导致的潜在死循环 =====
// 危险示例:continue可能跳过迭代更新语句
// 问题分析:当i=5时,continue会跳过i++,导致i永远为5
// 结果:无限循环,i始终为5,console.log(i)永不执行
for (let i = 0; i < 10; ) {
if (i === 5) {
continue; // 危险:会跳过i++,导致死循环
}
console.log(i); // 这行永远不会执行
i++; // 当i=5时,这行被跳过
}

// ===== 控制流对比总结 =====
// break vs continue 的区别:
// break: 立即终止整个循环,跳到循环后的代码
// continue: 跳过本次循环剩余语句,继续下一次迭代

// 最佳实践:
// 1. break通常用于找到目标值后提前退出
// 2. continue用于跳过特定条件的处理
// 3. 避免在循环更新部分使用continue
// 4. 确保continue不会跳过必要的迭代更新

// ===== 安全使用continue的示例 =====
// 正确做法:将更新语句放在continue之前
for (let i = 0; i < 10; ) {
i++; // 先更新,避免死循环
if (i === 5) {
continue;
}
console.log(i); // 输出:1,2,3,4,6,7,8,9,10
}

3.3 for 双向嵌套循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// ===== 双向嵌套循环:正序与倒序遍历对比 =====
// 本示例展示嵌套循环的两种遍历方向及其差异

// ===== 示例1:正序嵌套循环(0→10) =====
// 外层循环:i从0递增到10(包含10,共11个值)
// 内层循环:j从0递增到10(包含10,共11个值)
// 总迭代次数:11 × 11 = 121次

for (let i = 0; i <= 10; i++) { // 外层:i = 0,1,2,...,10
// 每次外层循环,i递增1

for (let j = 0; j <= 10; j++) { // 内层:j = 0,1,2,...,10
// 每次内层循环,j递增1

// 输出格式:i-j
// 输出序列(前几个和后几个):
// 开始:0-0, 0-1, 0-2, ..., 0-10
// 中间:5-0, 5-1, 5-2, ..., 5-10
// 结束:10-0, 10-1, 10-2, ..., 10-10
console.log(i + "-" + j);
}
}

// ===== 示例2:倒序嵌套循环(10→0) =====
// 外层循环:i从10递减到0(包含0,共11个值)
// 内层循环:j从10递减到0(包含0,共11个值)
// 总迭代次数:11 × 11 = 121次(与正序相同)

for (let i = 10; i >= 0; i--) { // 外层:i = 10,9,8,...,0
// 每次外层循环,i递减1

for (let j = 10; j >= 0; j--) { // 内层:j = 10,9,8,...,0
// 每次内层循环,j递减1

// 输出格式:i-j
// 输出序列(前几个和后几个):
// 开始:10-10, 10-9, 10-8, ..., 10-0
// 中间:5-10, 5-9, 5-8, ..., 5-0
// 结束:0-10, 0-9, 0-8, ..., 0-0
console.log(i + "-" + j);
}
}

// ===== 遍历方向对比分析 =====
// 正序遍历特点:
// - 从小到大,符合常规思维
// - 适合处理时间序列、索引递增的场景
// - 内存访问模式通常更友好(缓存局部性)

// 倒序遍历特点:
// - 从大到小,适合逆序处理
// - 常用于栈结构、后进先出场景
// - 某些算法中更高效(如动态规划)

// ===== 实际应用场景 =====
// 正序嵌套:
// - 矩阵初始化:按行优先填充
// - 图像处理:从上到下,从左到右扫描像素
// - 数据表格:按行列顺序处理

// 倒序嵌套:
// - 矩阵转置:从右下角开始处理
// - 游戏开发:从后向前渲染图层
// - 缓存清理:从最近最少使用开始

// ===== 性能分析 =====
// 两种方式的性能差异:
// - 时间复杂度:都是O(n²),n=11 → 121次操作
// - 空间复杂度:都是O(1),仅存储循环变量
// - 实际性能:在现代JavaScript引擎中几乎无差异

// ===== 边界值处理 =====
// 注意:两个循环都使用 <= 而不是 <,因此包含边界值0和10
// 如果改为 < 10,则只包含0-9,共100次迭代

// ===== 扩展变体 =====
// 混合方向:外层正序 + 内层倒序
// for (let i = 0; i <= 10; i++) {
// for (let j = 10; j >= 0; j--) {
// console.log(i + "-" + j);
// }
// }

// ===== 示例3:控制循环终止的标志变量 =====
/**
* 控制循环终止的标志变量
* 初始值为 false,表示循环可以继续执行
* 当内层循环满足特定条件时,将其设置为 true 以终止所有循环
*/
let lock = false;

/**
* 外层循环:控制变量 i 从 0 到 10(包括10)的迭代
* 每次迭代代表一组内层循环的执行
*/
for (let i = 0; i <= 10; i++) {
/**
* 检查锁标志状态
* 如果 lock 为 true,表示需要终止所有循环
* 使用 break 语句立即跳出外层循环
*/
if (lock) {
break; // 终止外层循环
}

/**
* 内层循环:控制变量 j 从 0 到 10(包括10)的迭代
* 每次迭代执行特定的逻辑判断和操作
*/
for (let j = 0; j <= 10; j++) {
/**
* 检查内层循环变量 j 是否等于 6
* 当 j 达到 6 时,触发终止条件
*/
if (j === 6) {
/**
* 设置锁标志为 true
* 这将导致外层循环在下次迭代时终止
*/
lock = true;

/**
* 跳出内层循环,继续执行外层循环的后续代码
* 外层循环会检查 lock 状态并相应终止
*/
break; // 终止内层循环
}

/**
* 输出当前循环计数器的组合值
* 格式为 "i-j",显示外层和内层循环的当前值
* 只有当 j 不等于 6 时才会执行此输出
*/
console.log(i + "-" + j);
}
}

/**
* 代码执行逻辑总结:
* 1. 初始化 lock 为 false
* 2. 开始外层循环 (i 从 0 到 10)
* 3. 每次外层循环开始时检查 lock:
* - 如果 lock 为 true,立即终止所有循环
* - 否则继续执行内层循环
* 4. 开始内层循环 (j 从 0 到 10)
* 5. 在内层循环中检查 j 是否等于 6:
* - 如果 j === 6,设置 lock = true 并终止内层循环
* - 否则输出 "i-j" 到控制台
* 6. 当内层循环因 j === 6 终止后,外层循环会检测到 lock 为 true
* 并在下一次迭代时终止所有循环
*
* 实际效果:
* - 输出所有 i 从 0 到 10 和 j 从 0 到 5 的组合
* - 当任意内层循环中 j 首次达到 6 时,设置终止标志
* - 在下一个外层循环迭代时,检测到终止标志并完全退出
*/

3.4 标签化循环控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// ===== 标签化循环控制:break与标签的精确控制 =====
// 本示例展示如何使用标签(label)精确控制break语句的作用范围

// ===== 示例1:break inner - 仅终止内层循环 =====
// 标签定义:outter和inner为循环语句的标签名
// 语法:labelName: for (...) { ... }

outter: for (let i = 0; i < 10; i++) { // 外层循环标签:outter
inner: for (let j = 0; j < 10; j++) { // 内层循环标签:inner

if (j === 3) {
break inner; // 仅终止标记为"inner"的内层循环
// 效果:相当于普通的break,只跳出当前内层循环
// 外层循环继续执行,i会递增到下一个值
}

console.log(i + "-" + j); // 输出:i-0, i-1, i-2(每个i值输出3个组合)
}
// 执行流程:
// i=0: 输出0-0, 0-1, 0-2 → break inner → i=1
// i=1: 输出1-0, 1-1, 1-2 → break inner → i=2
// ...
// i=9: 输出9-0, 9-1, 9-2 → break inner → 循环结束
}

// ===== 示例2:break outter - 终止外层循环 =====
// 关键区别:break标签可以跳出任意层级的循环

outter: for (let i = 0; i < 10; i++) { // 外层循环标签:outter
inner: for (let j = 0; j < 10; j++) { // 内层循环标签:inner

if (j === 5) {
break outter; // 终止标记为"outter"的外层循环
// 效果:直接跳出整个嵌套循环结构
// 不仅终止内层循环,也终止外层循环
}

console.log(i + "-" + j); // 输出:0-0, 0-1, 0-2, 0-3, 0-4
}
}
// 执行流程:
// i=0: 输出0-0, 0-1, 0-2, 0-3, 0-4 → j=5触发break outter → 整个循环结束
// 注意:i=1及以后的值永远不会被处理

// ===== 标签化控制的优势 =====
// 1. 精确控制:可以指定跳出哪一层循环
// 2. 多层嵌套:在深层嵌套中特别有用
// 3. 代码清晰:标签名使意图更加明确

// ===== 标签命名最佳实践 =====
// - 使用有意义的标签名(如rowLoop, colLoop)
// - 避免与变量名冲突
// - 保持简洁但描述性强

// ===== 扩展应用:continue与标签 =====
// continue也可以与标签配合使用:
// continue inner; // 跳过内层循环的剩余部分
// continue outter; // 跳过外层循环的剩余部分,直接开始下一次外层迭代

// ===== 性能考虑 =====
// 标签化break/continue在性能上与普通break/continue无差异
// 主要优势在于代码可读性和控制精度

// ===== 实际应用场景 =====
// 1. 二维数组搜索:找到目标后立即跳出多层循环
// 2. 复杂算法:在嵌套条件中精确控制流程
// 3. 状态机实现:根据状态标签跳转

3.5 示例1:嵌套/跳出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 定义一个布尔变量lock,初始值为true
// 这个变量用作控制外层循环是否继续执行的标志
let lock = true;

// 外层for循环:从i=0开始,每次循环i增加1,直到i<10(即i=9时最后一次循环)
// 这个循环控制行号(i的值)
for (let i = 0; i < 10; i++) {
// 检查lock变量的值,只有当lock为true时才执行内层循环
// 这个条件判断使得循环可以在特定条件下提前终止
if (lock) {
// 内层for循环:从j=0开始,每次循环j增加1,直到j<10(即j=9时最后一次循环)
// 这个循环控制列号(j的值)
for (let j = 0; j < 10; j++) {
// 当j等于5时,将lock设置为false
// 这个条件是关键:它会在内层循环的第6次迭代(j=5时)触发
// 一旦lock被设为false,外层循环的下一次迭代就不会再进入内层循环
if (j === 5) {
lock = false;
}

// 打印当前的i和j值,格式为"i-j"
// 例如:0-0, 0-1, 0-2, ..., 0-9(第一次外层循环)
// 然后:1-0, 1-1, 1-2, ..., 1-9(第二次外层循环)
// 直到lock被设为false为止
console.log(i + "-" + j);
}
}
// 新增:在外层循环的每次迭代中打印当前的i值
// 无论lock是true还是false,这行代码都会执行
console.log(i);
}

// 更新后的程序执行结果分析:
// 1. 当i=0时,lock为true,内层循环完整执行,输出:0-0, 0-1, 0-2, 0-3, 0-4, 0-5, 0-6, 0-7, 0-8, 0-9
// 2. 然后输出i的值:0
// 3. 在i=0的内层循环中,当j=5时lock被设为false,但这不影响当前内层循环的完成
// 4. 当i=1时,检查lock发现为false,跳过内层循环,但console.log(i)仍然执行,输出:1
// 5. 同样地,i=2,3,4,5,6,7,8,9时都会跳过内层循环,但都会执行console.log(i),分别输出:2,3,4,5,6,7,8,9
// 6. 最终输出序列:0-0到0-9(10个值),然后0,然后1,2,3,4,5,6,7,8,9(9个值)

3.6 示例2:倒序循环

1
2
3
4
5
6
7
8
9
console.log("循环写法1");
for (i = 10; i >= 1; i--) {
console.log(i);
}
console.log("循环写法2");
for (let i = 10; i >= 1; i--) {
console.log(i);
}

3.6 示例3:嵌套倒序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 这是一个使用嵌套循环的JavaScript程序
* 该程序创建了一个从10-0的倒序数字组合
* 外层循环控制第一个数字(i),内层循环控制第二个数字(j)
* 最终输出格式为:j-i
*/

// 外层循环:从10开始递减到0(包括0)
// i变量代表外层循环的计数器,初始值为10
// 循环条件:当i大于或等于0时继续执行
// 每次循环后i减1(i--)
for (let i = 10; i >= 0; i--) {

// 内层循环:对于每个i值,j从10开始递减到0(包括0)
// j变量代表内层循环的计数器,初始值为10
// 循环条件:当j大于或等于0时继续执行
// 每次循环后j减1(j--)
for (let j = 10; j >= 0; j--) {

// 在内层循环中,将当前的j值和i值组合成字符串并输出
// 使用字符串拼接操作符(+)将数字转换为字符串并连接
// 输出格式为:j-i,例如"10-10"、"9-10"、"8-10"等
console.log(j + "-" + i);
}

// 注意:内层循环会完整执行11次(j从10到0)
// 外层循环也会执行11次(i从10到0)
// 因此总共会输出11×11=121行结果
}

// 程序执行流程说明:
// 1. 当i=10时,内层循环j从10到0,输出:10-10, 9-10, 8-10, ..., 0-10
// 2. 当i=9时,内层循环j从10到0,输出:10-9, 9-9, 8-9, ..., 0-9
// 3. 以此类推,直到i=0时,输出:10-0, 9-0, 8-0, ..., 0-0

3.7 while & do while 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 示例1: while循环 - 倒计时
// 初始化计数器变量i为10
let i = 10;

// while循环:先检查条件,再执行循环体
// 条件:当i大于或等于0时继续循环
// 这个循环会从10倒数到0
while (i >= 0) {
// 输出当前的i值
console.log(i);
// 递减计数器,每次循环减1
i--;
}
// 循环结束后,i的值为-1

// 示例2: while循环 - 正计数
// 初始化计数器变量j为0
let j = 0;

// while循环:先检查条件,再执行循环体
// 条件:当j小于或等于10时继续循环
// 这个循环会从0数到10
while (j <= 10) {
// 输出当前的j值
console.log(j);
// 递增计数器,每次循环加1
j++;
}
// 循环结束后,j的值为11

// 示例3: do-while循环 - 单次执行
// 初始化变量a为10
let a = 10;

// do-while循环:先执行循环体,再检查条件
// 特点:循环体至少会执行一次
do {
// 输出当前的a值(第一次输出10)
console.log(a);
// 递增a的值
a++;
// 条件检查:如果a小于或等于10则继续循环
// 由于a初始为10,执行a++后变为11,不满足a <= 10的条件
// 因此这个循环只会执行一次
} while (a <= 10);
// 循环结束后,a的值为11

// 示例4: do-while循环 - 无限重置问题
// 声明变量b但不初始化(值为undefined)
let b;

// do-while循环:先执行循环体,再检查条件
do {
// 每次循环都将b重置为0
// 这是一个逻辑错误:b的值永远不会递增到超过1
b = 0;
// 输出b的值(总是输出0)
console.log(b);
// 递增b的值(从0变为1)
b++;
// 条件检查:如果b小于或等于10则继续循环
// 由于每次循环b都被重置为0,然后递增到1
// 1 <= 10始终为true,导致无限循环
} while (b <= 10);
// 注意:这个循环会导致无限循环,因为b在每次迭代开始时都被重置为0

3.7 for in 遍历/迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
let person = {
name: "王刚",
age: 30,
gender: "男",
};

for (let key in person) {
console.log(key + ": " + person[key]); // 输出每个属性的键值对
}

let student = {
name: "李四",
age: 22,
gender: "女",
};

for (let key in student) {
if (key === "age") {
continue; // 跳过 age 属性
}
console.log(key + ": " + student[key]); // 输出每个属性的键值对
}

let somebody = {
name: "张三",
age: 28,
gender: "男",
};

// for (let key in somebody) {
// if (key === "age") {
// somebody[key] = 18;
// }
// console.log(key + ": " + somebody[key]); // 输出每个属性的键值对
// }

// if ("language" in somebody) {
// console.log("有language属性");
// } else {
// console.log("没有language属性");
// }

// 错误示例:in运算符用于检查属性是否存在,而不是检查值
// 28 in somebody.age 是错误的用法,因为somebody.age是数字28,不是对象
// 正确用法应该是检查属性是否存在
if ("age" in somebody) {
console.log("有age属性");
} else {
console.log("没有age属性");
}

// 如果要检查值是否为28,应该这样写:
if (somebody.age === 28) {
console.log("年龄是28岁");
} else {
console.log("年龄不是28岁");
}

// 创建一个学生对象,包含基本信息属性
// 使用对象字面量语法定义一个空的学生对象模板
let student = {
name: "", // 学生姓名,初始为空字符串
age: 0, // 学生年龄,初始为0
gender: "", // 学生性别,初始为空字符串
language: "", // 学生使用的语言,初始为空字符串
height: 0, // 学生身高(厘米),初始为0
};

// 第一个循环:遍历学生对象的所有属性并赋值
// for...in循环用于遍历对象的可枚举属性
// key变量代表对象中的每个属性名(字符串形式)
for (let key in student) {
// 使用if-else结构根据属性名进行条件判断和赋值

if (key === "name") {
// 如果当前属性是"name",将其值设置为"张三"(中文姓名)
student[key] = "张三";
} else if (key === "age") {
// 如果当前属性是"age",将其值设置为20岁
student[key] = 20;
} else if (key === "gender") {
// 如果当前属性是"gender",将其值设置为"女"(女性)
student[key] = "女";
} else if (key === "language") {
// 如果当前属性是"language",将其值设置为"中文"
student[key] = "中文";
} else if (key === "height") {
// 如果当前属性是"height",将其值设置为160厘米
student[key] = 160;
}
}

// 第二个循环:遍历并打印学生对象的所有属性及其值
// 再次使用for...in循环遍历对象的每个属性
for (let key in student) {
// 使用console.log()方法在控制台输出属性信息
// 字符串拼接:将属性名、固定文本"是:"和对应的属性值连接起来
// 输出格式示例:"name是:张三"
console.log(key + "是:" + student[key]);
}

// 代码执行后的预期输出:
// name是:张三
// age是:20
// gender是:女
// language是:中文
// height是:160

// 创建一个学生对象,包含多个属性
// 使用对象字面量语法定义一个包含学生信息的对象
let student = {
name: "张三", // 学生姓名,字符串类型
age: 20, // 学生年龄,数字类型
gender: "女", // 学生性别,字符串类型
language: "中文", // 使用语言,字符串类型
height: 160, // 身高(厘米),数字类型
};

// 初始化一个空字符串变量,用于存储最终拼接的结果
let result = "";

// 初始化一个临时字符串变量,用于存储每次循环处理的键值对字符串
let temp = "";

// 初始化一个计数器变量,用于记录当前处理的属性序号
let count = 0;

// 使用for...in循环遍历student对象的所有可枚举属性
// key变量会依次获取对象的每个属性名(字符串形式)
for (let key in student) {
// 每次循环计数器加1,用于标识当前是第几个属性
count++;

// 判断是否是第5个属性(最后一个属性)
if (count === 5) {
// 如果是最后一个属性,不需要添加&符号作为分隔符
// 将属性名、等号和属性值拼接成字符串
temp = key + "=" + student[key];
} else {
// 如果不是最后一个属性,需要在末尾添加&符号作为分隔符
// 这样可以将多个键值对连接成URL查询字符串格式
temp = key + "=" + student[key] + "&";
}

// 将当前处理的键值对字符串追加到结果字符串中
result += temp;
}

// 在控制台输出最终拼接完成的字符串
// 预期输出:name=张三&age=20&gender=女&language=中文&height=160
console.log(result);

4 函数 function

4.1 函数简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// JavaScript函数说明
// 函数是可重复使用的代码块,用于执行特定任务

// 1. 函数声明方式

// 函数声明
function greet(name) {
return "Hello, " + name + "!";
}

// 函数表达式
const add = function(a, b) {
return a + b;
};

// 箭头函数 (ES6+)
const multiply = (a, b) => {
return a * b;
};

// 箭头函数简写(单行返回)
const subtract = (a, b) => a - b;

// 2. 函数调用示例
console.log("=== 函数调用示例 ===");

const greeting = greet("World");
console.log(greeting); // 输出: Hello, World!

const sum = add(5, 3);
console.log("5 + 3 =", sum); // 输出: 5 + 3 = 8

const product = multiply(4, 6);
console.log("4 * 6 =", product); // 输出: 4 * 6 = 24

const difference = subtract(10, 7);
console.log("10 - 7 =", difference); // 输出: 10 - 7 = 3

// 3. 带参数的函数
function calculateArea(width, height) {
return width * height;
}

const area = calculateArea(5, 8);
console.log("矩形面积:", area); // 输出: 矩形面积: 40

// 4. 默认参数
function sayHello(name = "Guest") {
return "Hello, " + name;
}

console.log(sayHello()); // 输出: Hello, Guest
console.log(sayHello("Alice")); // 输出: Hello, Alice

// 5. 函数作为一等公民(可以作为参数传递)
function processNumbers(numbers, operation) {
return numbers.map(operation);
}

const numbers = [1, 2, 3, 4, 5];
const doubled = processNumbers(numbers, x => x * 2);
console.log("加倍后的数组:", doubled); // 输出: 加倍后的数组: [2, 4, 6, 8, 10]

// 6. 立即执行函数(IIFE)
(function() {
console.log("这是一个立即执行函数");
})();

console.log("=== JavaScript函数核心特性 ===");
console.log("1. 参数: 接收输入值");
console.log("2. 返回值: 使用 return 返回结果");
console.log("3. 作用域: 函数内部变量只在函数内有效");
console.log("4. 重用性: 一次定义,多次调用");
console.log("5. 一等公民: 函数可以作为参数传递或作为返回值");

function find() {
console.log("1. 参数: 接收输入值");
console.log("2. 返回值: 使用 return 返回结果");
console.log("3. 作用域: 函数内部变量只在函数内有效");
console.log("4. 重用性: 一次定义,多次调用");
console.log("5. 一等公民: 函数可以作为参数传递或作为返回值");
}

find();

let find2 = find;

find2();

// 7. 函数作为对象的方法
// 在JavaScript中,对象可以拥有函数作为属性,这些函数称为"方法"

// 创建一个空对象
let persion = {};

// 将find函数赋值给对象的hello属性
// 现在hello成为了persion对象的一个方法
persion.hello = find;

// 查看对象的hello属性(会显示函数定义)
console.log(persion.hello);

// 调用对象的方法
// 使用对象名.方法名()来调用对象的方法
persion.hello();

// 8. 对象字面量中的方法定义
// 在创建对象时直接定义方法

let obj = {
// 使用函数表达式定义方法
hello: function () {
console.log("Hello from obj");
},
};

// 调用对象的方法
obj.hello();

// 9. ES6方法简写语法
// 在对象字面量中可以使用更简洁的方法定义语法

let obj2 = {
// 方法简写语法(ES6+)
go() {
console.log("Go from obj2");
},
};

// 调用简写方法
obj2.go();

// 10. 方法的重要概念:
// - 方法中的this关键字指向调用该方法的对象
// - 方法可以访问和修改对象的其他属性
// - 方法可以被其他对象"借用"(使用call/apply/bind)

4.2 嵌套循环函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// 11. 嵌套循环示例 - 函数中的循环结构

// 定义一个包含嵌套循环的函数
function test() {
// 外层循环:i从0到8(包括8)
for (let i = 0; i <= 8; i++) {
// 内层循环:j从0到8(包括8)
for (let j = 0; j <= 8; j++) {
// 输出i和j的组合
console.log(i + "-" + j);
}
}
// 这个嵌套循环总共会执行 9 * 9 = 81 次
}
// 调用test函数执行嵌套循环
test();

// 12. 对象方法中的嵌套循环 - 使用函数表达式

let test1 = {
// 使用函数表达式定义hello方法
hello: function () {
// 外层循环:i从9到9(只执行一次,因为9 >= 9为true,但i--后8 >= 9为false)
for (let i = 9; i >= 9; i--) {
// 内层循环:j从9到7(包括7),递减循环
for (let j = 9; j >= 7; j--) {
// 输出i和j的组合
console.log(i + "-" + j);
}
}
// 这个循环会输出:9-9, 9-8, 9-7
},
};
// 调用对象的hello方法
test1.hello();

// 13. 对象方法中的嵌套循环 - 使用ES6方法简写语法

let test2 = {
// 使用ES6方法简写语法定义go方法
go () {
// 外层循环:i从9到9(只执行一次)
for (let i = 9; i >= 9; i--) {
// 内层循环:j从9到7(包括7),递减循环
for (let j = 9; j >= 7; j--) {
// 输出i和j的组合
console.log(i + "-" + j);
}
}
// 这个循环会输出:9-9, 9-8, 9-7
},
};
// 调用对象的go方法
test2.go();

// 14. 循环的重要概念:
// - for循环语法:for(初始化; 条件; 增量/减量) { 循环体 }
// - 嵌套循环:外层循环每执行一次,内层循环完整执行所有迭代
// - let声明的循环变量具有块级作用域
// - break和continue可以控制循环流程


// ==================== JavaScript函数深入讲解 ====================

// 函数是JavaScript的核心概念,具有以下重要特性:

// 1. 函数定义与调用
// 函数通过function关键字定义,通过函数名()调用
// 函数可以接收参数并返回结果

// 2. 参数传递机制
// - 值传递:基本类型按值传递
// - 引用传递:对象类型按引用传递
// - 默认参数:ES6支持参数默认值 function(a, b = 10) {}
// - 剩余参数:使用...接收多个参数 function(...args) {}

// 3. 返回值与作用
// - return语句终止函数执行并返回结果
// - 没有return的函数返回undefined
// - 函数可以返回任何类型,包括其他函数(高阶函数)

// 4. 作用域与闭包
// - 函数内部可以访问外部变量(词法作用域)
// - 闭包:函数可以记住并访问其词法作用域,即使函数在其作用域外执行
// - 每个函数调用都会创建新的作用域

// 5. this关键字
// - 普通函数:this指向调用者
// - 箭头函数:this继承自外层作用域
// - 方法:this指向所属对象

// 6. 函数类型
// - 普通函数:function声明或表达式
// - 箭头函数:简洁语法,无自己的this
// - 生成器函数:function*,可以暂停和恢复
// - 异步函数:async/await语法

// ==================== 嵌套循环示例 - 函数中的循环结构 ====================

// 定义一个包含嵌套循环的函数
// 此函数演示了函数如何封装复杂逻辑
function test() {
// 外层循环:i从0到8(包括8)
for (let i = 0; i <= 8; i++) {
// 内层循环:j从0到8(包括8)
for (let j = 0; j <= 8; j++) {
// 输出i和j的组合
console.log(i + "-" + j);
}
}
// 这个嵌套循环总共会执行 9 * 9 = 81 次
}
// 调用test函数执行嵌套循环
test();

// 12. 对象方法中的嵌套循环 - 使用函数表达式

let test1 = {
// 使用函数表达式定义hello方法
hello: function () {
// 外层循环:i从9到9(只执行一次,因为9 >= 9为true,但i--后8 >= 9为false)
outter: for (let i = 9; i >= 0; i--) {
// 内层循环:j从9到7(包括7),递减循环
inner: for (let j = 9; j >= 7; j--) {
// 输出i和j的组合
console.log(i + "-" + j);
if (j === 8) {
break outter; // 跳出外层循环
}
}
}
// 这个循环会输出:9-9, 9-8, 9-
},
};
// 调用对象的hello方法
test1.hello();

// 13. 对象方法中的嵌套循环 - 使用ES6方法简写语法

let test2 = {
// 使用ES6方法简写语法定义go方法
go() {
let lock = false; // 将变量声明移到方法内部
// 外层循环:i从9到9(只执行一次)
for (let i = 9; i >= 0; i--) {
if (lock === true) {
break; // 跳出外层循环
}
for (let j = 9; j >= 7; j--) {
// 输出i和j的组合
console.log(i + "-" + j);
if (j === 7) {
lock = true;
break; // 跳出内层循环
}
}
}
// 这个循环会输出:9-9, 9-8, 9-7
},
};
// 调用对象的go方法
test2.go();

// 14. 循环的重要概念:
// - for循环语法:for(初始化; 条件; 增量/减量) { 循环体 }
// - 嵌套循环:外层循环每执行一次,内层循环完整执行所有迭代
// - let声明的循环变量具有块级作用域
// - break和continue可以控制循环流程

4.3 函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
/**
* JavaScript 数据类型检测示例
* 这个文件展示了两种不同的方法来检测JavaScript中的数据类型
*/

// ==================== 第一部分:使用 typeof 操作符 ====================

/**
* type 函数 - 使用 typeof 操作符检测变量类型
* @param {any} x - 要检测类型的变量
* @returns {void} - 在控制台输出变量的类型
*/
function type(x) {
console.log(typeof x); // 使用 typeof 操作符获取变量的类型
}

// 测试各种数据类型的 typeof 输出
console.log("=== typeof 操作符测试 ===");

type(1); // number - 数字类型
type("hello"); // string - 字符串类型
type(true); // boolean - 布尔类型
type([1, 2, 3]); // object - 数组在JavaScript中实际上是对象类型
type({ name: "Alice", age: 30 }); // object - 对象类型
type(undefined); // undefined - 未定义类型
type(null); // object - null 在JavaScript中被错误地识别为对象类型(历史遗留问题)
type(function () {}); // function - 函数类型
type(Symbol("id")); // symbol - Symbol类型(ES6新增)
type(BigInt(12345678901234567890n)); // bigint - BigInt类型(ES2020新增,用于大整数)
type(); // undefined - 没有传递参数时,x为undefined
type([]); // object - 空数组也是对象类型

// ==================== 第二部分:使用条件判断 ====================

/**
* type1 函数 - 使用条件语句检测特定类型并输出中文消息
* @param {any} x - 要检测类型的变量
* @returns {void} - 在控制台输出中文类型描述
*/
function type1(x) {
if (typeof x === "boolean") {
console.log("我是布尔"); // 布尔类型
} else if (typeof x === "string") {
console.log("我是字符串"); // 字符串类型
} else if (typeof x === "number") {
console.log("我是数字"); // 数字类型
} else if (Array.isArray(x)) {
console.log("我是数组"); // 使用 Array.isArray() 专门检测数组
} else if (typeof x === "function") {
console.log("我是函数"); // 函数类型
} else {
console.log("我是其他类型"); // 其他类型(主要是对象)
}
}

// 测试 type1 函数
console.log("\n=== 中文类型检测测试 ===");

type1(true); // 布尔值 - 输出 "我是布尔"
type1("Hello"); // 字符串 - 输出 "我是字符串"
type1(123); // 数字 - 输出 "我是数字"
type1([1, 2, 3]); // 数组 - 输出 "我是数组"
type1({ key: "value" }); // 对象 - 输出 "我是其他类型"(因为对象不是数组、函数等特定类型)
type1(function () {}); // 函数 - 输出 "我是函数"

// ==================== JavaScript 类型系统说明 ====================
/*
JavaScript 有 8 种基本数据类型:
1. undefined - 未定义
2. null - 空值
3. boolean - 布尔值
4. number - 数字
5. bigint - 大整数
6. string - 字符串
7. symbol - 符号
8. object - 对象(包括数组、函数、日期等)

typeof 操作符的返回值:
- "undefined" - 未定义
- "boolean" - 布尔值
- "number" - 数字
- "bigint" - 大整数
- "string" - 字符串
- "symbol" - 符号
- "function" - 函数
- "object" - 对象或null

注意:
- typeof null 返回 "object" 是JavaScript的一个历史遗留bug
- 数组使用 typeof 也返回 "object",需要使用 Array.isArray() 来专门检测
- 函数使用 typeof 返回 "function",但函数在JavaScript中也是对象的一种
*/

/**
* type2 函数 - 有逻辑问题的类型检测函数示例
* 这个函数演示了JavaScript条件判断中可能出现的逻辑错误
*
* @param {any} x - 要检测类型的变量
* @returns {void} - 在控制台输出类型描述(但存在逻辑问题)
*/
function type2(x) {
// 外层条件:只有当x是函数类型时才执行内层代码
// 问题:这个条件限制了只有函数类型才能进入内层判断
if (typeof x === "function") {
// 内层类型判断 - 但这些判断永远不会全部被执行
// 因为外层已经确定了x是函数,内层的其他类型判断都是多余的

if (typeof x === "boolean") {
console.log("我是布尔"); // 这个分支永远不会执行,因为x已经是函数类型
} else if (typeof x === "string") {
console.log("我是字符串"); // 这个分支永远不会执行
} else if (typeof x === "number") {
console.log("我是数字"); // 这个分支永远不会执行
} else if (typeof x === "function") {
console.log("我是函数"); // 这是唯一可能执行的分支
} else if (Array.isArray(x)) {
console.log("我是数组"); // 这个分支永远不会执行
} else {
console.log("我是其他类型"); // 这个分支永远不会执行
}
} else {
// 空else块 - 当x不是函数类型时,什么都不做
// 这意味着对于非函数类型的输入,函数不会有任何输出
}
}

// ==================== 测试 type2 函数 ====================
console.log("=== type2 函数测试(有逻辑问题) ===");

// 这些调用都不会有输出,因为输入的不是函数类型
type2(1); // 数字 - 无输出(因为外层条件不满足)
type2(true); // 布尔 - 无输出
type2("Hello"); // 字符串 - 无输出
type2(123); // 数字 - 无输出
type2([1, 2, 3]); // 数组 - 无输出
type2({ key: "value" }); // 对象 - 无输出

// 只有这个调用可能有输出,但内层逻辑也有问题
type2(function () {}); // 函数 - 可能输出"我是函数"

// ==================== 逻辑问题分析 ====================
/*
这个函数的主要问题:
1. 外层条件限制:只有函数类型才能进入内层判断,其他类型都被忽略
2. 内层冗余判断:在外层已经确定是函数的情况下,内层又检查其他类型
3. 空else块:对于非函数类型,函数没有任何输出

正确的实现应该是:
function type2(x) {
if (typeof x === "boolean") {
console.log("我是布尔");
} else if (typeof x === "string") {
console.log("我是字符串");
} else if (typeof x === "number") {
console.log("我是数字");
} else if (typeof x === "function") {
console.log("我是函数");
} else if (Array.isArray(x)) {
console.log("我是数组");
} else {
console.log("我是其他类型");
}
}

或者移除外层不必要的条件判断。
*/

// ==================== 修正后的函数 ====================
/**
* 修正后的类型检测函数
* @param {any} x - 要检测类型的变量
*/
function type2Corrected(x) {
if (typeof x === "boolean") {
console.log("我是布尔");
} else if (typeof x === "string") {
console.log("我是字符串");
} else if (typeof x === "number") {
console.log("我是数字");
} else if (typeof x === "function") {
console.log("我是函数");
} else if (Array.isArray(x)) {
console.log("我是数组");
} else {
console.log("我是其他类型");
}
}

console.log("\n=== 修正后的函数测试 ===");
type2Corrected(1); // 输出 "我是数字"
type2Corrected(true); // 输出 "我是布尔"
type2Corrected("Hello"); // 输出 "我是字符串"
type2Corrected(123); // 输出 "我是数字"
type2Corrected([1, 2, 3]); // 输出 "我是数组"
type2Corrected({ key: "value" }); // 输出 "我是其他类型"
type2Corrected(function () {}); // 输出 "我是函数"


# type2函数修复方案

## 原始代码问题分析

### 原始代码 ([main.js:1-27](Javascript/main.js:1))
```javascript
function type2(x) {
if (typeof x === "function") { // ❌ 问题1:外层条件限制过窄
if (typeof x === "boolean") { // ❌ 问题2:内部条件永远不会为真
console.log("我是布尔");
} else if (typeof x === "string") {
console.log("我是字符串");
} else if (typeof x === "number") {
console.log("我是数字");
} else if (typeof x === "function") {
console.log("我是函数");
} else if (Array.isArray(x)) {
console.log("我是数组");
} else {
console.log("我是其他类型");
}
} else {
// ❌ 问题3:空else块,非函数类型被忽略
}
}
```

## 修复方案

### 修复后的代码
```javascript
/**
* 检测输入值的类型并输出中文描述
* @param {any} x - 需要检测类型的值
*/
function type2(x) {
// 1. 首先检查 null,因为 typeof null 返回 'object'(JavaScript的历史遗留问题)
if (x === null) {
console.log("我是null");
}
// 2. 检查数组,因为数组也是对象类型,需要优先判断
else if (Array.isArray(x)) {
console.log("我是数组");
}
// 3. 检查函数
else if (typeof x === "function") {
console.log("我是函数");
}
// 4. 检查 undefined
else if (typeof x === "undefined") {
console.log("我是undefined");
}
// 5. 检查布尔值
else if (typeof x === "boolean") {
console.log("我是布尔");
}
// 6. 检查字符串
else if (typeof x === "string") {
console.log("我是字符串");
}
// 7. 检查数字
else if (typeof x === "number") {
console.log("我是数字");
}
// 8. 检查 Symbol (ES6+)
else if (typeof x === "symbol") {
console.log("我是Symbol");
}
// 9. 检查普通对象(包括Date、RegExp等)
else if (typeof x === "object") {
console.log("我是对象");
}
// 10. 其他未知类型
else {
console.log("我是其他类型");
}
}

// 测试用例
console.log("=== 类型检测测试 ===");
type2(1); // 预期输出: "我是数字"
type2(true); // 预期输出: "我是布尔"
type2("Hello"); // 预期输出: "我是字符串"
type2(123); // 预期输出: "我是数字"
type2([1, 2, 3]); // 预期输出: "我是数组"
type2({ key: "value" }); // 预期输出: "我是对象"
type2(function () {}); // 预期输出: "我是函数"
type2(null); // 预期输出: "我是null"
type2(undefined); // 预期输出: "我是undefined"
type2(Symbol("test")); // 预期输出: "我是Symbol" (如果支持ES6)
```

## 类型检测顺序说明

### 检测顺序流程图
```mermaid
graph TD
A[开始检测] --> B{x === null?}
B -- 是 --> C[输出null]
B -- 否 --> D{Array.isArray?}
D -- 是 --> E[输出数组]
D -- 否 --> F{typeof function?}
F -- 是 --> G[输出函数]
F -- 否 --> H{typeof检测}
H -- undefined --> I[输出undefined]
H -- boolean --> J[输出布尔]
H -- string --> K[输出字符串]
H -- number --> L[输出数字]
H -- symbol --> M[输出Symbol]
H -- object --> N[输出对象]
H -- 其他 --> O[输出其他类型]
```

### 为什么这个顺序很重要:
1. **null优先**:`typeof null` 返回 `'object'`,需要特殊处理
2. **数组优先于对象**:数组也是对象,需要先用 `Array.isArray()` 区分
3. **函数单独检测**:函数也是对象,但需要单独识别
4. **基本类型检测**:使用 `typeof` 按优先级检测
5. **对象最后**:普通对象会落在 `typeof x === 'object'` 条件中

## 测试预期结果

| 输入值 | 预期输出 | 说明 |
|--------|----------|------|
| `1` | "我是数字" | 数字类型 |
| `true` | "我是布尔" | 布尔类型 |
| `"Hello"` | "我是字符串" | 字符串类型 |
| `[1,2,3]` | "我是数组" | 数组类型 |
| `{key:"value"}` | "我是对象" | 普通对象 |
| `function(){}` | "我是函数" | 函数类型 |
| `null` | "我是null" | null值 |
| `undefined` | "我是undefined" | undefined值 |
| `Symbol()` | "我是Symbol" | Symbol类型(ES6+) |

## 下一步操作

请切换到Code模式来实际实现这个修复方案。

/**
* 修复后的type2函数 - 正确检测JavaScript数据类型
* 修复了原始代码的逻辑问题,能够正确识别所有数据类型
*/

/**
* 检测输入值的类型并输出中文描述
* @param {any} x - 需要检测类型的值
*/
function type2(x) {
// 1. 首先检查 null,因为 typeof null 返回 'object'(JavaScript的历史遗留问题)
if (x === null) {
console.log("我是null");
}
// 2. 检查数组,因为数组也是对象类型,需要优先判断
else if (Array.isArray(x)) {
console.log("我是数组");
}
// 3. 检查函数
else if (typeof x === "function") {
console.log("我是函数");
}
// 4. 检查 undefined
else if (typeof x === "undefined") {
console.log("我是undefined");
}
// 5. 检查布尔值
else if (typeof x === "boolean") {
console.log("我是布尔");
}
// 6. 检查字符串
else if (typeof x === "string") {
console.log("我是字符串");
}
// 7. 检查数字
else if (typeof x === "number") {
console.log("我是数字");
}
// 8. 检查 Symbol (ES6+)
else if (typeof x === "symbol") {
console.log("我是Symbol");
}
// 9. 检查普通对象(包括Date、RegExp等)
else if (typeof x === "object") {
console.log("我是对象");
}
// 10. 其他未知类型
else {
console.log("我是其他类型");
}
}

// ========== 测试用例 ==========
console.log("=== type2函数测试 ===");
console.log("测试1 - 数字:");
type2(1); // 预期输出: "我是数字"
console.log(); // 空行

console.log("测试2 - 布尔:");
type2(true); // 预期输出: "我是布尔"
console.log(); // 空行

console.log("测试3 - 字符串:");
type2("Hello"); // 预期输出: "我是字符串"
console.log(); // 空行

console.log("测试4 - 另一个数字:");
type2(123); // 预期输出: "我是数字"
console.log(); // 空行

console.log("测试5 - 数组:");
type2([1, 2, 3]); // 预期输出: "我是数组"
console.log(); // 空行

console.log("测试6 - 对象:");
type2({ key: "value" }); // 预期输出: "我是对象"
console.log(); // 空行

console.log("测试7 - 函数:");
type2(function () {}); // 预期输出: "我是函数"
console.log(); // 空行

console.log("测试8 - null:");
type2(null); // 预期输出: "我是null"
console.log(); // 空行

console.log("测试9 - undefined:");
type2(undefined); // 预期输出: "我是undefined"
console.log(); // 空行

console.log("测试10 - Symbol:");
// 检查是否支持Symbol(ES6特性)
if (typeof Symbol !== "undefined") {
type2(Symbol("test")); // 预期输出: "我是Symbol"
} else {
console.log("Symbol类型测试跳过(当前环境不支持ES6 Symbol)");
}
console.log(); // 空行

console.log("=== 测试完成 ===");