【面试题】Javascript的这些运算符,你都都掌握哪些?

简介: 【面试题】Javascript的这些运算符,你都都掌握哪些?

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!

theme: devui-blue highlight: a11y-light

无论是JavaScript还是其他语言,运算符是基础,表达式和语句中都可能存在运算符。下面就列举一些JavaScript中一些你也许不知道的运算符的作用及使用场景。

+ 加法运算符

如果是单目运算(也称一元运算)即一个操作数,会将其 隐式转换number类型:

// 获取当前时间戳
new Date().getTime() // 1681607509065
Date.now() // 1681607509065
// better
+new Date() // 1681607509065

如果有两个操作数,+会先将两边的操作数 隐式转换 成基本数据类型,并按照以下顺序判断:

  1. 如果有一侧是string,则将另一侧也转成string
[1, 2, 3] + '' // '1,2,3'
  1. 如果两侧都是BigInt则执行BigInt的加法,如果只有一侧是BigInt,则抛出TypeError错误,因为BigInt不支持单目+运算:
10n + 20n; // → 30n
+ 10n // → TypeError: Cannot convert a BigInt value to a number
  1. 否则将两边再转换成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):false0-00n""nullundefinedNaN

真值(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天是周几:

null.name // TypeError: Cannot read properties of null (reading 'name')
undefined.name //  TypeError: Cannot read properties of undefined (reading 'name')

?. 可选链运算符

我们知道当读取null或者undefined的属性时,js会抛出一个TypeError

1. null.name // TypeError: Cannot read properties of null (reading 'name')
2. 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

异或运算符^满足以下三个性质:

  1. 任何数和0做异或运算,结果仍然是原来的数字:�⊕0=�x⊕0=x
  2. 任何数和自身做异或运算,结果是0:�⊕�=0x⊕x=0
  3. 异或运算满足交换律和结合律:�⊕�⊕�=�⊕�⊕�=�⊕(�⊕�)=�⊕0=�x⊕y⊕x=y⊕x⊕x=y⊕(x⊕x)=y⊕0=y

下面是一道利用上述三个性质求解的一道算法题:

给你一个 非空 整数数组 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位有符号整型,然后按位取反:

重点:

  • 计算机存储数字都以补码的形式存储(至于为什么涉及到电路,只能做加运算,不能减,因此设计了反码和补码来存储负数)
  • 正数的反码和补码等于原码
  • 负数的补码等于原码取反+1
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来说不是自有属性;同时给自己创建了自由属性myNameage,但是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中的关于部分运算符的小知识,希望能对大家有所帮助。

 

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!

相关文章
|
3月前
|
JavaScript 前端开发 Java
JavaScript基础语法(运算符)
JavaScript基础语法(运算符)
32 0
|
4月前
|
JavaScript 前端开发 开发者
从0开始学习JavaScript--JavaScript 表达式与运算符
JavaScript中的表达式和运算符是构建逻辑、进行计算的基础。本文将深入研究JavaScript中各类表达式,包括算术表达式、关系表达式、逻辑表达式,以及运算符的使用方法,并通过丰富的示例代码来帮助读者更全面地了解和运用这些概念。
|
4月前
|
JavaScript 前端开发
JavaScript变量、数据类型、运算符及类型转换
JavaScript变量、数据类型、运算符及类型转换
40 0
|
1月前
|
JavaScript
js开发:请解释什么是ES6的扩展运算符(spread operator),并给出一个示例。
ES6的扩展运算符(...)用于可迭代对象展开,如数组和对象。在数组中,它能将一个数组的元素合并到另一个数组。例如:`[1, 2, 3, 4, 5]`。在对象中,它用于复制并合并属性,如`{a: 1, b: 2, c: 3}`。
12 3
|
1月前
|
JavaScript 前端开发 程序员
编程笔记 html5&css&js 074 Javascript 运算符
编程笔记 html5&css&js 074 Javascript 运算符
|
3月前
|
JavaScript 前端开发
什么是 JavaScript 中的 Spread 运算符
什么是 JavaScript 中的 Spread 运算符
28 0
|
3月前
|
JavaScript 前端开发
JS-基础:JavaScript运算符 个人总结
JS-基础:JavaScript运算符 个人总结
20 0
|
3月前
|
前端开发 JavaScript 算法
JavaScript不常见但好用的运算符
JavaScript不常见但好用的运算符
|
3月前
|
前端开发 JavaScript
【Javascript保姆级教程】运算符
【Javascript保姆级教程】运算符
|
8月前
|
JavaScript
【js学习笔记二十二】...扩展运算符
【js学习笔记二十二】...扩展运算符
33 0