Topic
逻辑运算符
考虑下面的代码,哪个是正确的?
var a = true;
var b = 1;
- (a || b) === true;
- (b || a) === true;
答案:第一个正确。
💡 解析
| 运算符 | 语法 | 说明 |
|---|---|---|
逻辑与,AND(&&) | expr1 && expr2 | 若 expr1 可转换为 true,则返回 expr2;否则,返回 expr1。 |
| 逻辑或,OR(` | `) | |
逻辑非,NOT(!) | !expr | 若 expr 可转换为 true,则返回 false;否则,返回 true。 |
因此,上面代码中,(a || b) 表达式中的 a 是 true,则直接返回了 true,于是第一个正确。(b || a) 中的 b 是 1 可以转成 true,于是返回 b。但 1 === true 会返回 false,因此第二个选项不正确。
会被转换为 false 的表达式有:
null;NaN;0;- 空字符串(
""or''or ``); undefined。
使用双重非运算符(
!!)的一个场景,是显式地将任意值强制转换为其对应的布尔值,或者使用Boolean构造函数。
✅ 拓展:考虑下面的表达式,都会返回什么结果?
var a = 0 && "" && 1 && null;
var b = 0 || "" || 1 || null;
var c = false && (true || true);
var d = false && true || true;
var e = n5 = !!"";
var f = ({} || ![]) && 1 || !!false;
需要说明的是,这三个运算符都是从左到右进行运算的。首先先看第一个,全都是 && 运算符,&& 运算符第一个为真值就返回第二个值,不是真值则返回第一个。因此判断步骤如下:
var a = 0 && "" && 1 && null;
a = 0 && 1 && null; // 0 不是真值,则返回它本身
a = 0 && null; // 0 不是真值,则返回它本身
a = 0; // 0 不是真值,则返回它本身
第二项全是 ||,|| 运算符前一个是真值就返回这个值,不是真值就返回另一个值,于是:
var b = 0 || "" || 1 || null;
b = "" || 1; // 0 不是真值,就返回另一个:""
b = 1 || null; // "" 不是真值,就返回另一个:1
b = 1; // 1 是真值,返回自身
通过上面也能发现一个规律,当一个表达式中的运算符全是
||或者&&时,如果是||运算符,它的第一个值是真值时,后面的表达式就不用再看了,必定返回第一个值。例如:1 || "" || 2 || 3 || null,返回结果是1。而&&运算符与||刚好相反,只要前面的值是假值,后面的就不用在看了,最终返回的必定是这个假值。例如:null && 0 && 1 && 2 && "",返回的结果是null。
c 中,先算括号里的内容,(true || true) 必定返回 true,false && true 的前一项是假值,直接返回这个值:false,即:c == false。
d 中,false && true 返回 false,变成 false || true,前一个值是假值,则返回第二个值,因此 d == true。
e 中,!!"" "" 是假值,!"" 变成真值(true),!!"" 又变成了假值(false)。因此,e == false。
f 的表达式比较复杂,首先计算括号里的内容。{} 是真值,![] 是假值(此时转变成了 false),括号里其实是 {} || false,前一个是真值,于是返回第一个,然后就变成了 {} && 1 || !!false。{} && 1 中 {} 是真值,于是返回第二个,然后就变成了 1 || !!false,因为 1 是正值,于是直接返回该值。所以 f == 1。
数据类型
- 问:JavaScript 中有几种原始数据类型(有时候也叫基本数据类型)?
答:六种,它们是一种既非对象也无方法的数据。分别是:string、number、boolean、null、undefined、symbol。
- 问:除了原始数据类型还有哪些类型?
答:除了原始数据类型就是引用类型,即:Object 对象类型。它是内存中的可以被标识符(指针)引用的一块区域。{} !== {},比较的是地址。 引用类型主要有:Object、Array、Date、Set、Map、类型数组(比如 Int8Array)、WeakMap、WeakSet、JSON等。
因此,Object 加上上面的六种原始数据类型,ECMAScript 标准定义了 7 种数据类型。
✅ 拓展:第七种原始数据类型:BigInt
BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()。例如:
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n
const hugeString = BigInt("9007199254740991");
// ↪ 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff"); // 16进制
// ↪ 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111"); // 二进制
// ↪ 9007199254740991n
typeof hugeBin;
// ↪ bigint
// 使用 Object 包装后, BigInt 被认为是一个普通 "object" :
typeof Object(1n) === 'object'; // true
它在某些方面类似于 Number ,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。
可以和 BigInt 一起使用的操作符: +、*、-、**、% 。除 >>> (无符号右移)之外的位操作也可以支持。/ 操作符对于整数的运算也没问题。但该操作符结果会向零取整,也就是说不会返回小数部分。
const rounded = 5n / 2n; // 2n
var a = 1n + 3n; // 4n
比较
BigInt 和 Number 不是严格相等的,但是宽松相等的。
1n === 1; // false
0n == 0; // true
0n === 0n; // true
1n < 2; // false
1n <= 2; // true
Boolean(0n); // false
Boolean(1n); // true
!0n; // true
bigint 还可以存入数组中,并且能做排序操作:
const mixed = [4n, 6, -12n, 10, 4, 0, 0n];
mixed.sort();
// ↪ [-12n, 0, 0n, 10, 4n, 4, 6]
有关更多关于 BigInt 的知识可以阅读 MDN 上的文档:BigInt
字符转义
下面的代码会返回什么结果?
'\\\\\\'.replace(new RegExp('\\\\\\\\', 'gi'), '/');
答案:
/\
💡 解析
这道题看似是考察 replace 方法的用法,其实是考察正则表达式和字符串中的字符转义。new RegExp('\\\\\\\\') 返回值是这 样的:/\\\\/,本来八个 \,变成了四个。原因是在 js 字符串中,\ 是特殊字符,它用于转义特殊字符,\\ 在字符串中相当于一个 \。
当你在控制台上输入一个 var str = '\\\\\\' 时,发现 str 的值实际是 \\\,六个变成了三个。
当定义下面的表达式将会报错,原因是 \ 会把它后面的 ' 给转义。
var str = '\'; // Invalid or unexpected token
要想让一个字符串等于 ' 或者 " 或者 \,可以这么做:
var str = '\''; // str == '
var str = "\""; // str == "
var str = '""'; // str == ""
var str = '\\'; // str = \
var str = "'\""; // str = '"
var str = '/'; // str = /
var str = '\/'; // str = /
除了上面的转义字符之外,还有下面这些 :
\0空字符\n换行符\t水平制表符\f换页符\r回车符\b退格符\v垂直制表符\uXXXXunicode 码
最终这个代码变成了 '\\\'.replace(/\\\\/gi, '/')。字符转义转义完了,但是正则表达式中也需要字符转义,\ 同样也是用于转义特殊字符,\\ 在正则表达式中会匹配一个 \。因此 /\\\\/ 正则表达式其实匹配的是 \\,于是会把 \\\ 字符串中的 \\ 替换成 /,最终结果是 /\。
正则表达式中的字符转义:
\t匹配一个水平制表符(tab)\r匹配一个回车符(carriage return)\n匹配一个换行符(linefeed)\v匹配一个垂直制表符(vertical tab)\f匹配一个换页符(form-feed)[\b]匹配一个退格符(backspace)(不要与\b混淆,\b表示匹配一个单词边界)\0匹配一个 NUL 字符。不要在此后面跟小数点;
正则表达式中的 \ 对于那些通常被认为字面意义的字符来说,表示下一个字符具有特殊用处,并且不会被按照字面意义解释。例如 /b/ 匹配字符 'b'。在 b 前面加上一个反斜杠,即使用 /\b/,则该字符变得特殊,以为这匹配一个单词边界。
对于那些通常特殊对待的字符,表示下一个字符不具有特殊用途,会被按照字面意义解释。例如,* 是一个特殊字符,表示匹配某个字符 0 或多次,如 /a*/ 意味着 0 或多个 "a"。 为了匹配字面意义上的 * ,在它前面加上一个反斜杠,例如,/a\*/匹配 a*。
在正则表达式中,如果要匹配 *、[、]、{、}、(、)、-、^、$、\、|、?、+、/、\ 等一些在正则表达式中有特殊含义的字符时,应在前面加一个 \ 作转义。
变量提升
下面程序打印的结果是?
function fn(a){
console.log(a);
var a = 123;
console.log(a);
function a(){}
console.log(a);
console.log(b);
var b = function(){}
}
fn(2);
答案:function a()、123、123、undefined
💡 解析
在运行 fn 函数时,首先会扫描代码,把变量声明和函数声明提到函数顶部,因此上面代码就变成了:
function fn(a){
var a;
function a(){};
var b;
console.log(a); // function
a = 123;
console.log(a); // 123
console.log(a); // 123
console.log(b); // undefined
b = function(){}
}
这里需要注意的是:var b = function() 是函数表达式,而非函数声明。因此提升的是变量 b,它会默认等于 undefined。function b() 才是函数声明。
再看下一个例子:
console.log(fn);
function fn(fn){
console.log(fn);
var fn = 111;
function fn(){};
}
fn(222);
var fn = 123;
可能会出乎意料,可能会认为:fn 函数先提升,后又声明了 fn,此时打印 fn 的值不应该是 undefined 吗? 这里有一个误区,当一个变量有值但有声明了一次,这两个变量会合并成一个,值会保留,例如:
var aaa = 111;
var aaa;
console.log(aaa);
打印结果是 111,当你明确给第二个 aaa 变量赋为 undefined 时打印结果才是 undefined。
上面的问题也是,var fn = 123; 中 fn 变量会提升,发现已经有一个 fn 变量了,而且是个函数,于是 第一次打印 fn 就是一个函数。然后调用 fn 函数。
调用 fn 函数,fn 函数内部也需要变量提升,先提升 fn 变量,然后提升 fn 函数声明,于是打印出 fn 是一个函数。
function fn(fn){
function fn(){};
console.log(fn);
fn = 111;
}
✅ 拓展:ES6 中的 let、const 关键字
ES6 中可以使用 let、const 声明变量。使用这两个关键字将不会提升变量到代码块的顶部。因此,在变量声明之前引用这个变量,将抛出引用错误。被 let、const 声明的变量将从代码块一开始的时候就处在一个“暂时性死区”,直到这个变量被声明为止。而且使用 let 或者 const 不能重复声明变量。使用 let、const 必须先声明再赋值。
比如下面的例子:
function fn(n){
console.log("start i == ", i); // undefined
for(var i = 0;i < n;i ++){
setTimeout(function(){
console.log(i);
}, 100);
}
console.log("end i == ",i); // 3
}
fn(3);
定时器的打印结果是 3 3 3。而且循环两端也能打印出变量 i 的值,这是因为开始循环之前,i 被提升到了函数顶端。而 setTimout 执行完毕后会在最近的作用域中寻找变量 i,此时 i 已经变成了 3。而如果使用 let 声明变量 i,报错,表示 i 没有定义,说明变量不会提升,去掉两边的打印后,就不再报错,定时器就会输出 0 1 2。
let 不仅不会提升变量,声明的变量相当于声明了一个作用域,这个作用域被限制在块级中的变量、语句或者表达式中。
for(let i = 0;i < n;i ++>){}
for 循环就是一个语句,let 使得每次程序进入花括号就产生了一个块级作用域,相当于是每个 setTimeout 处在不同的作用域内,每个作用域的 i 值各不相同。然后打印出了 0 1 2。let 最好不要在 if 语句中使用,不然外部访问不到 if 语句内部声明的变量。在 switch-case 中声明变量时,别的 case 语句也访问不到:
let x = 1;
switch(x) {
case 0:
let foo;
break;
case 1:
let foo; // SyntaxError for redeclaration.
break;
}
闭包
一个经典的例子:
function fn(n){
for(var i = 0;i < n;i ++){
setTimeout(function(){
console.log(i);
}, 100);
}
}
fn(3);
答案:3 3 3