表达式是JavaScript程序的基础构建单元,它代表了一段可以被JavaScript解释器计算并产生值的代码片段。简单来说,表达式就像数学中的算式一样,通过运算得出结果。在JavaScript中,从最简单的数字常量到复杂的函数调用,都可以看作是表达式。
当我们深入理解表达式的本质时,会发现它们具有组合性的特点。简单的表达式可以通过运算符组合成更复杂的表达式。比如一个变量名是最简单的表达式,它的值就是变量所存储的内容。而数组访问表达式则由一个数组表达式、一个方括号、一个索引表达式和另一个方括号组成,最终得到数组中指定位置的元素值。

运算符在表达式的构建过程中起着核心作用。它们将操作数(通常是两个)按照特定的方式组合,计算出新的值。乘法运算符就是一个典型的例子,表达式width * height会计算出两个变量的乘积。为了便于理解,我们有时会说运算符“返回”一个值,而不是严格地说“计算出”一个值。
这节课我们将介绍JavaScript的所有运算符,同时解释那些不使用运算符的表达式形式,如数组索引和函数调用。如果你已经熟悉其他使用C风格语法的编程语言,会发现JavaScript的大部分表达式和运算符语法都很相似。
基本表达式是最简单的表达式形式,它们独立存在,不包含任何更简单的子表达式。在JavaScript中,基本表达式包括常量值、特定的语言关键字以及变量引用。
|// 基本表达式示例 let score = 95; // 数字字面量 let message = "欢迎学习"; // 字符串字面量 let isValid = true; // 布尔字面量 let data = null; // null字面量 let currentUser = this; // this关键字 let result = score; // 变量引用 console.log(typeof score); // "number" console.log(typeof message); // "string" console.log(typeof isValid); // "boolean"
对象和数组初始化器是用于创建新对象或数组的表达式。它们提供了一种简洁的方式来构建数据结构。
|// 数组初始化器示例 let numbers = [1, 2, 3, 4, 5]; // 数字数组 let fruits = ['苹果', '香蕉', '橙子']; // 字符串数组 let mixed = [1, 'hello', true, null]; // 混合类型数组 let matrix =
属性访问表达式是用来获取对象中属性的值,或者访问数组中的某个元素。
在JavaScript中,访问对象属性时可以直接使用点(.)的方式,例如obj.prop,这种方式要求属性名是合法的标识符。
如果属性名中包含特殊字符、空格或者是变量动态指定的,就需要用方括号([])的方式,例如obj["属性名"]或obj[变量]。
对于数组,通常使用方括号加索引号的方式来访问对应位置的元素,比如arr[0]。
点表示法和方括号表示法都可以用来读取、修改或添加对象的属性,但方括号方式更灵活,可以处理动态属性名或不符合标识符规范的属性名。
|// 创建测试对象 let car = { brand: "丰田", model: "卡罗拉", year: 2023, "max-speed": 180, features: ["自动驾驶", "倒车影像", "GPS导航"] }; // 点表示法访问 console.log(car.brand); // "丰田" console.log(car.year); // 2023 // 方括号表示法访问 console.
运算符用于JavaScript的算术表达式、比较表达式、逻辑表达式、赋值表达式等。下表总结了所有运算符,可以作为便利的参考。
大多数运算符由标点符号表示,如加号和等号。但是,有些运算符由关键字表示,如delete和instanceof。关键字运算符是常规运算符,就像那些用标点符号表示的运算符一样,只是语法不够简洁。
下表按运算符优先级组织,列在前面的运算符比列在后面的运算符有更高的优先级。用水平线分隔的运算符具有不同的优先级。 标记为A的列给出运算符的结合性,可以是L(从左到右)或R(从右到左),N列指定操作数的数量。Types列列出了操作数的预期类型以及运算符的结果类型。
根据预期操作数的数量,运算符可以分为不同类型。大多数JavaScript运算符,如乘法运算符,是二元运算符,将两个表达式组合成一个更复杂的表达式。
某些运算符适用于任何类型的值,但大多数期望其操作数为特定类型,大多数运算符返回特定类型的值。表中的Types列指定了操作数类型和结果类型。
JavaScript运算符通常根据需要转换其操作数的类型。乘法运算符期望数字操作数,但表达式"6" * "7"是合法的,因为JavaScript可以将操作数转换为数字。
当然,这个表达式的值是数字42,而不是字符串"42"。记住,每个JavaScript值要么是"真值"要么是"假值",所以期望布尔操作数的运算符适用于任何类型的操作数。
某些运算符根据与它们一起使用的操作数类型表现不同。最显著的是,加号运算符对数字操作数执行加法,但对字符串操作数执行连接。 类似地,比较运算符如小于号根据操作数的类型执行数字或字母顺序比较。各个运算符的描述解释了它们的类型依赖性并指定了它们执行的类型转换。
注意赋值运算符和其他一些列在表中的运算符期望lval类型的操作数。lvalue是一个历史术语,意思是"可以合法出现在赋值表达式左侧的表达式"。在JavaScript中,变量、对象的属性和数组的元素都是左值。
计算像2 * 3这样的简单表达式永远不会影响程序的状态,程序执行的任何未来计算都不会受到该计算的影响。
然而,某些表达式有副作用,它们的计算可能影响未来计算的结果。赋值运算符是最明显的例子:如果你给变量或属性赋值,那会改变使用该变量或属性的任何表达式的值。
递增和递减运算符也类似,因为它们执行隐式赋值。delete运算符也有副作用:删除属性类似于(但不等同于)给属性赋值undefined。
运算符优先级控制操作执行的顺序。具有较高优先级的运算符在具有较低优先级的运算符之前执行。JavaScript中运算符的优先级跟数学中的运算符优先级类似,乘法和除法优先级高于加法和减法,赋值运算符的优先级最低。
考虑以下表达式:
|result = x + y * z;
乘法运算符*的优先级高于加法运算符+,所以先执行乘法。此外,赋值运算符=的优先级最低,所以在右侧的所有操作完成后才执行赋值。
运算符优先级可以通过显式使用圆括号来覆盖。要强制在上述例子中先执行加法,可以写成:
|result = (x + y) * z;
注意属性访问和调用表达式的优先级高于表中列出的任何运算符。考虑这个表达式:
|typeof myObject.properties[index](parameter)
虽然typeof是最高优先级的运算符之一,但typeof操作是在两次属性访问和函数调用之后执行的。
实际上,如果你对运算符的优先级有任何不确定,最简单的做法是使用圆括号明确计算顺序。 需要知道的重要规则是:乘法和除法在加法和减法之前执行,赋值的优先级很低,几乎总是最后执行。
表中标记为A的列指定运算符的结合性。L值指定从左到右结合性,R值指定从右到左结合性。 运算符的结合性指定相同优先级的操作执行的顺序。从左到右结合性意味着操作从左到右执行。例如,减法运算符具有从左到右结合性,所以:
|result = x - y - z;
等同于:
|result = ((x - y) - z);
另一方面,以下表达式:
|x = ~-y; result = x = y = z; condition = a ? b : c ? d : e ? f : g;
等价于:
|x = ~(-y); result = (x = (y = z)); condition = a ? b : (c ? d : (e ? f : g));
因为一元、赋值和三元条件运算符具有从右到左结合性。
运算符优先级和结合性指定复杂表达式中操作执行的顺序,但它们不指定子表达式求值的顺序。
JavaScript总是严格从左到右求值表达式。例如,在表达式result = x + y * z中,首先求值子表达式result,然后是x,然后是y,然后是z。
然后将y和z的值相乘,加到x的值上,并赋值给result指定的变量或属性。给表达式添加圆括号可以改变乘法、加法和赋值的相对顺序,但不能改变从左到右的求值顺序。
只有当被求值的表达式之一具有影响另一个表达式值的副作用时,求值顺序才有所不同。如果表达式x递增一个被表达式z使用的变量,那么x在z之前求值这一事实很重要。
算术表达式用于执行数学计算和数字操作。JavaScript提供了完整的算术运算符集合。
|// 基本算术运算 let a = 10, b = 3; console.log(a + b); // 13 (加法) console.log(a - b); // 7 (减法) console.log(a * b); // 30 (乘法) console.log(a / b); // 3.333... (除法) console.log(a % b); // 1 (取模/余数) // 一元运算符
/运算符将其第一个操作数除以第二个操作数。如果你习惯于区分整数和浮点数的编程语言,你可能期望当一个整数除以另一个整数时得到整数结果。然而,在JavaScript中,所有数字都是浮点数,所以所有除法操作都有浮点结果:5/2计算为2.5,而不是2。除以零得到正无穷大或负无穷大,而0/0计算为NaN:这些情况都不会引发错误。
|5 / 2; // 2.5 5 / 2.0; // 2.5 5 / 0; // Infinity 5 / -0; // -Infinity 5 / NaN; // NaN
%运算符计算第一个操作数对第二个操作数取模。换句话说,它返回第一个操作数除以第二个操作数的整数部分后的余数。结果的符号与第一个操作数的符号相同。例如,5 % 2计算为1,-5 % 2计算为-1。
虽然取模运算符通常与整数操作数一起使用,但它也适用于浮点值。例如,6.5 % 2.1计算为0.2。
|5 % 2; // 1 -5 % 2; // -1 6.5 % 2.1; // 0.2
二元+运算符对数字操作数执行加法或对字符串操作数执行连接:
|1 + 2 // => 3 "你好" + "世界" // => "你好世界" "1" + "2" // => "12"
当两个操作数的值都是数字或都是字符串时,加号运算符的作用是显而易见的。然而,在任何其他情况下,都需要进行类型转换,要执行的操作取决于执行的转换。加法的转换规则优先考虑字符串连接:如果任一操作数是字符串或转换为字符串的对象,另一个操作数被转换为字符串并执行连接。只有当两个操作数都不是字符串类型时才执行加法。
从技术上讲,+运算符的行为如下:
如果其任一操作数值是对象,它使用对象到基本类型的算法将其转换为基本类型:Date对象通过其toString()方法转换,所有其他对象通过valueOf()转换(如果该方法返回基本值)。然而,大多数对象没有有用的valueOf()方法,所以它们也通过toString()转换。
在对象到基本类型转换之后,如果任一操作数是字符串,另一个被转换为字符串并执行连接。
否则,两个操作数都被转换为数字(或NaN)并执行加法。
这里有一些例子:
|1 + 2 // => 3: 加法 "1" + "2" // => "12": 连接 "1" + 2 // => "12": 数字转字符串后连接 1 + {} // => "1[object Object]": 对象转字符串后连接 true + true // => 2: 布尔值转数字后加法 2 + null // => 2: null转换为0后加法 2 + undefined // => NaN: undefined转换为NaN后加法
重要的是要注意,当+运算符与字符串和数字一起使用时,它可能不是结合的。也就是说,结果可能取决于操作执行的顺序。例如:
|1 + 2 + " 只老鼠"; // => "3 只老鼠" 1 + (2 + " 只老鼠"); // => "12 只老鼠"
第一行没有圆括号,+运算符具有从左到右结合性,所以先将两个数字相加,然后将它们的和与字符串连接。在第二行中,圆括号改变了这个操作顺序:数字2与字符串连接产生新字符串。然后数字1与新字符串连接产生最终结果。
一元运算符修改单个操作数的值以产生新值。在JavaScript中,一元运算符都具有高优先级并且都是右结合的。本节描述的算术一元运算符(+、-、++和--)都将其单个操作数转换为数字(如果需要)。注意标点符号+和-既用作一元运算符也用作二元运算符。
一元算术运算符如下:
一元加号(+)
一元加号运算符将其操作数转换为数字(或NaN)并返回转换后的值。当与已经是数字的操作数一起使用时,它什么也不做。
一元减号(-)
当-用作一元运算符时,它将其操作数转换为数字(如果需要),然后改变结果的符号。
递增(++)
++运算符递增(即,加1到)其单个操作数,该操作数必须是左值(变量、数组元素或对象属性)。运算符将其操作数转换为数字,在该数字上加1,并将递增的值赋回变量、元素或属性。
++运算符的返回值取决于它相对于操作数的位置。当在操作数之前使用时,称为前递增运算符,它递增操作数并求值为该操作数的递增值。当在操作数之后使用时,称为后递增运算符,它递增其操作数但求值为该操作数的未递增值。考虑这两行代码的区别:
|let i = 1, j = ++i; // i和j都是2 let i = 1, j = i++; // i是2,j是1
注意表达式++x并不总是等同于x=x+1。++运算符从不执行字符串连接:它总是将其操作数转换为数字并递增它。如果x是字符串"1",++x是数字2,但x+1是字符串"11"。
还要注意,由于JavaScript的自动分号插入,你不能在后递增运算符和它前面的操作数之间插入换行符。如果你这样做,JavaScript会将操作数视为一个完整的语句,并在它之前插入分号。
这个运算符,在其前递增和后递增形式中,最常用于递增控制for循环的计数器。
递减(--)
--运算符期望左值操作数。它将操作数的值转换为数字,减去1,并将递减的值赋回操作数。像++运算符一样,--的返回值取决于它相对于操作数的位置。当在操作数之前使用时,它递减并返回递减值。当在操作数之后使用时,它递减操作数但返回未递减值。当在其操作数之后使用时,操作数和运算符之间不允许换行符。
关系表达式用于比较两个值之间的关系,结果总是返回布尔值(true或false),常用于条件判断。
|// 比较运算符 let age = 18; let score = 85; console.log(age > 16); // true (大于) console.log(age < 20); // true (小于) console.log(score >= 80); // true (大于等于) console.log(score <= 90); // true (小于等于) // 相等性比较
==和===运算符使用两种不同的相同性定义检查两个值是否相同。两个运算符都接受任何类型的操作数,如果它们的操作数相同则返回true,如果它们不同则返回false。
===运算符被称为严格相等运算符(有时称为恒等运算符),它使用严格的相同性定义检查其两个操作数是否"相同"。==运算符被称为相等运算符;它使用更宽松的相同性定义检查其两个操作数是否"相等",该定义允许类型转换。
JavaScript支持=、==和===运算符。确保你理解这些赋值、相等和严格相等运算符之间的差异,编码时要小心使用正确的运算符!
虽然很容易将所有三个运算符都读作"等于",但这可能有助于减少混淆,如果你读=为"得到或被赋值",==为"等于",===为"严格等于"。
!=和!==运算符测试与==和===运算符完全相反的内容。!=不等运算符如果两个值根据==彼此相等则返回false,否则返回true。
!==运算符如果两个值严格相等则返回false,否则返回true。正如你将在后面看到的,!运算符计算布尔非操作。这使得很容易记住!=和!==代表"不等于"和"不严格等于"。
严格相等运算符===计算其操作数,然后比较两个值如下,不执行类型转换:
x !== x。NaN是唯一使此表达式为true的x值。===或==运算符认为相等。相等运算符==类似于严格相等运算符,但它不太严格。如果两个操作数的值不是同一类型,它会尝试一些类型转换并再次尝试比较:
==运算符仍可能认为它们相等。使用以下规则和类型转换检查相等性:作为测试相等性的例子,考虑比较:
|"1" == true
这个表达式计算为true,表明这些看起来很不同的值实际上是相等的。布尔值true首先转换为数字1,然后进行比较。接下来,字符串"1"转换为数字1。由于两个值现在相同,比较返回true。
in运算符期望左侧操作数是或可以转换为字符串。它期望右侧操作数是对象。如果左侧值是右侧对象的属性名,它计算为true。例如:
|let point = { x: 1, y: 1 }; // 定义一个对象 "x" in point // => true: 对象有名为"x"的属性 "z" in point // => false: 对象没有"z"属性 "toString" in point // => true: 对象继承toString方法 let data = [7, 8, 9]; // 有元素0、1和2的数组 "0" in data // => true: 数组有元素"0" 1 in data
instanceof运算符期望左侧操作数是对象,右侧操作数标识对象的类。如果左侧对象是右侧类的实例,运算符计算为true,否则计算为false。
在JavaScript中,对象的类由初始化它们的构造函数定义。因此,instanceof的右侧操作数应该是函数。以下是例子:
|let d = new Date(); // 用Date()构造函数创建新对象 d instanceof Date; // => true: d用Date()创建 d instanceof Object; // => true: 所有对象都是Object的实例 d instanceof Number; // => false: d不是Number对象 let a = [1, 2, 3]; // 用数组字面量语法创建数组 a instanceof Array; // => true: a是数组 a
注意所有对象都是Object的实例。当决定对象是否是类的实例时,instanceof考虑"超类"。如果instanceof的左侧操作数不是对象,instanceof返回false。如果右侧不是函数,它抛出TypeError。
为了理解instanceof运算符如何工作,你必须理解"原型链"。这是JavaScript的继承机制。为了计算表达式o instanceof f,JavaScript计算f.prototype,然后在o的原型链中查找该值。如果找到它,那么o是f(或f的超类)的实例,运算符返回true。如果f.prototype不是o的原型链中的值之一,那么o不是f的实例,instanceof返回false。
逻辑表达式使用逻辑运算符组合多个条件,是程序控制流的重要组成部分。
|// 逻辑与 (&&) let age = 20; let hasLicense = true; console.log(age >= 18 && hasLicense); // true (两个条件都满足) console.log(age >= 25 && hasLicense); // false (年龄条件不满足) // 逻辑或 (||) let isWeekend = false; let isHoliday = true; console.
&&运算符可以在三个不同的层次上理解。在最简单的层次上,当与布尔操作数一起使用时,&&对两个值执行布尔与操作:当且仅当其第一个操作数和第二个操作数都为true时,它返回true。如果这些操作数中的一个或两个为false,它返回false。
&&经常用作连接词来连接两个关系表达式:
|x == 0 && y == 0 // 当且仅当x和y都为0时为true
关系表达式总是计算为true或false,所以当这样使用时,&&运算符本身返回true或false。关系运算符的优先级高于&&(和||),所以像这样的表达式可以安全地不用圆括号书写。
但&&不要求其操作数是布尔值。回想一下,所有JavaScript值要么是"真值"要么是"假值"。假值是false、null、undefined、0、-0、NaN和""。所有其他值,包括所有对象,都是真值。可以在第二个层次上将&&理解为真值和假值的布尔与运算符。如果两个操作数都是真值,运算符返回真值。否则,一个或两个操作数必须是假值,运算符返回假值。在JavaScript中,任何期望布尔值的表达式或语句都将使用真值或假值,所以&&不总是返回true或false的事实不会造成实际问题。
注意上面的描述说运算符返回"真值"或"假值",但没有指定该值是什么。为此,我们需要在第三个也是最终的层次上描述&&。这个运算符首先计算其第一个操作数,即其左侧的表达式。如果左侧的值是假值,整个表达式的值也必须是假值,所以&&简单地返回左侧的值,甚至不计算右侧的表达式。
另一方面,如果左侧的值是真值,那么表达式的整体值取决于右侧的值。如果右侧的值是真值,那么整体值必须是真值,如果右侧的值是假值,那么整体值必须是假值。所以当左侧的值是真值时,&&运算符计算并返回右侧的值:
|let o = { x: 1 }; let p = null; o && o.x // => 1: o是真值,所以返回o.x的值 p && p.x // => null: p是假值,所以返回它,不计算p.x
重要的是要理解&&可能会也可能不会计算其右侧操作数。在上面的代码中,变量p设置为null,表达式p.x如果被计算会导致TypeError。但代码以一种习惯的方式使用&&,使得只有当p是真值时才计算p.x——不是null或undefined。
&&的行为有时称为"短路",你有时可能看到故意利用这种行为有条件地执行代码的代码。例如,以下两行JavaScript代码有等效的效果:
|if (a == b) stop(); // 只有当a == b时才调用stop() (a == b) && stop(); // 这做同样的事情
一般来说,当你在&&的右侧写带有副作用(赋值、递增、递减或函数调用)的表达式时必须小心。这些副作用是否发生取决于左侧的值。
尽管这个运算符实际工作的方式有些复杂,但它最常用作在真值和假值上工作的简单布尔代数运算符。
||运算符对其两个操作数执行布尔或操作。如果一个或两个操作数是真值,它返回真值。如果两个操作数都是假值,它返回假值。
虽然||运算符最常简单地用作布尔或运算符,但它像&&运算符一样具有更复杂的行为。它首先计算其第一个操作数,即其左侧的表达式。如果这个第一个操作数的值是真值,它返回那个真值。否则,它计算其第二个操作数,即其右侧的表达式,并返回该表达式的值。
与&&运算符一样,你应该避免包含副作用的右侧操作数,除非你故意想要使用右侧表达式可能不被计算的事实。
这个运算符的一个惯用用法是在一组备选项中选择第一个真值:
|// 如果定义了max_width,使用它。否则在preferences对象中查找值。 // 如果没有定义,使用硬编码常量。 let max = max_width || preferences.max_width || 500;
这个习惯用法经常在函数体中用于为参数提供默认值:
|// 将o的属性复制到p,并返回p function copy(o, p) { p = p || {}; // 如果没有为p传递对象,使用新创建的对象 // 函数体在这里 }
!运算符是一元运算符;它放在单个操作数之前。它的目的是反转其操作数的布尔值。例如,如果x是真值,!x计算为false。如果x是假值,那么!x是true。
与&&和||运算符不同,!运算符在反转转换后的值之前将其操作数转换为布尔值(使用第3章中描述的规则)。这意味着!总是返回true或false,你可以通过应用这个运算符两次将任何值x转换为其等效的布尔值:!!x。
作为一元运算符,!具有高优先级并紧密绑定。如果你想反转像p && q这样的表达式的值,你需要使用圆括号:!(p && q)。值得在这里注意布尔代数的两个定理,我们可以使用JavaScript语法表达:
|// 这两个等式对p和q的任何值都成立 !(p && q) === !p || !q !(p || q) === !p && !q
赋值表达式用于将值存储到变量或对象属性中,是程序中最基本的操作之一。
|// 基本赋值 let name = "张三"; // 变量赋值 let age = 25; // 数字赋值 let isStudent = true; // 布尔赋值 // 对象属性赋值 let person = {}; person.name = "李四"; // 属性赋值 person["age"] = 30; // 方括号语法赋值 // 数组元素赋值 let
除了正常的=赋值运算符,JavaScript支持许多其他赋值运算符,它们通过将赋值与其他操作结合来提供快捷方式。例如,+=运算符执行加法和赋值。以下表达式:
|total += sales_tax
等价于这个:
|total = total + sales_tax
如你所期望的,+=运算符适用于数字或字符串。对于数字操作数,它执行加法和赋值;对于字符串操作数,它执行连接和赋值。
类似的运算符包括-=、*=、&=等等。下表列出了它们:
在大多数情况下,表达式:
|a op= b
其中op是运算符,等价于表达式:
|a = a op b
在第一行中,表达式a被计算一次。在第二行中它被计算两次。只有当a包含副作用(如函数调用或递增运算符)时,两种情况才会不同。以下两个赋值不相同:
|data[i++] *= 2; data[i++] = data[i++] * 2;
JavaScript的eval()函数可以将传入的字符串当作JavaScript代码进行解析和执行,这意味着你可以动态地生成和运行代码。
例如,eval("2 + 2")会返回4。eval()不仅可以计算表达式,还可以执行任意的语句,比如定义变量、函数等。
然而,eval()的强大也带来了很大的安全隐患:如果传入的字符串包含用户输入,可能导致恶意代码被执行,造成安全漏洞。此外,eval()会降低代码的可读性和可维护性,
并且会影响JavaScript引擎的性能优化。因此,除非有非常特殊的需求,否则在实际开发中应尽量避免使用eval(),可以通过更安全的替代方案(如对象映射、函数调用等)来实现动态行为。
|// 基本的 eval() 使用 let expression = "10 + 20 * 2"; let result = eval(expression); console.log(result); // 50 // 执行复杂表达式 let mathFormula = "Math.sqrt(16) + Math.pow(2, 3)"; console.log(eval(mathFormula)); // 12 (4 + 8) // 动态变量赋值 let varName = "dynamicVar"; let value = 100
eval()是一个函数,但它被包含在关于表达式的这一章中,因为它真的应该是一个运算符。语言的最早版本定义了eval()函数,从那时起,语言设计者和解释器编写者一直在对其施加限制,使其越来越像运算符。现代JavaScript解释器执行大量代码分析和优化。eval()的问题是它计算的代码一般来说是不可分析的。一般来说,如果函数调用eval(),解释器无法优化该函数。将eval()定义为函数的问题是它可以被赋予其他名称:
|let f = eval; let g = f;
如果允许这样做,那么解释器无法安全地优化任何调用g()的函数。如果eval是运算符(和保留字),这个问题本来可以避免。
eval()期望一个参数。如果你传递除字符串以外的任何值,它简单地返回该值。如果你传递字符串,它尝试将字符串解析为JavaScript代码,如果失败则抛出SyntaxError。如果它成功解析字符串,那么它计算代码并返回字符串中最后一个表达式或语句的值,如果最后一个表达式或语句没有值则返回undefined。如果字符串抛出异常,eval()传播该异常。
关于eval()的关键是(当像这样调用时)它使用调用它的代码的变量环境。也就是说,它查找变量的值并定义新变量和函数的方式与本地代码相同。如果函数定义本地变量x然后调用eval("x"),它将获得本地变量的值。如果它调用eval("x=1"),它改变本地变量的值。如果函数调用eval("var y = 3;"),它声明了一个新的本地变量y。类似地,函数可以用这样的代码声明本地函数:
|eval("function f() { return x+1; }");
如果你从顶级代码调用eval(),它当然操作全局变量和全局函数。
注意你传递给eval()的代码字符串必须在语法上自己有意义——你不能用它将代码片段粘贴到函数中。例如,写eval("return;")没有意义,因为return只在函数内合法,评估的字符串使用与调用函数相同的变量环境这一事实不会使它成为该函数的一部分。如果你的字符串作为独立脚本有意义(即使是像x=0这样非常短的脚本),传递给eval()是合法的。否则eval()将抛出SyntaxError。
eval()改变本地变量的能力对JavaScript优化器来说是如此有问题。然而,作为解决方法,解释器只是对任何调用eval()的函数执行较少的优化。但是,如果脚本为eval()定义别名然后用另一个名称调用该函数,JavaScript解释器应该做什么?为了简化JavaScript实现者的工作,ECMAScript 3标准声明,如果eval()函数被任何其他名称调用,解释器不必允许这样做。
实际上,大多数实现者做了其他事情。当被任何其他名称调用时,eval()将评估字符串,就好像它是顶级全局代码一样。评估的代码可能定义新的全局变量或全局函数,它可能设置全局变量,但它不能使用或修改任何本地变量,因此不会干扰本地优化。
ECMAScript 5弃用EvalError并标准化eval()的事实行为。"直接eval"是对eval()函数的调用,表达式使用确切的、不合格的名称"eval"(开始感觉像保留字)。对eval()的直接调用使用调用上下文的变量环境。任何其他调用——间接调用——使用全局对象作为其变量环境,不能读取、写入或定义本地变量或函数。以下代码演示:
|let geval = eval; // 使用另一个名称做全局eval let x = "global", y = "global"; // 两个全局变量 function f() { // 这个函数做本地eval let x = "local"; // 定义本地变量 eval("x += 'changed';"); // 直接eval设置本地变量 return x; // 返回改变的本地变量 } function g() { // 这个函数做全局eval let
注意做全局eval的能力不仅仅是对优化器需求的迁就,它实际上是一个非常有用的特性:它允许你执行代码字符串,就好像它们是独立的顶级脚本一样。如前所述,真正需要计算代码字符串是罕见的。但如果你确实发现有必要,你更可能想要做全局eval而不是本地eval。
在IE9之前,IE与其他浏览器不同:当eval()被不同名称调用时,它不做全局eval。(它也不抛出EvalError:它只是做本地eval。)但IE确实定义了一个名为execScript()的全局函数,它将其字符串参数作为顶级脚本执行。(然而,与eval()不同,execScript()总是返回null。)
ECMAScript 5严格模式对eval()函数的行为,甚至对标识符"eval"的使用施加了进一步的限制。当从严格模式代码调用eval()时,或当要计算的代码字符串本身以"use strict"指令开始时,eval()使用私有变量环境做本地eval。这意味着在严格模式下,计算的代码可以查询和设置本地变量,但它不能在本地作用域中定义新变量或函数。
此外,严格模式通过有效地将"eval"变成保留字,使eval()更像运算符。你不允许用新值覆盖eval()函数。你不允许声明名为"eval"的变量、函数、函数参数或catch块参数。
在JavaScript中,除了常见的算术和逻辑运算符外,还有一些具有特殊用途的运算符。例如,条件运算符(也称为三元运算符)允许根据条件表达式的真假来选择不同的值,这在简化代码结构、减少分支时非常有用。typeof运算符用于判断一个值的数据类型,可以区分数字、字符串、布尔值、对象、函数等。delete运算符可以用来删除对象的某个属性,从而动态地改变对象的结构。void运算符则用于对任何表达式求值后返回undefined,常见于需要表达式但不关心其结果的场景。逗号运算符可以在一条语句中依次执行多个表达式,并返回最后一个表达式的值。这些运算符在特定场景下能够让代码更加灵活和简洁。
|// 条件运算符 (三元运算符) let age = 17; let status = age >= 18 ? "成年人" : "未成年人"; console.log(status); // "未成年人" let score = 85; let grade = score >= 90 ? "A" : score >= 80 ? "B" : score
条件运算符是JavaScript中唯一的三元运算符(三个操作数),有时实际上被称为三元运算符。这个运算符有时写作?:,虽然它在代码中不完全是那样出现的。因为这个运算符有三个操作数,第一个在?之前,第二个在?和:之间,第三个在:之后。它这样使用:
|x > 0 ? x : -x // x的绝对值
条件运算符的操作数可以是任何类型。第一个操作数被计算并解释为布尔值。如果第一个操作数的值是真值,那么计算第二个操作数,并返回其值。否则,如果第一个操作数是假值,那么计算第三个操作数并返回其值。只计算第二个和第三个操作数中的一个,绝不会两个都计算。
虽然你可以使用if语句实现类似的结果,?:运算符通常提供方便的快捷方式。这里是典型用法,检查以确保变量已定义(并有有意义的真值)并在如此的情况下使用它,或者如果没有则提供默认值:
|greeting = "hello " + (username ? username : "there");
这等价于但比以下if语句更紧凑:
|greeting = "hello "; if (username) greeting += username; else greeting += "there";
typeof是一个一元运算符,放在其单个操作数之前,操作数可以是任何类型。它的值是指定操作数类型的字符串。下表指定typeof运算符对任何JavaScript值的值:
你可能在这样的表达式中使用typeof运算符:
|(typeof value == "string") ? "'" + value + "'" : value
typeof运算符在与switch语句一起使用时也很有用。注意你可以在typeof的操作数周围放置圆括号,这使typeof看起来像函数的名称而不是运算符关键字:
|typeof(i)
注意如果操作数值是null,typeof返回"object"。如果你想要区分null和对象,你必须显式测试这个特殊情况值。typeof可能为宿主对象返回除"object"以外的字符串。然而,实际上,客户端JavaScript中的大多数宿主对象的类型是"object"。
因为typeof对除函数以外的所有对象和数组值都计算为"object",它只对区分对象和其他原始类型有用。为了区分一类对象和另一类,你必须使用其他技术,如instanceof运算符、class属性或constructor属性。
虽然JavaScript中的函数是一种对象,typeof运算符认为函数足够不同,它们有自己的返回值。JavaScript在函数和"可调用对象"之间做出微妙的区别。所有函数都是可调用的,但有可能有可调用对象——可以像函数一样调用——但不是真正的函数。ECMAScript 3规范说typeof运算符对所有可调用的原生对象返回"function"。ECMAScript 5规范扩展这个要求,要求typeof对所有可调用对象返回"function",无论是原生对象还是宿主对象。大多数浏览器供应商对其宿主对象的方法使用原生JavaScript函数对象。然而,Microsoft总是对其客户端方法使用非原生可调用对象,在IE 9之前,typeof运算符对它们返回"object",即使它们表现得像函数。在IE9中,这些客户端方法现在是真正的原生函数对象。
delete是一个一元运算符,尝试删除指定为其操作数的对象属性或数组元素。像赋值、递增和递减运算符一样,delete通常用于其属性删除副作用,而不是其返回的值。一些例子:
|let o = { x: 1, y: 2}; // 从一个对象开始 delete o.x; // 删除它的一个属性 "x" in o // => false: 属性不再存在 let a = [1,2,3]; // 从一个数组开始 delete a[2]; // 删除数组的最后一个元素 a.length // => 2: 数组现在只有两个元素
注意删除的属性或数组元素不只是设置为undefined值。当属性被删除时,属性不再存在。尝试读取不存在的属性返回undefined,但你可以用in运算符测试属性的实际存在。
delete期望其操作数是左值。如果它不是左值,运算符不采取行动并返回true。否则,delete尝试删除指定的左值。如果它成功删除指定的左值,delete返回true。然而,不是所有属性都可以被删除:一些内置核心和客户端属性免于删除,用var语句声明的用户定义变量不能被删除。用function语句定义的函数和声明的函数参数也不能被删除。
在ECMAScript 5严格模式下,如果其操作数是如变量、函数或函数参数之类的非限定标识符,delete引发SyntaxError:它只在操作数是属性访问表达式时才工作。严格模式还指定如果要求删除任何不可配置属性,delete引发TypeError。在严格模式之外,这些情况不发生异常,delete简单地返回false以指示操作数不能被删除。
这里是delete运算符的一些例子用法:
|let o = {x:1, y:2}; // 定义变量;将它初始化为对象 delete o.x; // 删除对象属性之一;返回true typeof o.x; // 属性不存在;返回"undefined" delete o.x; // 删除不存在的属性;返回true delete o; // 不能删除声明的变量;返回false。 // 在严格模式下会引发异常。 delete 1; // 参数不是左值:返回true this.x = 1; // 定义全局对象的属性,不用var delete x; // 尝试删除它:在非严格模式下返回true // 严格模式下异常。使用'delete this.x'代替
void是一个一元运算符,出现在其单个操作数之前,操作数可以是任何类型。这个运算符不寻常且不常用:它计算其操作数,然后丢弃值并返回undefined。由于操作数值被丢弃,使用void运算符只有在操作数有副作用时才有意义。
这个运算符最常见的用途是在客户端javascript: URL中,它允许你为其副作用计算表达式,而浏览器不显示计算表达式的值。例如,你可能在HTML <a>标签中使用void运算符如下:
|<a href="javascript:void window.open();">打开新窗口</a>
当然,这个HTML可以使用onclick事件处理器而不是javascript: URL更清洁地写,在那种情况下不需要void运算符。
逗号运算符是二元运算符,其操作数可以是任何类型。它计算其左操作数,计算其右操作数,然后返回右操作数的值。因此,以下行:
|i=0, j=1, k=2;
计算为2并基本上等价于:
|i = 0; j = 1; k = 2;
左手表达式总是被计算,但其值被丢弃,这意味着只有当左手表达式有副作用时使用逗号运算符才有意义。逗号运算符常用的唯一情况是具有多个循环变量的for循环:
|// 下面的第一个逗号是var语句语法的一部分 // 第二个逗号是逗号运算符:它让我们将2个表达式(i++和j--) // 压缩到期望1个的语句(for循环)中。 for(let i=0,j=10; i < j; i++,j--) console.log(i+j);
"5" + 3 的结果是什么操作?5 > 3 && 2 < 4 的结果是什么?typeof null 的返回值是什么?"5" + 3 的结果是什么?完成以下表达式计算,并解释结果:
|let x = 10; let y = 5; let result = x > y ? x + y : x - y; console.log(result); // 输出:_______
|let x = 10; let y = 5; let result = x > y ? x + y : x - y; console.log(result); // 输出:15
条件运算符(三元运算符)的语法是:条件 ? 值1 : 值2。如果条件为真,返回值1,否则返回值2。
在这个例子中,x > y为true(10 > 5),所以返回x + y,即10 + 5 = 15。
填写运算符,使代码达到预期效果:
|let a = 15; a _____ 5; // 复合赋值,使a变为20 a _____ 2; // 复合赋值,使a变为10 console.log(a _____ 3); // 比较运算,输出true
|let a = 15; a += 5; // a = a + 5,a变为20 a /= 2; // a = a / 2,a变为10 console.log(a > 3); // 10 > 3,输出true
复合赋值运算符可以简化代码:
+=:加法赋值,a += 5等价于a = a + 5-=:减法赋值*=:乘法赋值/=:除法赋值%=:取余赋值完成逻辑表达式,判断是否可以开车(年龄>=18且有驾照):
|let age = 18; let hasLicense = true; // 判断是否可以开车(年龄>=18且有驾照) let canDrive = age _____ 18 _____ hasLicense; console.log(canDrive); // 输出:_______
|let age = 18; let hasLicense = true; // 判断是否可以开车(年龄>=18且有驾照) let canDrive = age >= 18 && hasLicense; console.log(canDrive); // 输出:true
逻辑与运算符&&要求两边的条件都为真才返回true。在这个例子中,age >= 18为true,hasLicense也为true,所以canDrive为true。
逻辑运算符:
&&:逻辑与,两边都为真才返回真||:逻辑或,至少一边为真就返回真!:逻辑非,取反填写属性访问代码:
|let person = { name: "张三", "home-address": "北京" }; console.log(person._______); // 使用点表示法访问name console.log(person[_______]); // 使用方括号访问home-address
|let person = { name: "张三", "home-address": "北京" }; console.log(person.name); // 使用点表示法访问name console.log(person["home-address"]); // 使用方括号访问home-address
访问对象属性有两种方式:
person.name,适用于属性名是合法标识符的情况person["home-address"],适用于属性名包含特殊字符、空格,或者需要动态访问的情况在这个例子中,"home-address"包含连字符,不是合法的标识符,所以必须使用方括号表示法。
编写一个表达式计算器程序:
|<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表达式计算器</title> </head> <body> <script> let a = 8; let b =
| L |
| 2 |
| 任意,任意→任意 |
| ?: | 三元条件运算符 | R | 3 | 布尔,任意,任意→任意 |
| = | 赋值 | R | 2 | 左值,任意→任意 |
| *=, /=, %=, +=, -=, &=, ^=, |=, <<=, >>=, >>>= | 复合赋值 | R | 2 | 左值,任意→任意 |
| , | 逗号运算符 | L | 2 | 任意,任意→任意 |
这个例子综合运用了:
+、-、*、/、%>、>=、<=&&用于组合多个条件? :用于简单的条件判断注意括号的使用,确保运算顺序正确。