地址:前端面试题库
【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!
无论是JavaScript还是其他语言,运算符是基础,表达式和语句中都可能存在运算符。下面就列举一些JavaScript中一些你也许不知道的运算符的作用及使用场景。
如果是单目运算(也称一元运算)即一个操作数,会将其 隐式转换 成number
类型:
// 获取当前时间戳
new Date().getTime() // 1681607509065
Date.now() // 1681607509065
// better
+new Date() // 1681607509065
如果有两个操作数,+
会先将两边的操作数 隐式转换 成基本数据类型,并按照以下顺序判断:
string
,则将另一侧也转成string
:[1, 2, 3] + '' // '1,2,3'
BigInt
则执行BigInt
的加法,如果只有一侧是BigInt
,则抛出TypeError
错误,因为BigInt
不支持单目+
运算:10n + 20n; // → 30n
+ 10n // → TypeError: Cannot convert a BigInt value to a number
number
,执行数字加法:let a = {
valueOf: () => false
}
a + true // 1
**
是ES7新增的幂运算符,可以代替Math.pow()
:
Math.pow(2, 3) // 8
// better
2 ** 3 // 8
**
运算符的一个特点是右结合,而不是常见的左结合。多个幂运算符连用时,是从最右边开始计算的:
2 ** 2 ** 3 // 256
// 相当于
2 ** (2 ** 3) // 256
可以使用圆括号运算符()
来控制表达式的优先级:
(2 ** 2) ** 3 // 64
condition ? exprIfTrue : exprIfFalse
在简单的条件判断下,可以使用三元运算? :
代替if else
:
if (age >= 18) {
console.log('成年')
} else {
console.log('未成年')
}
// better
age >= 18 ? console.log('成年') : console.log('未成年')
当然条件运算符也可以链接使用,它也满足右结合:
age >= 70 ? console.log('从心所欲')
: age >= 60 ? console.log('耳顺')
: age >= 50 ? console.log('知天命')
:age >= 40 ? console.log('不惑')
: age >= 30 ? console.log('立')
: console.log('啥也不是')
注意:condition 如果不是一个boolean 将会发生隐式转换,如果是一个真值Truthy
,就会执行exprIfTrue
:
let studyList = [{ name: 'jude', age: 25 }, { name: 'andy', age: 24 }]
let person = studyList.find(item => item.age < 18) // undefined
person ? console.log('要开花') : console.log('要发芽') // '要发芽'
补充一下Truthy和Falsy:
假值(Falsy): false
、0
、-0
、0n
、""
、null
、undefined
和 NaN
真值(Truthy): 除了假值以外的都是真值
逗号运算符可以创建一个以上的复合表达式,整个复合表达式的值为最右侧的表达式的值:
x => {
x = x + 1
return x
}
// better
x => (x++, x)
// or
x => ++x
最常用于在for循环中提供多个参数:
for (let i = a.length - 1, j = b.length - 1; i >= 0 || j >= 0; i--, j--) {
//do sth
}
取余运算符返回左侧操作数除以右侧操作数的余数,返回值符号与被除数符号保持一致:
13 % 5 // 3
13 % -5 // 3
13.5 % 5 // 3.5
-13 % 5 // -3
NaN % 2 // NaN
Infinity % R // NaN // R表示任意一个实数
R % Infinity // R
Infinity % Infinity // NaN
在一些算法题中可以得到应用:
已知2019年的第一天是周四,求第x天是周几:
function getDay(x) {
return [4, 5, 6, 0, 1, 2, 3][x % 7]
}
我们知道当读取null
或者undefined
的属性时,js会抛出一个TypeError
:
null.name // TypeError: Cannot read properties of null (reading 'name')
undefined.name // TypeError: Cannot read properties of undefined (reading 'name')
ES2020
新增可选链运算符,可以作用在上述情况,并短路返回undefined
:
null?.name // undefined
undefined?.name // undefined
针对函数调用的可选链:
let personList = [
{ name: 'jude' }
]
// personList[0].sleep() // person.sleep is not a function
if (personList[0].sleep) {
personList[0].sleep()
}
// better
personList[0].sleep?.() // undefined
// 如果前面的对象也可能不存在的话:
personList[1]?.sleep?.() // undefined
// 当然如果,该属性虽然存在但是不是一个函数,就会报is not a function:
personList[0]?.name() // TypeError: personList[0]?.name is not a function
也可用于方括号属性访问器 和 访问数组元素:
let propertyName = 'name'
null?.[propertyName] // undefined
let arr = []
arr?.[0] // undefined
要注意的是可选链运算符不可用于赋值操作:
({})?.name = 'jude' // SyntaxError: Invalid left-hand side in assignment
从左往右,&&
找Falsy
, ||
找Truthy
,找到了则将返回找到的值,否则返回下一个:
1 && {} && ' ' && NaN && undefined // NaN
'' || 0 || null || [] || 1 // []
因此它们都属于短路运算符:
&&
可以用作函数的判断执行:
if (age >= 22) {
work()
}
// or
age >= 22 && work()
||
可以用来设置备用值:
name => {
if (name) {
return name
} else {
return '未知'
}
}
// better
name => name ? name : '未知'
// or
name => name || '未知'
以上写法都会判断name
是否是Falsy
来设置其默认值,而ES6
的默认值只会判断是否是undefined
:
(name = '未知') => name
// 相当于
name => name === undefined ? name : '未知'
// such as
((name = '未知') => name)(null) // null
((name = '未知') => name)(0) // 0
((name = '未知') => name)(undefined) // '未知'
((name = '未知') => name)() // '未知'
要注意的是&&
的优先级高于||
:
1 || 1 && 0 // 1
空值合并运算符??
当且仅当左侧操作数为null
或者undefined
时才会返回右侧操作数。
上边说到逻辑或运算符||
可以用来设置备用值,但其实有隐患:
function getScore(x) {
x = x || '未知'
console.log('张三的英语成绩是:' + x)
}
getScore(0) // '张三的英语成绩是:未知'
逻辑或运算符||
会在左侧操作数为Falsy
时返回右侧操作数。而0
,''
也属于Falsy,但是实际某些场景中它们正想要的结果,如上代码。
空值合并运算符??
解决了这个问题:
function getScore(x) {
x = x ?? '未知'
console.log('张三的英语成绩是:' + x)
}
getScore(0) // '张三的英语成绩是:0'
常与可选链运算符?.
一起用:
let person
person?.name ?? '未注册' // '未注册'
逻辑非运算符!
,会检测操作数是真值还是假值,如果是Truthy
则返回false
,如果是Falsy
则返回true
。 而双飞运算符!!
,在这基础上再取反,其作用相当于Boolean()
:
Boolean('') // false
// or
!!'' // false
位运算符将操作数看作是4byte(32bit)
的二进制串。在这基础上进行运算,但最终返回十进制数字。
x << n
会将 x
转成 32
位的二进制,然后左移 n
位,左侧越界的位被丢弃:
10 * 2³ // 80
// better
10 << 3 // 80
x >> n
会将 x
转成 32
位的二进制,然后右移 n
位,右侧越界的位被丢弃:
Math.floor(a / Math.pow(2,n))
// or
Math.floor(a / 2 ** n)
// better
a >> n
这在二分查找可以得以应用:
function BinarySearch(arr, target) {
const n = arr.length
let left = 0,
right = n - 1
while (left <= right) {
// let mid = Math.floor((left + right) / 2)
// better
let mid = (left + right) >> 1
if (arr[mid] === target) {
return mid
} else if (arr[mid] > target) {
right = mid - 1
} else {
left = mid + 1
}
}
return -1
}
按位异或运算符 ^
将两边的操作数都转成32位的二进制数后,逐一比较每一位,有且仅有一个1
时,则返回1
:
3 ^ 5 // 6
// 00000000000000000000000000000011 // 3
// 00000000000000000000000000000101 // 5
// 00000000000000000000000000000110 // 6
可以用于交换两个数值:
let a = 3,
b = 5;
let temp = a
a = b // 5
b = temp //3
// 以上交换使用额外的内存temp,而^可以in-place原地交换:
// better
a = a ^ b
b = a ^ b // 5
a = a ^ b // 3
异或运算符^
满足以下三个性质:
下面是一道利用上述三个性质求解的一道算法题:
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。(你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。)
/**
* @param {number[]} nums
* @return {number}
*/
function singleNumber (nums) {
let ans = 0
for (let i = 0; i < nums.length; i++) {
ans ^= nums[i]
}
return ans
};
singleNumber([4, 1, 2, 1, 2]) // 4
此外异或运算符可以用于简单的加密和解密操作。例如,我们可以将一个字符串的每个字符和一个密钥进行异或运算,得到一个加密后的字符串,然后再将加密后的字符串和密钥进行异或运算,就可以得到原来的字符串:
let str = 'Hello World'
let key = 123
let encrypted = ''
for (let i = 0; i < str.length; i++) {
encrypted += String.fromCharCode(str.charCodeAt(i) ^ key)
}
console.log(encrypted) // '3[, '
let decrypted = ''
for (let i = 0; i < encrypted.length; i++) {
decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ key)
}
console.log(decrypted) // 'Hello World'
按位非运算符~将操作数转换成32位有符号整型,然后按位取反:
重点:
const a = 5; // 32位二进制:00000000000000000000000000000101
// 取反后:11111111111111111111111111111010(补码)
~a // -6
总之: 按位非运算时,任何数字 x
的运算结果都是 -(x + 1)
。
因此可以用~
代替!== -1
的判断:
// == -1 的写法不是很好称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里指用-1作为失败的返回值,这些细节应该屏蔽调。————出自《你不知道的JavaSript(中卷)》
if (str.indexOf('xxx') !== -1) {}
// better
if (~str.indexOf('xxx')) {}
扩展符...
可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。
只会复制目标对象的自有且可枚举属性:
let _a = { name: 'jude' }
let a = Object.create(
_a, // 原型链上的属性name,不自有
{
myName: { // 自有属性myName,可枚举
value: '张三',
enumerable: true
},
age: { // 自由属性age,不可枚举
value: 30,
enumerable: false
}
}
)
let b = {...a} // {myName: '张三'}
上述代码中,使用 Object.create() 将_a
作为a
的原型对象,因此_a
上的属性name
对于a
来说不是自有属性;同时给自己创建了自由属性myName
和age
,但是age
设置为不可枚举。最后使用扩展符实现对a
对象的克隆,只克隆了myName
这个自有且可枚举属性。这和 Object.assign() 的结果一样:
let c = Object.assign({}, a) // {myName: '张三'}
用于(浅)克隆数组,对于复杂数据类型的数组项,只会克隆其引用:
let arr = [{ a: 1 }]
let copyArr = [...arr] // [{ a: 1 }]
arr[0].a = 2
copyArr // [{ a: 2 }]
用于连接数组:
let arr1 = [0, 1, 2]
let arr2 = [3, 4, 5]
let arr3 = arr1.concat(arr2) // [0, 1, 2, 3, 4, 5]
// better
let arr4 = [...arr1, ...arr2] // [0, 1, 2, 3, 4, 5]
用于函数调用:
function fn(a, b, c) { }
let args = [0, 1, 2]
fn.apply(null, args)
// better
fn(...args)
'123'.split('') // ['1', '2', '3']
// or
[...'123'] // ['1', '2', '3']
类数组对象是具有.length属性的对象。
Array.from()从可迭代或类数组对象创建一个新的浅拷贝的数组实例。 而在数组或函数参数中使用展开语法时,该语法只能用于 可迭代对象:
let fakeArray = {
0 : 1,
1 : 2,
2 : 3,
length: 3
}
Array.from(fakeArray) // [1, 2, 3]
[...fakeArray] // TypeError: fakeArray is not iterable
或许你会问[...'123']
不也是在数组中使用展开语法吗,而'123'
是基本数据类型啊,怎么会可迭代呢?
其实,引擎会将'123'
包装成String
对象,而String
对象上封装了Symbol.iterator()
方法:
let str = '123';
let strIterator = str[Symbol.iterator]();
strIterator.next() // {value: '1', done: false}
strIterator.next() // {value: '2', done: false}
strIterator.next() // {value: '3', done: false}
strIterator.next() // {value: undefined, done: true}
如果函数最后一个参数以...
为前缀,则它将是由剩余参数组成的真数组,而arguments是伪数组:
function fn (a, ...b) {
console.log(b)
console.log(arguments)
}
fn(1, 2, 3)
// [2, 3]
// Arguments(3) [1, 2, 3, callee: (...), Symbol(Symbol.iterator): ƒ]
剩余参数...
可以被解构:
function fn(...[a, b, c]) {
return a + b + c;
}
fn(1) // NaN (b and c are undefined)
fn(1, 2, 3) // 3
fn(1, 2, 3, 4) // 6
剩余参数...
必须在末尾:
function fn (a, ...b, c) {} // SyntaxError: Rest parameter must be last formal parameter
在解构赋值中,剩余属性...
可以获取数组或对象剩余的属性,并存储到新的数组或对象中:
const { a, ...others } = { a: 1, b: 2, c: 3 }
console.log(others) // { b: 2, c: 3 }
const [first, ...others2] = [1, 2, 3]
console.log(others2) // [2, 3]
同样,这里的...
必须在末尾:
let [a , ...b , c] = [1, 2, 3] // SyntaxError: Rest element must be last element
let { a, ...b, c } = { a: 1, b: 2, c: 3 } // SyntaxError: Rest element must be last element
何为优雅,代码少不一定优雅,优雅要在简洁的基础上保证一定的可读性。而可读性是基于团队的,如果团队水平很高,对于某些"隐式转换"已经形成肌肉记忆,那对他们来说就不是“隐式”的,就具备可读性。
而对这些基础知识足够掌握之后,起码能有选择得根据团队整体风格灵活变通。
以上就是JavaScript中的关于部分运算符的小知识,希望能对大家有所帮助。
地址:前端面试题库
【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!