介绍
运算符包括赋值,比较,算数,位运算,逻辑,模板字符串,三元等。
空值合并
从 chrome 80 开始支持,空值合并运算符(??)是一个逻辑运算符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。
- 与布尔逻辑或运算符(||)不同,布尔逻辑或运算符会在左侧操作数为假值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为,比如为假值 ‘’、0 或 NaN 时同样会返回右侧操作数。1 
 2
 3
 4
 5
 6
 7null ?? 'default' // default 
 '' ?? 'default' // ''
 1 ?? 'default' // 1
 NaN ?? 'default' // NaN
 '' || 'default' // default
 NaN || 'default' // default
- 与布尔逻辑或(||)、布尔逻辑与(&&)运算符相同,当左表达式不为 null 或 undefined 时,不会对右表达式进行求值。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12function A() { console.log('函数 A 被调用了'); return undefined; } 
 function B() { console.log('函数 B 被调用了'); return false; }
 function C() { console.log('函数 C 被调用了'); return "foo"; }
 console.log( A() ?? C() );
 // 依次打印 "函数 A 被调用了"、"函数 C 被调用了"、"foo"
 // A() 返回了 undefined,所以运算符两边的表达式都被执行了
 console.log( B() ?? C() );
 // 依次打印 "函数 B 被调用了"、"false"
 // B() 返回了 false(既不是 null 也不是 undefined)
 // 所以右侧表达式没有被执行
- 目前,空值合并运算符和其他逻辑运算符之间的运算优先级/运算顺序是未定义的,将它们组合使用会抛出 SyntaxError。但是,可以使用括号来显式指定运算优先级。1 
 2
 3null || undefined ?? "foo"; // SyntaxError 
 true || undefined ?? "foo"; // SyntaxError
 (null || undefined ) ?? "foo"; // foo
可选链
从 chrome 80 开始支持,可选链运算符(?.)的功能类似于 . 链式运算符,不同之处在于,?. 运算符不必明确验证链中的每个引用是否有效,在引用为空 (null 或者 undefined) 的情况下不会引起错误。当尝试访问可能不存在的对象属性时,可选链运算符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链运算符是很有帮助的。
| 1 | obj.val?.prop | 
- 当在表达式中使用可选链时,如果左操作数是 null 或 undefined,表达式将不会被继续计算。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10const potentiallyNullObj = null; 
 let x = 0;
 const prop = potentiallyNullObj?.[x++];
 console.log(x); // x 将不会被递增,依旧输出 0
 potentiallyNullObj?.a.b; // 不会引起错误,因为在第一个可选链已经终止计算
 // 如果对可选链的一部分进行分组,则仍将计算后续的属性
 (potentiallyNullObj?.a).b; // TypeError: Cannot read properties of undefined (reading 'b')
- 不能用于赋值。1 
 2const object = {}; 
 object?.property = 1; // SyntaxError: Invalid left-hand side in assignment
- 不能用于未声明的变量。1 undeclaredVar?.prop; // ReferenceError 
- 可选链联合空值合并使用:1 
 2
 3const customer = {}; 
 const customerCity = customer?.city ?? "Unknown";
 console.log(customerCity); // Unknown
求幂
求幂运算符(**)返回将第一个操作数加到第二个操作数的幂的结果。
- 等效于 Math.pow,不同之处在于它也接受 BigInt 作为操作数。
- 求幂运算符是右结合的: a ** b ** c 等于 a ** (b ** c)。
- 不能将一个一元运算符(+/-/~/!/delete/void/typeof)放在基数前,这样会导致一个不明确的求幂表达式,抛出语法错误。
- 可以与等号结合,形成一个求幂赋值运算符。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 123 ** 4 // 81 
 10 ** -2 // 0.01
 2 ** 3 ** 2 // 512
 (2 ** 3) ** 2 // 64
 -(2 ** 2) // -4
 (-2) ** 2 // 4
 -2 ** 2 // SyntaxError
 let a = 3;
 a **= 2 // 9
 a **= 'hello' // NaN
数字取整
二进制按位非运算符(~)将每个二进制位都变为相反值(将二进制位码 0 变为 1,1 变为 0),可以简单记忆成,一个数与自身的取反值相加,等于 -1。
- 对一个整数连续两次二进制按位非运算,将得到它自身。
- 所有的位运算都只对整数有效,当二进制否运算遇到小数时,会将小数部分舍去,只保留整数部分。
- 所以,对一个小数连续进行两次二进制按位非运算,能达到取整效果。
- 近似等效于 Math.trunc,但 Math.trunc 对于 NaN 和非数字都返回 NaN,而 ~~ 都返回 0。
- 使用二进制否运算取整,是所有取整方法中最快的一种。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12~ 3 // -4 
 ~ -3 // 2
 ~ NaN // -1
 ~~3 // 3
 ~~-2.9 // -2
 ~~1.9999 // 1
 Math.trunc('a') // NaN
 Math.trunc(NaN) // NaN
 ~~'a' // 0
 ~~NaN // 0
标签函数
标签函数用来解析模板字符串。当我们使用模板字符串时,模板字符串和占位符表达式被传递给一个默认解析函数。默认函数仅执行字符串插值以替换占位符,然后将部分连接成单个字符串。标签函数即是提供自己的解析函数,在模板文字前加上函数名称(结果称为标记模板),模板文字会被传递给标签函数,然后可以在其中对模板文字的不同部分执行想要的任何操作。
- 标签函数的第一个参数是包含一个字符串值的数组,它的长度等于占位符表达式的数量加一,因此总是非空的。
- 为了进一步保证数组值的稳定性,第一个参数和它的 raw 属性都被冻结了,所以不能以任何方式改变它们。
- 其余的参数是模板字符串中的占位符表达式(${expression})。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16const person = "Mike"; 
 const age = 28;
 function myTag(strings, personExp, ageExp) {
 const str0 = strings[0]; // "That "
 const str1 = strings[1]; // " is a "
 const str2 = strings[2]; // "."
 const ageStr = ageExp > 99 ? "centenarian" : "youngster";
 // 还可以返回使用默认模板文本构建的字符串
 return `${str0}${personExp}${str1}${ageStr}${str2}`;
 }
 const output = myTag`That ${person} is a ${age}.`;
 console.log(output); // That Mike is a youngster.
- 标签函数甚至不需要返回字符串。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20function template(strings, ...keys) { 
 return (...values) => {
 const dict = values[values.length - 1] || {};
 const result = [strings[0]];
 keys.forEach((key, i) => {
 const value = Number.isInteger(key) ? values[key] : dict[key];
 result.push(value, strings[i + 1]);
 });
 return result.join("");
 };
 }
 const t1Closure = template`${0}${1}${0}!`; // 相当于 template(["","","","!"],0,1,0)
 t1Closure("Y", "A"); // YAY!
 const t2Closure = template`${0} ${"foo"}!`; // 相当于 template([""," ","!"],0,"foo");
 t2Closure("Hello", { foo: "World" }); // Hello World!
 // 相当于 template(["I'm ", ". I'm almost ", " years old."], "name", "age");
 const t3Closure = template`I'm ${"name"}. I'm almost ${"age"} years old.`;
 t3Closure({ name: "MDN", age: 30 }); // I'm MDN. I'm almost 30 years old.
- 标签也不必是函数,可以是属性访问、函数调用、任何优先级大于 16 的表达式,甚至是另一个标记模板。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11console.log`Hello`; // [ 'Hello' ] 
 console.log.bind(null, 2)`Hello`; // 2 [ 'Hello' ]
 new Function("console.log(arguments)")`Hello`; // [Arguments] { '0': [ 'Hello' ] }
 function recursive(strings, ...values) {
 console.log(strings, values);
 return recursive;
 }
 recursive`Hello``World`;
 // [ 'Hello' ] []
 // [ 'World' ] []
- 可选链会引发语法错误。1 console?.log`Hello`; // SyntaxError: Invalid tagged template on optional chain 
- 一个模板字符串标签函数始终使用完全相同的字符串数组参数,无论模板字符串被解析多少次。1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16const callHistory = []; 
 function tag(strings, ...values) {
 callHistory.push(strings);
 return {}; // 返回一个新对象
 }
 function evaluateLiteral() {
 return tag`Hello, ${"world"}!`;
 }
 console.log(callHistory[0] === callHistory[1]); // true
 console.log(evaluateLiteral() === evaluateLiteral()); // false 每次返回的是一个新对象
 const strings = callHistory[0], { raw } = strings;
 console.log(Object.isExtensible(strings), Object.isExtensible(raw)); // false false
参考资料
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 张坤的博客!
 评论