【表达式和运算符】原始表达式,初始化表达式(对象和数组的),函数定义表达式,属性访问表达式,调用表达式,对象创建表达式,运算符概述,算术表达式,关系表达式,逻辑表达式,赋值表达式,表达式计算,其他运算符。
程序中的常量是最简单的一类表达式。变量名也是一种简单的表达式,它的值是赋值给变量的值。复杂表达式是由简单表达式组成的。
将简单表达式组合成复杂表达式最常用的方法就是使用运算符。
【原始表达式】
最简单的表达式是“原始表达式”(primary expression)。原始表达式是表达式的最小单位——它们不再包含其他表达式。
JavaScript中的原始表达式包含常量或直接量、关键字和变量。
当JavaScript代码中出现了标识符,JavaScript会将其当做变量去查找它的值。如果变量名不存在,表达式运算结果为undefined。
在ECMAScript 5的严格模式中,对不存在的变量进行求值会抛出一个引用错误异常。
【对象和数组的初始化表达式】
对象和数组初始化表达式实际上是一个新创建的对象和数组。这些初始化表达式有时称做“对象直接量”和“数组直接量”。
和布尔直接量不同,它们不是原始表达式,因为它们所包含的成员或元素都是子表达式。
数组初始化表达式,初始化的结果是一个新创建的数组。
数组初始化表达式中的元素初始化表达式也可以是数组初始化表达式,即,这些表达式可以嵌套:
JavaScript对数组初始化表达式进行求值时,数组初始化表达式中的元素表达式也会各自计算一次。即,数组初始化表达式每次计算的值有可能是不同的。
【函数定义表达式】
函数定义表达式定义了一个JavaScript函数。表达式的值是这个新定义的函数。从某种意义上讲,函数定义表达式可称为“函数直接量”。
一个典型的函数定义表达式包括关键字function,一对圆括号跟随其后,括号内是一个以逗号分割的列表,列表含有0个或多个标识符(参数名),
再跟随一个有花括号包裹的JavaScript代码段(函数体)。
函数定义表达式可包含函数的名字。函数也可通过函数语句来定义。
【属性访问表达式】
属性访问表达式运算得到一个对象属性或一个数组元素的值。JavaScript为属性访问定义了两种语法:
第一种写法:一个表达式后跟随一个句点和标识符。表达式指定对象,标识符则指定需要访问的属性的名称。
第二种写法:使用方括号,方括号内是另一个表达式(这种方法适用于对象和数组)。第二个表达式指定要访问的属性的名称或代表要访问数组元素的索引。
不管使用哪种形式的属性访问表达式,在“.”和“[”之前的表达式总是会首先计算。
如果计算结果是null和undefined,表达式会抛出一个类型错误异常,因为这两个值都不能包含任意属性。
如果运算结果不是对象(或数组),JavaScript会将其转换为对象。
如果对象表达式后跟随句点和标识符,则会查找由这个标识符所指定的属性的值,并将其作为整个表达式的值返回。
如果对象表达式后跟随一对方括号,则会计算方括号内的表达式的值并将它转换为字符串。
不论哪种情况,如果命名的属性不存在,那么整个属性访问表达式的值就是undefined。
【调用表达式】
JavaScript中的调用表达式(invocation expression)是一种调用(或执行)函数或方法的语法表示。
当对调用表达式进行求值时,先计算函数表达式,再计算参数表达式,得到一组参数值。
如果函数表达式的值不是一个可调用的对象,则抛出一个类型错误异常(所有的函数都是可调用的,即使宿主对象不是函数也有可能被调用)。
然后,实参的值被依次赋值给形参,这些形参是定义函数时指定的,接着开始执行函数体。
如果函数使用return语句给出一个返回值,这个返回值就是整个调用表达式的值。否则,调用表达式的值就是undefined。
【对象创建表达式】
对象创建表达式(object creation expression)创建一个对象并调用一个函数(这个函数称为构造函数)初始化新对象的属性。
和函数调用表达式类似,只是对象创建表达式之前多了一个关键字new:
当计算一个对象创建表达式的值时,和对象初始化表达式通过{}创建对象的做法一样:
JavaScript先创建一个新的空对象,然后,JavaScript通过传入指定的参数并将这个新对象当做this的值来调用一个指定的函数。
这个函数可使用this来初始化这个新创建对象的属性。那些被当成构造函数的函数不会返回一个值,且这个新创建并被初始化后的对象就是整个对象创建表达式的值。
如果一个构造函数确实返回了一个对象值,那么这个对象就作为整个对象创建表达式的值,而新创建的对象就废弃了。
【运算符概述】
JavaScript中的运算符用于算术表达式、比较表达式、逻辑表达式、赋值表达式等。
需注意:
大多数运算符都是由标点符号表示,比如“+”和“=”。
而另外一些运算符则是由关键字表示的,比如delete和instanceof。
注:上表是按照运算符的优先级排序的,前面的运算符优先级高于后面的运算符优先级。
被水平线分隔开来的运算符具有不同的优先级。标题为A的列表表示运算符的结合性,L(从左到右)或R(从右到左),标题为N的列表表示操作数的个数。
标题为“类型”的列表表示期望的操作数类型,以及运算符的结果类型(在“→”符号之后)。
1.操作数的个数
运算符可根据其操作数的个数进行分类。
JavaScript中大多数运算符是一个二元运算符(binary operator),将两个表达式合并成一个稍复杂的表达式。它们的操作数均是两个。
JavaScript也支持一些一元运算符(unary operator),它们将一个表达式转换为另一个稍复杂的表达式。
例如:表达式 -x中的“-”运算符就是一个一元运算符,是将操作数x求负值。
JavaScript支持一个三元运算符(ternary operator),条件判断运算符“?:”,它将三个表达式合并成一个表达式。
2.操作数类型和结果类型
一些运算符可作用于任何数据类型,但仍然希望它们的操作数是指定类型的数据,并且大多数运算符返回(或计算出)一个特定类型的值。
在表4-1标题为“类型”的一列中列出了运算符操作数的类型(箭头前)和运算结果的类型(箭头后)。
JavaScript运算符通常会根据需要对操作数进行类型转换。
乘法运算符“ * ”希望操作数为数字,但表达式“3” * “5”是合法的,因为JavaScript会将操作数转换为数字。
这个表达式的值是数字15,而不是字符串“15”。
JavaScript中的所有值不是真值就是假值,因此对那些希望操作数是布尔类型的操作符来说,它们的操作数可以是任意类型。
一些运算符对操作数有着不同程度的依赖。例如:
“+”运算符可对数字进行加法运算,也可对字符串做连接。
“<”比较运算符可根据操作数类型的不同对数字进行大小值的比较,也可比较字符在字母表中的次序先后。
3.左值
左值(lvalue)是一个古老的术语,指“表达式只能出现在赋值运算符的左侧”。
在JavaScript中,变量、对象属性和数组元素均是左值。
ECMAScript规范允许内置函数返回一个左值,但自定义的函数则不能返回左值。
4.运算符的副作用
计算一个简单的表达式(比如2*3)不会对程序的运行状态造成任何影响,程序后续执行的计算也不会受到该计算的影响。
而有些表达式则具有很多副作用,前后的表达式运算会相互影响。例如:
赋值运算符:如果给一个变量或属性赋值,那些使用这个变量或属性的表达式的值都会改变。
“++”和“--”递增和递减运算符与此类似,因为它们包含隐式的赋值。
delete运算符也有副作用:删除一个属性就像(但不完全一样)给这个属性赋值undefined。
在函数体或者构造函数内部运用了这些运算符并产生了副作用时,我们就说函数调用表达式和对象创建表达式是有副作用的。
5.运算符优先级
表4-1所示的运算符是按照优先级从高到低排序的。优先级高的运算符(表格的顶部)的执行总是先于优先级低(表格的底部)的运算符。
需注意:属性访问表达式和调用表达式的优先级要比表4-1中列出的所有运算符都要高。
需熟记:乘法和除法的优先级高于加法和减法,赋值运算的优先级非常低,通常总是最后执行。
6.运算符的结合性
表4-1中标题为A的列说明了运算符的结合性。L指从左至右结合,R指从右至左结合。
结合性指定了在多个具有同样优先级的运算符表达式中的运算顺序。
从左至右是指运算的执行按照由左到右的顺序进行。例如:
减法运算符具有从左至右的结合性。
一元操作符、赋值和三元条件运算符都具有从右至左的结合性。
7.运算顺序
运算符的优先级和结合性规定了它们在复杂的表达式中的运算顺序,但并没有规定子表达式的计算过程中的运算顺序。
JavaScript总是严格按照从左至右的顺序来计算表达式。
只有在任何一个表达式具有副作用而影响到其他表达式的时候,其求值顺序才会和看上去有所不同。
【算术表达式】
1.“+”运算符
二元加法运算符“+”可对两个数字做加法,也可做字符串连接操作:
加号的转换规则优先考虑字符串连接,如果其中一个操作数是字符串或者转换为字符串的对象,另外一个操作数将会转换为字符串,加法进行字符串连接操作。
如果两个操作数都不是类字符串,那么将进行算术加法运算。
需注意:当加号运算符和字符串和数字一起使用时,需考虑加法的结合性对运算顺序的影响。
2.一元算术运算符
一元运算符作用于一个单独的操作数,并产生一个新值。在JavaScript中,一元运算符具有很高的优先级,且是右结合。
“+”、“-”、“++”、“--”,需注意,“+”和“-”既是一元运算符,也是二元运算符。
3.位运算符
位运算符可对由数字表示的二进制数据进行更低层级的按位运算。
这些运算符在JavaScript编程中并不常用,可跳过本节内容。
【关系表达式】
关系运算符用于测试两个值之间的关系(相等、小于、是...的属性),根据关系是否存在而返回true或false。
关系表达式总是返回一个布尔值,通常在if、while或者for语句中使用关系表达式,用以控制程序的执行流程。
1.相等和不等运算符
“ = ” →→ “得到或赋值”,“ == ” →→ “相等”,“ === ” →→ “恒等/严格相等”。
“ != ”→→ “不相等”,“!==” →→ “不严格相等”。
2.比较运算符
用来检测两个操作数的大小关系(数值大小或者字母表的顺序)
比较操作符的操作数可能是任意类型。然而,只有数字和字符串才能真正执行比较操作。
那些不是数字和字符串的操作数都将进行类型转换,转换规则如下:
1.如果操作数是对象,这个对象转换为原始值:如果valueof()返回一个原始值,直接使用这个原始值。否则,使用toString()的转换结果进行比较操作。
2.在对象转换为原始值之后,如果两个操作数都是字符串,将依照字母表的顺序对两个字符串进行比较,这里的“字母表顺序”是指组成这个字符串的16位Unicode字符的索引顺序。
3.在对象转换为原始值之后,如果至少有一个操作数不是字符串,那么两个操作数都将转换为数字进行值比较。
0和-0是相等的。Infinity比其他任何数字都大(除了它本身),-Infinity比其他任何数字都小(除了它本身)。
如果其中一个操作数是(或转换后是)NaN,那么比较操作符总是返回false。
需注意:
JavaScript字符是一个由16位整数值组成的序列,字符串的比较只是两个字符串中的字符的比较。
由Unicode定义的字符编码顺序和任何特定语言或者本地语言字符集中的传统字符编码顺序不尽相同。
注意,字符串比较是区分大小写的,所有的大写的ASCII字母都“小于”小写的ASCII字母。例如:
Zoo < aardvark // 结果为true
对于数字和字符串操作符来说,加号运算符更偏爱字符串,如果它的其中一个操作数是字符串的话,则进行字符串连接操作。而比较运算符更偏爱数字,只有在两个操作数都是字符串的时候,才会进行字符串的比较:
3.in运算符
in运算符希望它的左操作数是一个字符串或可以转换为字符串,希望它的右操作数是一个对象。如果右侧的对象拥有一个名为左操作数值的属性名,那么表达式返回true。
例如:
4.instanceof 运算符
instanceof 运算符希望左操作数是一个对象,右操作数标识对象的类。如果左侧的对象是右侧类的实例,则表达式返回true;否则返回false。
JavaScript中对象的类是通过初始化它们的构造函数来定义的。这样,instanceof的右操作数应当是一个函数。比如:
需注意:所有的对象都是Object的实例。当通过instanceof判断一个对象是否是一个类的实例时,这个判断也会包含对“父类”(superclass)的检测。
如果instanceof的左操作数不是对象的话,instanceof返回false。如果右操作数不是函数,则抛出一个类型错误异常。
【逻辑表达式】
逻辑运算符“&&”、“||” 和 “!”是对操作数进行布尔算术运算,经常和关系运算符一起配合使用。
下面6个值可以被转换成false:
false和上面6个可以转换为false的值有时称做“假值”(falsy value),其他值称做“真值”(truthy value)。
JavaScript期望使用一个布尔值的时候,假值会被当成false,真值会被当成true。
1.逻辑与(&&)
可从三个层次进行理解:
第一层理解:
当操作数都是布尔值的时候,“&&”对两个值执行布尔与(AND)操作,只有在第一个操作数和第二个操作数都是true的时候,它才会返回true。
如果其中一个操作数是false,它返回false。
关系表达式的运算结果总是true或false,因此当这样使用时,“&&”运算符本身也会返回true或false。关系运算符的优先级比“&&”(“||”)要高。
但“&&”的操作数不一定是布尔值。有些值可当做“真值”和“假值”。(假值是false、null、undefined、0、-0、NaN和" ",所有其他的值包括所有对象都是真值。)
第二层理解:
“&&”可对真值和假值进行布尔与(AND)操作。如果两个操作数都是真值,返回一个真值;否则,至少一个操作数是假值的话,则返回一个假值。
第三层理解:
运算符首先计算左操作数的值,即首先计算“&&”左侧的表达式。如果计算结果是假值,整个表达式的结果一定也是假值,因此“&&”这时简单地返回左操作数的值,而并不会对右操作数进行计算。
反过来讲,如果左操作数是真值,整个表达式的结果则依赖于右操作数的值。如果右操作数是真值,整个表达式的值一定是真值;如果右操作数是假值,
整个表达式的结果一定也是假值。因此,左操作数是真值时,“&&”运算符将计算右操作数的值并将其返回作为整个表达式的计算结果:
“&&”的行为有时称做“短路”(short circuiting),下面两行代码是完全等价的:
一般来讲,当“&&”右侧的表达式具有副作用时(赋值、递增、递减和函数调用表达式)要格外小心,这些带有副作用的表达式的执行依赖于左操作数的计算结果。
2.逻辑或(||)
“||”运算符对两个操作数做布尔或(OR)运算。如果其中一个或者两个操作数是真值,它返回一个真值。如果两个操作数都是假值,它返回一个假值。
它会首先计算第一个操作数的值(左侧表达式),如果计算结果为真值,那么返回这个真值。否则,再计算第二操作数的值,即计算右侧的表达式,
并返回这个表达式的计算结果。
3.逻辑非(!)
“!”运算符是一元运算符。它放置在一个单独的操作数之前。目的是将操作数的布尔值求反。例如:
如果x是真值,则 !x返回false;如果x是假值,则!x返回true。
和“&&”与“||”运算符不同,“!”运算符先将其操作数转换为布尔值,然后再对布尔值求反。即,“!”总是返回true或者false,且可以通过
使用两次逻辑非运算来得到一个值的等价布尔值:!!x
作为一个一元运算符,“!”具有很高的优先级,并且和操作数紧密绑定在一起。如果希望对类似 p && q 的表达式做求反操作,
需使用圆括号:!(p && q)。
【赋值表达式】
“=”运算符希望它的左操作数是一个左值:一个变量或者对象属性(或数组元素)。它的右操作数可是任意类型的任意值。
赋值表达式的值就是右操作数的值。赋值表达式的副作用是:右操作数的值赋值给左侧的的变量或对象属性,后续对这个变量和对象属性的引用都将得到这个值。
赋值操作符的结合性是从右至左,即,如果一个表达式中出现了多个赋值运算符,运算顺序shi是从右到左。因此通过如下方式对多个变量赋值:
带操作的赋值运算
在第一行中,表达式a计算了一次,在第二行中,表达式a计算了两次。只有在a包含具有副作用的表达式(比如函数调用和赋值操作)的时候,两者才不等价。
下面两表达式不等价:
【表达式计算】
和其他很多解释性语言一样,JavaScript同样可以解释运行由JavaScript源代码组成的字符串,并产生一个值。JavaScript通过全局函数eval():
动态判断源代码中的字符串是一种强大的语言特性,几乎没有必要在实际中应用。如果你使用了eval(),你应当仔细考虑是否真的需要使用它。
1.eval()
eval()只有一个参数。如果传入的参数不是字符串,它直接返回这个参数。如果参数是字符串,它会把字符串当成JavaScript代码进行编译,如果编译失败
则抛出一个语法错误(SyntaxError)异常。如果编译成功,则开始执行这段代码,并返回字符串中的最后一个表达式或语句的值,如果最后一个表达式或语句没有值,
则最终undefined。如果字符串抛出一个异常,这个异常将该调用传递给eval()。
关于eval()最重要的是,它使用了调用它的变量作用域环境。即,它查找变量的值和定义新变量和函数的操作和局部作用域中的代码完全一样。
如果一个函数定义了一个局部变量x,然后调用eval("x"),它会返回局部变量的值。
如果它调用eval("x=1"),他会改变局部变量的值。
如果函数调用了eval("var y = 3;"),它声明一个新的局部变量y。
同样一个函数可通过以下代码声明一个局部函数:
如果在最顶层代码中调用eval(),它会作用于全局变量和全局函数。
需注意,传递给eval()的字符串必须在语法上讲的通——不能通过eval()往函数中任意粘贴代码片段,比如,eval("return;")是没有意义的,因为return只有在函数中才起作用。
并且事实上,eval的字符串执行时的上下文环境和调用函数的上下文环境是一样的,这不能使其作为函数的一部分来运行。如果字符串作为单独的脚本是有语义的
(如x=0的短代码),那么将其传递给eval()作参数是完全没有问题的,否则,eval()将抛出语法错误异常。
2.全局eval()
eval()具有更改局部变量的能力,这对于JavaScript优化器来说是一个很大问题。而作为权宜之计,JavaScript解释器针对那些调用了eval()的函数所做的优化并不多。
但当脚本定义了eval()的一个别名,且用另一个名称调用它,JavaScript解释器又会如何工作?
为了让JavaScript解释器的实现更加简化,ECMAScript 3标准规定了任何解释器都不允许对eval()赋予别名。如果eval()函数通过别名调用时,会抛出一个EvalError异常。
实际上,大多数的实现并不是这么做的。当通过别名调用时,eval()会将其字符串当成顶层的全局代码来执行。执行的代码可能会定义新的全局变量和全局函数,或者给全局变量赋值,但不能使用或修改主调函数中的局部变量,因此,这不会影响到函数内的代码优化。
ECMAScript 5是反对使用EvalError,并规范了eval()的行为。“直接的eval”,当直接使用非限定的“eval”名称(eval看起来像一个保留字)来调用eval()函数时,通常称为“直接eval”(direct eval)。直接调用eval()时,它总是在调用它的上下文作用域内执行。其他的间接调用则使用全局对象作为其上下文作用域,并且无法读、写、定义局部变量和函数。
全局eval的这些行为不仅仅是出于代码优化器的需要而做出的一种折中方案,它实际上是一种非常有用的特性,它允许我们执行那些对上下文没有任何依赖的全局脚本代码段。
真正需要eval来执行代码段的场景并不多见。但当你意识到它的必要性时,你更可能会使用全局eval而不是局部eval。
3.严格eval()
ECMAScript 5严格模式对eval()函数的行为施加了更多的限制,甚至对标识符eval的使用也施加了限制。
在严格模式下调用eval()时,或者eval()执行的代码段以“use strcit”指令开始,这里的eval()是私有上下文环境中的局部eval。即,
在严格模式下,eval执行的代码段可查询或更改局部变量,但不能在局部作用域中定义新的变量或函数。
此外,严格模式将“eval”列为保留字,这让eval()更像一个运算符。不能用一个别名覆盖eval()函数。且变量名、函数名、函数参数或者异常捕获的参数都不能取名为“eval”。
【其他运算符】
1.条件运算符(?:)
条件运算符是JavaScript中唯一的一个三元运算符(三个操作数),有时直接称做“三元运算符”。
条件运算符的操作数可以是任意类型。第一个操作数当成布尔值,如果它是真值,将计算第二个操作数,并返回其计算结果。
否则,如果第一个操作数是假值,将计算第三个操作数,并返回其计算结果。第二个和第三个操作数的会计算其中之一,不会两者同时执行。
使用if语句也会带来同样的效果,“?:”运算符提供了一个简写形式。“?:”一个典型应用场景,判断一个变量是否有定义(并拥有一个有意义的真值),
如果有定义则使用它,如果无定义则使用一个默认值:
2.typeof运算符
typeof是一元运算符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。
typeof运算符在switch语句中非常有用,需注意,typeof运算符可以带上圆括号,这让typeof看起来像一个函数名。
当操作数是null时,typeof将返回“object”。如果想将null和对象区分开来,则必须针对特殊值显式检测。
对宿主对象来说,typeof有可能并不返回“object”,而返回字符串。但实际上客户端JavaScript中的大多数宿主对象都是“object”类型。
3.delete运算符
delete是一元操作符,它用来删对象属性或者数组元素。delete也有副作用,它是用来做删除操作的,不是用来返回一个值的,例如:
需注意,删除属性或者删除数组元素不仅仅是设置了一个undefined的值。当删除一个属性时,这个属性将不再存在。读取一个不存在的属性将返回一个undefined的值,但可以通过in运算符来检测这个属性是否在对象中存在。
delete希望他的操作数是一个左值,如果它不是左值,那么delete将不进行任何操作同时返回true。否则,delete将试图删除这个指定的左值。如果删除成功,delete将返回true。
然而并不是所有的属性都可删除,一些内置核心和客户端属性是不能删除的,用户通过var语句声明的变量不能删除。同样,通过function定义的函数和函数参数也不能删除。
在ECMAScript 5严格模式下,如果delete的操作数是非法的,比如变量、函数或函数参数,delete操作将抛出一个语法错误异常,只有操作数是一个属性访问表达式的时候
它才会正常工作。在严格模式下,delete删除不可配置的属性时会抛出一个类型错误异常。在非严格模式下,这些delete操作都不会报错,只是简单地返回false,
以表明操作数不能执行删除操作。
4.void运算符
void是一元运算符,它出现在操作数之前,操作数可以是任意类型。这个运算符并不是经常使用:操作数会照常计算,但忽略计算结果并返回undefined。
由于void会忽略操作数的值,因此在操作数具有副作用的时候使用void来让程序更具语义。
这个运算符zuicagn最常用在客户端的URL——javascript:URL中,在URL中可写带有副作用的表达式,而void则让浏览器不必显示这个表达式的j计算结果。
例如,经常在HTML代码中的<a>标签里使用void运算符:
5.逗号运算符
总是会计算左侧的表达式,但计算结果忽略掉,即,只有左侧表达式具有副作用,才会使用逗号运算符让代码变得更通顺。逗号运算符最常用的场景是在for循环中,这个for循环通常具有多个循环变量:
问题:
1.