语言基础

语法

区分大小写

ECMAScript中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。

标识符

变量、函数、属性或者函数参数的名称就叫做标识符。标识符的组成要求:

  • 第一个字符必须是字母、下划线(_)或者美元符号($);
  • 剩下的其他字符可以是字母、下划线、美元符号或数字。

按照惯例,ECMAScript标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写,如:

1
2
3
firstSecond
myCar
doSomethingImportant

注释

ECMAScript采用C语言风格的注释,包括单行注释和块注释。单行注释以两个斜杠字符开头,如:

1
// 单行注释

ECMAScript采用C语言风格的注释,包括单行注释和块注释。单行注释以两个斜杠字符开头,如:

1
2
/*多行
注释*/

严格模式

ECMAScript 5增加了严格模式(strict mode)的概念。严格模式是一种不同的JavaScript解析和执行模型,ECMAScript 3的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。启用严格模式在开头加上这一行:

1
"use strict";

如何要在指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:

1
2
3
4
function doSomething() {
"use strict";
// 函数体
}

语句

ECMAScript中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾:

1
2
let sum = a + b        // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐

即使语句末尾的分号不是必需的,也应该加上。记着加分号有助于防止省略造成的问题,比如可以避免输入内容不完整。

多条语句可以合并到一个C语言风格的代码块中。代码块由一个左花括号({)标识开始,一个右花括号(})标识结束:

1
2
3
4
if (test) {
test = false;
console.log(test);
}

if之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句:

1
2
3
4
5
6
7
// 有效,但容易导致错误,应该避免
if (test)
console.log(test);
// 推荐
if (test) {
console.log(test);
}

关键字和保留字

ECMA-262描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作。

1
2
3
4
5
6
7
8
9
break         do             in               typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try

ECMA-262第6版为将来保留的所有词汇。

1
2
3
4
5
6
7
8
始终保留:
enum
严格模式下保留:
implements package public
interface protected static
let private
模块代码中保留:
await

不要使用关键字和保留字作为标识符和属性名。

变量

ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有3个关键字可以声明变量:varconstlet。其中,var在ECMAScript的所有版本中都可以使用,而const和let只能在ECMAScript 6及更晚的版本中使用。

var关键字

要定义变量,可以使用var操作符(注意var是一个关键字),后跟变量名(即标识符):

1
var message;

这行代码定义了一个名为message的变量,可以用它保存任何类型的值。(不初始化的情况下,变量会保存一个特殊值undefined。)ECMAScript实现变量初始化,因此可以同时定义变量并设置它的值:

1
var message = "hi";
  1. var声明作用域

使用var操作符定义的变量会成为包含它的函数的局部变量。

比如,使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:

1
2
3
4
5
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!

在函数内定义变量时省略var操作符,可以创建一个全局变量:

1
2
3
4
5
function test() {
message="hi"; //全局变量
}
test();
console.log(message); // "hi"

注意 虽然可以通过省略var操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略var是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出ReferenceError。

  1. var声明提升

使用var关键字声明的变量会自动提升到函数作用域的顶部:

1
2
3
4
5
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined

之所以不会报错,是因为ECMAScript运行时把它看成等价于如下代码:

1
2
3
4
5
6
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined

这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。

let声明

let和var的作用很相似,不同的是,let声明的范围是块作用域 ,而var声明的范围是函数作用域

1
2
3
4
5
6
7
8
9
10
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age没有定义

age变量之所以不能在if块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let。

let也不允许同一个块作用域中出现冗余声明,这样会报错:

1
2
3
4
var name;
var name;
let age;
let age; // SyntaxError;标识符age已经声明过了

JavaScript引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明:

1
2
3
4
5
6
7
8
9
10
11
12
var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
var name = 'Matt';
console.log(name); // 'Matt'
}
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
  1. 暂时性死区

和var不同,let声明的变量不会在作用域中被提升。

1
2
3
4
5
6
// name会被提升
console.log(name); // undefined
var name = 'Matt';
// age不会被提升
console.log(age); // ReferenceError:age没有定义
let age = 26;

在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。

  1. 全局声明

与var关键字不同,使用let在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)。

1
2
3
4
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined

为了避免SyntaxError,必须确保页面不会重复声明同一个变量。

  1. for循环中的let声明

在let出现之前,for循环定义的迭代变量会渗透到循环体外部:

1
2
3
4
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5

改成使用let之后,这个问题就消失了,因为迭代变量的作用域仅限于for循环块内部:

1
2
3
4
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i没有定义

const声明

const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,const声明的变量不能被修改。

声明风格及最佳实践

  1. 不使用var
  2. const优先,let次之

数据类型

ECMAScript有6种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、String和Symbol。Symbol(符号)是ECMAScript 6新增的。还有一种复杂数据类型叫Object(对象)。Object是一种无序名值对的集合。因为在ECMAScript中不能定义自己的数据类型,所有值都可以用上述7种数据类型之一来表示。

typeof操作符

typeof操作符用来确定任意变量的数据类型,对一个值使用typeof操作符会返回下列字符串之一:

1
2
3
4
5
6
7
"undefined"表示值未定义;
"boolean"表示值为布尔值;
"string"表示值为字符串;
"number"表示值为数值;
"object"表示值为对象(而不是函数)或null
"function"表示值为函数;
"symbol"表示值为符号。

比如:

1
2
3
4
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"

因为typeof是一个操作符而不是函数,所以不需要参数(但可以使用参数)。

Undefined类型

Undefined类型只有一个值,就是特殊值undefined。当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值:

1
2
let message;
console.log(message == undefined); // true

默认情况下,任何未经初始化的变量都会取得undefined值。

在对未初始化的变量调用typeof时,返回的结果是”undefined”,但对未声明的变量调用它时,返回的结果还是”undefined”:

1
2
3
4
5
let message; // 这个变量被声明了,只是值为undefined
// 确保没有声明过这个变量
// let age
console.log(typeof message);//"undefined"
console.log(typeof age); //"undefined"

Null类型

Null类型同样只有一个值,即特殊值null。逻辑上讲,null值表示一个空对象指针 ,这也是给typeof传一个null会返回”object”的原因:

1
2
let car = null;
console.log(typeof car); // "object"

在定义将来要保存对象值的变量时,建议使用null来初始化,不要使用其他值。

undefined值是由null值派生而来的,因此ECMA-262将它们定义为表面上相等:

1
console.log(null == undefined);   // true

任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用null来填充该变量。

Boolean类型

Boolean(布尔值)类型是ECMAScript中使用最频繁的类型之一,有两个字面值:truefalse。这两个布尔值不同于数值,因此true不等于1,false不等于0。

要将一个其他类型的值转换为布尔值,可以调用特定的Boolean()转型函数:

1
2
3
4
5
6
let str = "Hello world! ";
let strAsBoolean = Boolean(str);
console.log(strAsBoolean) // true
str = "";
strAsBoolean = Boolean(str);
console.log(strAsBoolean) // false

image-20230203220916846

Number类型

Number类型使用IEEE 754格式表示整数和浮点值(在某些语言中也叫双精度值)。

最基本的数值字面量格式是十进制整数:

1
let intNum = 55;   // 整数

八进制字面量,第一个数字必须是零(0),然后是相应的八进制数字(数值0~7)。如果字面量中包含的数字超出了应有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数,如下所示:

1
2
3
let octalNum1 = 070;   // 八进制的56
let octalNum2 = 079; // 无效的八进制值,当成79 处理
let octalNum3 = 08; // 无效的八进制值,当成8 处理

十六进制字面量,加前缀0x,然后是十六进制数字(09以及AF),十六进制数字中的字母大小写均可:

1
2
let hexNum1 = 0xA;    // 十六进制10
let hexNum2 = 0x1f; // 十六进制31
  1. 浮点值

定义浮点值数值中包含小数点即可。

1
2
3
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐

如果小数点后是0如(1.0),那么它会被转为整数。

科学记数法用于表示一个应该乘以10的给定次幂的数值。ECMAScript中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母e,再加上一个要乘的10的多少次幂。比如:

1
let floatNum = 3.125e7; // 等于31250000
  1. 值的范围

ECMAScript可以表示的最小数值保存在Number.MIN_VALUE中,这个值在多数浏览器中是5e-324;可以表示的最大数值保存在Number.MAX_VALUE中,这个值在多数浏览器中是1.7976931348623157e+308。任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以Infinity(正无穷大)表示。

  1. NaN

有一个特殊的数值叫NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。

1
2
console.log(0/0);     // NaN
console.log(-0/+0); // NaN
  1. 数值转换

有3个函数可以将非数值转换为数值:Number()、parseInt()和parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。

String类型

String(字符串)数据类型表示零或多个16位Unicode字符序列。字符串可以使用双引号(”)、单引号(’)或反引号(`)标示。

  1. 字符字面量

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

image-20230203225655914

字符串的长度可以通过其length属性获取:

1
console.log(text.length); // 28
  1. 字符串的特点

ECMAScript中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:

1
2
let lang = "Java";
lang = lang + "Script";
  1. 转换为字符串

有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的toString()方法。

1
2
3
4
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"

toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示,比如:

1
2
3
4
5
6
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
  1. 模板字符串

ECMAScript 6新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:

1
2
3
4
5
6
7
8
9
10
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line
// second line
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
  1. 字符串插值

字符串插值通过在${}中使用一个JavaScript表达式实现:

1
2
3
4
let value = 5;
let exponent = 'second';
let interpolatedTemplateLiteral = `${ value } to the ${ exponent } power is ${ value*value }`;
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
  1. 模板字面量标签函数

模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。

标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为,如下例所示。标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
lettaggedResult=simpleTag`${a}+${b}=${a+b}`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"

因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到一个数组中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = 6;
let b = 9;
functionsimpleTag(strings, ...expressions){
console.log(strings);
for(const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"

对于有n个插值的模板字面量,传给标签函数的表达式参数的个数始终是n,而传给标签函数的第一个参数所包含的字符串个数则始终是n+1。

  1. 原始字符串

使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或Unicode字符),可以使用默认的String.raw标签函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Unicode示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(`first line
second line`);
// first line
// second line
console.log(String.raw`first line
second line`);
// first line
// second line

也可以通过标签函数的第一个参数,即字符串数组的.raw属性取得每个字符串的原始内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters; ');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and' }\n`;
// Actual characters:
// ©
//(换行符)
// Escaped characters:
// \u00A9
// \n

Symbol类型

Symbol(符号)是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

  1. 符号的基本用法

符号需要使用Symbol()函数初始化。因为符号本身是原始类型,所以typeof操作符对符号返回symbol。

1
2
let sym = Symbol();
console.log(typeof sym); // symbol

调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

1
2
3
4
5
6
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

1
2
3
4
let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);

Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用Boolean、String或Number那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:

1
2
3
4
5
6
7
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
letmySymbol=new Symbol();//TypeError: Symbolisnotaconstructor

操作符

一元操作符

只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是ECMAScript中最简单的操作符。

  1. 递增/递减操作符

前缀递增操作符把age的值变成了30(给之前的值29加1):

1
2
let age = 29;
++age;

它实际上等于如下表达式:

1
2
let age = 29;
age=age+1;

后缀版与前缀版的主要区别在于,后缀版递增和递减在语句被求值后才发生。

1
2
3
4
5
6
let num1 = 2;
let num2 = 20;
let num3=num1--+num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21
  1. 一元加和减

一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值,如把1转换为-1。

1
2
3
let num = 25;
num = -num;
console.log(num); // -25

位操作符

  1. 按位非

按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数。

1
2
3
let num1 = 25;        // 二进制00000000000000000000000000011001
let num2 = ~num1; // 二进制11111111111111111111111111100110
console.log(num2); // -26

按位非操作符作用到了数值25,得到的结果是-26。由此可以看出,按位非的最终效果是对数值取反并减1,就像执行如下操作的结果一样:

1
2
3
let num1 = 25;
let num2 = -num1-1;
console.log(num2); // "-26"
  1. 按位与

按位与操作符用和号(&)表示,有两个操作数。按位与操作在两个位都是1时返回1,在任何一位是0时返回0。

1
2
3
// 对数值25和3求与操作
let result = 25 & 3;
console.log(result); // 1

25和3的按位与操作的结果是1。为什么呢?看下面的二进制计算过程:

1
2
3
4
25	= 00000000000000000000000000011001
3 = 00000000000000000000000000000011
---------------------------------------------
AND = 00000000000000000000000000000001
  1. 按位或

按位或操作符用管道符(|)表示,同样有两个操作数。按位或操作在至少一位是1时返回1,两位都是0时返回0。

1
2
let result = 25 | 3;
console.log(result); // 27

可见25和3的按位或操作的结果是27:

1
2
3
4
25 = 00000000000000000000000000011001
3 = 00000000000000000000000000000011
---------------------------------------------
OR = 00000000000000000000000000011011
  1. 按位异或

按位异或用脱字符(^)表示,同样有两个操作数。按位异或与按位或的区别是,它只在一位上是1的时候返回1(两位都是1或0,则返回0)。

对数值25和3执行按位异或操作:

1
2
3
4
25 = 00000000000000000000000000011001
3 = 00000000000000000000000000000011
---------------------------------------------
XOR = 00000000000000000000000000011010

两个数在4位上都是1,但两个数的第0位都是1,因此那一位在结果中就变成了0。其余位上的1在另一个数上没有对应的1,因此会直接传递到结果中。二进制码11010等于26。

  1. 左移

左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。比如,如果数值2(二进制10)向左移5位,就会得到64(二进制1000000),如下所示:

1
2
let oldValue = 2;                // 等于二进制10
let newValue = oldValue << 5; // 等于二进制1000000,即十进制64

注意在移位后,数值右端会空出5位。左移会以0填充这些空位,让结果是完整的32位数值。

  1. 有符号右移

有符号右移由两个大于号(>>)表示,会将数值的所有32位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。

1
2
let oldValue = 64;                // 等于二进制1000000
let newValue = oldValue >> 5; // 等于二进制10,即十进制2
  1. 无符号右移

无符号右移用3个大于号表示(>>>),会将数值的所有32位都向右移。对于正数,无符号右移与有符号右移结果相同。

对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补0,而不管符号位是什么。对正数来说,这跟有符号右移效果相同。但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大,如下面的例子所示:

1
2
letoldValue=-64;                 //等于二进制11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制134217726

布尔操作符

  1. 逻辑非

逻辑非操作符由一个叹号(!)表示,可应用给ECMAScript中的任何值。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。换句话说,逻辑非操作符会遵循如下规则。

  • 如果操作数是对象,则返回false。
  • 如果操作数是空字符串,则返回true。
  • 如果操作数是非空字符串,则返回false。
  • 如果操作数是数值0,则返回true。
  • 如果操作数是非0数值(包括Infinity),则返回false。
  • 如果操作数是null,则返回true。
  • 如果操作数是NaN,则返回true。
  • 如果操作数是undefined,则返回true。
1
2
3
4
5
6
console.log(! false);    // true
console.log(! "blue"); // false
console.log(!0); // true
console.log(! NaN); // true
console.log(! ""); // true
console.log(!12345); // false

逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(! !),相当于调用了转型函数Boolean()。无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反,从而给出变量真正对应的布尔值。结果与对同一个值使用Boolean()函数是一样的:

1
2
3
4
5
console.log(! ! "blue"); // true
console.log(! !0); // false
console.log(! ! NaN); // false
console.log(! ! ""); // false
console.log(! !12345); // true
  1. 逻辑与

逻辑与操作符由两个和号(&&)表示,应用到两个值,如下所示:

1
let result = true && false;

逻辑与操作符遵循如下真值表:

image-20230209000225850

逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则。

  • 如果第一个操作数是对象,则返回第二个操作数。
  • 如果第二个操作数是对象,则只有第一个操作数求值为true才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是null,则返回null。
  • 如果有一个操作数是NaN,则返回NaN。
  • 如果有一个操作数是undefined,则返回undefined。

逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。对逻辑与操作符来说,如果第一个操作数是false,那么无论第二个操作数是什么值,结果也不可能等于true。

  1. 逻辑或

逻辑或操作符由两个管道符(||)表示,比如:

1
let result = true || false;

逻辑或操作符遵循如下真值表:

image-20230209000508916

与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则。

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为false,则返回第二个操作数。
  • 如果两个操作数都是对象,则返回第一个操作数。
  • 如果两个操作数都是null,则返回null。
  • 如果两个操作数都是NaN,则返回NaN。
  • 如果两个操作数都是undefined,则返回undefined。

同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。

乘性操作符

  1. 乘法操作符

乘法操作符由一个星号(*)表示,可以用于计算两个数值的乘积。

1
let result = 3456;
  • 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果ECMAScript不能表示乘积,则返回Infinity或-Infinity。
  • 如果有任一操作数是NaN,则返回NaN。
  • 如果是Infinity乘以0,则返回NaN。
  • 如果是Infinity乘以非0的有限数值,则根据第二个操作数的符号返回Infinity或-Infinity。
  • 如果是Infinity乘以Infinity,则返回Infinity。
  • 如果有不是数值的操作数,则先在后台用Number()将其转换为数值,然后再应用上述规则。
  1. 除法操作符

除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商,比如:

1
letresult=66/11;

跟乘法操作符一样,除法操作符针对特殊值也有一些特殊的行为。

  • 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果ECMAScript不能表示商,则返回Infinity或-Infinity。
  • 如果有任一操作数是NaN,则返回NaN。
  • 如果是Infinity除以Infinity,则返回NaN。
  • 如果是0除以0,则返回NaN。
  • 如果是非0的有限值除以0,则根据第一个操作数的符号返回Infinity或-Infinity。
  • 如果是Infinity除以任何数值,则根据第二个操作数的符号返回Infinity或-Infinity。
  • 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。
  1. 取模操作符

取模(余数)操作符由一个百分比符号(%)表示,比如:

1
letresult=26%5;//等于1

与其他乘性操作符一样,取模操作符对特殊值也有一些特殊的行为。

  • 如果操作数是数值,则执行常规除法运算,返回余数。
  • 如果被除数是无限值,除数是有限值,则返回NaN。
  • 如果被除数是有限值,除数是0,则返回NaN。
  • 如果是Infinity除以Infinity,则返回NaN。
  • 如果被除数是有限值,除数是无限值,则返回被除数。
  • 如果被除数是0,除数不是0,则返回0。
  • 如果有不是数值的操作数,则先在后台用Number()函数将其转换为数值,然后再应用上述规则。

指数操作符

ECMAScript 7新增了指数操作符,Math.pow()现在有了自己的操作符**,结果是一样的:

1
2
3
4
console.log(Math.pow(3, 2);     // 9
console.log(3 ** 2); // 9
console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4

加性操作符

  1. 加法操作符

加法操作符(+)用于求两个数的和,比如:

1
let result = 1 + 2;

如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:

  • 如果有任一操作数是NaN,则返回NaN;
  • 如果是Infinity加Infinity,则返回Infinity;
  • 如果是-Infinity加-Infinity,则返回-Infinity;
  • 如果是Infinity加-Infinity,则返回NaN;
  • 如果是+0加+0,则返回+0;
  • 如果是-0加+0,则返回+0;
  • 如果是-0加-0,则返回-0。

如果有一个操作数是字符串,则要应用如下规则:

  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。

如果有任一操作数是对象、数值或布尔值,则调用它们的toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于undefined和null,则调用String()函数,分别获取”undefined”和”null”。

1
2
3
4
5
6
7
8
let result1 = 5 + 5;          // 两个数值
console.log(result1); // 10
let result2 = 5 + "5"; // 一个数值和一个字符串
console.log(result2); // "55"
let result3 = null + 1;
console.log(result3); // 1
let result4 = undefined + 1;
console.log(result3); // 1
  1. 减法操作符

减法操作符(-)也是使用很频繁的一种操作符,比如:

1
let result = 2-1;

与加法操作符一样,减法操作符也有一组规则用于处理ECMAScript中不同类型之间的转换。

  • 如果两个操作数都是数值,则执行数学减法运算并返回结果。
  • 如果有任一操作数是NaN,则返回NaN。
  • 如果是Infinity减Infinity,则返回NaN。
  • 如果是-Infinity减-Infinity,则返回NaN。
  • 如果是Infinity减-Infinity,则返回Infinity。
  • 如果是-Infinity减Infinity,则返回-Infinity。
  • 如果是+0减+0,则返回+0。
  • 如果是+0减-0,则返回-0。
  • 如果是-0减-0,则返回+0。
  • 如果有任一操作数是字符串、布尔值、null或undefined,则先在后台使用Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是NaN,则减法计算的结果是NaN。
  • 如果有任一操作数是对象,则调用其valueOf()方法取得表示它的数值。如果该值是NaN,则减法计算的结果是NaN。如果对象没有valueOf()方法,则调用其toString()方法,然后再将得到的字符串转换为数值。

关系操作符

关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=)。这几个操作符都返回布尔值,如下所示:

1
2
let result1 = 5 > 3; // true
let result2 = 5 < 3; // false

与ECMAScript中的其他操作符一样,在将它们应用到不同数据类型时也会发生类型转换和其他行为。

  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有valueOf()操作符,则调用toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较。

对字符串而言,关系操作符会比较字符串中对应字符的编码,而这些编码是数值。比较完之后,会返回布尔值。问题的关键在于,大写字母的编码都小于小写字母的编码,因此以下这种情况就会发生:

1
let result = "Brick" < "alphabet"; // true

相等操作符

  1. 等于和不等于

ECMAScript中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回true。不等于操作符用叹号和等于号(! =)表示,如果两个操作数不相等,则会返回true。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。

在转换操作数的类型时,相等和不相等操作符遵循如下规则。

  • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false转换为0, true转换为1。
  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再根据前面的规则进行比较。

在进行比较时,这两个操作符会遵循如下规则。

  • null和undefined相等。
  • null和undefined不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true。记住:即使两个操作数都是NaN,相等操作符也返回false,因为按照规则,NaN不等于NaN。
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true。否则,两者不相等。
  1. 全等和不全等

全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操作符由3个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回true,比如:

1
2
let result1 = ("55" == 55);    // true,转换后相等
let result2 = ("55" === 55); // false,不相等,因为数据类型不同

不全等操作符用一个叹号和两个等于号(! ==)表示,只有两个操作数在不转换的前提下不相等才返回true。比如:

1
2
let result1 = ("55" ! = 55);   // false,转换后相等
let result2 = ("55" ! == 55); // true,不相等,因为数据类型不同

条件操作符

条件操作符是ECMAScript中用途最为广泛的操作符之一:

1
variable = boolean_expression ? true_value : false_value;

根据条件表达式boolean_expression的值决定将哪个值赋给变量variable。如果boolean_expression是true,则赋值true_value;如果boolean_expression是false,则赋值false_value。

赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量,如下所示:

1
let num = 10;

复合赋值使用乘性、加性或位操作符后跟等于号(=)表示。这些赋值操作符是类似如下常见赋值操作的简写形式:

1
2
let num = 10;
num = num + 10;

每个数学操作符以及其他一些操作符都有对应的复合赋值操作符:

  • 乘后赋值(*=)
  • 除后赋值(/=)
  • 取模后赋值(%=)
  • 加后赋值(+=)
  • 减后赋值(-=)
  • 左移后赋值(<<=)
  • 右移后赋值(>>=)
  • 无符号右移后赋值(>>>=)

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如下所示:

1
let num1 = 1, num2 = 2, num3 = 3;

在一条语句中同时声明多个变量是逗号操作符最常用的场景。不过,也可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:

1
let num = (5, 1, 4, 8, 0); // num的值为0

语句

if语句

if语句是使用最频繁的语句之一,语法如下:

1
if (condition) statement1 else statement2

这里的条件(condition)可以是任何表达式,并且求值结果不一定是布尔值。ECMAScript会自动调用Boolean()函数将这个表达式的值转换为布尔值。如果条件求值为true,则执行语句statement1;如果条件求值为false,则执行语句statement2。这里的语句可能是一行代码,也可能是一个代码块(即包含在一对花括号中的多行代码)。

1
2
3
4
5
if (i > 25)
console.log("Greater than 25."); // 只有一行代码的语句
else {
console.log("Less than or equal to 25."); // 一个语句块
}

可以像这样连续使用多个if语句:

1
if (condition1) statement1 else if (condition2) statement2 else statement3

示例:

1
2
3
4
5
6
7
if (i > 25) {
console.log("Greater than 25.");
} else if (i < 0) {
console.log("Less than 0.");
} else {
console.log("Between 0 and 25, inclusive.");
}

do-while语句

do-while语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。do-while的语法如下:

1
2
3
do {
statement
} while (expression);

比如:

1
2
3
4
5
let i = 0;
do {
i += 2
console.log(i)
} while (i < 10);

while语句

while语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while循环体内的代码有可能不会执行。下面是while循环的语法:

1
while(expression) statement

比如:

1
2
3
4
let i = 0;
while (i < 10) {
i += 2;
}

for语句

for语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,语法如下:

1
for (initialization; expression; post-loop-expression) statement

下面是一个用例:

1
2
3
4
let count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}

for循环跟下面的while循环是一样的:

1
2
3
4
5
6
let count = 10;
let i = 0;
while (i < count) {
console.log(i);
i++;
}

for-in语句

for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

1
for (property in expression) statement

下面是一个例子:

1
2
3
for (const propName in window) {
document.write(propName);
}

for-of语句

for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

1
for (property of expression) statement

下面是示例:

1
2
3
for (const el of [2, 4, 6, 8]) {
console.log(el);
}