第二章 运算符

209 阅读17分钟

运算符概述

操作符和操作数

操作符:运算符,即参与运算的符号

操作数:参与运算的数据,每个数据称为一个“元”或“目”

“长相一样”的操作符出现在不同的位置,可能具有不同的含义

常见操作符:

  • =:赋值操作符,将右边的数据赋值给左边

  • .:属性访问操作符,用于访问对象的属性

  • []:属性访问操作符,用于访问对象的属性

  • ():函数调用操作符,用于调用函数

    ()也可用于提升优先级

运算符的分类

按操作数的数量区分:

  • 一元运算符(一目运算符)

    +-++--

  • 二元运算符(双目运算符)

    +-*/%=

  • 三元运算符(三目运算符)

    ?:

按功能区分:

  • 算术运算符
  • 比较运算符
  • 逻辑运算符
  • 位运算符
  • 其它

表达式

表达式 = 操作符 + 操作数

每个表达式都有一个运算结果,称之为返回值,返回值的类型叫做返回类型

由于表达式有返回值,因此所有的表达式都可以当做数据进行使用

  • 赋值表达式,返回值为所赋的值

    var a;
    console.log(a = 1);			// 1
    

    利用这个特点,可以实现多次连续的赋值

    var a = b = c = 1;
    

    注意:上面的变量中,只有a进行了声明,b和c均是未声明直接赋值

    对于一个声明 + 赋值的表达式,返回结果为undefined

    console.log(var a = 1);			// undefined
    
  • 属性访问表达式,返回值为属性的值

  • 函数调用表达式,返回值为函数的return的内容

    console.log函数的返回值为undefined

Chrome浏览器控制台的环境是REPL环境

REPL:Read Eval Print Loop,读-执行-打印-循环

当直接在该环境中书写代码时,除了会运行代码外,还会输出该表达式的返回值

算术运算符

  1. +-*/

    加 、减、乘、除

    JS中数字运算的结果可能是不精确的

    在JS的除法运算中,允许除数为0

    除数为0,被除数为正值,得到结果Infinity,表示正无穷,属于number类型

    除数为0,被除数为负值,得到结果-Infinity,表示负无穷

    除数为0,被除数也为0,得到结果NaN(Not a Number),属于number类型

    除数为0,被除数为NaN,得到结果NaN

    注意:

    • NaN表示不是一个数,其没有正负之分
    • 除加法运算外,NaN和任何类型值进行算术运算,得到的结果都是NaN
    • 在加法运算中,NaN和数字类型值进行加法运算,得到的结果都是NaN;跟引用值或字符串进行相加,得到的结果是字符串
    • Infinity - Infinity得到NaN、Infinity + Infinity得到Infinity
    • isNaN函数,用于判断一个数据是否是NaN,返回类型为boolean
    • isFinite函数,用于判断一个数据是否是有限的数字,返回类型为boolean,对于NaN或非数字或无穷,统一返回false
  2. %

    取余

    取余运算中,返回值的正负性与被除数正负性有关,与除数正负性无关

    10 % 3得到1,-10 % 3得到-1,10 % -3得到1

  3. +-

    正、负

  4. ++--

    自增、自减

  5. **

    求幂

    5**35 * 5 * 5,结果为125

细节

当操作数不全为数字时,会进行下面的类型转换

除加法之外的算术运算:

  • 原则:统一先将操作数都转换为数字类型,然后再对数字进行算术运算

  • 原始类型转换为数字类型

    boolean:true转为1,false转为0

    string:先将字符串中的首尾空白字符去除,再将去除后的字符串转换为数字

    • 如果字符串内部是一个正确的数字,则直接转为数字
    • 如果字符串是空串,则转为0
    • 如果字符串是"Infinity"或"-Infinity",则转为Infinity或-Infinity
    • 如果字符串是"NaN"或者内部是一个非数字,则转为NaN

    注意:

    • 不能把字符串内部的东西当做表达式,如+"2*2"得到NaN而不是4

    • 特别记忆:+"0000±1"得到NaN+"+00001"得到1+"-00001"得到-1

    undefined:转为NaN

    null:转为0

  • 引用值需要先转换为原始值,然后再将原始值转为数字类型

    任何运算中涉及到的引用值转原始值,其具体过程请看第五章的Object.prototype.valueOf()小结

加法运算:

  • 当两边都是原始值,但都不是字符串,会将原始值转为数字,然后相加

  • 当一边为字符串,另一边为非字符串,会将非字符串数据转为字符串数据,然后再进行字符串拼接

    非字符串原始值:直接给原始值外套上引号得到字符串

    引用值:将引用类型转换为原始值,让原始值与字符串做加法

  • 两边都是引用值,需要把引用值都转换为原始值,然后再进行原始值之间的加法运算

当直接使用对象字面量{}配合运算符进行运算时,JS会把{}解析为一个代码块,而不会把它当做一个对象,因此这样的语句会导致语法错误

使用()包裹住语句即可正确解析,因为加上括号后,JS就会认为这是一个表达式,因此会把{}解析为对象而非代码块

{} * 5;			// 语法错误
({}) * 5;		// NaN

自增和自减

基本功能

++:将某个变量的值自增1

--:将某个变量的值自减1

自增自减表达式

x++:将变量x自增1,表达式的返回值是自增之前的值

++x:将变量x自增1,表达式的返回值是自增之后的值

x--:将变量x自减1,表达式的返回值是自减之前的值

--x:将变量x自减1,表达式的返回值是自减之后的值

优先级

级别运算符
++--
*/%
+-

包含自增和自减的复杂表达式的运算技巧:

  1. 从左到右对表达式进行扫描

    若扫描到变量且其左右不含自增或自减操作符,则将变量的值取出,并覆盖到变量的位置

    若扫描到变量且左右包含自增或自减操作符,则让先变量自增或自减,再让自增或自减表达式的返回值覆盖原自增或自减表达式的位置

  2. 之后对新表达式进行正常的数学运算

练习:

  1. var x = 1;
    console.log(x + x++ * ++x);
    

比较运算符

大小比较:><>=<=

相等比较:==!====!==

比较运算表达式的返回类型为boolean

算术运算符的优先级高于比较运算符

字符串之间比较规则

字符串之间的比较,比较的是字典序大小:

  1. 从头开始逐个对两个字符串的相同位置处的字符进行比较,比较的是字符的字符编码
  2. 若上一步比较结果相等,则继续向后比较;若比较结果不同,则比较结束,字符编码大的一方的字符串大于另一个字符串
  3. 比较时若一个字符串已经结束,另一个字符串没有结束,则长字符串大于短字符串;若两者都已结束,则两者相等

注意:将字符串转为数字并不是指将字符串中的字符转换为对应的字符编码,尽管编码也是数字。字符编码是针对单个字符的,而字符串转换为数字是针对整个字符串的。前者会得到零个或多个具体的数(看字符串的长度和当前字符),而后者可能得到一个具体的数,也可能得到NaN(看字符串中的内容)

大小比较

><>=<=

  1. 若比较双方都是字符串,则按照字符串的比较规则进行大小比较

  2. 若比较双方都是原始值,但不全为字符串,则将它们转换都为数字后再进行大小比较

    "1" > 10得到falsenull > -1得到true

  3. NaN与任何值进行大小比较,得到的都是false

    undefined > -1得到false

  4. Infinity是JS中最大的数,它比任何有限数字都大,且不会小于任何一个数

    -Infinity是JS中最小的数,它比任何有限数字都小,且不会大于任何一个数

    Infinity < Infinity + 1得到false

  5. 若双方只要有一方是引用值,则先将引用值转换为原始值,再进行原始值之间的比较

相等比较

==!====!==

相等==与不相等!=

  1. 若两端都是原始值且数据类型相同,直接比较两个数据本身是否相同

  2. 若两端都为引用值,则比较双方地址是否相同

  3. 若两端数据类型不同,且不全为引用值时:

    nullundefined

    • null == undefined得到true
    • null == null得到true
    • undefined == undefined得到true
    • 除上面三者外,nullundefined不等于其它任何类型,即null(undefined) == ?得到false

    其它原始类型:先统一转为数字,再进行相等比较

    NaN不等于任何值,包括NaN,即NaN == ?得到false

    Infinity-Infinity只与自身或自身加上有限数相等,其它情况均不等

    • Infinity == Infinity得到true
    • Infinity == Infinity + 1得到true

    +0 == -0得到true

    一端引用值,一端为原始值:先将引用值转为原始值,然后再进行原始值的相等比较

严格相等===与严格不相等!==

===:比较两端的数据类型以及数据是否相同,若数据类型或数据不一样,则直接返回false

!==:比较两端的数据类型或数据是否不相同,若数据类型或数据不一样,则直接返回true

  1. 若两端为相同原始类型的原始值,则比较的是具体的值

    若两端为相同引用类型的引用值,则比较的是地址

  2. NaN不等于任何值,包括NaN,即NaN !== ?得到true

  3. Infinity-Infinity只与自身或自身加上有限数相等,其它情况均不等

    Infinity === Infinity得到true

    Infinity === Infinity + 1得到true

  4. null === null得到true

    undefined === undefined得到true

    null === undefined得到false

  5. +0 === -0得到true

建议使用严格相等和严格不相等对两个数据进行比较,而不要使用相等和不相等

逻辑运算符

&&,或||,非!

逻辑与和逻辑或的优先级低于比较运算符,但非运算符的优先级比乘除还高

格式:表达式1 && 表达式2

步骤:

  1. 对表达式1的返回值进行布尔判定

    判定为假的数据有:false、""、0、NaN、null、undefined

    其它均判定为真

  2. 如果表达式1的判定结果为假,则直接返回表达式1的结果,而不执行表达式2;否则,返回表达式2的结果

格式:表达式1 || 表达式2

步骤:

  1. 对表达式1的返回值进行布尔判定
  2. 如果表达式1的判定结果为真,则直接返回表达式1的结果,而不执行表达式2;否则,返回表达式2的结果

注意:或的优先级要低于与,如1 || 0 && 2得到1而不是2

格式:!数据

含义:将数据的布尔判定结果取反

非运算符表达式的返回类型一定为boolean

注意:非运算符具有较高的优先级,其优先级比乘除高。如:!2 * 2得到0而不是false

三目运算符

格式:表达式1 ? 表达式2 : 表达式3

步骤:

  1. 对表达式1进行布尔判定
  2. 如果表达式1的判定结果为真,则直接返回表达式2的结果,而不执行表达式3;否则,直接返回表达式3的结果,而不执行表达式2

运算符补充

复合运算符

+=-=*=/=

*=为例,x *= n等价于x = x * n

var x = 2;
x *= x++ + 2;		// 等价于x = x * (x++ + 2)

void运算符

void运算符属于一元运算符

作用:运行表达式,然后返回undefined

格式:

  • void 表达式
  • void(表达式)

注意:void运算符的优先级较高,因此推荐使用括号的格式

typeof运算符

typeof运算符属于一元运算符

作用:运行表达式,然后返回表达式的结果类型字符串

格式:

  • typeof 表达式
  • typeof(表达式)

注意:typeof运算符的优先级较高,因此推荐使用括号的格式

常见的类型判断:

  • typeof undefined得到"undefined"
  • typeof null得到"object"
  • typeof {}得到"object"
  • typeof []得到"object"
  • typeof function(){}得到"function"
  • typeof ()=>{}得到"function"
  • typeof NaN得到"number"
  • typeof Infinity得到"number"
  • typeof class {}得到"function"
  • typeof BigInt()得到"bigint"
  • typeof Symbol()得到"symbol"

注意:

  • 对未定义的变量使用typeof不会导致报错,而是会得到"undefined"
  • 对不合法的标识符使用typeof会导致报错,如typeof 123abc
  • 在let或const定义变量位置的前面使用typeof检测变量类型,会报错而不是得到"undefined"

逗号运算符

格式:表达式1, 表达式2

作用:依次运行两个表达式,然后返回第二个表达式的结果

注意:逗号运算符的优先级比赋值运算符低,因此如果要将逗号表达式的结果赋值给某个变量,需要给逗号表达式的整体加上括号

in运算符

格式:"propName" in obj

作用:判断propName属性是否在obj或obj的原型链上

JS中数字的存储

问题

  1. JS中的小数运算是精确的吗?

    不一定

  2. JS中的整数运算是精确的吗?

    不一定

  3. JS中表示的整数是连续的吗?

    不是

  4. JS中表示的最大数字是多少?

    通过Number.MAX_VALUE查看

计算机底层采用二进制对数字进行存储,而某些十进制小数,其转换为二进制后得到的可能是一个二进制的无限循环小数,而计算机的存储空间有限,因此计算机在存储层面上,就无法对所有十进制数做到精确存储

在JS中,不管是整数还是小数,都统一采用的是浮点数存储法对数字进行存储,因此对于整数运算,也会发生精度丢失的现象

JS采用IEEE 754标准的双精度浮点型格式对数字进行存储

IEEE 754标准下的双精度浮点数占用连续的64bit的空间,其中第1位表示符号位,第2 ~ 12位表示指数,第13 ~ 64位表示尾数

注意:符号位,指数和尾数均为二进制形式

标准规定,一个能正常表示的数字,其指数部分最小为00000000001,最大为11111111110,全0和全1用于表示特殊情况:

  • 当指数为全0,尾数为全0时,表示数字0

  • 当符号位为0,指数为全1,尾数为全0,表示正无穷Infinity

    当符号位为1,指数为全1,尾数为全0,表示负无穷-Infinity

  • 当指数为全1,尾数不为全0,表示NaN

安全数字:安全数字n是指一个整数,从1开始加1加到n+1的过程是必须连续的,中间不会出现跳跃。使用Number.MAX_SAFE_INTEGER查看IEEE 754双精度型的最大安全数字,其格式为0 10000110011 111...111,十进制为2^53^ - 1

浮点数的精度取决于尾数的位数

IEEE 754的双精度浮点型尾数占52位,要让尾数前的小数点移到尾数之后且不丢失精度,就只能乘上2^52^,因此最大安全数字的指数部分就只能是十进制52,而十进制52对应指数的二进制表示为10000110011

最大数字:使用Number.MAX_VALUE查看IEEE 754双精度型所能表示的最大数字,其格式为0 11111111110 111...111

最小数字:使用Number.MIN_VALUE查看IEEE 754双精度型所能表示的最小数字,其格式为1 11111111110 111...111

位运算符

&、或|、非~、异或^、左移<<、右移>>、全右移>>>

JS中的位运算是针对整数的有符号32位补码形式进行运算的

位运算得到的结果是一个有符号位的32位补码

位运算是通过CPU直接计算的,因此速度极高

注意:

  • 当操作数是一个小数时,会截取数字的整数部分,然后把整数转换为有符号位的32位补码
  • 当操作数是NaN或±Infinity时,直接当做整数0处理
  • 当操作数是非数字时,先将数据转换为数字

格式:整数1 & 整数2

作用:将两个整数的每一位进行比较,若两位都为1,则结果中的该位为1,否则为0

格式:整数1 | 整数2

作用:将两个整数的每一位进行比较,若两位都为0,则结果中的该位为0,否则为1

格式:~整数

作用:对整数按位取反

非的快速手算技巧:~整数等价于-数字 - 1

JS中最快速的取整方式:~~小数

异或

格式:整数1 ^ 整数2

作用:将两个整数的每一位进行比较,若两位相同,则结果中的该位为0,否则为1(相同取0,不同取1)

交换两个变量的技巧:

var x = 1, y = 2;
x = x ^ y;
y = x ^ y;
x = x ^ y;

位移

左移

格式:整数1 << 整数2

作用:返回整数1的32位有符号补码算数左移整数2位的结果

右移

格式:整数1 >> 整数2

作用:返回整数1的32位有符号补码算数右移整数2位的结果

全右移

格式:整数1 >>> 整数2

作用:返回整数1的32位有符号补码逻辑右移整数2位的结果

应用

// 让每一个bit位代表着一种权限
var perm = {				// 某种资源的权限信息
    read: 0b001,			// 读资源的权限
    write: 0b010,			// 写资源的权限
    create: ob100			// 创建资源的权限
};

var user;

// 给用户分配权限
user = perm.read | perm.write;			// 给予用户读和写的权限

// 删除用户的某个权限
user = user | perm.read ^ perm.read;	// 删除用户的读权限

// 判断用户是否拥有某个权限
user.read & perm.read ? console.log("用户拥有读权限") : console.log("用户没有读权限");

取余和求模

取余

xy取余可以记作x rem y

取余的计算方式:x - n * y,其中n为对x / y向0取整的结果

取余的结果的正负性只与被除数有关

x = 7, y = 3;
n = x / y = 2.333... → 2
x rem y = 7 - 2 * 3 = 1
x = 7, y = -3;
n = x / y = -2.333... → -2
x rem y = 7 - (-2) * (-3) = 1
x = -7, y = 3;
n = x / y = -2.333... → -2
x rem y = (-7) - (-2) * 3 = -1

JS中的x % y就是x取余y

求模

xy求模可以记作x mod y

求模的计算方式:x - n * y,其中n为对x / y向下取整的结果

求模的结果的正负性只与除数有关

x = 7, y = 3;
n = x / y = 2.333... → 2
x mod y = 7 - 2 * 3 = 1
x = 7, y = -3;
n = x / y = -2.333... → -3
x mod y = 7 - (-3) * (-3) = -2
x = -7, y = 3;
n = x / y = -2.333... → -3
x mod y = (-7) - (-3) * 3 = 2

对于求模和取余,只有当n为负数时才会有区别,其他情况得到的结果一致