JavaScript 语法

JavaScript 的引入方式

使用 script 标签。

那些老旧的实例可能会在 <script> 标签中使用
type="text/javascript"。现在已经不必这样做了。JavaScript 是所有现代浏览器以及
HTML5 中的默认脚本语言。

在 HTML 页面上,因为脚本文本包围在 <script>
标签中,所以它不会显示在用户的屏幕上,而 Web 浏览器知道应该运行 JavaScript
程序。

通常的做法是把函数放入 <head>
部分中,或者放在页面底部。这样就可以把它们安置到同一处位置,不会干扰页面的内容。

第二种方法是把 JavaScript 代码放到一个单独的 .js 文件,然后在 HTML 中通过
<script src="..."></script> 引入这个文件。

您可以在 HTML 文档中放入不限数量的脚本。 脚本可位于 HTML 的 <body><head>
部分中,或者同时存在于两个部分中。 通常的做法是把函数放入 <head>
部分中,或者放在页面底部。这样就可以把它们安置到同一处位置,不会干扰页面的内容。

常量和变量

字面量

在编程语言中,一般固定值称为字面量,如 3.14。

变量

在计算机程序中,经常会声明无值的变量。

未使用值来声明的变量,其值实际上是 undefined。例如 var a; console.log(a);

变量的作用域

局部作用域

变量在函数内声明,变量为局部作用域。该变量是局部变量。该变量的作用域为整个函数体,在函数体外不可引用该变量。

如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。

JavaScript 的函数定义有个特点,它会先扫描整个函数体的语句,把所有用 var
申明的变量“提升”到函数顶部,这称为变量提升。由于JavaScript
的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量:

1
2
3
4
5
6
7
8
9
10
function foo() {
var
x = 1, // x初始化为1
y = x + 1, // y初始化为2
z, i; // z和i为undefined
// 其他语句:
for (i=0; i<100; i++) {
...
}
}

为了避免 var 申明变量时带来的隐患,为了解决块级作用域。ES6
引入了新的关键字let,用let替代var可以申明一个块级作用域的变量:

1
2
3
4
5
6
7
8
function foo() {
let sum = 0;
for (let i = 0; i < 100; i++) {
sum += i;
}
// SyntaxError:
i += 1;
}
全局作用域

不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性:

如果您把值赋给尚未声明的变量,该变量将被自动作为全局变量声明。例如
carname = "Volvo";

变量在函数外定义,即为全局变量。

全局变量有全局作用域: 网页中所有脚本和函数均可使用。

我们每次直接调用的alert()函数其实也是window的一个变量。这说明 JavaScript
实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。

名字空间

全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

1
2
3
4
5
6
7
8
9
10
11
// 唯一的全局变量MYAPP:
let MYAPP = {};

// 其他变量:
MYAPP.name = "myapp";
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
return "foo";
};

把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。

许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore 等等。

常量

由于varlet申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:

1
let PI = 3.14;

ES6 标准引入了新的关键字 const 来定义常量,constlet都具有块级作用域:

1
2
3
const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14

JavaScript 数据类型

  1. 值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol。
  2. 引用数据类型(对象类型):对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。

**注:**Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。

数值 Number

JavaScript 中的所有数据都是以 64 位浮点型数据(float) 来存储。因此 JavaScript
不区分整数和浮点数,统一用 Number 表示,以下都是合法的 Number 类型:

1
2
3
4
5
6
123; // 整数 123
0.456; // 浮点数 0.456
1.2345e3; // 科学计数法表示 1.2345x1000,等同于 1234.5
-99; // 负数
NaN; // NaN 表示 Not a Number,当无法计算结果时用 NaN 表示
Infinity; // Infinity 表示无限大,当数值超过了 JavaScript 的 Number 所能表示的最大值时,就表示为 Infinity

计算机由于使用二进制,所以,有时候用十六进制表示整数比较方便,十六进制用0x前缀和0-9,a-f表示,例如:0xff00
,0xa5b4c3d2 ,等等,它们和十进制表示的数值完全一样。

Number 可以直接做四则运算,规则和数学一致

浮点型数据使用注意事项: 所有的编程语言,包括
JavaScript,对浮点型数据的精确度都很难确定。要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值:Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; // true

BigInt

要精确表示比 2 的 53 次方还大的整数,可以使用内置的 BigInt
类型,它的表示方法是在整数后加一个 n ,例如 9223372036854775808n ,也可以使用
BigInt() 把 Number 和字符串转换成 BigInt:

1
2
3
4
5
6
7
// 使用 BigInt:
var bi1 = 9223372036854775807n;
var bi2 = BigInt(12345);
var bi3 = BigInt("0x7fffffffffffffff");
console.log(bi1 === bi2); // false
console.log(bi1 === bi3); // true
console.log(bi1 + bi2);

布尔值

布尔值和布尔代数的表示完全一致,一个布尔值只有 true 、false 两种值。

注:JavaScript 把 null 、undefined 、0 、NaN 和空字符串 ‘’ 视为 false
,其他值一概视为 true。

字符串

字符串是以单引号’或双引号"括起来的任意文本。

ASCII字符可以以 \x## 形式的十六进制表示。还可以用 \u#### 表示一个 Unicode 字符。

常用方法:

toUpperCase() 把一个字符串全部变为大写 toLowerCase() 把一个字符串全部变为小写
indexOf() 会搜索指定字符串出现的位置 substring() 返回指定索引区间的子串

数组

数组(array)。数组是一种可以存储一组信息的变量。与变量一样,数组可以包含任何类型的数据:文本字符串、数字、其他
JavaScript 对象。

1
2
3
const newCars1 = new Array("Toyota", "Honda", "Nissan");
// 出于代码的可读性考虑,强烈建议直接使用 []
const newCars2 = ["Toyota", "Honda", "Nissan"];

在赋值数组后动态添加元素方式:

1
2
3
4
var cars = [];
cars[0] = "Saab";
cars[1] = "Volvo";
cars[2] = "BMW";

这里我们看到,数组的元素可以通过索引来访问。请注意,索引的起始值为 0。

请注意,如果通过索引赋值时,索引超过了范围,同样会引起 Array 大小的变化。

1
2
3
4
// 索引超出范围会导致数组大小自动调整:
let arr = ["A", "B", "C"];
arr[5] = "x";
console.log(arr); // arr变为['A', 'B', 'C', undefined, undefined, 'x']

大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript
的 Array 却不会有任何错误。在编写代码时,不建议直接修改 Array
的大小,访问索引时要确保索引不会越界。

  • 要取得 Array 的长度,直接访问 length 属性
  • indexOf() 与 String 类似, Array 也可以通过 indexOf()
    来搜索一个指定的元素的位置
  • slice() 就是对应 String 的 substring() 版本,它截取 Array
    的部分元素,然后返回一个新的 Array
  • push() 向 Array 的末尾添加若干元素,pop() 则把 Array 的最后一个元素删除掉
  • unshift() 方法可以往 Array 的头部添加若干元素,shift() 方法则把 Array
    的第一个元素删掉
  • sort() 可以对当前 Array 进行排序,它会直接修改当前 Array
    的元素位置,直接调用时,按照默认顺序排序
  • reverse() 把整个 Array 的元素给调个个,也就是反转:
  • splice() 方法是修改 Array
    的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素
  • concat() 方法把当前的 Array 和另一个 Array 连接起来,并返回一个新的 Array
  • join() 方法是一个非常实用的方法,它把当前 Array
    的每个元素都用指定的字符串连接起来,然后返回连接后的字符串。

多维数组

如果数组的某个元素又是一个 Array ,则可以形成多维数组,例如:
let arr = [[1, 2, 3], [400, 500, 600], '-'];

对象

JavaScript的对象是一组由键-值组成的无序集合。JavaScript
对象的键都是字符串类型,值可以是任意数据类型。

1
2
3
4
5
6
var person = { firstname: "John", lastname: "Doe", id: 5566 };
// 也可以展开写,这样观感更清晰
var person2 = {
firstname: "Mike",
id: 5567,
};

要获取一个对象的属性,我们用 对象变量.属性名
的方式。如果属性名包含特殊字符,就必须用’’ 括起来。

1
2
person.name; // 'Bob'
person.zipcode; // null

如果访问一个不存在的属性会返回什么呢?JavaScript规定,访问不存在的属性不报错,而是返回
undefined。

由于JavaScript的对象是动态类型,你可以自由地给一个对象添加或删除属性。

如果我们要检测对象是否拥有某一属性,可以用 in
操作符。要判断一个属性是否是自身拥有的,而不是继承得到的,可以用
hasOwnProperty()。

1
2
3
4
5
let xiaoming = {
name: "小明",
};
"name" in xiaoming; // true
xiaoming.hasOwnProperty("name"); // true

特殊的 undefined 和 null

null 表示一个“空”的值,它和 0 以及空字符串’’ 不同,0 是一个数值,‘’
表示长度为0的字符串,而 null 表示“空”。 在其他语言中,也有类似 JavaScript 的
null 的表示,例如 Java 也用 null ,Swift 用 nil ,Python 用 None 表示。但是,在
JavaScript 中,还有一个和 null 类似的
undefined,它表示“未定义”。我们可以通过将变量的值设置为 null 来清空变量。

JavaScript 的设计者希望用 null 表示一个空的值,而 undefined
表示值未定义
。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用
null。undefined 仅仅在判断函数参数是否传递的情况下有用。

注释

注释一般分为单行注释和多行注释两种,文档注释也可纳入。

1
2
3
4
5
6
7
8
9
10
/*
This is an example of a long JavaScript comment. Note the characters at the beginning and ending of the comment.
This script adds the words "Hello, world!" into the body area of the HTML page.
*/
window.onload = writeMessage; // Do this when page finishes loading

function writeMessage() {
// Here's where the actual work gets done
document.getElementById("helloMessage").innerHTML = "Hello, world!";
}

运算符

比较运算符

实际上,JavaScript 允许对任意数据类型做比较。要特别注意相等运算符 == 和全等符号
===。

第一种是 == 比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;
第二种是 === 绝对等于(值和类型均相等),!==
不绝对等于(值和类型有一个不相等,或两个都不相等)。

由于 JavaScript 这个设计缺陷,不要使用== 比较,始终坚持使用 === 比较。

赋值运算符应用错误的情况详解

在 JavaScript 程序中如果你在 if 条件语句中使用赋值运算符的等号 (=)
将会产生一个错误结果, 正确的方法是使用比较运算符的两个等号 (==)。

if 条件语句返回 true (不是我们预期的)因为条件语句执行为 x 赋值 10,10 为 true:

1
2
var x = 0;
if (x = 10)

当采用这种形式则直接报错, 是不是很让人困惑:

1
2
var x = 0;
if (10 = x)

这种错误经常会在 switch 语句中出现,switch
语句会使用恒等计算符(===)进行比较
,这一点需要注意。

以下实例由于类型不一致,则不会执行 alert 弹窗:

1
2
3
4
5
6
// 以下实例由于类型不一致,不会执行 alert 弹窗:
var x = 10;
switch (x) {
case "10":
alert("Hello");
}

条件语句

JavaScript使用 if () { … } else { … } 来进行条件判断

循环语句

标准 for 循环

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

for … in

for 循环的一个变体是for ... in
循环,它可以把一个对象的所有属性依次循环出来:

1
2
3
4
5
6
7
8
let o = {
name: "Jack",
age: 20,
city: "Beijing",
};
for (let key in o) {
console.log(key); // 'name', 'age', 'city'
}

由于 Array 也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in
循环可以直接循环出Array的索引:

1
2
3
4
5
let a = ["A", "B", "C"];
for (let i in a) {
console.log(i); // '0', '1', '2'
console.log(a[i]); // 'A', 'B', 'C'
}

while

for循环在已知循环的初始和结束条件时非常有用。而上述忽略了条件的for循环容易让人看不清循环的逻辑,此时用while循环更佳。

while循环只有一个判断条件,条件满足,就不断循环,条件不满足时则退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:

1
2
3
4
5
6
7
let x = 0;
let n = 99;
while (n > 0) {
x = x + n;
n = n - 2;
}
x; // 2500

在循环内部变量n不断自减,直到变为-1时,不再满足while条件,循环退出。

do … while

最后一种循环是do { ... } while()循环,它和while循环的唯一区别在于,不是在每次循环开始的时候判断条件,而是在每次循环完成的时候判断条件:

1
2
3
4
5
let n = 0;
do {
n = n + 1;
} while (n < 100);
n; // 100

do { ... } while()循环要小心,循环体会至少执行1次,而 forwhile
循环则可能一次都不执行。

交互方式

alert, confirm, prompt

弹出的警告窗口 alert

1
alert("Welcome to my JavaScript page!");

确认用户的选择 confirm

1
2
3
4
5
if (confirm("Are you sure you want to do that?")) {
alert("You said yes");
} else {
alert("You said no");
}

提示用户 prompt

1
2
3
4
5
6
var ans = prompt("Are you sure you want to do that?", "");
if (ans) {
alert("You said " + ans);
} else {
alert("You refused to answer");
}

switch 小案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
window.onload = initAll;

function initAll() {
document.getElementById("Lincoln").onclick = saySomething;
document.getElementById("Kennedy").onclick = saySomething;
document.getElementById("Nixon").onclick = saySomething;
}

function saySomething() {
switch (this.id) {
case "Lincoln":
alert("Four score and seven years ago...");
break;
case "Kennedy":
alert("Ask not what your country can do for you...");
break;
case "Nixon":
alert("I am not a crook!");
break;
default:
}
}

也可以向 switch 语句传递字符串之外的其他值。可以在 switch
语句中使用数字值,甚至对数学计算的结果进行评估。

处理错误

1
2
3
4
5
6
7
8
9
10
11
12
13
window.onload = initAll;

function initAll() {
var ans = prompt("Enter a number", "");
try {
if (!ans || isNaN(ans) || ans < 0) {
throw new Error("Not a valid number");
}
alert("The square root of " + ans + " is " + Math.sqrt(ans));
} catch (errMsg) {
alert(errMsg.message);
}
}

说明: 如果 !ans 是 true,就意味着用户没有输入任何内容。 内置的 isNaN()
方法检查传递给它的参数是否“不是数字”(Not a Number)。 如果 isNaN() 返回
true,就说明输入的内容是无效的。如果 ans 小于
0,它就是负数。对于以上任何情况,都希望抛出一个错误。

JavaScript Math 函数

  • abs 绝对值
  • sin、cos、tan 标准三角函数,参数用弧度表示
  • acos、asin、atan 反三角函数,返回值用弧度表示
  • exp、log 以 e 为底数的指数和自然对数
  • ceil 返回大于等于当前参数的最小整数
  • floor 返回小于等于当前参数的最大整数
  • min 返回两个参数中较小者
  • max 返回两个参数中较大者
  • pow 指数函数,第一个参数是底数,第二个参数是幂
  • random 返回介于 0 和 1 之间的随机数
  • round 返回当前参数最接近的整数,四舍五入
  • sqrt 平方根

生产随机数 0~9 的随机数var randomNum = Math.floor (Math.random() * 10);
1~10 的随机数var randomNum = Math.floor (Math.random() * 10) + 1;

探测对象

在编写脚本时,你可能希望检查浏览器是否有能力理解你要使用的对象。进行这种检查的方法称为对象探测(object
detection)。 方法是对要寻找的对象进行条件测试,如下所示:

1
2
3
if (document.getElementById) {
...
}

如果对象存在,if 语句就为
true,脚本继续执行。但是,如果浏览器不理解这个对象,测试就返回
false,并执行条件语句的 else 部分。

已过时的探测方式
对于检查浏览器支持哪些对象,另一种替代方法是进行浏览器探测(browser
detection),这种方法尝试查明用户使用哪种浏览器查看页面。它向浏览器请求用户代理字符串,这个字符串会报告浏览器名称和版本。然后就可以让脚本以一种方式为某些浏览器服务,而对其他浏览器采用另一种方式。这是一种已经过时的脚本编程方法,因为它的效果不太好。

直接写入 HTML 输出流

1
document.write("<p>这是一个段落。</p>");

注:只有页面还在解析渲染阶段(HTML 从上到下加载过程中)调用才正常插入内容。

如果页面已经加载完成后再执行(比如放在
DOMContentLoaded、window.onload、点击事件、定时器里),document.write
会直接清空整个当前页面所有 DOM,重写一个空白页面。

对事件的反应

1
<button type="button" onclick="alert('欢迎!')">点我!</button>;

改变 HTML 内容

1
2
3
4
5
6
7
8
9
10
11
// 查找元素
x = document.getElementById("demo");
x.innerHTML = "Hello JavaScript"; // 改变内容

// HTML 图像的来源(src)
element = document.getElementById("myimage");
element.src = "/statics/images/course/pic_bulboff.gif";

// 改变 HTML 样式
x = document.getElementById("demo"); // 查找元素
x.style.color = "#ff0000"; // 改变样式

JavaScript:验证输入

1
2
3
4
var x = document.getElementById("demo").value;
if (isNaN(x)) {
alert("不是数字");
}

JavaScript 输出

JavaScript 可以通过不同的方式来输出数据:

  • 使用 window.alert() 弹出警告框。
  • 使用 document.write() 方法将内容写到 HTML 文档中。使用 innerHTML 写入到 HTML
    元素。
  • 使用 console.log() 写入到浏览器的控制台。

JavaScript 标识符 (关键字、保留字)

JavaScript
同样保留了一些关键字,这些关键字在当前的语言版本中并没有使用,但在以后
JavaScript 扩展中会用到。

语句 描述
break 用于跳出循环。
catch 语句块,在 try 语句块执行出错时执行 catch 语句块。
continue 跳过循环中的一个迭代。
do … while 执行一个语句块,在条件语句为 true 时继续执行该语句块。
for 在条件语句为 true 时,可以将代码块执行指定的次数。
for … in 用于遍历数组或者对象的属性(对数组或者对象的属性进行循环操作)。
function 定义一个函数
if … else 用于基于不同的条件来执行不同的动作。
return 返回结果,并退出函数
switch 用于基于不同的条件来执行不同的动作。
throw 抛出(生成)错误 。
try 实现错误处理,与 catch 一同使用。
var 声明一个变量。
while 当条件语句为 true 时,执行语句块。

以下是 JavaScript 中的全量保留关键字(按字母顺序):

abstract else instanceof super
boolean enum int switch
break export interface synchronized
byte extends let this
case false long throw
catch final native throws
char finally new transient
class float null true
const for package try
continue function private typeof
debugger goto protected var
default if public void
delete implements return volatile
do import short while
double in static with

this 关键字

面向对象语言中 this 表示当前对象的一个引用。

但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。

  • 在方法中,this 表示该方法所属的对象。
  • 如果单独使用,this 表示全局对象。
  • 在函数中,this 表示全局对象。
  • 在函数中,在严格模式下,this 是未定义的(undefined)。
  • 在事件中,this 表示接收事件的元素。
  • 类似 call() 和 apply() 方法可以将 this 引用到任何对象。

JavaScript for 循环

  • for - 循环代码块一定的次数
  • for/in - 循环遍历对象的属性, 还可以遍历数组。
1
2
3
4
5
var person = { fname: "John", lname: "Doe", age: 25 };

for (x in person) {
txt = txt + person[x];
}
  • while - 当指定的条件为 true 时循环指定的代码块
  • do/while - 同样当指定的条件为 true 时循环指定的代码块

操作符

typeof 操作符

你可以使用 typeof 操作符来检测变量的数据类型。

  • typeof “John” // 返回 string
  • typeof 3.14 // 返回 number
  • typeof false // 返回 boolean
  • typeof [1,2,3,4] // 返回 object
  • typeof {name:‘John’, age:34} // 返回 object

var person = undefined; // 值为 undefined, 类型为 undefined

数组中使用名字来索引

在 JavaScript 中, 对象 使用 名字作为索引。

如果你使用名字作为索引,当访问数组时,JavaScript
会把数组重新定义为标准对象。注意执行这样操作后,数组的方法及属性将不能再使用,否则会产生错误。

定义数组元素,最后不能添加逗号

数组最后一个值的后面添加逗号虽然语法没有问题,但是在不同的浏览器可能得到不同的结果。

定义对象,最后不能添加逗号

同理

自定义排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data = [
{
项目个数: 2,
地区: "中国",
},
{
项目个数: 1,
地区: "德国",
},
{
项目个数: 3,
地区: "美国",
},
];

console.log(data[0].项目个数);
console.log(data[0]["项目个数"]);

// 按“项目个数”降序排列
data.sort((a, b) => {
return a["项目个数"] - b["项目个数"];
});

console.log(data);

注意点

JavaScript 对大小写是敏感的。

当编写 JavaScript 语句时,请留意是否关闭大小写切换键。 函数 getElementById 与
getElementbyID 是不同的。 同样,变量 myVariable 与 MyVariable 也是不同的。

JavaScript 字符集

JavaScript 使用 Unicode 字符集。 Unicode 覆盖了所有的字符,包含标点等字符。
如需进一步了解,请学习我们的 完整 Unicode 参考手册。

ES 6 新特性

strict 模式

JavaScript 在设计之初,为了方便初学者学习,并不强制要求用 var
申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过 var
申明就被使用,那么该变量就自动被申明为全局变量。

为了修补 JavaScript 这一严重设计缺陷,ECMA在后续规范中推出了 strict 模式,在
strict 模式下运行的 JavaScript 代码,强制通过 var 申明变量,未使用 var
申明变量就使用的,将导致运行错误。

多行字符串、模板字符串

ES6 标准新增了一种多行字符串的表示方法,用反引号 ... 表示

模板字符串,表示方法和上面的多行字符串一样,但是它会自动替换字符串中的变量:

1
2
3
4
let name = "小明";
let age = 9;
let message = `你好, ${name}, 你今年${age}岁了!`;
console.log(message);

for of 语句

适用遍历数组对象/字符串/map/set等拥有迭代器对象的集合,它可以正确响应
break、continue 和 return 语句。

1
2
3
4
5
6
7
8
9
let arr = [
{ name: "张三", age: 13 },
{ name: "王五", age: 13 },
{ name: "赵六", age: 13 },
];

for (let it of arr) {
console.log(it);
}

Map 和 Set

JavaScript的默认对象表示方式{}可以视为其他语言中的 MapDictionary
的数据结构,即一组键值对。

但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。

为了解决这个问题,最新的ES6规范引入了新的数据类型Map

Map

Map是一组键值对的结构,具有极快的查找速度。

如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用JavaScript写一个Map如下:

初始化Map需要一个二维数组,或者直接初始化一个空MapMap具有以下方法:

1
2
3
4
5
6
7
let m = new Map(); // 空Map
m.set("Adam", 67); // 添加新的key-value
m.set("Bob", 59);
m.has("Adam"); // 是否存在key 'Adam': true
m.get("Adam"); // 67
m.delete("Adam"); // 删除key 'Adam'
m.get("Adam"); // undefined

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

1
2
3
4
let m = new Map();
m.set("Adam", 67);
m.set("Adam", 88);
m.get("Adam"); // 88

Set

SetMap类似,也是一组 key 的集合,但不存储 value。由于 key
不能重复,所以,在 Set 中,没有重复的key。

要创建一个Set,需要提供一个 Array 作为输入,或者直接创建一个空Set

1
2
let s1 = new Set(); // 空Set
let s2 = new Set([1, 2, 3]); // 含1, 2, 3

重复元素在Set中自动被过滤:

1
2
let s = new Set([1, 2, 3, 3, "3"]);
s; // Set {1, 2, 3, "3"}

注意数字3和字符串'3'是不同的元素。

通过add(key)方法可以添加元素到Set中,可以重复添加,但不会有效果:

1
2
3
4
s.add(4);
s; // Set {1, 2, 3, 4}
s.add(4);
s; // 仍然是 Set {1, 2, 3, 4}

通过delete(key)方法可以删除元素:

1
2
3
4
let s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}

Map 和 Set 是 ES6 标准新增的数据类型,请根据浏览器的支持情况决定是否要使用。

iterable 类型

遍历 Array 可以采用下标循环,遍历 MapSet
就无法使用下标。为了统一集合类型,ES6 标准引入了新的 iterable
类型,ArrayMapSet 都属于 iterable 类型。

具有 iterable 类型的集合可以通过新的 for ... of 循环来遍历。

for ... of 循环遍历集合,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
let a = ["A", "B", "C"];
let s = new Set(["A", "B", "C"]);
let m = new Map([[1, "x"], [2, "y"], [3, "z"]]);
for (let x of a) { // 遍历 Array
console.log(x);
}
for (let x of s) { // 遍历 Set
console.log(x);
}
for (let x of m) { // 遍历 Map
console.log(x[0] + "=" + x[1]);
}

你可能会有疑问,for ... of 循环和 for ... in 循环有何区别?

for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个 Array
数组实际上也是一个对象,它的每个元素的索引被视为一个属性。

当我们手动给 Array
对象添加了额外的属性后,for ... in循环将带来意想不到的意外效果:

1
2
3
4
5
let a = ["A", "B", "C"];
a.name = "Hello";
for (let x in a) {
console.log(x); // '0', '1', '2', 'name'
}

for ... in循环将把name包括在内,但Arraylength属性却不包括在内。

for ... of循环则完全修复了这些问题,它只循环集合本身的元素:

1
2
3
4
5
let a = ["A", "B", "C"];
a.name = "Hello";
for (let x of a) {
console.log(x); // 'A', 'B', 'C'
}

这就是为什么要引入新的for ... of循环。

然而,更好的方式是直接使用 iterable 内置的 forEach
方法,它接收一个函数,每次迭代就自动回调该函数。以 Array 为例:

1
2
3
4
5
6
7
let a = ["A", "B", "C"];
a.forEach(function (element, index, array) {
// element: 指向当前元素的值
// index: 指向当前索引
// array: 指向 Array 对象本身
console.log(`${element}, index = ${index}`);
});

注意forEach()方法是 ES5.1 标准引入的,你需要测试浏览器是否支持。

SetArray 类似,但 Set 没有索引,因此回调函数的前两个参数都是元素本身:

1
2
3
4
let s = new Set(["A", "B", "C"]);
s.forEach(function (element, sameElement, set) {
console.log(element);
});

Map的回调函数参数依次为valuekeymap本身:

1
2
3
4
let m = new Map([[1, "x"], [2, "y"], [3, "z"]]);
m.forEach(function (value, key, map) {
console.log(value);
});

如果对某些参数不感兴趣,由于 JavaScript
的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Arrayelement

1
2
3
4
let a = ["A", "B", "C"];
a.forEach(function (element) {
console.log(element);
});

解构赋值

从 ES6 开始,JavaScript 引入了解构赋值,可以同时对一组变量进行赋值。

什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:

1
2
3
4
let array = ["hello", "JavaScript", "ES6"];
let x = array[0];
let y = array[1];
let z = array[2];

现在,在 ES6 中,可以使用解构赋值,直接对多个变量同时赋值:

1
2
3
4
5
// 如果浏览器支持解构赋值就不会报错:
let [x, y, z] = ["hello", "JavaScript", "ES6"];

// x, y, z分别被赋值为数组对应元素:
console.log(`x = ${x}, y = ${y}, z = ${z}`);

注意,对数组元素进行解构赋值时,多个变量要用 [...] 括起来。

如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:

1
2
3
4
let [x, [y, z]] = ["hello", ["JavaScript", "ES6"]];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

解构赋值还可以忽略某些元素:

1
2
let [, , z] = ["hello", "JavaScript", "ES6"]; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'

如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性。

对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = {
name: "小明",
age: 20,
gender: "male",
passport: "G-12345678",
school: "No.4 middle school",
address: {
city: "Beijing",
street: "No.1 Road",
zipcode: "100001",
},
};
let { name, address: { city, zip } } = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined

使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined,这和引用一个不存在的属性获得undefined是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {
name: "小明",
age: 20,
gender: "male",
passport: "G-12345678",
school: "No.4 middle school",
};

// 把passport属性赋值给变量id:
let { name, passport: id } = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量 id 获得 passport 属性:
passport; // Uncaught ReferenceError: passport is not defined

解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined的问题:

1
2
3
4
5
6
7
8
9
10
11
let person = {
name: "小明",
age: 20,
gender: "male",
passport: "G-12345678",
};

// 如果person对象没有single属性,默认赋值为true:
let { name, single = true } = person;
name; // '小明'
single; // true

有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:

1
2
3
4
5
// 声明变量:
let x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =

这是因为 JavaScript 引擎把 {
开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来:

1
({ x, y } = { name: "小明", x: 100, y: 200 });

解构的使用场景

解构赋值在很多时候可以大大简化代码。例如,交换两个变量xy的值,可以这么写,不再需要临时变量:

1
2
let x = 1, y = 2;
[x, y] = [y, x];

快速获取当前页面的域名和路径:

1
let { hostname: domain, pathname: path } = location;

如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date对象:

1
2
3
function buildDate({ year, month, day, hour = 0, minute = 0, second = 0 }) {
return new Date(`${year}-${month}-${day} ${hour}:${minute}:${second}`);
}

它的方便之处在于传入的对象只需要 yearmonthday 这三个属性:

1
buildDate({ year: 2017, month: 1, day: 1 });

使用解构赋值可以减少代码量,但是,需要在支持 ES6
解构赋值特性的现代浏览器中才能正常运行。

参考

简介 - JavaScript教程 -
廖雪峰的官方网站