一、数据结构
1.1 Object
我们可以向Object对象添加任何属性,所以Object也是一个容器或集合:
let obj = new Object("hello");
console.log(obj); // [String: 'hello']
obj.name = "jeff";
console.log(obj.name); // jeff
obj.print = function() {
console.log("I'm object");
}
obj.print(); // I'm object
1.2 数组
JavaScript中使用Array类创建数组对象:
// 使用Array类创建数组
let cars1 = new Array("Saab", "Volvo", "BMW");
// 使用Array类创建数组
let cars2 = new Array();
cars2[0] = "Saab"; // 支持[]方式访问和赋值,下标从0开始
cars2[1] = "Volvo";
cars2[2] = "BMW";
// 简写方式
let cars3 = ["Saab", "Volvo", "BMW"];
console.log(cars3.length); // 3
// 数组中的元素可以为不同的类型
let a = [1, "x", true];
console.log(a[1]); // x
console.log(a[5]); // undefined,访问数组中不存在的元素会返回undefined,不会报错
a.testProp = "xyz"; // 可以向Array对象添加任意属性
console.log(a.testProp); // xyz
a.print = function() { console.log("I'm a"); }
a.print(); // I'm a
1.2.1 Array类完整的属性
属性 | 描述 |
---|---|
length | 设置或获取数组元素的个数 |
prototype | 允许你向数组对象添加属性或方法 |
1.2.2 Array类完整的方法
方法 | 描述 |
---|---|
concat() | 连接两个或更多的数组,并返回结果 |
copyWithin() | 从数组的指定位置拷贝元素到数组的另一个指定位置中 |
entries() | 返回数组的可迭代对象 |
every() | 检测数值元素的每个元素是否都符合条件 |
fill() | 使用一个固定值来填充数组 |
filter() | 检测数值元素,并返回符合条件所有元素的数组 |
find() | 返回符合传入测试(函数)条件的数组元素 |
findIndex() | 返回符合传入测试(函数)条件的数组元素索引 |
forEach() | 数组每个元素都执行一次回调函数 |
from() | 通过给定的对象中创建一个数组 |
includes() | 判断一个数组是否包含一个指定的值 |
indexOf() | 搜索数组中的元素,并返回它所在的位置 |
isArray() | 判断对象是否为数组 |
join() | 把数组的所有元素放入一个字符串 |
keys() | 返回数组的可迭代对象,包含原始数组的键(key) |
lastIndexOf() | 搜索数组中的元素,并返回它最后出现的位置 |
map() | 通过指定函数处理数组的每个元素,并返回处理后的数组 |
pop() | 删除数组的最后一个元素并返回删除的元素 |
push() | 向数组的末尾添加一个或更多元素,并返回新的长度 |
reduce() | 将数组元素计算为一个值(从左到右) |
reduceRight() | 将数组元素计算为一个值(从右到左) |
reverse() | 反转数组的元素顺序 |
shift() | 删除并返回数组的第一个元素 |
slice() | 选取数组的一部分,并返回一个新数组 |
some() | 检测数组元素中是否有元素符合指定条件 |
sort() | 对数组的元素进行排序 |
splice() | 从数组中添加或删除元素 |
toString() | 把数组转换为字符串,并返回结果 |
unshift() | 向数组的开头添加一个或更多元素,并返回新的长度 |
valueOf() | 返回数组对象的原始值 |
1.2.3 数组的复制
let a1 = [1, 2, 3];
let a2 = a1;
a2[1] = 4;
console.log(a1[1]); // 4
从上面的例子中,我们发现改变数组a2[1]
的值时,数组a1[1]
的值也随着改变了,这说明a1和a2指向的是同一块内存区域,这个在C/C++中就是指针的概念,let a2 = a1;
做的是浅拷贝
操作。
如果需要做深拷贝
,也就是将数组a1的所有元素克隆一份给a2,可以通过下面的两种方式:
let a1 = [1, 2, 3];
let a2 = a1.concat()
a2[1] = 4;
console.log(a1[1]); // 2
let a1 = [1, 2, 3];
let a2 = [...a1]; // 也可以写成 let [...a2] = a1;
a2[1] = 4;
console.log(a1[1]); // 2
1.3 Map
Map对象用来存储键值对(key-value),并且能够记住键的原始插入顺序。任何对象都可以作为键或值。
在Map中Key是唯一的。
let myMap = new Map();
let keyObj = {};
let keyFunc = function() {};
let keyString = "a string";
// 添加键
myMap.set(keyString, "和键'a string'关联的值");
myMap.set(keyObj, "和键keyObj关联的值");
myMap.set(keyFunc, "和键keyFunc关联的值");
console.log(myMap.size); // 3
// 读取值
console.log(myMap.get(keyString)); // 和键'a string'关联的值
console.log(myMap.get("a string")); // 和键'a string'关联的值
console.log(myMap.get(keyObj)); // 和键keyObj关联的值
console.log(myMap.get(keyFunc)); // 和键keyFunc关联的值
console.log(myMap.get({})); // undefined, 因为keyObj !== {}
console.log(myMap.get(function() {})); // undefined, 因为keyFunc !== function () {}
console.log(myMap.size); // 3,返回有多少个键值对
// 和Array一样,可以添加任意属性和方法
myMap.test = "a";
console.log(myMap.test);
myMap.print = function() {console.log("I'm map");}
myMap.print(); // I'm map
1.3.1 Map与Array相互转换
Array => Map
let map = new Map([
[1, "one"],
[2, "two"]
]);
console.log(map); // Map { 1 => 'one', 2 => 'two' }
Map => Array
let map = new Map();
map.set(1, "one");
map.set(2, "tow");
let arr2 = Array.from(map);
console.log(arr2); // [ [ 1, 'one' ], [ 2, 'two' ] ]
1.3.2 Map的复制
Map在直接赋值的时候会遇到和Array同样的“浅拷贝”的问题,如:
let map1 = new Map();
map1.set(1, "one");
map1.set(2, "tow");
let map2 = map1;
map2.set(1, "three");
console.log(map1.get(1)); // three,修改map2会导致map1的值也被修改了
可以通过下面的方式完成Map的深拷贝:
let map1 = new Map();
map1.set(1, "one");
map1.set(2, "tow");
let map2 = new Map(map1);
map2.set(1, "three");
console.log(map1.get(1)); // one
console.log(map2.get(1)); // three
1.4 Set
Map是键值对的集合,而Set则只是键(key)的集合。
Set中的每个元素都是唯一的。
任何对象都可以作为Set的元素。
let keys = [1,1,2,3,"str"];
let s = new Set(keys);
// 自动去重了
console.log(s); // Set { 1, 2, 3, 'str' }
// 新增使用Add
s.add(9);
s.add(function(){});
console.log(s); // Set { 1, 2, 3, 'str', 9, [Function] }
// 删除使用delete
s.delete(1);
console.log(s); // Set { 2, 3, 'str', 9, [Function] }
1.5 迭代器
前面介绍Object、Array、Map、Set这些容器的时候,都避开了一个话题:遍历。本节主要介绍如何遍历JavaScript中的容器或集合。
迭代器(Iterator)就是一个接口,为各种不同的数据结构提供统一的遍历访问机制。任何数据结构只要实现Iterator 接口,就可以完成遍历操作。
在学习如何自定义迭代器之前,我们先学习一下如何遍历JavaScript常用的数据集合:
1.5.1 遍历字符串
let str = "hello";
for(let v of str) {
console.log(v);
}
/*
h
e
l
l
o
*/
1.5.2 遍历数组
let arr = [1, "str", 2];
for (let v of arr) {
console.log(v);
}
/*
1
str
2
*/
1.5.3 遍历Map
let map = new Map([
[1, "one"],
[2, "two"],
[3, function () { }]
]);
for (let [k, v] of map) {
console.log(k + ":" + v);
}
/*
1:one
2:two
3:function () { }
*/
二、解构赋值
解构(Destructuring)赋值分为“数组的解构赋值”和“对象的解构赋值”。
2.1 数组的解构赋值
let [a, b, c] = [1, 2, 3]; // 根据位置依次取值
// a = 1
// b = 2
// c = 3
let [foo, [[bar], baz]] = [1, [[2], 3]];
// foo = 1
// bar = 2
// baz = 3
let [ , , third] = ["foo", "bar", "baz"];
// third = "baz"
let [head, ...tail] = [1, 2, 3, 4];
// head = 1
// tail = [2,3,4]
let [x, y, ...z] = ["a"];
// x = "a"
// y = undefined
// z = []
let [foo] = []; // foo = undefined
let [bar, foo] = [1]; // foo = undefined
let [bar, foo = true] = [1]; // 可以赋默认值
// bar = 1
// foo = true
let [x, y] = [1, 2, 3];
// x = 1
// y = 2
let [a, [b], d] = [1, [2, 3], 4];
// a = 1
// b = 2
// d = 4
2.2 对象的解构赋值
let { foo, bar } = { foo: "aaa", bar: "bbb" }; // 和顺序没关系,根据属性名来取值
// foo = "aaa"
// bar = "bbb"
let { foo } = { bar: 'baz' };
// foo = undefined
// 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量
// 如:console对象有log方法,我们可以这样使用:
const { log } = console;
log('hello') // hello
三、promise、async、await
promise、async、await这三个关键字都和异步编程有关。
3.1 Promise
Promise翻译成中文就是“承诺”的意思,声明一个Promise就是立下了一个承诺,无论怎么样,都会给被承诺人一个结果,而且这个结果是板上钉钉的,不会再变。
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
声明Promise对象时需要传入一个函数对象作为参数,这个函数对象的2个参数也是函数对象(resolve
, reject
),resolve
和reject
不需要开发者定义,Javascript引擎会自动生成这2个函数。
当Promise对象生成后会立即变成pending
状态,调用resolve
函数会将Promise对象标记为fulfilled
状态,而调用reject
函数则会将当前Promise对象标记rejected
状态。
resolve
和rejected
函数,我们只能调用它们中的一个,不能即调用resolve
又调用rejected
。如果我们先调用了resolve
,此时Promise状态会标记为fulfilled
,然后又调用了rejected
函数,此时Promise状态并不会再改变,仍然使fulfilled
状态,因为承诺的结果是板上钉钉的,不会再变。建议将resolve
和reject
作为最后一行代码调用,简单起见,可以在这2个函数前面加上return
, 即return resolve();
或return reject();
// Promise的参数为一个函数对象,函数有2个参数resolve, reject
let promise = new Promise((resolve, reject) => {
// 做一些耗时的操作,比如网络请求
// 这里我们使用一个延时器来模拟耗时的网络请求
setTimeout(() => { // 延时1000ms之后成功
resolve("ok");
}, 1000);
// 成功则调用 resolve();
// 失败则调用 reject();
});
那么,我想在这个异步操作完成之后,再根据结果(是成功了,还是失败了)来继续做下一件事情,那我们该怎么做了?
Promise对象提供了then
方法,该方法接受2个函数对象作为参数:
第一个回调函数是Promise对象的状态变为resolved
时调用;
第二个回调函数是Promise对象的状态变为rejected
时调用。
其中,第二个函数是可选的,不一定要提供。每个回调函数都可以接受一个参数,这个参数就是上一步调用resolve或reject时传入的。
// 上面的耗时操作完成之后,我们可能还需要根据结果来继续做一些事情
// 此时就可以使用then,then函数有2个参数,分别为2个函数对象。
// 上一步操作中,如果调用resolve(data),则then函数第一个函数对象参数会被调用;
// 如果调用reject(data),则then函数第二个函数对象参数会被调用
promise.then((data) => {
console.log("resolve1: " + data);
return "hello";
}, (data) => {
console.log("reject1: " + data);
});
在这个例子中,我在第一个参数中直接通过return返回了"hello"字符串,那这个返回值的意义在哪里了?还有其他人可以使用到这个返回值吗?
是的,还可以继续使用。因为then
的返回值是一个Promise对象,虽然我只是使用的return "hello";
,并没有new Promise
,但JavaScript引擎会自动包装成一个Promise对象,等同于:
promise.then((data) => {
console.log("resolve1: " + data);
return new Promise((resolve, reject) => {
return resolve("hello");
});
}, (data) => {
console.log("reject1: " + data);
});
到这儿了,我们知道了then
的返回的是一个Promise
对象。既然then
返回的是Promise
对象,那么Promise
就可以继续then
呀,然后一直then
下去…这样我们就可以将一系列异步的操作串联起来了:
let p = new Promise((resolve, reject) => {
// ...
// resolve(); 或 reject();
})
.then(() => {
// ...
})
.then(() => {
// ...
})
.then(() => {
// ...
});
但多个异步操作串联执行,还有一点需要注意,我们看下面的例子:
let promise = new Promise((resolve, reject) => {
setTimeout(() => { // 延时1000ms
resolve("ok1");
}, 1000);
}).then((data) => {
console.log("resolve1: " + data);
setTimeout(() => { // 延时1000ms
return "ok2";
}, 1000);
}).then((data) => {
console.log("resolve2: " + data);
});
我们期望的输出是:
resolve1: ok1
resolve2: ok2
但实际的输出却是:
resolve1: ok1
resolve2: undefined
问题出在第二个setTimeout模拟的耗时操作,我们以为程序会等第二个setTimeout执行完了再执行第二个then,但事实上setTimeout也是一个异步操作,虽然其延时了一秒执行其回调函数,但setTimeout这条语句却马上执行完成了,导致第一个then没有任何返回,针对这种情况,我们需要将代码改成下面的:
let promise = new Promise((resolve, reject) => {
setTimeout(() => { // 延时1000ms
resolve("ok1");
}, 1000);
}).then((data) => {
console.log("resolve1: " + data);
return new Promise((resolve, reject) => {
setTimeout(() => { // 延时1000ms
resolve("ok2");
}, 1000);
});
}).then((data) => {
console.log("resolve2: " + data);
});
3.2 Promise异常捕获
Promise对象还提供了catch
方法,用来捕获异常。在介绍catch
前,我们先看看下面的代码:
let promise = new Promise((resolve, reject) => {
throw new Error("an error");
resolve("ok");
}).then((data) => {
console.log("resolve: " + data);
}, (data) => {
console.log("reject: " + data);
});
// 输出:
// reject: Error: an error
我们在Promise中人为抛出了一个异常,但是程序却还是没有中止,而是运行到了reject过程中去了。
这是因为Promise默认会捕获其操作过程中的异常,如果有异常发生,其状态就会自动变成rejected
,还记得前面说过Promise状态一旦确定就不会再改变了吧,所以即便后面的resolve("ok");
执行了,也不会改变promise状态(事实上throw语句后的代码并没有机会执行)。
那么,假如我们没有写reject回调函数会怎么样了?看看下面的代码:
let promise = new Promise((resolve, reject) => {
throw new Error("an error");
resolve("ok");
}).then((data) => {
console.log("resolve: " + data);
});
上面的代码中由于没有指定异常处理函数,所以程序抛出了异常信息,中止执行了。
另外,Promise的异常是会一直向下传递的,直到最后有人处理,如果始终没人处理,程序就会抛出异常信息,然后中止:
let promise = new Promise((resolve, reject) => {
throw new Error("an error");
resolve("ok");
}).then((data) => {
console.log("resolve1: " + data);
}).then((data) => {
console.log("resolve2: " + data);
}, (data) => {
console.log("reject2: " + data);
});
// 输出:
// reject: Error: an error
上面的代码中,第一个then没有处理异常,异常向下传递给第二个then, 第二个then处理了该异常,程序继续运行。
现在理解Promise.catch()
方法就容易多了。catch()
方法其实就是.then(null, rejectiion)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
我们一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。
3.3 async, await
async 是ES7
才有的与异步操作有关的关键字,需要和Promise
配合使用,async
函数返回一个 Promise
对象,可以使用then
方法添加回调函数:
async function helloAsync() {
return "helloAsync";
}
console.log(helloAsync())
helloAsync().then(v => {
console.log(v);
})
// 输出:
// Promise { 'helloAsync' }
// helloAsync
await
关键字只
能用在被async
标记的函数体内,async
函数执行时,如果遇到await
就会先暂停执行,等到触发的异步操作完成后,恢复async
函数的执行并返回解析值。
function testAwait() {
return new Promise((resolve) => {
setTimeout(function () {
console.log("testAwait");
resolve();
}, 1000);
});
}
async function helloAsync() {
await testAwait();
console.log("helloAsync");
}
helloAsync();
// 输出:
// testAwait
// helloAsync