前言

本文是通过 廖雪峰的 JavaScript 教程 学习而来. 全文内容几乎与教程无差别, 即本人跟着教程文字和代码均手敲一边, 遂产生此笔记. 笔记文件太大, 遂拆分成多个部分, 这是第一部分, 主要学习的是 JavaScript 基础语法, 包括数据类型、条件判断、循环、Map 和 Set 等.

1. 基础语法

1.1 数据类型与变量

计算机可以处理各种类型(文本、图形、音频、视频等)的数据,不同的数据需要用不同的类型定义,JavaScript 中定义了以下几种数据类型.

1.1.1 Number

JavaScript 不区分整型和浮点型,统一使用 Number 表示。

1
2
3
4
5
6
123;		整数		 123
0.456; 浮点数 0.456
1.23e4; 科学计数法 12300
-99; 负数 -99
NaN; 表示 Not a Number,无法计算结果时表示
Infinity; 表示无限大

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

1
2
3
4
5
6
7
8
9
10
11
1 + 2;	 // 3
(1 + 2) * 3 / 2; // 4.5
2 / 0; // Infinity
0 / 0; // NaN
10 % 3; // 1
10.5 % 3; // 1.5

// 注意 Number 类型不区分整数和浮点数
12.00 === 12 // True
// JavaScript 整数最大范围是 ±2^53 次方,不是 63 次方
console.log(Number.MAX_SAFE_INTEGER)

1.1.2 字符串

字符串是以单引号或者双引号括起来的任意文本,比如 'abc''xyz' 等等。

1.1.3 布尔值

布尔值和布尔代数表示完全一致,布尔值只有 truefalse 两种值。

1.1.4 BigInt

用来表示比 2^53 还有大的整数,可以用内置的 BigInt 类型,它的表示方法是在整数后加一个 n ,例如 9223372036854775808n。不能将一个 BigInt 和一个 Number 放在一起运算,需要将 Number 转换成 BigInt

1.1.5 null 和 undefined

null 表示一个空值,它和 0 以及空字符串 '' 不同,0 是一个数值,'' 表示长度为0的字符串,而 null 表示”空”。

undefined 表示未定义,区分两者的意义并不大,一般都用 null ,undefined 仅仅在判断函数参数是否传递的情况才有用。

1.1.6 数组

数组是一组按顺序排列的集合,集合的每个值称为元素。JavaScript 的数组中可以包括任意数据类型的元素。例如:

1
[1, 3.14, 'Hello', null, true]

上述是通过字面量的方式创建数组,还可以用 Array() 函数定义数组:

1
new Array(1, 2, 3);

1.1.7 对象

JavaScript 的对象是由一组键值对(key-value)组成的 无序集合 ,例如:

1
2
3
4
5
6
7
8
const person = {
name: 'zhangsan',
age: 20,
tags: ['js', 'ts', 'python'],
city: 'suzhou',
hasCar: false,
zipcode: null
}

获取对象的属性,通过 对象变量.属性名 的方式:

1
2
person.name;
person.zipcode;

1.1.8 变量

变量基本和初中代数中的方程变量概念一致,只是计算机中,变量不仅可以是数值,还可以是任意数据类型。变量在 JavaScript 中就是一个变量名表示,变量名是大小写、数字、$_ 的组合,但不能是数字开头。变量名也不能是 JavaScript 的保留字,ifwhile 等。申明变量用 var 语句,例如:

1
2
3
4
5
var a;
var $b = 1;
var s_007 = '007';
var isTrue = true;
var t = null;

JavaScript 中变量名也可以是中文,但没必要麻烦。使用 = 为变量赋值,可以重复赋值,但不能重复申明。JavaScript 中变量本身类型不固定,这样的语言称为动态语言,而声明变量时必须指定类型的是静态语言,例如 Java,赋值语句如下:

1
2
int a = 123;
a = 'ABC'; // 错误:不能把字符串赋值给整型变量

不要把 = 等同于数学的等号,如下例子:

1
2
var x = 2;
x = x + 2; // 在数学中是不成立的,而 JS 中,先执行右边的语句,将结果 4 赋值给 x

1.1.9 strict 模式

JavaScript 设计之处,并不强制使用 var 声明变量, 产生了严重的后果,如果一个变量没有通过 var 声明就使用,将变成全局变量:

1
i = 10

在同一个页面不同的 JavaScript 文件中,如果都不用 var 声明,而恰好都使用了变量 i ,将造成变量 i 互相影响,产生很难调试的 BUG。使用 var 声明,则不是全局变量,它的范围被限制在该变量声明的函数体内,同名变量在不同的函数体内不会冲突。为了修补这个设计缺陷,ECMA 在后续推出了 strict 模式,在 strict 模式中运行的 JavaScript 代码,强制通过 var 声明变量,未使用 var 声明变量的,将导致运行错误。

启用 strict 模式的方法是在 JavaScript 代码第一行写上:

1
'use strict';

这是一个字符串,不支持 strict 模式的浏览器会把它当作字符串语句运行,支持 strict 模式的浏览器才会开启 strict 模式运行 JavaScript。

另一种声明变量的方式是 let ,这是现代 JavaScript 推荐的方式:

1
2
let s = 'Hello';
console.log(s);

1.2 字符串

JavaScript 的字符串就是用 ''"" 包裹的字符表示。

如果 ' 本身是一个字符,就需要用 "" 包裹,比如 I'am OK! ,如果 " 也是一个字符,就需要用转义字符 \ 表示,比如:

1
'I\'am \"OK\"!';

转义字符可以表示很多字符,如下:

1
2
3
4
5
'\n'; 表示换行
'\t'; 表示制表符
'\\'; 表示 \
'\x41'; 表示 'A'
'\u4e2d\u6587'; 表示 '中文'

1.2.1 多行字符串

由于多行字符串用 \n,写起来费劲,最新的 ES6 标准新增了一种多行字符串表示方法,用反引号`…` 表示:

1
2
3
`这是一个
多行
字符串`

1.2.2 模板字符串

要把多个字符串连接起来,可以使用 + 号连接:

1
2
3
4
let name = 'zhang san';
let age = 20;
let message = '你好, ' + name + ',你今年' + age + '岁了!';
alert(message);

显然这种方法很麻烦,ES6 新增了一种模板字符串,表示方法和上面的多行字符一样,但是它会自动替换字符串中的变量:

1
2
3
4
let name = 'zhang san';
let age = 20;
let message = `你好,${name},你今年${age}岁了!`;
alert(message);

1.2.3 操作字符串

字符串常规操作如下:

1
2
let s = 'Hello World!';
s.length; // 12

要获取字符串某个指定位置的字符, 使用类似 Array 的下标操作, 索引号从 0 开始:

1
2
3
4
let s = 'Hello, World!';

s[0]; // 'H'
s[6]; // ' '
toUpperCase

toUpperCase() 方法是把一个字符串全部变为大写:

1
2
let s = 'Hello';
s.toUpperCase(); // HELLO
toLowerCase

toLowerCase() 方法是把一个字符串全部变为小写:

1
2
let s = 'World';
s.toLowerCase(); // world
indexOf

indexOf() 方法是搜索指定字符的出现位置:

1
2
3
4
let s = 'Hello, World!';
s.indexOf('world'); // 返回 7
s.indexOf('o') // 返回 4
s.indexOf('XXX') // 找不到子串, 返回 -1
substring

substring() 返回指定索引区间的子串:

1
2
3
let s = 'Hello, World';
s.substring(0, 5); // 从索引 0 开始到 5(不包括 5), 返回 'Hello'
s.substring(7); // 返回从 7 开始到结尾, 'World'

1.3 数组

JavaScript 的数组元素可以是任意数据类型, 并通过索引访问每个元素. 要取得 Array 的长度, 直接访问 length 属性:

1
2
3
// Array.length
let arr = [1, 2.3, 'Hello', null, true]
console.log(arr.length); // 5

如果直接给 Array.length 赋一个新的值, Array 的长度会发生变化:

1
2
3
4
5
6
let arr = ['a', 'b', 'c'];
console.log(arr.length); // 3
arr.length = 6;
console.log(arr); // ['a', 'b', 'c', undefined, undefined, undefined]
arr.length = 2;
console.log(arr); // ['a', 'b']

Array 通过索引修改元素的值, 如果索引超过 Array 长度, Array 长度也会发生变化:

1
2
3
let arr = ['a', 'b', 'c'];
arr[5] = 'x';
console.log(arr); // ['a', 'b', 'c', undefined, undefined, 'x']

1.3.1 indexOf

与 String 类似, Array 也可以通过 indexOf() 来搜索一个指定的元素的位置:

1
2
3
4
let arr = [10, 20, '30', 'xyz'];
arr.indexOf(10); // 0
arr.indexOf(30); // 找不到数字 30 返回 -1
arr.indexOf('30'); // 2

1.3.2 slice

slice() 就是对应 String 的 substring() 版本, 它截取 Array 的部分元素, 然后返回一个新的 Array:

1
2
3
let arr = ['a', 'b', 'c', 'd'];
arr.slice(0, 3); // 从索引 0 到 3(不包括 3), ['a', 'b', 'c']
arr.slice(3); // 从索引 3 开始到结束, ['d']

1.3.3 push 和 pop

push() 向 Array 的末尾添加 若干 元素, pop() 则把 Array 的最后一个元素删除掉:

1
2
3
4
5
6
let arr = [1, 2];
arr.push('a', 'b');
arr; // [1, 2, 'a', 'b']
arr.pop();
arr; // [1, 2, 'a']
arr = []; arr.pop(); // 空数组 pop() 不会报错,返回 undefined

1.3.4 unshift 和 shift

unshift() 用于向 Array 头部添加若干元素, shift() 用于删除 Array 的头部元素:

1
2
3
4
5
6
let arr = [1, 2];
arr.unshift('a', 'b');
arr; // ['a', 'b', 1, 2]
arr.shift();
arr; // ['b', 1, 2]
arr = []; arr.shift(); // 空数组继续 shift() 不会报错, 而是返回 undefined

1.3.5 sort

sort() 可以对当前 Array 进行排序, 它会直接修改当前 Array 的元素位置, 直接调用时,按照默认顺序排序:

1
2
3
let arr = ['b', 'c', 'a'];
arr.sort();
arr; // ['a', 'b', 'c']

后续函数中介绍指定顺序排序.

1.3.6 reverse

reverse() 把整个 Array 元素给调个个, 也就是反转:

1
2
3
let arr = ['one', 'two', 'three'];
arr.reverse();
arr; // ['three', 'two', 'one']

1.3.7 splice

splice() 方法是修改 Array 的万能方法, 它可以从指定的索引开始删除若干元素, 然后再从该位置添加若干元素:

1
2
3
4
5
6
let arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Oracle'];
// 从索引 2 开始删除 3 个元素, 然后添加两个元素
arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Oracle']
arr; // 返回 ['Microsoft', 'Apple', 'Google', 'Facebook']
arr.splice(3, 0, 'HUAWEI'); // 返回 [], 因为没有删除元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'HUAWEI']

1.3.8 concat

concat() 方法把当前的 Array 和另一个 Array 连接起来, 并返回一个新的 Array:

1
2
3
4
let arr = ['a', 'b', 'c'];
let added = arr.concat([1, 2, 3]);
added; // ['a', 'b', 'c', 1, 2, 3];
arr; // ['a', 'b', 'c'] concat 并没有修改原来的 Array, 而是返回一个新的 Array

实际上 concat() 可以接受任意个 元素和 Array , 并且自动把 Array 拆分, 全部添加到新的 Array 中去.

1
2
let arr = ['a', 'b', 'c'];
arr.concat(1, 2, ['3', '4']); // ['a', 'b', 'c', 1, 2, '3', '4']

1.3.9 join

join() 方法是一个实用的方法, 它把当前 Array 的每一个元素都用指定的字符串连接起来, 然后返回连接后的字符串:

1
2
let arr = ['a', 'b', 'c', 1, 2];
arr.join('-'); // 'a-b-c-1-2'

1.3.10 多维数组

一个 Array 的元素中也有 Array, 就形成了多维数组, 例如:

1
let arr = [1, 2, 3, [4, 5]]

1.4 对象

JavaScript 的对象是一种无序的集合数据类型, 它由若干键值对组成. JavaScript 的对象用于描述现实世界中的某个对象, 例如, 描述 “小明” 这个小朋友:

1
2
3
4
5
6
7
8
let xiaoming = {
name: '小明',
birth: 2005,
school: 'No.1 Middle School',
height: 1.70,
weight: 65,
score: null
}

JavaScript 用 {~~~} 表示一个对象, 键值对用 xxx: xxx 声明, 用 , 隔开. 上述对象声明了一个 name 属性, 值是 小明, birth 属性值是 2005, 以及其他属性. 最后把这个对象赋值给变量 xiaoming 后, 就可以通过变量 xiaoming 来获取小明的属性了.

1
2
xiaoming.name;
xiaoming.height;

访问属性是通过 . 操作符完成的, 但这要求属性名必须是一个有效的变量名, 如果属性名包含特殊字符,就必须用 '' 括起来, 访问这种属性, 必须使用 ['xxx'] 来访问:

1
2
3
4
5
6
7
8
9
let xiaohong = {
name: '小红',
'middle-school': 'No.1 Middle School'
}

// 获取小红的属性
xiaohong['middle-school'];
xiaohong['name'];
xiaohong.name;

也可以用 xiaohong['name'] 来访问 xiaohongname 属性, 不过 xiaohong.name 写法更简洁. 写 JavaScript 代码时, 属性名尽可能使用标准的变量名, 这样就可以直接通过 Object.prop 的形式访问属性了.

实际上 JavaScript 对象的所有属性都是字符串, 不过属性对应的值可以是任意属性. 如果访问一个不存在的属性, 不会报错, 返回 undefined.

1
2
3
4
let obj = {
a = 100;
}
obj.x; // undefined

因为 JavaScript 的对象属性是动态的, 可以自由地给一个对象添加或者删除属性:

1
2
3
4
5
6
7
8
let xiaoming = {
name: 'xiaoming'
}
xiaoming.age; // undefined
xiaoming.age = 18;
xiaoming.age; // 18
delete xiaoming.age;
xiaoming.age; // undefined

要检测一个对象中是否有一个特定的属性, 可以使用 in 操作符:

1
2
3
4
5
6
let xiaoming = {
name: '小明',
height: 1.70
}
'name' in xiaoming; // true
'grade' in xiaoming; // false

但是用 in 判断一个属性是否存在时, 这个属性不一定是这个对象定义的, 可以是继承的:

1
'toString' in xiaoming; // true

因为 toString 定义在 object 对象中, 而所有对象都最终在原型链上指向 object , 所以 xiaoming 也拥有 toString 属性. 要判断是不是对象自己本身的, 可以用 hasOwnProperty() 方法:

1
2
3
4
5
let xiaoming = {
name: '小明'
}
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false

1.5 条件判断

JavaScript 使用 if () {~~~} else {~~~} 进行条件判断, 例如,根据年龄显示不同内容:

1
2
3
4
5
6
let age = 20;
if (age >= 18) {
console.log('adult');
} else {
console.log('teenager');
}

省略 {} 的危险在于, 如果后来想添加一些语句, 却忘了写 {} 就改变了 if ~~~ else ~~~ 的语义:

1
2
3
4
5
6
7
8
9
10
let age = 20;
if (age >= 20)
console.log('adult');
else
console.log('age < 18'); // 添加这一行日志
console.log('teenager'); // 这行语句已经不在 else 控制范围了

// 输出
adult
teenager

1.5.1 多行条件判断

需要更细致的判断条件, 可以使用多个 if ~~~ else ~~~ 的组合:

1
2
3
4
5
6
7
8
let age = 3;
if (age >= 18) {
console.log('adult');
} else if (age >= 6) {
console.log('teenager');
} else {
console.log('kid');
}

注意 : if ~~~ else ~~~ 语句执行特点是二选一, 在多个 if ~~~ else ~~~ 语句中, 如果某个执行条件成立, 后续就不再继续判断了, 例如, 下面的代码输出 teenager:

1
2
3
4
5
6
7
8
let age = 20;
if (age >= 6) {
console.log('teenager');
} else if (age >= 18) {
console.log('adult');
} else {
console.log('kid');
}

JavaScript 把 nullundefined0NaN 和空字符串 '' 视为 false, 其他值一概视为 true.

1.6 循环

要让计算机计算上千上万次的重复运算, 比如从 1 加到 100, 我们就需要循环语句. JavaScript 循环语句有两种, 一种是 for 循环, 通过初始条件,结束条件和递增条件来循环执行语句块:

1
2
3
4
5
let s = 0;
for (let i = 1; i <= 100; i++) {
s += i;
}
console.log(s); // 输出 5050

for 循环最常用的是通过索引来遍历数组:

1
2
3
4
let arr = ['Apple', 'Google', 'Microsoft'];
for (let i = 0; i < arr.length; i ++) {
console.log(arr[i]);
}

for 循环的三个条件都是可以省略的, 如果没有退出循环的判断条件, 就必须使用 break 退出循环, 否则就是死循环:

1
2
3
4
5
6
7
let x = 0;
for (;;) {
if (x > 100) {
break;
}
x ++;
}

1.6.1 for … in

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

1
2
3
4
5
6
7
8
let obj = {
name: 'Jack',
age: 20,
city: 'suzhou',
};
for (let key in obj) {
console.log(`key: ${key}, value: ${obj[key]}`);
}

要过滤掉对象继承的属性, 使用 hasOwnProperty() 方法实现:

1
2
3
4
5
6
7
8
9
10
let obj = {
name: 'Jack',
age: 20,
city: 'suzhou'
};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key);
}
}

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

1
2
3
4
5
let a = ['a', 'b', 'c'];
for (let i in a) {
console.log(i);
console.log(a[i]);
}

注意 : for ... in 对 Array 的循环得到的索引是 String 类型, 而不是 Number 类型.

1.6.2 while

for 循环在已知循环的初始状态和结束条件时非常有用. 而上述忽略了条件的 for 循环容易让人看不清逻辑, 此时用 while 循环更好. while 循环只有一个判断条件, 条件满足, 就不断循环, 条件不满足就退出循环. 比如用 while 循环计算 1~100 以内所有奇数和:

1
2
3
4
5
6
7
8
9
let s = 0;
let n = 100;
while (n > 0) {
if (n % 2 === 1) {
s += n;
}
n -= 1;
}
console.log(s); // 2500

1.6.3 do … while

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

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

注意 : 使用 do { ... } while() 循环要小心, 循环体最少会执行一次, 而 forwhile 循环则可能一次都不执行.

1.7 Map and Set

JavaScript 的默认对象表示方式 {} 可以视为其它语言中的 Map 或者 Dictionary 的数据结构, 即一组键值对. 但是 JavaScript 的对象有个问题, 即使键必须是字符串, 但实际上 Number 或者其他数据类型作为键也是非常合理的. 为了解决这个问题, 最新的 ES6 规范引入了新的数据类型, Map .

1.7.1 Map

Map 是一组键值对结构, 具有极快的查找速度. 假设要根据同学的名字查找对应的成绩, 如果用 Array 实现, 就需要两个 Array:

1
2
let names = ['Michael', 'Bob', 'Tracy'];
let scores = [95, 75, 85];

给定一个名字, 要查找对应的成绩, 就需要先在 names 中找到对应的位置, 再从 scores 取出对应的成绩, Array 越长, 查询越慢.

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

1
2
let m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

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

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

1.7.2 Set

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

我认为, Set 存储的是不重复的 value 而不是 key

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

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

重复元素在 Set 中被过滤掉:

1
2
let s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 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}

1.8 iterable

遍历 Array 可以采用下标循环, 便利 Map 和 Set 就无法使用下标. 为了统一集合类型, ES6 标准引入了新的 iterable 类型, Array、Map 和 Set 都属于 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) {
cosole.log(x);
}
for (let x of s) {
cosole.log(x);
}
for (let x of m) {
cosole.log(x[0] + '=' + x[1]);
}

for ... infor ... of 有什么区别 :

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 包括在内, 但 Array 的 length 属性却不包括在内. 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}`);
})

Set 与 Array 类似, 但是 Set 没有索引, 因此回调函数的 elementindex 都是元素本身:

1
2
3
4
let s = new Set(['a', 'b', 'c']);
s.forEach(function (element, sameElement, set) {
console.log(element + ' => sameElement: ' + sameElement);
});

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

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

如果不需要某些参数, 由于 JavaScript 的函数调用不要求参数必须一致, 因此可以忽略. 例如, 只需要获取 Array 的 element:

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

第一部分结束