操作符
ECMA-262描述了一组用于操作数据值的操作符,ES的操作符是特殊的,因为它可以应用于各种类型的值,甚至包括对象,在应用给对象时,通常会先调用valueOf()或toString()转换为可计算值
一元操作符
只操作一个值的操作符叫做一元操作符
1.递增/递减操作符
递增/递减操作符分为前缀递增/递减和后缀递增/递减,它们的作用是一元的,都可以让原值递增或递减1
let num = 1;
num++;
console.log(num); // 2
--num;
console.log(num); // 1
前缀和后缀的区别是,前缀递增/递减具有副作用
let num = 1;
let count = ++num;
console.log(num); // 2
console.log(count); // 2
let num = 1;
let count = num++;
console.log(num);// 2
console.log(count);// 1
一元操作符可以应用于任何类型的值,应用规则:
操作数是数值字符串,将字符串转为数值在操作
操作数是非数值字符串,返回NaN
操作数是布尔值,true转为1,false转为0再操作
操作数是对象,应用valueOf()方法取得可操作数,如果是NaN,则应用toString()转换后再操作
操作数是浮点值,操作整数部分
操作数是特殊值,null转为0,undefined返回NaN
let str1 = "2";
let str2 = "z";
let True = true;
let False = false;
let f = 1.1;
let obj = {
valueOf: function () {
return 1;
},
};
console.log(++str1);// 3
console.log(++str2); // NaN
console.log(++True); // 2
console.log(++False); // 1
console.log(--f); // 0.10000000000000009
console.log(++obj);// 2
2.一元加和一元减
在使用上,一元加和一元减放在变量前面
在区别上:
一元加放在数值前面,对数值没有任何影响,但是放在非数值前面时,会应用同Number()转型函数规则一样的转型操作
let str1 = "01";
let str2 = "z";
let True = true;
let False = false;
let f = 1.1;
let obj = {
valueOf: function () {
return 1;
},
};
console.log(+str1); // 1
console.log(+str2); // NaN
console.log(+True); // 1
console.log(+False); // 0
console.log(+f); // 1.1
console.log(+obj); // 1
一元减,在对操作数应用这个操作符会使得操作数变为负数,对于其他类型,操作方式同一元加一致。先对它们进行转换,再取负值
let str1 = "01";
let str2 = "z";
let True = true;
let False = false;
let f = 1.1;
let obj = {
valueOf: function () {
return 1;
},
};
console.log(-str1); // -1
console.log(-str2); // NaN
console.log(-True); // -1
console.log(-False); // -0
console.log(-f); // -1.1
console.log(-obj); // -1
一元加和一元减主要用于简单的运算,但是也可以用于数据类型转换
位操作符
位操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)
ECMAScript中所有数值都是以IEEE754 64位格式存储的,但是位操作不直接应用于64位表示,而是先把值转换为32位,进行位操作后,再转换到64位表示。
32位整数存储规则:
对于有符号整数,前31位表示整数,第32位表示符号,0表示正,1表示负
正值,以真正的二进制格式存储, 第一位从右开始,如果一个位是空的,则以0填充
负值,以二补数(或补码)的二进制编码存储,一个数值的二补数通过如下计算:
1)确定绝对值的二进制表示
2)找到数值的一补数(或反码)
3)给结果+1
注意,在处理有符号整数时,无法访问第31位
由于对ECMAScript中数值应用位操作时,后台会发生,64位转32位,执行位操作,32位转64位存储的过程。这个转换会导致一个奇特的副作用,即特殊值NaN和Infinity在位操作中都会被当成0处理
如果将位操作符应用到非数值,会先用Number()将非数值转为数值后操作,最终结果是数值
1.按位非
按位非的操作符用波浪线(~)表示,作用是返回数值的一补数
const num1 = 25;
const num2 = ~num1;
console.log(num2); // -26
可以看出,按位非操作符的最终效果是对数值去取反再-1:
-34 === ~33 // true
2.按位与
按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个操作数的32位二进制数对齐,然后按照真值表中的规则操作
真值表规律,只有所有位上都是1时才为1,其他都为0
3.按位或
按位或操作符用管道符( | )表示,同按位与使用方式一致
真值表规律,只有所有位上都是0时才为0
4.按位异或
按位异或用脱字符(^)表示
真值表规律,只有同时1或同时0时才返回0
5.左移
左移用两个小于符号表示(<<),用于按照指定的位数将所有数值的所有位向左移动
console.log(2 << 5); // 64
左移后空出来的位用0填充,符号位不发生移动
6.有符号右移
无符号右移用两个大于号表示(>>)。用于按照指定的位数将所有数值的位向右移动
const nums1 = 100;
const nums2 = 100 >> 2; // 25
nums1.toString(2)
// '1100100' 十进制100
nums2.toString(2)
// '11001'向右移动两位,1在首位 十进制25
7.无符号右移
无符号有移用三个大于号表示(>>>),会将数值的所有32位都移动,对于整数没有影响,因为整数的符号位为0,右移只会填充空位的0,但是对于负数影响很大,负数的二进制表示是二补数,也就是其绝对值的二进制取反再+1,此时符号位变为1,当移动时,连同符号位都被填充成了0,转换为十进制表示时,就是一个变化非常大的数
let oldValue = -64
let absBin = Math.abs(oldValue).toString(2);
// --------------无符号右移计算过程----------------
absBin = '00000000000000000000000001000000' // 32位二进制
// '00000000000000000000000001000000'
absBin = '11111111111111111111111110111111' // 32位二进制取反
// '11111111111111111111111110111111'
absBin = '11111111111111111111111111000000' // 32位二进制取反,再加1
// '11111111111111111111111111000000'
absBin = '00000111111111111111111111111110' // 32位二进制取反,再加1,右移5位
// '00000111111111111111111111111110'
parseInt(absBin,2); // 手动右移后结果134217726
// 134217726
// ----------------计算结束-------------------------
oldValue >>> 5; // 计算结果与无符号右移操作符一致
// 134217726
布尔操作符
布尔操作符一共有三个:逻辑或( || )、逻辑与(&&)、逻辑非( ! )
1.逻辑非(!)
逻辑非由一个感叹号表示( ! ),可以应用于任何值,这个操作符始终会将操作的值转为布尔值再执行取反操作
规则如下:
对象,为false
null,为true
undefined,为true
NaN,为true
非空字符串,为false
空字符串,为true
0,为ture
非0,包括Infinity,为false
!{}
// false
!null
// true
!undefined
// true
!0
// true
!1
// false
!''
// true
!'false'
// false
!NaN
// true
逻辑非同一元加一样,具有转换类型的功用,不同的是,一元加是将值转换为数值,而 逻辑非通过双感叹号(!!)将值转为它本身对应的布尔值
2.逻辑与
逻辑与使用两个与号表示(&&),应用于两个值,结果遵从真值表
第一个值 | 第二个值 | 结果 |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
逻辑与可以用于任何值,但是注意注意:如果操作数不是布尔值,则逻辑与不一定返回布尔值
规则如下:
如果第一个操作数是对象,则返回第二个操作数
如果第二个操作数是对象,则看第一个操作数计算为true返回第二个对象,否则返回第一个操作数
如果两个操作数都是对象,则返回第二个操作数
如果有一个操作数是null、undefined、NaN,则返回这个操作数
总结:只有当第一个操作数的布尔值是true时,才会返回第二个操作数,否则返回第一个操作数,并且不转换类型
注意:逻辑与具有短路性,当第一个操作数决定了结果时(即第一个值为false),不会再对第二个值求值
let res = true && ud;// ReferenceError: ud is not defined
let res = false && ud;
console.log(res)// false
第二个res发生了短路,并没有执行逻辑与后面的代码
3.逻辑或
逻辑或操作符用两个管道符表示( || ),应用于两个值,结果遵从真值表
第一个操作数 | 第二个操作数 | 结果 |
---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
注意:如果有一个操作数不是布尔值,那么它也不一定返回布尔值,规则如下
如果第一个操作数是对象,则返回该对象
如果第一个操作数是false,则返回第二个操作数
如果两个操作数都是对象,则返回第一个操作数
如果两个操作数都是undefined、null、NaN,则返回对应的值
逻辑或也是短路操作符,当第一个操作数为true时,不会计算第二个操作数的值
乘性操作符
ECMAScript规定了三种乘性操作符:乘、除和取模,同样它们也具有隐式转换的能力,操作数不是数值时会在后台调用Number()转型为数值,false为0,true为1
1.乘法操作符
乘法操作符由星号(*)表示,规则如下:
操作数是数值的,正正都正,正负得负,负负得正,不能表示的乘积结果用Infinity和-Infinity表示
如果有任意一项操作数是NaN,则返回NaN
正负Infinity乘以0,返回NaN
Infinity乘以非0数值,看第二个操作数返回Infinity或-Infinity
如果有操作数不是数值,先应用Number()转换为Number后再应用以上规则
2.除法操作符
除法操作符用斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商,规则如下:
如果操作数都是数值,应用正正得正,正负得负,负负等正,不能表示的商用Infinity和-Infinity表示
如果有任意操作数是NaN,则返回NaN
如果是Infinity相除,则返回NaN
0/0返回NaN
0除以任何数值都为0,任何数除以0都为Infinity或-Infinity
Infinity除以任何数值,看数值符号返回Infinity或-Infinity
如果操作数不是数值,先调用Number()转换为数值后再应用上面的规则
3.取模操作符
取模(余数)操作符用百分号(%)表示,应用规则如下
如果操作数是常规数值,按照常规除法计算,返回余数
如果被除数是无限值,除数是有限值,则返回NaN
如果被除数是有限值,除数是0,则返回NaN
如果是Infinity除以Infinity,返回NaN
如果被除数是有限值,除数是无限值,则返回被除数
如果被除数是0,除数不是0,则返回0
如果有操作数不是数值,先使用Number()转换为数值后再应用上面的规则
指数操作符
指数操作符使用双星号表示(**),这个操作符是在ES7新增的,用来简化Math.pow()方法
2**3
// 8
同时,指数操作符也有指数赋值操作符,用(**=表示)
let num = 3;
num **= 3; // => num = num ** 3;
// 27
加性操作符
加性操作符,即加法和减法操作符,在ECMAScript中,加性操作符也具有隐式转换的行为,并且这种行为是比较怪异的
1.加法操作符
加法操作符用加号(+)表示,表示两个数值相加的值或者字符串拼接的结果,具体规则可以分为计算值和字符串拼接,如下:
数值相加:
如果任意操作数是NaN,则返回NaN
对于操作数都是Infinity无穷值的情况,正正得正,正负得NaN,负负得负
对于操作数都是0的情况,正正得正0,正负得0,负负得负0
字符串拼接:
如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面
如果只有一个操作数是字符串,则将另外一个非字符串操作数转为字符串,然后拼接
总结,数值应用加法操作符,另一个操作数如果是对象、布尔值,则会调用它们的toString()方法,再应用规则;字符串应用加法操作符,另一个操作数如果是对象、数值、布尔值,则会调用它们的toString()方法,再应用规则
注意,对于undefined和null没有toString()函数,则会调用String()方法,转换为字符串
注意,不要忽略加法操作符种涉及的数据转换问题
const num1 = 1;
const num2 = 2;
const result = 'num1 + num2 = '+num1+num2;
console.log(result)// num1 + num2 =12
2.减法操作符
减法操作符由一个减号表示(-),规则如下
两个操作数都是数值的,按数学计算规则返回计算结果
如果任意一个操作数是NaN的,返回NaN
对于无穷值Infinity,正正得NaN,负负得NaN,正负得正,负正得负
对于有符号0,正正得正,正负得负,负负得正
如果有任意操作数是字符串、布尔值、null或undefined,则在后台先调用Number()函数,转换为数值后再应用规则计算。如果转换结果是NaN,则结果返回NaN
如果有任意操作数是对象,先调用valueOf()方法转换为数值,如果是NaN,则返回NaN,如果没有valueOf()方法,则调用toString()方法,运算后转换为数值结果
const obj = {
toString:()=>{
return '12'
}
}
'100' - obj // 88
关系操作符
关系操作符执行比较两个值的操作,包括大于(>)、小于(<)、大于等于(>=),小于等于(<=),比较结果以布尔值表示,具体规则如下:
操作数都是数值,比较数值
操作数都是字符串,比较字符串中对应的字符的编码
如果有任一操作数是数值,则将另一个操作数转为数值,执行数值比较
如果有任一操作数是对象,则调用valueOf()方法,取得结果后比较,如果没有valueOf()方法,则调用toString()方法
如果任一值是布尔值,则将其转换为数值再进行操作
注意,如果两个比较数都是字符串时,比较的是字符串中对应字符的编码,这些编码是数值,在比较完后会返回布尔值
因此会发生奇怪的现象
'B' < 'a' // true B的字符编码是66,a是97,因此'a'>'B'
'B' < 'A' // false
解决这种问题的方法是将字母都转换为相同的大小写形式
另一个奇怪的现象
'23' < '3' // true
解决这个问题的方式也很简单,只要其中一个是数值就会应该操作符的规则进行正确的比较
注意,这种转换只会发生在数值字符串这种能转换为数值的情况中,对于字符串时非数值的字符,会转换为NaN
'a' <= 3 // false
'a' >= 3 // false
一般情况下,如果一个值不小于等于另一个值,那么这个值肯定大于等于这个值,但是在比较NaN时,无论怎么比较,结果都是NaN
相等操作符
ECMAScript中定义两组相等操作符,一组是等于和不等于,一组是全等和不全等,它们之间的区别是等于和不等于操作符会进行隐式转换,而全等和不全等操作符执行的是值和类型的完全相同比较
1.等于和不等于
等于和不等于操作符分别使用双等(==)和感叹号等于(!=)符号表示,等于操作符描述如果两个操作数相等则返回true,不全等操作符描述如果两个操作数不相等则返回true
注意,这两个数都会先进行类型转换(通常称为强制类型转换)后,在进行比较
规则如下:
如果任意操作数是布尔值,则将其转换为数值后再比较,true -> 1 ,false -> 0
如果任一操作数是字符串,另一操作数是数值,尝试将字符串转换为数值再比较
如果操作数是对象的,调用valueOf()获取其值原始值再比较
还有一些特别重要的规则:
null == undefined相等,返回true
null 和 undefined不能转换其他类型的值再比较
NaN == NaN不相等,返回false,NaN
!= NaN返回true
如果操作数都是对象,则比较对象的引用是否是同一个对象,引用相同则表示指向的是同一个对象,相当于自己和自己比较
const obj1 = new Object();
const obj2 = obj1;
obj1 == obj2; // true
obj1.a = 111; //
obj1 == obj2; // 仍然相等 true
obj1 // obj1的对象
// {a: 111}
obj2 // obj2的对象,内容一致
// {a: 111}
特殊情况以及比较结果
null == undefined //true
"NaN" == NaN // false
5 == NaN // false
NaN == NaN // false
NaN != NaN //true
false == 0 // true
true == 1 // true
true == 2 // false
undefined == 0 // false
null == 0 // false undefined 和 null使用等于操作符时不转换
"5" == 5 // true
2.全等和不全等
全等和不全等操作符用(===和!==)表示,全等操作符描述只有两个值在不发生转换的情况下相等才会返回true,不全等操作符也是一样的
注意:undefined和null在全等操作符中不相等
undefined === null // false
条件操作符
条件操作符由boolean_expression ? true_value : false_value;的格式组成,根据条件表达式boolean_expression的值决定true_value 或false_value
赋值操作符
赋值操作符用(=)表示,把等号右边的值赋值给左边的变量
复合赋值操作符用乘性、加性、位操作符后跟一个=号表示
乘后赋值(*=)
let num = 2;
num *= 2
// 4
除后赋值
let num = 4;
num /= 2;
// 2
取模后赋值
let num = 5;
num %= 1
// 0
加后赋值
let num = 1;
num += 1
// 2
减后赋值
let num = 2;
num -= 1
// 1
左移后赋值
let num = 2;
num >>= 1
// 1
总结,复合赋值操作符可以用于简化操作两个操作数的操作符运算后赋值的行为,基本上操作两个数的操作符都可以使用复合赋值操作符简化
注意,复合赋值操作符只是简化操作,并不会使得性能提升
逗号操作符
逗号操作符用于在一条语句中执行多个操作,用逗号( , )表示
let num = 1,num2 = 2;
它可以用于辅助赋值
let num = (1,2,3,4);
// num === 4
语句
ECMA-262描述了一些语句(也称流控制语句),语句可以满足分支操作,也可以满足循环操作,这是一种逻辑在代码中体现的方式。
if语句
if语句用于分支操作,它可以将一个流程分化为两个分支,流程条件满足哪一个分支,就只执行满足的那个分支的代码
语法:
if(condition) statement1 else statement2;
这里的condition可以是任何表达式,ECMAScript会自动调用Boolean()转型函数将表达式的值转换为布尔值。布尔值的结果决定了执行的语句:
true --> statement1
false --> statement2
使用上,在statement只有一条语句的时候,可以不使用语句块({}),也就是单行代码,但是这不是规范操作,因为if语句没有语句块包裹时,只执行其后的第一条语句
if(false)
console.log('a');
console.log('b');
// 输出b,但是有可能你想做的是同时屏蔽ab的输出
if语句有一个连续多个if语句的语法简写方式
if (condition1) statement1 else if (condition2) statement2 else statement3
do-while语句
do-while语句是一种后测试语句,即先执行代码块内的语句,再计算表达式决定是步入还是步出循环
语法:
do{
statement;
}while(expression);
也就是说,这个结构会至少执行一次statement
let num = 0;
do{
num++;
}while(false);
console.log(num);
// num输出1,意味着在退出前,执行了一次num++操作
while语句
while语句是一个先测试语句,也就是说,如果不满足while的循环条件,那么while将永远不会执行
语法:
while(expression){
statement;
}
while(false){
num++;
}
let num = 0;
while(false){
num++;
}
console.log(num); // 0,从未执行过num++
for语句
for语句也是先测试语句,只不过增加了循环执行前的初始化代码,以及循环执行后的表达式
语法:
for(init; expression; post-loop-expression) statement;
执行顺序:
1.init只执行一次,
2.执行expression
2.执行statement
4.执行post-loop-expression
5.循环2-4直到expression不符合后结束for循环
for(let i = 0, count = 10; i < count;i++){
console.log(i);
}
循环执行2-4.直到不满足expression后跳出for语句,并且对于post-loop-expression如果执行了statement,他也会被执行,反之不执行,他的行为看起来像这样的while结构:
let count = 10;
let i = 0;
while (i < count) {
console.log(i);
i++;
}
无法通过while语句实现的逻辑,同样也无法使用for循环实现。for循环语句只是将循环相关的代码绑定到一个语句块中
注意,for语句初始化时,可以不使用声明关键字初始化,但是这个行为会造成初始化的变量变为全局对象的属性,这样不光会造成性能问题,也会在下一次使用for循环时造成局限性,因此最佳的做法是使用let声明限制初始化变量的作用域,让他在循环后被销毁
for(i = 0; i < 5;i++){
// console.log(i);
}
console.log(i); // 5
console.log(window.i);// 5
无穷循环
for(;;){
// 无限执行
}
for变体
let count = 10;
let i = 0;
for (; i < count; ) {
console.log(i);
i++;
}
严格迭代:指的是在遍历对象或者数组时,确保属性或元素都被访问一次,不会漏掉或者重复访问
for-in语句
for-in语句是一种严格的迭代语句,它会迭代对象中的非符号键属性。
语法:
for (property in expression) statement;
for-in语句行为与for一致,但有两点注意:
for-in不能保证遍历的属性键的顺序性,这是因为ES规范没有强制指出for-in必须保证顺序性,其次是因为存储属性键的方式是通过哈希表,哈希表结构不能保证顺序性
如果for-in循环迭代的对象是null或undefined,则不执行循环体
for-of语句
for-of语句是一种严格迭代语句,用于遍历可迭代对象中的元素
语法:
for(element of expression) statement;
for-of行为与for-in一致,只是遍历的目标不同,也有两点需要注意:
关于for-of的顺序性,for-of的顺序性是根据可迭代对象的next()方法来决定的
与for-in不同,如果遍历的对象不是可迭代对象,会抛出不是可迭代对象的错误
标签语句
标签语句用于给语句加标签
语法:
label: statement
可以在break或continue语句后面使用,典型的使用场景是嵌套循环
break和continue
break和continue用于中断循环语句的执行,它们加强了对循环语句的控制手段
break:立即退出循环,执行循环的吓一条语句
continue:立即跳出循环,回到循环顶部继续执行
二者的区别非常明显,break是直接跳出,continue是跳出一次
关于标签语句和中断循环语句的组合使用:
对于普通嵌套循环:
let n = 0;
for(let i = 0; i < 10; i++){
for(let j = 0; j < 10; j++){
if(i === 5 && j === 5){
break;
}
n++;
}
}
// n === 95
break的特性是退出当前循环,但是它不会退出当前循环的外部循环
组合使用:
let n = 0;
label: for(let i = 0; i < 10; i++){
for(let j = 0; j < 10; j++){
if(i === 5 && j === 5){
break label;
}
n++;
}
}
// n === 55;
在break后使用标签语句,标签语句标识的是第一个循环,此时的break有两个作用,停止当前循环和停止标签语句标识的循环
with语句
with语句的用途是将代码作用域设置为特定的对象
语法:
with(expression){
statement;
}
let qs = '';
let hn= '';
let url = '';
with(location){
qs = search.substring(1);
hn = hostname;
url = url;
}
// 上面的代码相当于
qs = location.search.substring(1);
hn = location.hostname;
url = location.url
注意:由于with语句会影响性能和内部不好维护的原因,这个语句并不常用,也不推荐用
switch语句
switch语句是一种与if语句紧密相关的流控制语句,它的语法从C语言借鉴而来。if语句是将流一分为二,而switch是将流一分为多。
语法:
switch(expression){
case value1:
statement;
break;
case value2:
statement;
break;
default:
statement;
}
每一个case与expression表达式计算出来的值相等,则执行相应部分的代码,如果都不相等,那么执行default内的代码。而break关键字会阻止流继续匹配下一个case,即退出当前switch语句
let a = 1 + 0;
switch(a){
case 1:
console.log('1');
case 1:
console.log('2');
}
// 输出1,2
switch(a){
case 1:
console.log('1');
break;
case 1:
console.log('2');
}
// 由于break跳出了switch语句,只执行1
switch语言的独特性:
expression可以用于任何任何数据类型
case条件不需要是常量,可以变量或表达式
let hello = 'hello';
switch('hello world'){
case `${hello} world`:
console.log('hello world');
break;
case 1+1:
console.log(2)
}
VM1513:4 hello world
由于switch对于case的限制性不强,还有一种特别的switch语句,并不使用expression决定执行哪一个语句,而是通过外部的num决定,expression只是为switch中default之前的语句的执行提供了必要性条件
let num = 25;
switch(true){
case num < 0:
console.log('less than 0');
break;
case num > 10 && num < 20:
console.log('Between 10 and 20');
break;
case num > 20 && num < 30:
console.log('Between 20 and 30');
break;
default:
console.log('Not');
}
注意:switch的每一个比较每一个case语句的值时,使用的是全等操作符,并不会发生隐式转换
函数
函数是一个语言的核心组件,它可以封装代码,然后在任何地方任何时间执行
ECMAScript使用function关键字声明函数
基础语法:
function functionName(arg0,arg1...argN){
statements;
}
可以在作用域链能访问到任何地方通过函数名调用函数。
不需要指定是否返回 值,可以通过return来返回值或者空返回,空返回实际上是返回undefined;
函数只要碰到return,就会马上停止执行并退出,不执行return后面的语句
严格模式函数的一些限制:
不能使用eval或argument作为名称
函数的参数不能叫eval或者argument
两个命名参数不能拥有同一个名称
这些限制会导致程序抛出语法错误
第三章小结
JavaScript的核心特性在ECMA-262中以伪语言ECMAScript的形式来定义。
ECMAScript包含所有的基本语法、操作符、数据类型和对象,能完成基本的计算任务
但是没有提供输入和产生输出的机制。
ECMAScript基本元素:
ECMAScript的基本数据类型包括:Null、Undefined、Boolean、Number、String、Symbol
不区分整数值和浮点值,数值只有Number类型
Objcet是一种复杂数据类型,它是这门语言中所有对象的基类
提供非常丰富的C语言或类C语言的操作符、从其他语言借鉴而来的流控制语句
函数比较特别:
不需要指定函数的返回值,因为函数可以在任何地方任何时间返回值
不指定返回值的函数实际上会返回特殊值undefined
评论区