第 3 章 语言基础

127 阅读1分钟

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 描述了一组保留的关键字,保留的关键字不能用作标识符或属性名。

image.png

规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定 用途,但它们是保留给将来做关键字用的。

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

image.png

这些词汇不能用作标识符,但现在还可以用作对象的属性名。一般来说,最好还是不要使用关键字 和保留字作为标识符和属性名,以确保兼容过去和未来的 ECMAScript 版本。

3.3 变量

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

3.3.1 var 关键字

要定义变量,可以使用 var 操作符

var message

这行代码定义了一个名为 message 的变量,可以用它保存任何类型的值。

var message = "hi";
message = 100; // 合法,但不推荐

在这个例子中,变量 message 首先被定义为一个保存字符串值 hi 的变量,然后又被重写为保存了 数值 100。虽然不推荐改变变量保存值的类型,但这在 ECMAScript 中是完全有效的。

  1. 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(),就会定义 这个变量,并且可以在函数外部访问到。

  1. 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
  1. 暂时性死区

letvar 的另一个重要的区别,就是 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。

  1. 全局声明

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

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

不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了 避免 SyntaxError,必须确保页面不会重复声明同一个变量。

  1. 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 和 SymbolSymbol(符号)是 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。当使用 varlet 声明了变量但没有初始 化时,就相当于给变量赋予了 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 的规则取决于数据类型和实际的值。

image.png

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
  1. 浮点值

要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不 是必须有整数,但推荐加上。

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。由于这种微小的舍入错误,导致很难测试特定的浮点值。

  1. 值的范围

ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e324;

可以表示的最大数值保存在Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。

如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。

任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。

如果计算返回正 Infinity 或负 Infinity,则该值将不能再进一步用于任何计算。

  1. 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)字符串,应用以下规则。

  1. 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。
Number("1") // 返回 1
Number("123") // 返回 123
Number("011") // 返回 11(忽略前面的0)
  1. 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。

  2. 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。

  3. 如果是空字符串(不包含字符),则返回 0。

  4. 如果字符串包含除上述情况之外的其他字符,则返回 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`
  1. 转换为字符串

有两种方式把一个值转换为字符串。(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()方法就直接返回了这两个值的字面量文本。

  1. 模板字面量

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

let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line 
second line`;
console.log(myMultiLineString);
// first line
// second line"
  1. 字符串插值

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

let value = 5
console.log(`Hello ${value}`); // Hello 5

3.4.7 Symbol 类型