3.1 语法
ECMAScript 的语法很大程度上借鉴了 C 语言和其他类 C 语言,如 Java 和 Perl。
(1)区分大小写
ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。换句话说,变量 test 和变量 Test 是两个不同的变量。类似地,typeof 不能作为函数名,因为它是一个关键字。
(2)标识符
所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:
第一个字符必须是一个字母、下划线(_)或美元符号($);
剩下的其他字符可以是字母、下划线、美元符号或数字。
标识符中的字母可以是扩展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符
(3)注释
注释分为单行注释和多行注释
(4)严格模式
ECMAScript 5 增加了严格模式(strict mode)的概念。严格模式是一种不同的 JavaScript 解析和执行模型,ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。要对整个脚本启用严格模式,在脚本开头加上这一行:
"use strict";
它其实是一个预处理指令。任何支持的 JavaScript引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏 ECMAScript 3 语法。
也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:
function doSomething() {
"use strict";
// 函数体
}
(5)语句
ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子
所示:
let sum = a + b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐
即使语句末尾的分号不是必需的,也应该加上多条语句可以合并到一个 C 语言风格的代码块中。代码块由一个左花括号({)标识开始,一个右花括号(})标识结束:
if (test) {
test = false;
console.log(test);
}
3.2 关键字与保留字
ECMA-262 描述了一组保留的关键字,保留的关键字不能用作标识符或属性名。
规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定 用途,但它们是保留给将来做关键字用的。
以下是 ECMA-262 第 6 版为将来保留的所有词汇。
这些词汇不能用作标识符,但现在还可以用作对象的属性名。一般来说,最好还是不要使用关键字 和保留字作为标识符和属性名,以确保兼容过去和未来的 ECMAScript 版本。
3.3 变量
ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一
个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中
使用。
3.3.1 var 关键字
要定义变量,可以使用 var 操作符
var message
这行代码定义了一个名为 message 的变量,可以用它保存任何类型的值。
var message = "hi";
message = 100; // 合法,但不推荐
在这个例子中,变量 message 首先被定义为一个保存字符串值 hi 的变量,然后又被重写为保存了
数值 100。虽然不推荐改变变量保存值的类型,但这在 ECMAScript 中是完全有效的。
- var 声明作用域
关键的问题在于,使用 var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用 var 在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
不过,在函数内定义变量时省略 var 操作符,可以创建一个全局变量:
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
去掉之前的 var 操作符之后,message 就变成了全局变量。只要调用一次函数 test(),就会定义 这个变量,并且可以在函数外部访问到。
- var 声明提升
使用var关键字声明的变量会自动提升到函数作用域顶部:
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:
function foo() {
var age
console.log(age);
age = 26;
}
foo(); // undefined
此外,反复多次使用 var 声明同一个变量也没有问题:
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
3.3.2 let 声明
let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。
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 也不允许同一个块作用域中出现冗余声明。
let age;
let age; // SyntaxError;标识符 age 已经声明过了
当然,JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标 识符不会报错,而这是因为同一个块中没有重复声明:
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
}
对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量, 它们只是指出变量在相关作用域如何存在。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
- 暂时性死区
let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。
// name 会被提升
console.log(name); // undefined
var name = 'Matt';
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方 式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此 阶段引用任何后面才声明的变量都会抛出 ReferenceError。
- 全局声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声 明的变量则会)。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了 避免 SyntaxError,必须确保页面不会重复声明同一个变量。
- for 循环中的 let 声明
在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部:
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
3.3.3 const 声明
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且
尝试修改 const 声明的变量会导致运行时错误。
const age = 26;
age = 36; // TypeError: 给常量赋值
// const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const 声明的作用域也是块
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
const 声明的限制只适用于它指向的变量的引用。,如果 const 变量引用的是一个对象,
那么修改这个对象内部的属性并不违反 const 的限制。
const person = {};
person.name = 'Matt'; // ok
3.4 数据类型
简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。
复杂数据类型:Object(对象)
3.4.1 typeof 操作符
"undefined"表示值未定义;
"boolean"表示值为布尔值;
"string"表示值为字符串;
"number"表示值为数值;
"object"表示值为对象(而不是函数)或 null;
"function"表示值为函数;
"symbol"表示值为符号。
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof 95); // "number"
注意:调用typeofnull 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。
3.4.2 Undefined 类型
Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始
化时,就相当于给变量赋予了 undefined 值:
let message;
console.log(message == undefined); // true
在对未初始化的变量调用 typeof 时,返回的结果是"undefined",但对未声明的变量调用它时, 返回的结果还是"undefined"
let message; // 这个变量被声明了,只是值为 undefined
// 确保没有声明过这个变量
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined()
undefined 是一个假值。(可以理解为是一个false的类型的值)
3.4.3 Null 类型
Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给
typeof 传一个 null 会返回"object"的原因:
let car = null;
console.log(typeof car); // "object"
undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等。
console.log(null == undefined); // true
null 是一个假值。
3.4.4 Boolean 类型
Boolean(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值:true 和 false。这两个布尔值不同于数值,因此 true 不等于 1,false 不等于 0。
let found = true;
let lost = false;
虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()转型函数:
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
在这个例子中,字符串 message 会被转换为布尔值并保存在变量 messageAsBoolean 中。Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。什么值能转换为 true
或 false 的规则取决于数据类型和实际的值。
3.4.5 Number 类型
Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。
(1)数值字面量格式是十进制整数
let intNum = 55; // 整数
(2)对于八进制字面量,第一个数字必须是零(0),然后是相应的八进制数字(数值 0~7)。如果字面量中包含的数字超出了应有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数,
let octalNum1 = 070; // 八进制的 56
let octalNum2 = 079; // 无效的八进制值,当成 79 处理
let octalNum3 = 08; // 无效的八进制值,当成 8 处理
八进制字面量在严格模式下是无效的,会导致 JavaScript 引擎抛出语法错误。
(3)要创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(09 以
及 AF)。十六进制数字中的字母大小写均可。
let hexNum1 = 0xA; // 十六进制 10
let hexNum2 = 0x1f; // 十六进制 31
- 浮点值
要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不 是必须有整数,但推荐加上。
let floatNum3 = .1; // 有效,但不推荐
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小数点后面跟着 0(如 1.0),那它也会被转换为整数,如下例所示:
let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以10 的给定次幂的数值。ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:
let floatNum = 3.125e7; // 等于 31250000 3.125*10的7次方
浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。
- 值的范围
ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e324;
可以表示的最大数值保存在Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。
如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。
任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。
如果计算返回正 Infinity 或负 Infinity,则该值将不能再进一步用于任何计算。
- NaN
有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在 ECMAScript 中,0、+0 或0 相除会返回 NaN:
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或-Infinity:
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
NaN 有几个独特的属性。首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算时这可能是个问题。其次,NaN 不等于包括 NaN 在内的任何值。
console.log(NaN == NaN); // false
ECMAScript 提供了 isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。“不是数值”返回true,否则返回false
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值 10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值 1
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个函数执行的操作也不同。
一、Number()函数基于如下规则执行转换。
(1)布尔值,true 转换为 1,false 转换为 0。 (2)数值,直接返回。 (3)null,返回 0。 (4)undefined,返回 NaN。 (5)字符串,应用以下规则。
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。
Number("1") // 返回 1
Number("123") // 返回 123
Number("011") // 返回 11(忽略前面的0)
-
如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
-
如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
-
如果是空字符串(不包含字符),则返回 0。
-
如果字符串包含除上述情况之外的其他字符,则返回 NaN。
(6)对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()方法,再按照转换字符串的规则转换。
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
二、parseInt()函数更专注于字符串是否包含数值模式。
字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
不同的数值格式很容易混淆,因此 parseInt()也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数,以便正确解析:
let num = parseInt("0xAF", 16); // 175
let num2 = parseInt("AF"); // NaN
在这个例子中,第一个转换是正确的,而第二个转换失败了。区别在于第一次传入了进制数作为参数,告诉 parseInt()要解析的是一个十六进制字符串。而第二个转换检测到第一个字符就是非数值字符,随即自动停止并返回 NaN。
三、parseFloat()函数解析
parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换成 22.34。
parseFloat()函数的另一个不同之处在于,它始终忽略字符串开头的零。这个函数能识别前面讨论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。十六进制数值始终会返回 0。因为parseFloat()只解析十进制值,因此不能指定底数。最后,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat()返回整数。
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
3.4.6 String 类型
String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号(")、单引号(')或反引号(`)标示
let firstName = "John";
let lastName = 'Jacob';
let lastName1 = `Jingleheimerschmidt`
- 转换为字符串
有两种方式把一个值转换为字符串。(toString()和String())
首先是使用几乎所有值都有的 toString()方法。这个方法唯一的用途就是返回当前值的字符串等价物。比如:
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString()方法,该方法只是简单地返回自身的一个副本。)null 和 undefined 值没有 toString()方法。
toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示,比如:
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"
这个例子展示了传入底数参数时,toString()输出的字符串值也会随之改变
如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。
如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
如果值是 null,返回"null"。
如果值是 undefined,返回"undefined"。 下面看几个例子:
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
这里展示了将 4 个值转换为字符串的情况:一个数值、一个布尔值、一个 null 和一个 undefined。
数值和布尔值的转换结果与调用 toString()相同。因为 null 和 undefined 没有 toString()方法, 所以 String()方法就直接返回了这两个值的字面量文本。
- 模板字面量
ECMAScript 6 新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString);
// first line
// second line"
- 字符串插值
字符串插值通过在${}中使用一个 JavaScript 表达式实现:
let value = 5
console.log(`Hello ${value}`); // Hello 5