JavaScript第四章 表达式与操作符

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

第四章 表达式与操作符

本章讲述js表达式和用于构建各种表达式操作符.表达式是一个可以被求值并产生一个值的js短语.

直接嵌入在程序中的常量是最简单的表达式,变量名也是简单的表达式,可以求值为之前赋给他的值a=10.

复杂表达式由简单表达式构成.比如,数组访问表达式由一个求值为数组的表达式,一个左方括号,一个求值为整数的表达式和一个右括号构成.这个新的更复杂的表达式求值为保存在指定数组的指定索引位置的值.

类似的地,函数调用表达式由由一个求值为函数对象的表达式和零或多个作为函数参数的表达式构成

基于简单表达式构建复杂表达式最常见的方式是使用操作符.

操作符以某种方式组合其操作数的值(通常有两个),然后求值为一个新的值.

以乘法操作符*为例.表达式x * y求值为表达式x和y值的积.为简单起见,有时也说操作符返回值,而不是**"求值为"一个值**.

本章讲述所有js操作符,也会解释不使用操作符的表达式(如数组索引和函数调用).如果你熟悉其他C风格语法的编程语言,那么一定会觉得js中多数表达式和操作符的语法并不陌生.

4.1主表达式

最简单的表达式称为主表达式(primary expression),即那些独立存在,不再包含更简单表达式的表达式.js中的主表达式包括常量字面量值,某些语言关键字变量引用.

字面量是可以直接嵌入在程序中的常量值.例如:

1.23 // 数值字面量
"hello" // 字符串字面量
/pattern/ // 正则表达式字面量

3.2节介绍了js数值字面量的语法.

3.3节介绍了字符串字面量的语法.

3.3.5节介绍了正则表达式字面量的语法,

11.3节还会详细介绍.

js的一些保留字也是主表达式:

true
false
null
this // 求值为当前对象

我们在3.4和3.5节学习了true,false和null.与其他关键字不同,this不是常量,他在程序中的不同地方会求值为不同的值,this是面向对象编程中使用的关键字.在方法体中,this求值为调用方法的对象.要了解关于this的更多信息,可以参考4.5章节.第八章(8.2.2节)和第九章

第三种主表达式是变量,常量或全局对象属性的引用:

i//求值为变量i的值
sum // 求值为变量sum的值
undefined // 全局对象undefined属性的值

4.2对象和数组初始化程序

对象和数组初始化程序也是一种表达式其值为新创建的对象或数组.这些初始化程序表达式有时候也称为对象字面量和数组字面量.但与真正的字面量不同,他们不是主表达式,因为他们包含用于指定属性或元素值的子表达式.数组初始化程序的语法稍微简单一点.

数组初始化程序是一个包含在方括号内的逗号分割的表达式列表.数组初始化程序的值是新创建的数组.这个新数组的元素被初始化为逗号分隔的表达式的值:

[] // 空数组:方括号中没有表达式 意味着没有元素
[1+2,3+4] // 两个元素的数组.第一个元素是3 第二个元素的7

数组初始化程序中的元素表达式本身也可以是数组初始化程序,这意味着一下表达式可以创建嵌套数组:

let matrix = [[1,2,3],[4,5,6],[7,8,9]];

数组初始化程序中的元素表达式在每次数组初始化程序被求值时也会被求值.这意味着数组初始化程序表达式每次求值的结构可能不一样.

在数组字面量中省略逗号键的值可以包含未定义元素.例如,一下数组包含5个元素,其中有3个未定义元素:

let sparseArray = [1,,,,,5]

数组初始化程序的最后一个表达式后面可以再跟一个逗号,而且这个逗号不会创建未定义元素.不过,通过数组访问表达式访问最后一个表达式后面的索引一定会求值为undefined.

对象初始化程序表达式与数组初始化程序表达式类似,但方括号变成了花括号,且每个子表达式前面多了一个属性名和一个冒号:

let p = {x:2.3,y:-1.2};//有两个属性的对象
let q = {}// 没有属性的空对象
q.x = 2.3;q.y = -1.2;// 现在q拥有了跟p一样属性

在ES6中,对象字面量拥有了更丰富的语法(6.10节).对象字面量可以嵌套.例如:

let rectangle = {
	upperLeft:{
		x:2,
		y:2
	},
	lowerRight:{
		x:4,
		y:5
	}
}

4.3函数定义表达式

函数定义表达式定义js函数,求值为新定义的函数,某种意义上说,函数定义表达式也是"函数字面量",就像对象初始化程序是"对象字面量"一样.函数定义表达式通常又关键字function,位于括号中的逗号分隔的零或多个标识符(参数名),以及一个位于花括号中的js代码块(函数体)构成.例如:

// 这个函数返回传入值的平方
let square = function(x){retrun x*x};

函数定义表达式也可以包含函数的名字.函数也可以使用函数语句而非函数表达式来定义.在ES6及之后的版本中,函数表达式可以使用更简洁的"箭头表达式"语法.第八章详细讲述了函数定义.

4.4属性访问表达式

属性访问表达式求值为对象属性或数组元素的值,js定义了两种访问属性的语法:

expression.identifier
expression[expression]

第一种属性访问语法是表达式后跟一个句点和一个标识符.其中,表达式指定对象,标识符指定属性名.第二种属性访问语法是表达式(对象或数组)后跟另一个位于方括号中的表达式.这第二个表达式指定属性名或数组元素的索引.

let o = {x:1,y:{z:3}};// 示例对象
let a = [o,4,[5,6]];// 包含前面对象的示例数组
o.x // 1
o.y.z // 3 表达式o.y的属性z
a[1] // 4
a[2]["1"] // 6
a[0].x // 1 表达式a[0]的属性x

**无论那种属性访问表达式,位于.或[前面的表达式都会先求值].如果求值结果为null或undefined,则表达式会抛出 ** TypeError,因为他们是js中不能有属性的两个值.

如果对象表达式后跟一个点和一个表示符,则会对以该标识符为名字的属性求值,且该值会成为整个表达式的值.

如果对象表达式后跟位于方括号中的另一个表达式,则第二个表达式会被求值并转换为字符串.整个表达式的值就是名字为该字符串的属性的值.任何一种情况下,如果指定名字的属性不存在,则属性访问表达式的值为undefined

在两种属性访问表达式中,加标识符的语法更简单,但是通过他访问的属性名字必须是合法的标识符,而且在写代码时已经知道了这个名字.如果属性名中包含空格或标点字符,或者是一个数值(对于数组而言),则必须使用方括号.方括号也可以用来访问非静态属性名,即属性本身是计算结果(参见6.3.1节的例子)

对象及其属性将在第六章介绍,数组及其元素将在7章介绍

4.4.1条件式属性访问

ES2020增加了两个新的属性访问表达式:

expression?.identifier
expression?.[expression]

js中,null和undefined是唯一两个没有属性的值.在使用普通的属性访问表达式时,如果.或[]左侧的表达式求值为null和undefined,会报错TypeError.可以使用?.或?.[]语法防止这种错误发生.

比如表达式a?.b 如果a是null或undefined,那个整个表达式求值结果为undefined,不会尝试访问属性b.如果a是其他值,则a?.b求值为a.b的值(如果a没有名为b的属性,则整个表达式的值还是undefined).

这种形式的属性访问表达式有时候也被称为"可选链接",因为他也适用于下面这样更长的属性访问表达式链条:

let a = {b:null}
a.b?.c.d // undefined

a是个对象,因此a.b是有效的属性访问表达式.但a.b的值是null,因此a.b.c会抛出TypeError.但通过使用?.而非.就可以避免这个TypeError,最终a.b?.c求值为undefined.这意味着(a.b?.c).d也会抛出TypeError,因为这个表达式尝试访问undefined值的属性.但如果没有括号,即a.b?.c.d(这种形式是"可选链接"的重要特征)就会直接求值为undefined而不会抛出错误.这个因为通过?.访问属性是"短路操作":如果?.左侧的子表达式求值为null或undefined,那么整个表达式立即求值为undefined,不会再进一步尝试访问属性.

当然,如果a.b是对象,且这个对象没有名为c的属性,则a.b?.c.d仍然会抛出TypeError.此时应该再加一个条件属性访问:

let a = {
	b:{}
};
a.b?.c?.d // undefined

条件式属性访问也可以让我们使用?.[]而非[].在表达式a?.[b][c]中,如果a的值是null和undefined,则整个表达式立即求值为undefined,字表达式b和c不会被求值.换句话说,如果a没有定义,那么b和c无论谁有副效应(side effect),这个副效应都不会发生:

let a;// 忘记初始化这个变量了
let index = 0;
try{
	a[index++];//TypeError // index=>1
}catch(e){
	index // TypeError
}
a?.[index++] // undefined:因为a是undefined
index // 1 因为?.[]短路所有没有发生递增
a[index++]//!TypeError,B不能索引undefined

使用?.和?.[]的条件式属性访问是js最新的特性之一.在2020年初多数浏览器已经支持这个语法了.

4.5调用表达式

调用表达式是js中调用(或执行)函数或方法的一种语法.这种表达式开头是一个表示要调用函数的函数表达式.函数表达式后面跟着左圆括号,逗号分割的零或多个参数表达式的列表和右圆括号.

f(0)
Math.max(x,y,z)
a.sort()

求值调用表达式时,首先求值函数表达式,然后求值参数表达式以产生参数值的列表,如果函数表达式的值不是函数,则抛出TypeError.然后,按照函数定义时参数的顺序给参数赋值,之后再执行函数体.如果函数使用了return语句返回一个值,则该值就成为调用表达式的值.否则,调用表达式的值是undefined.关于函数调用的完整细节,包括子参数表达式个数与函数定义的参数个数不匹配会发生什么,都会在第八章讲解.

每个调用表达式都包含一对圆括号和做圆括号前面的表达式.如果该表达式是属性访问表达式,则这种调用被称为方法调用,.在方法调用中,作为属性访问主体的对象或数组在执行函数体时会变成this关键字的值.这样就可以支持面向对象编程范式,即函数(这样使用时我们称其为"方法")会附着在其所属对象上来执行操作.详细内容可以参考第9章.

4.5.1条件式调用

在es2020中,可以使用?.()而非()来调用函数.正常情况下,我们调用函数时,如果圆括号左侧的表达式是null或undefined或任何其他非函数值,都会抛出TypeError.而使用?.()调用语法.如果?左侧的表达式求值为null或undefined,则整个调用表达式求值为undefined,不会抛出异常.

数组对象有一个sort()方法,接受一个可选的函数参数,用来定义对数组元素排序的规则.在es2020之前,如果想写一个sort()的这种接受可选函数参数的方法,通过需要在函数内使用if语句检查该函数参数是否有定义,然后在调用:

function square(x,log){// 第二个参数是一个可选的函数
	if(log){// 如果传入了这个参数就调用这个方法
		log()
	}
	retrurn x*x
}

但有了ES2020的条件式调用语法,可以简单的使用?.()来调用这个可选的函数,只有在函数有定义时才会真正调用:

function square(x,log){
	log?.(x);// 如果有定义则调用
	return x*x
}

不过要注意,?.()只会检查左侧的值是不是null或undefined,不会验证该值是不是函数,因此,这个例子中的square()函数在接受到两个数值仍然会抛出异常

与条件式属性访问表达式(参见4.4.1节)类似,使用?.()进行函数调用也是短路操作:

如果?.左侧的值是null或undefined.则圆括号中的任何参数表达式都不会被求值:

let f = null,x=0;
try{
	f(x++)//f是null所以抛出TypeError
}catch(e){
	x // 1抛出异常前x发送了递增
}
f?.(x++)// f是null,但不会抛出异常
x // 1因为短路,递增不会发生

使用?.()的条件表达式即适用于函数调用,也适用于方法调用.因为方法调用有涉及属性访问,所以有必要花时间确认一下自己是否理解下列表达式的区别:

o.m()//常规属性访问,常规调用
o?.m()// 条件式属性访问,常规调用
o.m?.()// 常规属性访问,条件式调用

第一个表达式中,o必须是一个对象且必须有一个m属性,且该属性的值必须是函数.第二个表达式中,如果o是null或undefined,则表达式求值为undefined.但如果o是任何其他值,则他必须有一个值为函数的属性m.第三个表达式中,o必须不是null或undefined.如果他没有属性m或属性m的值是nulllimg,则整个表达式求值为undefined.

使用?.()的条件式调用时js最新的特性之一.

4.6对象创建表达式

对象创建表达式创建一个新对象并调用一个函数(称为构造函数)来初始化这个新对象.对创建表达式类似于调用表达式,区别在于前面多了一个关键字new:

new Object()
new Point(2,4)

如果在对象创建表达式中不会给构造函数传参,则可以省略圆括号:

new Object
new Date

对象创建表达式的值是新创建的对象.第九章更详细的解释构造器

4.7操作符概念

操作符在js总用于算术表达式,比较表达式,逻辑表达式,赋值表达式等.

下面表4-1总结了所有操作符,可以作为一个参考.

注意,多数操作都以+和=这样的标点符号表示.不过,有一些也以delete和instanceof这样的关键字表示.关键字操作符也是常规操作符,与标点符号表示的操作符一样,只不过他们的语法没那么简短而已.

下表在安装操作符优先级组织.换句话说,表格前面的操作符比后面的操作符优先级更高

横线分隔的操作符具有不同优先级.“结合性"中的"左"表示"从左到右”,“右"表示"从右到左”.

"操作数"表示操作数的个数."类型"表示操作数的类型.以及操作符的结果类型(->后面).表格后面几节解释优先级,结合性和操作数类型的概念.

介绍完这个概念后,我们将详细介绍每一个操作符

4-1:js操作符

操作符操作结合性操作数类型
++先或后递增(有副效应)1lval->num
--先或后递减(有副效应)1lval->num
-负值1num->num
+转换为数值1any->num
~反转二进制位1int->int
!反转布尔值1bool->bool
delete删除属性(有副效应)1lval->bool
typeof确定操作数类型1any->str
void返回undefined1any->undef
**2num,num->num
*,/,%乘,除,取余2num,num->num
+,-加减2num,num->num
+拼接字符串2str,str->str
操作符操作结合性操作数类型
<<左移位2int,int->int
>>右移以符号填充2int,int->int
>>>右移位以零填充2int,int->int
<,<=,>,>=按数值顺序比较2num,num->bool
<,<=,>,>=按字母表顺序比较2str,str -> bool
instanceof测试对象类2obj,func->bool
in测试属性是否存在2any,obj ->bool
==非严格相等测试2any,any->bool
!=非严格不等测试2any,any->bool
===严格相等测试2any,any->bool
!==严格不相等测试2any,any->bool
&计算按位与2int,int->int
^计算按位异或2int,int->int
|计算按位或2int,int->int
&&**计算逻辑与**2any,any->any
||计算逻辑或2any,any->any
??选择第一个有定义的操作数2any,any->any
?:选择第二或第三个操作数3bool,any,any->any
=**为变量或属性赋值**(有副效应)2lval,any->any
**=,*=,/=操作并赋值2lval,any->any
%=,+=,-=,&=操作并赋值2lval,any->any
^=,|=,<<=,操作并赋值2lval,any->any
>>=,>>>=操作并赋值2lval,any->any
,丢弃第一个操作数,返回第二个2any,any->any

4.7.1操作数个数

操作符可以按照他们期待的操作数个数(参数数量)来分类.

多数js操作符(如乘法操作符*)都是二元操作符,可以将两个表达式组合成一个更复杂的表达式.换句话说,这些操作符期待两个操作数.

js也支持一些一元操作符,这个操作符将一个表达式转换为一个更复杂的表达式.

表达式-x中的操作符**-**就是一元操作符,对于对操作数x进行求负值操作.

最后,js也支持一个三元操作,即条件操作符?: ,用于将三个表达式组合为一个表达式.

4.7.2操作数与结果类型

有些操作符适用于任何类型的值,但多数操作符期待自己的操作数的某种特定类型,而且多数操作符也返回(或求值为)特定类型的值.表4-1的"类型"列标了操作数类型(箭头前)和结果类型(箭头后).

js操作符通常会按照需要转换操作数的类型(3.9节).比如,乘法操作符*期待数值参数,而表达式"3" * "5"之所有合法,是因为js可以把操作数转换为数值.因此这个表达式的值是数值15,而非字符串"15".也要记住,每个js值要么是
“真值"要么是"假值”,因此期待布尔值操作数的操作符可以用于任何类型的操作数

有些操作数的行为会因为操作数类型的不同而不同.最明显的,+操作符可以把数值加起来,也可以拼接字符串.类似,比较操作符(如<)根据操作数类型会按照数值顺序或字母表顺序比较.后面对每个操作符都有详细介绍,包括他们的类型依赖,以及他们执行的类型转换.

注意,表4-1中列出的赋值操作符合和少数其他操作符期待操作数类型为lval.

lval即lvalue(左值),是一个历史悠久的术语,意思是"一个可以合法地出现在赋值表达式左侧的表达式".

js中,变量,对象属性和数组元素都是"左值".

4.7.3操作符副效应

对类似2*3这样简单表达式求值不会影响程序状态,程序后续的任何计算也不会被这个求值所影响.但是有些表达式是有副效应的,即对他们求值可能影响将来求值的结果.

赋值操作符就是明显的例子:把一个值赋给变量或属性,会改变后续使用该变量或属性的表达式的值.

类似的地,递增和递减操作符++和–也有副效应,因为他们会执行隐式赋值.同样delete操作符也有副效应,因为删除属性类似于(但不同于)给属性赋值undefined.

其他js操作符都没有副效应,但函数调用和对象创建表达式是否有副效应,取决于函数或构造函数体内是否使用了副效应的操作符.

4.7.4操作符优先级

表4-1中的操作数是按照优先级从高到低的顺序排列的,表中横线分组了相同优先级操作符.操作符优先级控制操作符被执行顺序.优先级高(靠近表格顶部)的操作符先与优先级低(靠近表格底部)的操作符执行.

来看下面这个表达式:

w = x + y*z;

其中乘法操作符*比加法操作符+优先级高,因此乘法计算先于加法执行.另外,赋值操作符=的优先级最低,因此赋值会在右侧所有操作都执行完成之后才会执行.

操作符优先级可以通过圆括号显示改写.比如,要强制先执行上例中的加法计算,可以这样写

w = (x+y)*z

注意,属性访问和调用表达式的优先级高于4-1的列出的任何表达式.

// my是一个有function属性的对象,function属性
// 是一个函数的数组.这里调用了x号函数,并传给它
// 参数y,然后求值函数调用返回值的类型
typeof my.function[x](y)

尽管typeof是优先级最高的操作符,但typeof要基于属性访问,数组索引和函数调用的结果执行,这些操作的优先级全部高于操作符

实践中,如果你完全不确定自己所用操作的优先级,最简单的方法就是使用圆括号明确求值顺序.最重要的规则在于:乘和除先于加和减执行,而赋值优先级很低,几乎总是最后才执行.

js新增的操作符并不总是符合这个优先级.比如表4-1中,??操作符(参见4.13.2)比||和&&优先级低,而实际上他相对这两个操作符的优先级并没有定义,es2020要求混用??和||和&&时必须使用圆括号.类似地,新的幂操作符**相对于一元负值操作符的优先级也没有明确定义,因此在同时求负值和求幂时也必须使用圆括号.

4.7.5操作符结合性

4.1表中,"结合性"标明了操作符的结合性."左"表示结合性为从左到右,"右"表示结合性从右到左.操作符结合性规定了相同优先级操作的执行顺序.左结合意味着操作从左到右执行.例如,减操作符具有左结合性,因此:

w = x - y - z
就等价于
w = ((x-y)-z)

另一方面

y = a ** b ** c;
x = ~-y;
w = x = y = z
q = a?b:c?d:e?f:g;

等价于

y = (a**(b**c));
x = ~(-y);
w = (x = (y = z))
q = a?b:(c:?d:(e?f:g))

因为幂,一元,赋值和三元条件操作符具有右结合性.

4.7.6求值顺序

操作符的优先级和结合性规定了复杂表达式中操作的执行顺序,但他们没有规定子表达式的求值顺序.js始终严格按照从左到右的顺序对表达式求值.例如,在表达式w =x +y * z中,子表达式w首先被求值,再对x,y和z求值,然后将y和z相乘,加到x上,再给结果赋值给表达式w表示的变量或属性.在表达式中使用圆括号可以改变乘法,加法和赋值的相对顺序,但不会改变从左到右的求值顺序.

求值顺序只在一种情况下会造成差异,即被求值的表达式具有副效应,这会影响其他表达式的求值.比如,表达式x递增一个变量,而表达式z会使用这个变量,此时保证x先于z被求值就很重要了.

4.8算术表达式

本节介绍对操作数执行算术或其他数值操作的操作符.首先介绍幂,乘,除和减这几个简单直观的操作符.之后会介绍加操作符,因为他也执行字符串转换,且具有不同寻常的类型转换规则.一元操作符和为操作符也会在之后介绍.

多数算术操作符(除了下面提到的)都可以用于BigInt(3.2.5)操作数或常规数值,前提是不能混用两种类型.

基本的算术操作符是** 幂,*乘,/除,%(取模),+,-.

如前所诉,我们会在单独一节讨论+操作符.另外5个基本操作符都会对自己的操作数进行求值,必要时将操作数转换为数值,然后计算幂,积,商,余和差.无法转换为数值的为数值操作数转换为NaN.如果有操作数是(或被转换成)NaN,则操作结果(几乎始终)是NaN.

**操作符的优先级高于*,/和%(后三个的优先级又高于+和-).与其他操作符不用,**具有右结合性,即2**2**3相当于2**8而非4**3,另外,类似-3**2这样的表达式本质上是有歧义的.取决于一元减号和幂操作的相对优先级,这个表达式可能意味着(-3)**2,也可能意味-(3**2).对于这种情况,不同语言的处理方式也不同.js认为这种情况下不写括号是语法错误.强制你自己消除表达式的歧义.**js中最新的操作符,是es2016中增加的.但是Math.pow()函数在js很早的版本中就有了,他与**执行完全相同的操作.

/操作符用第二个操作数除第一个数.如果你习惯了区分整数和浮点数的编程语言,应该知道整数相处得到整数.但在js中,所有数值都是浮点数,因此所有除法得到的都是浮点数,比如5/2得到2.5而不是2.被0除得到正无穷或负无穷,而0/0求值为NaN.这两种情况都不是错误.

%操作符计算第一个操作数对第二个操作数取模的结果.换句换说,他返回第一个操作数被第二个操作数整除之后的余数.结果的符号和第一个操作数相同.5%2=1 -5%2 = -1

虽然模操作数通常用于整数,但也可以用于浮点数.比如,6.5%2.1求值为0.2

4.8.1 +操作符

二元+操作数将用于计算数值操作数的和或者拼接字符创操作数:

1+2 "hello"+" "+ 'three'
"1" + "2"

如果两个操作数都是数值或者都是字符串,+操作符执行后的结果自不必说.但除了这两个情况之外的任何情况,都会涉及类型转换,而实际执行的操作取决于类型转换的结果.

+操作符优先字符串拼接:只要有操作数是字符串或可以转换为字符串的对象,另一个操作数也会被转换为字符串并执行拼接操作.只有任何操作数都不是字符串或类字符串值时才会执行加法操作.

严格来将,+操作符的行为如下所示

  • 如果有一个操作数是对象,则+操作符3.9.3介绍的对象到原始值的算法把该操作数转换为原始值.Date对象用toString()方法来转换,其他对象通过valueOf()转换(如果这个方法返回原始值).不过,多数对象并没有valueOf()方法,因此他们会通toString()方法转换.
  • 完成对象到原始值的转换后,如果有操作数是字符串,另一个操作数也会被转换为字符串并且拼接
  • 否则,两个操作数都被转换为数值(NaN),计算加法.

下面是几个例子:

1+2 "1"+"2"
1+{} // "1[objec Object]"
true+true // 2
2+null // 2
2+undefined // NaN

最后,很重要的的一点是要注意+操作符在用于字符串和数值时,可能不遵守结合性,换句话说,结果取决于操作执行的顺序.

1+2+"抱抱抱抱不"
1+(2+"灌灌灌灌")// 字符串拼接

4.8.2 一元算术操作符

一元操作符修改一个操作数的值以产生一个新值.在js中,一元操作符全部具有高优先级和右结合性.本节介绍的元素一元操作符(+,-,++和–)都在必要时将自己唯一的操作数转换为数值.注意,操作符+和-既是一元操作符,而是二元操作数

一元算术操作符如下所示

一元+

一元加操作符将其操作数转换为数值(或NaN)并返回转换后的值.如果操作数是数值,则它什么不做.由于BigInt值不能转换为常规的数值,因此这个操作符不应该用于BigInt

一元-

当-用于一元操作符时,他在必要的时候将操作数转换为数值,然后改变结果的符号.

递增++

++操作符递增操作数(也就是加1),这个操作数必须是一个左值(变量,数组元素或对象).这个操作符将其操作数转换为数值,在这个数值加上1,然后将递增后的数值在赋值回这个变量,元素或属性.

++操作符的返回值取决于它与操作数的相对位置.如果位于操作数前面,则可以称其为前递增操作符,即先递增操作数,再求值为该操作数递增后的值.如果位于操作数后面,则可以称其为后递增操作符,即它也会递增操作数,但仍然就只为该操作数未递增的值

let i = 1,j = ++i;// i和j都是2
let n = 1,m = n ++;// n是2 m是1!!!!

这说明,表达式x++,不一定等价于x=x+1.++操作符不会执行字符串拼接,而始终会将其操作数转换为数值.如果x是字符串"1",++x就是数值2,但x+1则是字符串"11"

另外也注意,由于js会自动插入分号,因此不能在后递增操作符和他前面的操作数之间插入换行符.如果插入了换行符,js会将操作数当成一条完整的语句,在他后面插入一个分号.

递减–

–操作符也期待左值操作数.他会将这个操作数转换为数值,减1.然后把递减后的值在赋给操作数.与++操作符类似,–返回的值取决于他与操作数的相对位置.如果位于操作数前面,他递减并返回递减后的值.如果位于操作数后面,他递减操作数,但返回未递减的值.在位于操作数后面时.,操作数与操作符之间不能有换行符.

4.8.3 位操作数

位操作符对数值的二进制表示执行低级位操作.尽管他们执行的不是我们熟悉的算术计算,但由于他们操作的是数值操作数且返回数值,所以也可以把它们归类为算术操作符.4个位操作符对操作数的个别二进制位执行布尔代数计算,即将操作数中的每一位当成布尔值来对待(1true,0false),另外3个位操作数用于左右位移.位操作符在js编程中不太常用,如果对整数的二进制表示(包括负整数的二进制补码表示)不熟悉,可以先考虑跳过这一节.

位操作符期待整数操作数,而且将他们当成32位整数而非64位浮点数.这些操作符必要时将他们的操作数转换为数值,然后将得到的数值强制转换为32位整数,即丢弃小数部分和地32位以外的部分.移位操作符要求右侧操作数介于0到31之间.在把操作数转换为无符号32位整数后,他们会丢弃第五位以外的位,得到一个近似相等的数值,令人惊讶的是,NaN,Infinity和-Infinity在作为这些位操作符的操作数时都会转换为0

除了>>>之外的所有位操作符都可以用于常规数据或BigInt(3.2.5节)操作数

按位于&

&操作数对其整数参数的每一位执行布尔与操作,只有两个操作数对应的位都为1,结果中对应的为才为1.例如,0x12344&0x00ff求值为0x0034

按位或|

|操作符对其整数参数的每一位执行布尔或操作.两个操作数中对应的为都为1或有一个为1,结果中对应的位才为1 0x1234 0x00ff求值为0x12ff

按位异或^

^操作符对其整数参数的第一位执行布尔异或操作.异或的意思就是要么操作数一为true,要么操作数而为true,二者不能同时为true.两个操作数中对应的位只有一个为1,结果中对应的位就为1,例如:0xff00 ^0xf0f0 求值为0x0ff0

按位非~

操作符是一元操作符,要出现在其操作数前面.按位非的结果是反转操作数中的所有位.因此`js`表示有符号整数的方式,对一个值应用操作数等于修改符号并减一, ~0x0f求值为0xfffffff0,即-16

左移<<

<<操作符将第一个操作数的所有位向左移动第二个操作数指定的位数,第二个操作数应该是介于0到31之间的整数.例如:在a<<1操作中,a的第一位变成第二位,a的第二位变成第三位,以此类推.新的第一位会填充0,第32位的值丢失.将一个值左移1位等于这个值乘以2,左移两位等于乘以4,依次类推.例如:7<<2求值为28 (0 0 4 2 1->16 8 4 0 0)28

有符号右移>>

>>操作符将第一个操作数的所有位向右移动第二个操作数指定的位数(介于0到3之间).移出右边的为会被丢弃.填充到右边的为取决于原始操作数的符号,以便结果保持相同的符号,如果第一个操作数是正数,则结果的高位填充零;如果第一个操作数是负值,则结果的高位填充1.将一个正值右移1位等于这个值除以2丢弃余数.右移2位等于除以4,因此类推.例如 7>>1求值为3 (0421)->(0021),-7>>1求值为-4

对于负数的右移因为负数在内存中是以补码形式存在的所有首先根据负数的原码求出负数的补码(符号位不变其余位按照原码取反加1)然后保证符号位不变其余位向右移动到X位在移动的过程中高位补1.等移位完成以后然后保持符号位不变其余按位取反加1得到移位后所对应数的原码。即为所求。

零填充右移>>>

>>>操作符与>>操作符类似,只不过无论第一个操作数的符号是什么,左侧移动的为始终填充0,如果想把有符号32位看成无符号整数,那可以使用这个操作符.例如-1>>4求值为-1,而-1>>>4求值为0x0ffffffff.这是js唯一一个不能用于Bigint的位操作符.BigInt不像32位整数那样通过设置高位的方式表示负值,这个操作符只对特定补码表示有意义.

4.9关系表达式

本章介绍js的关系操作符.这些操作符测试两个值之间的关系(如等于,小于,或是…的属性),并依据相应关系是否存在返回true或false,关系表达式始终求值为布尔值,而该值经常用于控制程序的执行流,如if,while和for语句(参数5章)中使用

4.9.1相等和不相等操作符

=操作符分别用两个相同的标准检查两个值是否相同.这两个操作符都接受任意类型的操作数,都在自己操作数相同的时候返回true,不同时返回false.===操作符被称为严格相等操作符(或者全等操作符),他根据严格相同的定义检查两个操作数是否"完全相同".

==操作符被称为相等操作符,他根据更宽松的(允许类型转换的)相同定义检查两个操作数是相等

!=和!操作符测试的关系与和=恰好相反.!=不相等操作符在两个值用测试相等时返回false,否则返回true.

!==操作符在两个值严格相等时返回false,否则返回true,4.10将介绍,!操作符计算布尔非操作的值.

=,=操作符

他们三分别用于赋值,测试相等和严格相等

操作符是js早期的特性,被普遍认为是一个隐患.因此实践中坚持使用=而不是==,使用!==而不是!=

正如3.8节所说,js对象是按引用而不是按值比较的.对象与自己相等,但与其他任何对象都不相等,即使两个对象有同样多的属性,每个属性的名字和值也相同,那他们也不相等.类似地,两个数组即使元素相同,顺序相同,他们也不相等.

严格相等

严格相等操作符===求值其操作数,然后按下列步骤比较两个值,比作类型转换.

  • 如果两个值类型不同,则不相同
  • 如果两个值都是null或都是undefined,则相等
  • 如果两个值都是布尔值true或都是布尔值false,则相等.
  • 如果一个或两个值是NaN,则不相等(虽然有点意外,但NaN确实不等于任何其他值,也包括NaN自身,要检查某个值x是不是NaN,使用x !== x或全局isNaN()函数)
  • 如果两个值都是数值且值相同,则相等,如果一个值是0而另一个是-0,则也相同
  • 如果两个值都是字符串且相同位置包含完全相同的16位值(3.3),则相等,
  • 如果两个字符串长度或内容不同,则不相等.两个字符串有可能看起来相同,也表示同样的意思,但底层编码却使用不同的16位值序列.js不会执行Unicode归一化操作,将这样的两个字符串用=操作符都不会判定相等.
  • 如果两个值引用同一个对象,数组或函数,则相等,如果他们引用不同的对象,即使两个对象有完全相同的属性也不相同

基于类型转换的相等

相等操作符==与严格相等类似,但是没有那么严格.如果两个操作数的值类型不同,他会尝试做类型转换,然后再比较:

  • 如果两个值类型相同,则按照前面的规则测试他们是否严格相等,如果严格相等,则相等.如果不严格相等,则不相等
  • 如果两个值类型不用==操作符仍然可能认为他们相等.此时他们会使用一下规则,基于类型转化来判定相等关系.
    • 如果一个值是null,另一个值是undefined,则相等.
    • 如果一个值是数值,另一个值是字符串,把字符串 转换为数值,再比较转换后的数值
    • 如果有一个为true,把它转换为1,再比较.如果有一个值为false,把他转换为0再比较.
    • 如果一个值是对象,.另一个值是数值或字符串,先使用3.9.3节描述的算法把对象转换为原始值,再比较.对象转换为原始值时要么使用toString()方法,要么使用valueOf()方法.js内置的核心先尝试使用valueOf,在尝试toString().但Date类是个例外,这个类执行toString()转换.
    • 其他任何值的组合都不相等

下面来看一个比较相等的例子

"1" == true

这个表达式求值为true,意味着这两个看起来完全不一样的实际上相等.布尔值true首先被转换为数值1然后再比较.而字符串"1"也被转换为数值1.此时两个值相等,因此比较返回true.

4.9.2比较操作符

比较操作符测试操作数的相对顺序(数值或字母表顺序)

小于<

​ **<**操作符在第一个操作数小于第二个操作数时求值为true,否则求值为false

大于>

​ 操作符第一个操作数大于第二个操作数时求值为true,否则求值为false

小于等于<=

​ <=操作符在第一个操作数小于或等于第二个操作数时候求值为true,否则求值为false

大于等于

	>=操作数在第一个操作数大于或等于第二个操作数时求值为true,否则求值为false

这几个比较操作数的操作数可以是任何类型.但比较只能针对数值和字符串,因此不是数值或字符串的操作会被转换类型

比较和转换规则

  • 如果有操作数求值为对象,该对象会按照3.9.3节描述被转换为原始值.即如果他的valueOf()方法返回原始值,就使用这个值,否则就使用它的toString()方法返回他的值.

  • 如果在完成对象到原始值的转换后两个操作数都是字符串,则使用字母表顺序比较这个两个字符串,其中"字母表熟顺序"就是组成字符串的16位Unicode值的数值顺序.

  • 如果在完成对象到原始值的转换之后至少有一个操作数不是字符串,则两个操作数都会被转换为数值并按照数值顺序来比较. 0和-0被认为相等.Infinity比它本身之外的任何数都大,-Infinity比它本身之外的任何数都小.如果有一个操作数是(或转换后是NaN),则这些比较操作符都返回false.虽然算术操作符不允许BigInt值与常规数值混用,但比较操作符允许数值与BigInt进行比较.

  • Infinity>NaN//false
    

记住,js字符串是16位整数值的序列,而字符串比较就是比较两个字符串的数值顺序.Unicode定义的这个数值编码顺序不一定与特定语言或地区使用的传统校正顺序(collation order)匹配.特别要注意字符串比较是区大小写的,而所有大写ASCII字母比较所有小写ASCII字母都小.如果不留意,这条规则很可能导致令人不解的结果.例如,根据<操作符,字符串"Zoo"会排在字符串"aardvarfk"前面

如果需要更加可靠的字符串比较算法,可以执行String.localeCompare()方法,这个方法也会考虑特定地区的字母表顺序.要执行不区分大小写的比较,可以使用String.toLowerCase()或String.toUpperCase()把字符串转化为全小写全部大写.如果需要更通用和更好的本地化字符串比较工具,可以使用11.7.3节介绍的Intl.Collator类.

+操作符和比较操作符通用都会对数值和字符串操作数区别对待.+偏向字符串,即只要有一个操作数是字符串,他就会执行拼接操作.而比较操作符偏向数值,只有两个操作数均为字符串时才按字符串处理:

1+2 // 3
"1" + "2" // "12"
"1"+2 // "12"
11<3 // false数值比较
"11"<"3"// true字符比较
"11"<3 // false 数值比较
"one"<3// false 数值比较,"one"会转换为NaN

最后注意,<=和>=操作符不依赖相等和严格想等操作符确定两个值是否相等.其中,小于或等于操作符只是简单地定义为,“不大于”,而大于或等于操作符则定义为"不小于".还有一个例外情况,即只要有一个操作数是(或不可以转换为)NaN.则全部4个比较操作符都返回false.

4.3.9 in操作符

in操作符期待左值操作数字符串,符号或可以转换为字符串的值,期待右侧操作数对象.如果左侧的值是右侧的对象的属性名,则in返回true.例如:

let point = {x:1,y:10}// 定义对象
"x" in point // true
"z" in point // false
"toString" in point // true 对象继承了toString方法\

let data = [7,8,9]
"0" in data // true
1 in data  // true
3 in data // false

instanceof操作符

instanceof操作数期待左侧操作数是对象.右侧操作数是对象类的标识.这个操作符在左侧对象是右侧实例是求值为true,否则求值为false.第九章解释了,在js中,对象类通过初始化他们的构造函数定义的,因而,instanceof的右侧操作数应该是一个函数.下面看几个例子:

let d = new Date()// 通过Date()构造函数创建一个新对象
d instanceof Date // true:d是通过Date()创建的
d instanceof Object // true所有对象都是Object的实例
d instanceof Number // falsed不是Number对象
let a = [1,2,3];// 通过数组字面量语法创建一个数组
a instanceof Array // true a是个数组
a instanceof Object // true 所有数组都是对象
a instanceof RegExp // false 数组不是正则表达式

注意,所有对象都是Object的实例.instanceof在确定对象是不是某个类的实例时会考虑"超类".如果instanceof的左侧操作数不是对象,则会返回false.如果右侧操作数不是对象的类,则会抛出TypeError.

要理解instanceof的工作原理,必须理解"原型链".原型链是js继承机制,6.3.2节有详细介绍.为了对表达式o instanceof f求值,js会求值f.prototype,然后在o的原型链上查找这个值,如果找到了,则o是f(或f的子类)的实例,instanceof返回true.如果f.prototype不是o原型链上的一个值,则o不是f的实例,instanceof返回false.

4.10 逻辑表达式

逻辑操作符&&,||和!执行布尔代数操作,经常与关系操作符一起使用,把两个关系表达式组合为更复杂的表达式.接下来几个小节介绍这些操作符.为了彻底理解他们,建议回顾3.4介绍的额"真性值"和"假性值"的概念.

逻辑与&&

&&操作符可以从不同层次来理解.最简单的情况下,在于布尔操作数共同使用时,&&对两个值执行布尔操作:当且仅当第一个操作数为true并且第二个操作数也为true时才返回true.如果有一个操作数是false,或者两个操作数都是false,他返回false.

&&经常用于连接两个关系表达式:

x===0&&y===0// 当且仅当x,y都等于零的时候,整个表达式才为true

关系表达式始终返回true或false.因此在像这样使用时,&&操作符本身也返回true或false.关系操作符的优先级高于&&(以及||),因此类似这样的表达式可以不带圆括号.

但&&不要求其操作符是布尔值,我们知道,所有js值要么是"真值"要么是"假值".理解&&的第二个层次是它对真值,&&返回一个真值;否则(一个或两个操作数是假值),&&返回假值.在js中,期待布尔值的任何表达式或语句都可以处理真值或假值,因此&&并不总返回false或true的事实在实践中并不会导致什么问题.

注意,上面&&返回"一个真值"或"一个假值"时并没有说明这个值是什么.对此要从第三个层次上理解&&.这个操作符首先对第一个操作数即它左边的表达式求值,如果左边的值是假值,则整个表达式也一定是假值,因此&&返回它左侧的值,不再求值它右侧的表达式.

另一方面,如果&&左侧的值是真值,则整个表达式的值取决于右侧的值.如果右侧的值是真值.则整个表达式的值一定是真值;如果右侧的值是假值,则整个表达式一定为假值.因此,在左侧的值为真值时,&&操作符求值并返回它右侧的值:

let o = {x:1}
let p = null;
o && o.x // 1  // o是真值,因此返回o.x的值
p && p.x // null // 直接返回null,不会求值q.x

这里的关键是要理解,&&可能(也可能不会)对其右侧操作数求值.这个代码示例变量p的值为null,表达式p.x如果被求值会导致TypeError.但代码中以惯用方式利用&&只在p为真值(不是null或undefined)时才对p.x求值.

&&的这种行为有时候也别称为短路,可能你也会看到有代码利用这种行为条件式地执行代码.例如

if(a===b)stop()
(a===b)&&stop()//等价

一般来说,必须注意&&右侧包含副效应(赋值,递增,递减或函数调用)的表达式.

无论其副效应是否依赖左侧的值.

尽管这个操作符的工作方式比较复杂,但他最常见的用法还是对真值和假值执行布尔代数计算

4.10.2 逻辑或||

||操作符对他的操作数执行布尔或操作.如果有一个操作数是真值,这个操作符就返回真值.如果两个操作数都是假值,它就返回假值.

尽管||操作符最常用做简单的布尔或操作符.但是他与&&类似,也有更复杂的行为,他首先会对第一个操作数,即它左侧的表达式求值,如果第一个操作数的值为真值,||就会短路,直接返回该真值,不再对右侧表达式求值,而如果第一个操作数的值是假值,则||会求值其第二个操作数并返回该表达式的值

与&&操作符一样,应该避免让右侧操作数包含副效应,除非是有意利用右侧表达式可能不会被求值的事实.

这个操作符的习惯用法是在一系列备选项中选择第一个真值:

let ma = maxWidth || preference.maxWidth || 500

注意,如果0是maxWidth的有效值,则以上代码可能有问题,以为0是个假值,此时可以使用??操作符(4.13.2)

在ES6之前,这个惯用写法经常用于在函数中给参数提供默认值:

function copy(o,p){
	p = p || {}
	// ...
}

不过在es6以及之后的版本,这个技巧已经没有必要,因为默认参数可以直接写在函数定义中:

function copy(0,p={}){//....}

4.10.3逻辑非!

!操作符是个一元操作符,出现在操作数前面.这个操作符的目的是反转其操作数的布尔值,例如.如果x是真值,!x或求值为false.如果x是假值,!x求值为true

与&&和||不同,!操作符将其操作数转换为布尔值(使用三章的规则),然后再反转得到的布尔值.这意味着!始终返回true或false,而要取得任何x对应的布尔值,只要对x应用这个操作符两次即可:!!x(3.9.2节)

作为一元操作符,!优先级高,如果想反转表达式p&&q的值,需要使用圆括号:(p&&q).有必要说一下,可以通过如下js语法来表达布尔代数的两个法则:

// 德摩根定律
!(p&&q) === (!p || !q) // true p和q可以是任何值
!(p||q) === (!p && !q) // true p和q可以是任何值

4.11 赋值表达式

js使用 = 操作符为变量或属性赋值.例如:

i = 0;
o.x = 1;// 设置对象o的属性x为1

=操作符期待其左侧操作数是一个左值,即变量对象数组元素.他期待右侧的操作数是任意类型的任意值.赋值表达式的值是右侧操作数的值.作为副效应,=操作符将右侧的值赋给左侧的变量或属性**,以便将来对该变量或属性的引用可以求值为这个值.**

尽管赋值表达式通常很简单,但有时候你看能会看到一个大型表达式中会用到赋值表达式的值.例如,可以像下面这样在同一个表达式中赋值并测试这个值l:

(a=b) === 0

如果你要这样做,最好明白=和===操作符的区别,注意=的优先级很低,在较大的表达式中使用赋值的值通常需要使用圆括号.

赋值操作符具有右结合性,这意味着如果一个表达式中出现多个赋值操作符.他们会从右向左求值.因此,可以通过如下代码将一个值赋值给多个变量:

i=j=k=0;// 把3个变量都初始化0

4.11.1 通过操作赋值

除了常规的 = 赋值操作符,js还支持其他一些赋值操作符,这些操作符通过组合赋值和其他操作符提供了快捷操作,例如,+=操作符执行加法和赋值操作.下面这个表达式:

total += salesTax;
等价于
total = total + salesTax;

可能你也想到了,+=操作符可以处理数值和字符串.对数值操作数,他执行加法并赋值,对字符串操作数,他执行拼接操作.

类似的操作符号还有-=,*=,&=等等,4-2表列出

操作符示例等价于
+=a+=ba=a+b
-=a-=ba =a-b
*=a*=ba = a * b
/=a /=ba = a / b
%=a %= ba = a % b
**=a **= ba = a ** b
<<=a <<= ba = a << b
>>=a >>= ba = a >> b
>>>=a >>>= ba = a >>> b
&=a &= ba = a & b
|=a |= ba = a | b
^=a ^= ba = a ^ b

多数情况下,表达式:

多数情况下,表达式:
a op = vb
其中(op是操作符)等等价于表达式:
a = a op b

在第一行,表达式a只被求值一次.而在第二行,它会被求值两次.这两种情况只有在a包含副效应(如函数调用或递增操作符)时才会有区别.比如,下面这两个表达式就不一样了:

data[i++] *= 2
data[i++] = data[i++] * 2;

4.12求值表达式

与很多解释型语法一样,js有能力解释js源代码字符串,对他们求值以产生一个值,js是通过全局函数eval()来对源代码字符串求值的:

eval("3+2") //5

对源代码字符串的动态求值是一个强大的语言特性,但这种特性在实际项目中几乎用不到.如果你发现自己在使用eval函数,那应该好好思考一下到底是不是真的需要使用它,特别,eval()可能会成为安全漏洞,为此永远不要把来自用户输入的字符串交给它执行.对于将js这么复杂的语言,无法对用户输出脱敏,因此无法保证在eval()中安全的使用.由于这些安全问题,某些web服务器使用HTTP的"Content-Security-Policy"头部对整个网站禁用eval();

接下来几节将解释eval()的基本使用,并解析它的两个对优化程序影响不大的受限版本.

eval()是函数式操作符?

eval()是一个函数,但之所以在讲表达式的本章介绍他,是因为他其实应该是个操作符.

js语言最初的版本定义了一个eval()函数,而从那时候起,语言设计者和解释器开发者一直对他加以限制,导致他越来越像操作符.现代js解释器会执行大量代码分析和优化.一般来说,如果如果一个函数调用eval().则解释器将无法再优化该函数.把eval()定义为函数的问题在于可以给它起不同的名字:

let f = eval
let g = f;

如果可以这样,那么解释器无法确定哪个函数会调用eval(),也就无法激进优化.假如eval()是个操作符(即保留字),那这个问题就可以避免.后面(4.12.2节和4.12.3节)会介绍对eval()的限制.而这些限制也让它更像操作符.

4.12.1 eval()

eval()期待一个参数.如果传给他任何非字符串值,他会简单地返回这个值.如果传入字符串,他会尝试把这个字符串当成**js代码解析**,解析失败会抛出SyntaxError.如果解析成功,他会求值代码并返回该字符串中最后一个表达式或语句的值;如果最后一个表达式或语句没有值则返回undefined.如果求值字符串抛出异常,该异常会从调用eval()的地方传播出来.

对于eval()(在像这样调用时),关键在于他会使用调用他的代码的变量环境.也就是说,他会像本地代码一样查找变量的值,定义新变量和函数.如果一个函数定义了一个局部变量x,然后调用了eval(‘x’),那他会取得这个局部变量的值.如果这个函数调用了eval(“var y = 3;”),则会声明一个新局部变量y.

另外,如果被求值的字符串使用了let或const,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境中.

类似的,函数也可以像下面这样声明一个局部函数:

eval("function f(){return x+1;}");

如果在顶级代码中调用eval(),则他操作的一定是全局变量和全局函数.

注意,传给eval的代码字符串本身必须从语法上说得通:不能使用它向函数中粘贴代码.比如,eval(“return;”)是没有意义的,因为return只在函数中是合法的,即使被求值的字符使用与调用函数相同的变量环境,这个字符串也不会成为函数的一部分.只要这个字符串本身可以作为独立的脚本运行(即使像x=0这么短),都可以合法地传给eval(),否则,eval()将抛出SyntaxError.

4.12.2 全局eval()

之所以eval()会干扰js的优化程序,是因为他能够修改局部变量.不过作为应对,解释器也不会过多的优化调用eval()的函数.那么,如果某脚本为eval()定义了别名,然后又通过另一个名字调用这个函数,js解释器该怎么做?

**js规范中说,如果eval()被以"eval"之外的其他名字调用时,他应该把字符串当成顶级全局代码来求值.**被求值的代码可能定义新全局变量或全局函数,可能修改全局变量,但他不会再使用或修改调用函数的局部变量.因此也就不会妨碍局部优化.

相对而言,使用名字"eval"来调用eval()函数就叫做"直接eval"(这样就有点保留字的感觉).直接调用eval()使用的是调用上下文的变量环境.

任何其他调用方式,包括间接调用,都是用全局对象作为变量环境,因而不能读,写或定义局部变量或函数(无论直接调用还是间接调用都只能通过var来定义新变量.在被求值的字符串中使用let和const创建的变量会被限制在求值的局部作用域内,不会修改调用或全局环境).

const geval = eval;// 使用另一个名字,实现全局求值
let x = "global",y = "global"// 两个全局变量
function f(){// 这个函数直接调用eval()
	let x = "local";//定义一个局部变量
	eval("x += 'changed'");// 直接调用修改局部变量
	return x;// 返回修改或的局部变量
}
function g(){// 这个函数全局(间接)调用eval
	let y = "local" // 定义一个局部变量
	geval("y += 'changed';"); // 间接调用修改'全局变量'
	return y;// 返回未修改的局部变量
	
}
console.log(f(),"===",x) // 局部变量变了,打印:"localchanged global"
console.log(g(),"===",y) // 全局变量变了,打印:"local globalchanged"
VM57:14 localchanged === global
VM57:15 local === globalchanged

注意,这种全局求值的能力不仅仅是为了适应优化程序的需求,同时也是以后使用及其有用的特性,可以让我们把代码字符串作为独立,顶级的脚本来执行.正如本节开始时提到的,真正需要求值代码字符串的场景非常少.假如你必须使用eval(),那很可能应该使用它的全局求值而不是局部求值.

4.12.3严格eval()

严格模式(5.6.3节)对eval()函数增加了更多限制,甚至对标识符"eval"的使用也进行了限制.当我们在严格模式下调用eval()时候,或者当被求值的代码字符串以"use strict"指令开头,eval()会基于一个私有变量环境进行局部求值.这意味着在严格模式下,被求值的代码可以查询和设置局部变量,但不能在局部作用域中定义新变量或函数.

另外,严格模式让eval()变得更像操作符,因为"eval"在严格模式下会变成保留字.此时不能再使用新值来重写eval()函数.换句话说,通过名字"eval"来声明变量,函数,函数参数或捕获参数都是不允许的.

"use strict";
eval("var d = 'test';");
console.log(d);// Uncaught ReferenceError: d is not defined

4.13其他操作符

js还支持另外一些操作符,接下来介绍

4.13.1 条件操作符(?

条件操作符是js唯一一个三元操作符(有三个操作数),因此有时候也被叫做三元操作符.这个操作符有时候会被写作?:,尽管他在代码中并不是这样的.

这个操作符有三个操作数,第一个在?前面,第二个在?和:中间,第三个在:后面.因此在代码中一般是这样的:

x>0 ? x: -x // 求x的绝对值

条件操作符的操作数可以是任意类型.第一个操作数被求值并解释为一个布尔值.如果第一个操作数的值为真值,那么就求值第二个操作数,并返回他的值.否则,求值第三个操作数并返回他的值.第二个或第三个操作数只有一个会被求值,不可能两个都求值.

可以使用if语句(5.3.1节)实现类似的结果,但?:操作符更加简洁.下面展示了他的典型应用,其中检查了变量如果有定义(一个有意义的值)就使用它,否则就提供了一个默认值:

greeting = "hello "+ (username?username:"there")

这行代码等价于

greeting = "hello "
if(username){
	greeting += username;
}else{
	greeting += "there"
}

4.12.2先定义(??)

先定义(first-defined)操作符??求值其先定义的操作数,如果其做操作数不是null或undefined,就返回该值.否则,他会返回右操作数的值.与&&或||操作符类似,??是短路的:他只在第一个操作数求值为null或undefined时才会求值第二个操作数.

如果表达式a没有副效应,那么表达式a??b等价于:

(a!==null&&a!==undefined)?a:b

??是对||(参见4.10.2节)的一个有用的替代,适合选择先定义的操作数,而不是第一个为真值的操作数.尽管||名义上是个逻辑或操作符,习惯上也会使用它选择第一个非假值操作数,比如:

// 如果maxWidth是真值,就使用它,否则,看看preference
// 对象,如果preference里也没有真值,就使用硬编码的常量
let max = maxWidth || preference.maxWidth || 500

这种习惯用法的问题在于0,空字符串和false都是假值,但这些值在某些情况下是完全有效的.对上面的代码示例来说,maxWidth如果等于零,该值就会被忽略,如果我们把||改为??,那么对这个表达式来说,0也会称为有效的值:

// 如果maxWidth有定义,就使用它,否则看看preference
// 对象,如果preference也没有定义,就使用硬编码的常量
let max = maxWidth ?? preference.maxWidth ?? 500;

下面再看几个例子,其中??的第一个操作数都是假值.如果这个操作数是假值但有定义,??仍然返回这个值.只有当第一个操作数"缺值"(nullish)时(即null或undefined),这个操作符才会求值并返回第二个操作数:

let options = { timeout:0,title:"",verbose:false,n:null};
options.timeout ?? 100 // 0 在对象中有定义(不是undefined或null)
options.title??"Untitiled" // ""在对象中有定义
options.verbose ?? true // false 在对象中有定义
options.quiet ?? false // false 属性没有定义
options.n ?? 10 // 10,属性值为null

注意,如果我们使用||而不是??,这里的timeout,title和verbose表达式会求值为不同的结果.

??操作符与&&和||操作数类似,但优先级并不比他们更高或更低.如果表达式中混用了??和他们中的任何一个,必须使用圆括号说明先执行那个操作:

(a??b)||c // ??先执行,然后执行||
a??(b||c) // ||先执行,然后执行??
a??b||c // SyntaxError:必须有圆括号

??操作符是es2020定义的,在2020年初已经得到所有主流浏览器当前和预览版的支持.这个操作符正式的名字叫"缺值合并"(nullish coalesing)操作符,但我们没有使用这个叫法.因为这个操作符会选择自己的一个操作数,但我并没有看到他会"合并"操作数.

4.13.1typeof操作符

typeof是个一元操作符,放在自己的操作数前面,这个操作数可以是任意类型.typeof操作符的值是一个字符串,表明操作数的类型.表4-3列出了所有js值在应用typeof操作符后得到的值.

表4-3:typeof操作符的返回值

xtypeof x
undefined“undefined”
null“object”
true或false“boolean”
任意数值或NaN“number”
任意BigInt“bigint”
任意字符串“string”
任意函数“function”
任意非函数对象(数组,对象)“object”
任意符号“symbol”

可以像下面这样在表达式中使用typeof操作符:

// 如果value是字符串,把他包含在引导中,否则把他转换为字符串
(typeof value === "string")?"''"+value+"''":value.toString()

注意,如果操作数的值是null,typeof返回"object".如果想区分null和对象**,必须显式测试这个特殊值**.

尽管js函数是一种对象,typeof操作符也认为函数不一样,因为他们有自己的返回值.

因为对除函数之外的所有对象和数组值,typeof都求值为"object",所有可以只用他来区分对象其他原始类型.而要区分不同的类,必须使用其他方法,例如instanceof操作符(4.9.4节),class特性(14.4.3节),或者constructor属性(9.2.2和14.3节)

4.13.4 delete操作符

delete是一元操作符,尝试删除其操作数指定的对象属性数组元素.与赋值,递增和递减操作符一样,使用delete通常也是为发挥其属性删除的副效应,而不是使用它返回的值.

let o = {x:1,y:2};// 先定义一个对象
delete o.x;// 删除他的属性
"x" in o // false:这个属性不存在了
let a = [1,2,3] // 定义一个数组
delete a[2] // 删除数组的最后一个元素
2 in a // false 数组元素2不存在了
a.length // 3 注意,数组的长度没有变化

注意,被删除的属性或数组元素不仅会被设置undefined.当删除一个属性时,这个属性就不复存在了,尝试读取不存在的属性就会返回undefined,但是可以通过in操作符(4.9.3)测试某个属性是否存在.删除某个数组元素会在数组中留下一个"坑",并不改变数组的长度.结果数组是一个稀疏数组(7.3节)

delete期待他啊的操作数是个左值.如果操作数不是左值,delete什么也不做,且返回true.否则delete删除指定的左值.如果删除成功则返回true.但是并非所有属性都是可以删除的:不可以配置属性(14.1节)就无法删除

在严格模式下,delete的操作数如果是未限定标识符,比如变量,函数或函数参数,就会导致SyntaxError.此时,delete操作符只能作用于属性访问表达式(4.4节)严格模式也会在delete尝试删除不可配置(即不可删除)属性时抛出TypeError.但在严格模式之外,这两种情况都不会发生异常,delete只是简单地返回false,表示不能删除操作数.

下面是几个使用delete操作数的例子:

let o = {x:1,y:2}
delete o.x;// 删除对象的一个属性 返回true
typeof o.x;// 属性不存在 返回undefined
delete o.x;// 删除不存在的属性 返回true
delete 1 // 毫无意义,但是返回true
delete o;// 不删除变量;返回false,或在严格模式下报SyntaxError
delete Object.prototype;// 不可删除的属性:返回false,或在严格模式下报TypeError

6.4节会再看到delete操作符

4.13.5 await操作符

await是es2017增加的,用于让js中的异步编程更加自然,要理解这个操作符需要阅读13章.简单来说,await期待一个Promise对象(表示异步计算)作为其唯一操作符,可以让代码看起来是在等待异步计算完成(但实际上他不会阻塞主线程,不会妨碍其他异步操作进行).await操作符的值是Promise对象的兑现值.关键在于await只能出现在已经通过async关键字声明为异步的函数中.同样,要了解完整的细节参考13章.

4.13.6 void操作符

void是一元操作符,出现在他的操作数前面,这个操作数可以是任意类型.void是个与众不同的操作符,用处不过:它求值自己的操作数,然后丢弃这个值并返回undefined.由于操作数的值会被丢弃,只有在操作数有副效应时才有必要使用void操作符.

void操作符太难解释了,也很难给出一个实际的例子说明它的用法.一种情况时你要定义一个函数,这个函数什么也不会返回,但却使用了箭头函数的简写语法(8.1.3节)其中函数体是一个会被求值并返回的表达式.如果你只想对这个表达式求值,不想返回他的值,那最简单的方法是用花括号把函数包起来.此时,作为替代也可以使用void操作符:

let counter = 0;
const increment = () => void counter++;
increment() // undefined
counter // 1

4.13.7逗号操作符(,)

逗号操作符是二元操作符,其操作数可以是任意类型.这个操作符会求值其左操作数,求值其右操作数,然后返回右操作数的值.因此,下面这行代码:

 i = 0, j = 1, k = 2;

求值为2,基本上等价于:

i = 0;j=1;k=2;

换句话,逗号左侧的操作数始终会被求值,但这个值会被丢弃.而这也意味着只用当左侧表达式有副效应的时才有必要使用逗号操作符.逗号操作符唯一常见的使用场景就是有多个循环变量的for循环(5.4.3节)

// 第一个逗号是let语句语法的一部分
// 第二个逗号是逗号操作符,它让我们把两个表达式(i++与j--)
// 放到了本来期待一个表达式的语句(for循环中)
for(let i = 0,j = 10;i<j;i++,j--){
	console.log(i+j)
}

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: JavaScriptJava