目标
了解 JS 基础语法
了解掌握数学计算相关Api
了解掌握数组相关Api
了解对象相关知识点
掌握箭头函数的用法
掌握变量传递的原理
了解异步编程的基础
数学计算
Math 是一个内置对象,它拥有一些数学常数和数学函数方法,这里我们介绍几个常用的API。
PI
console.log(Math.PI) // 3.141592653589793
圆周率常数,可以直接引用
floor
返回小于一个数的最大整数
console.log(Math.floor(3.1)) // 3 console.log(Math.floor(3)) // 3
ceil
返回大于一个数的最小整数
console.log(Math.ceil(3)) // 3 console.log(Math.ceil(3.1)) // 4
round
返回四舍五入后的整数,需要注意,js的round有点不同,它并不总是舍入到远离0的方向,特别是负数的小数部分恰好等于0.5的情况下。
Math.round(3.49) // 3 Math.round(3.5) // 4 Math.round(-3.5) // -3 Math.round(-3.51) // -4
trunc
返回一个数的整数部分,直接去除小数点之后的部分,传入的参数会被隐式转换为数字类型
Math.trunc(3.1) // 3 Math.trunc(0.5) // 0 Math.trunc('-1.2') // -
random
该函数返回一个浮点数,伪随机数的范围从0到1,也就是说大于等于0,小于1,我们可以以此为种子,扩展到自己想要生成的随机数的范围,比如下面的例子,可以让我们生成指定范围的随机整数。
function random(min, max) { return Math.floor(Math.random() * (max - min) + min) }
数组操作
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array
数组是js中非常重要的数据结构,需要熟练掌握基本的数组操作方法。
遍历数组
JS里面有很多种遍历数组的方法,这里我们介绍几种常用的,有一些方法只是对数组进行读取,还有一些会直接改变原数组,这个需要注意区分。
let arr = [1, 2, 3, 4, 5] for (let item of arr) { console.log(item) // item为正在处理的当前元素 } arr.forEach((item, index) => { console.log(item, index) // item为正在处理的当前元素,index为索引值 })
需要注意的是,forEach 与 for..of 不同,除了抛出异常之外,没有办法中止或者跳出 forEach 循环。
filter
该方法创建一个新数组,将所有在过滤函数中返回 true 的数组元素放进一个新数组中并返回。
let words = ['spray', 'limit', 'elite', 'exuberant', 'destruction'] let result = words.filter(word => word.length > 6) console.log(result) // [ 'exuberant', 'destruction' ]
map
返回一个由回调函数的返回值组成的新数组。
let arr = [1, 2, 3]let tpl = arr.map(item => `<span>${item</span>`)console.log(tpl) // [ '<span>1</span>', '<span>2</span>','<span>3</span>' ]
reduce
从左到右为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次的回调函数,并返回最后一次回调函数的返回值。
let arr = [1, 2, 3]let sum = arr.reduce((previous, current, index) => { console.log(previous, current, index) return previous + current})console.log(sum) // 6
上面的代码中的回调函数会执行两次,让我们看一下 reduce 是如何运行的
callback previous current index return
第1次 1 2 1 3
第2次 3 3 2 6
previous 是上一次回调函数的返回值,current 是当前要处理的数值,我们可以利用 reduce 来改写上一个 map 中的例子,将返回的html数组拼接起来合并成一个字符串
let arr = [1, 2, 3]let tpl = arr.reduce((prev, curr) => prev + `<span>${curr}</span>`, '')console.log(tpl) // '<span>1</span><span>2</span><span>3</span>'
注意,上面的代码我们给 reduce 添加了第二个可选参数,是一个空字符串,作为第一次迭代的初始数值
find
找到第一个满足测试函数的元素并返回那个元素的值,如果找不到,则返回 undefined。
let arr = [1, 2, 3, 4, 5] let found = arr.find(item => item > 3) console.log(found) // 4
findIndex
找到第一个满足测试函数的元素并返回那个元素的索引,如果找不到,则返回 -1。
let arr = [1, 2, 3, 4, 5] let index = arr.findIndex(item => item > 3) console.log(index) // 3
includes
判断当前数组是否包含某指定的值,如果是返回 true,否则返回 false。
let arr = [1, 2, 3, 4, 5] console.log(arr.includes(3)) // true console.log(arr.includes('2')) // false
indexOf
返回数组中第一个与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。
let arr = [1, 2, 3, 4, 5] let index = arr.indexOf(4) console.log(index) // 3
join
连接所有数组元素组成一个字符串。
let arr = [1, 2, 3, 4, 5]console.log(arr.join('')) // '12345'console.log(arr.join('-')) // '1-2-3-4-5'
concat
用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
let arr1 = [1, 2, 3]let arr2 = [4, 5]let arr3 = arr1.concat(arr2)console.log(arr3) // [ 1, 2, 3, 4, 5 ]
slice
抽取当前数组中的一段元素组合成一个新数组,这是一个原数组的浅拷贝,原始数组不会被改变。
let arr = [1, 2, 3, 4, 5]console.log(arr.slice(2)) // [ 3, 4, 5 ]console.log(arr.slice(1, 3)) // [ 2, 3 ]
splice
在任意的位置给数组添加或删除任意个元素。这个很容易和 slice 搞混,此方法会改变原数组。
插入元素:
let arr = [1, 2, 3, 4, 5]arr.splice(2, 0, 6)console.log(arr) // [ 1, 2, 6, 3, 4, 5 ]
删除元素:
let arr = [1, 2, 3, 4, 5]let item = arr.splice(1, 2)console.log(item) // [ 2, 3 ]console.log(arr) // [ 1, 4, 5 ]
删除元素的同时插入两个元素:
let arr = [1, 2, 3, 4, 5]let item = arr.splice(1, 2, 6, 7)console.log(item) // [ 2, 3 ]console.log(arr) // [ 1, 6, 7, 4, 5 ]
reverse
颠倒数组中元素的排列顺序,即原先的第一个变为最后一个,原先的最后一个变为第一个,该方法会改变原数组。
let arr = [1, 2, 3, 4, 5] arr.reverse() console.log(arr) // [ 5, 4, 3, 2, 1 ],原数组被改变
push
在数组的末尾增加一个或多个元素,并返回数组的新长度。
let arr = [1, 2, 3, 4, 5] console.log(arr.push(6)) // 6 console.log(arr.push(7, 8)) // 8 console.log(arr) // [ 1, 2, 3, 4, 5, 6, 7, 8 ]
pop
删除数组的最后一个元素,并返回这个元素。
let arr = [1, 2, 3, 4, 5] let item = arr.pop() console.log(item) // 5 console.log(arr) // [ 1, 2, 3, 4 ]
unshift
在数组的开头增加一个或多个元素,并返回数组的新长度,与 push 对应
let arr = [1, 2, 3, 4, 5] console.log(arr.unshift(6)) // 6 console.log(arr.unshift(7, 8)) // 8 console.log(arr) // [ 7, 8, 6, 1, 2, 3, 4, 5 ]
shift
删除数组的第一个元素,并返回这个元素,与 pop 对应
let arr = [1, 2, 3, 4, 5]console.log(arr.shift()) // 1console.log(arr) // [ 2, 3, 4, 5 ]
push、pop、unshift、shift 这几个方法组合起来,可以用来实现栈、队列的功能
sort
方法用用于对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的
let arr = ['b', 'd', 'a', 'c']arr.sort()console.log(arr)
我们可以通过传入比较函数,来自定义排序逻辑,比较函数会每次传入两个要比较的值 a 和 b,如果函数返回小于0,那么 a 会排列到 b 的前面,称为升序排列,如果大于0,则会排到后面,称为降序排列,如果等于0,则相对位置不变(并非标准行为)
let arr = [3, 5, 1, 4, 2]arr.sort((a, b) => { if (a < b) return -1 if (a > b) return 1 return 0})console.log(arr) // [ 1, 2, 3, 4, 5 ],升序arr.sort((a, b) => { if (a < b) return 1 if (a > b) return -1 return 0})console.log(arr) // [ 5, 4, 3, 2, 1 ],降序
练习
自我练习
对象操作
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array
遍历对象
遍历对象我们常用的是 for...in 语句,例如:
let obj = { a: 1, b: 2, c: 3 }for (let key in obj) { console.log(key, obj[key])}
上面的代码会依次输出:
a 1b 2c 3
keys
返回一个包含所有给定对象自身可枚举属性名称的数组。
let obj = { a: 1, b: 2, c: 3 }console.log(Object.keys(obj)) // [ 'a', 'b', 'c' ]
values
返回给定对象自身可枚举值的数组。
let obj = { a: 1, b: 2, c: 3 }console.log(Object.values(obj)) // [ 1, 2, 3 ]
箭头函数
ES6允许使用箭头 => 来定义函数,例如:
let func = num => num + 1
相当于
let func = function(num) { return num + 1}
可以看到,代码简洁了很多,如果箭头函数不需要参数或者需要多个参数,就使用一个圆括号代表参数部分
let a = () => 5// 相当于let b = function() { return 5 }let c = (num1, num2) => num1 + num2// 相当于let d = function(num1, num2) { return num1 + num2}
如果箭头函数有多条语句,需要用大括号括起来,并且使用 return 语句返回
let sum = (num1, num2) => { let num = num1 + num2 return num}
箭头函数也可以直接返回一个对象,但是因为大括号会被当成代码块来执行,所以外面要加上小括号
let func = name => ({ name })console.log(func('Frank')) // { name: 'Frank' }
箭头函数最常用的应用场景是简化回调函数,例如
let arr = [1, 2, 3, 4, 5]let result = arr.map(item => item * 2)
等同于
let arr = [1, 2, 3, 4, 5]let result = arr.map(function(item) { return item * 2})
箭头函数有几个特性是和普通函数不同的,需要额外注意
箭头函数没有自己的 this,而是引用外层的 this
class Test { constructor() { this.num = 10 } calcOne(arr) { // 这里的this指向的是calcOne所在的对象 return arr.map(item => item * this.num) } calcTwo(arr) { let _this = this return arr.map(function(item) { // function的this发生变化,不能直接引用到外部的this return item * _this.num }) }}let test = new Test()let arr = [1, 2, 3]console.log(test.calcOne(arr))console.log(test.calcTwo(arr))
箭头函数不能当作构造函数,不可以使用 new 命令
箭头函数没有 arguments
Set
ES6提供了新的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。
let set = new Set()set.add(1) // Set(1) { 1 }set.add(5) // Set(2) { 1, 5 }set.add(5) // Set(2) { 1, 5 }set = new Set([1, 2, 2, 3]) // 可以直接从一个数组初始化,重复元素会被去除console.log(set) // Set(3) { 1, 2, 3 }let arr = [...set] // 可以将Set展开为一个数组console.log(arr) // [ 1, 2, 3 ]// 迭代Setfor (let item of set) { console.log(item)}set.delete(2) // 元素存在,返回 trueset.has(1) // 1在set中存在,返回 true
Set 特别适合用于存放唯一值的场景,例如我们需要记录当前已经登录的所有用户名,就可以直接使用 Set 来存放。
Map
ES6提供了 Map 数据结构,它类似于的对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值都可以当作键,也就是说,Object 提供了“字符串-值”的对应,Map 提供了“值-值”的对应,是一种更完善的Hash结构。
let obj = { a: 1 }let map = new Map()map.set('name', 'Frank')map.set(111, 123)map.set(obj, { b: 2 })console.log(map.keys()) console.log(map.has(obj)) // trueconsole.log(map.delete(obj)) // trueconsole.log(map.size) // 2
Map 的遍历会复杂一些,它提供了几个迭代器可供我们使用
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回所有成员的遍历器
forEach():遍历所有成员
let map = new Map()map.set('age', 21)map.set('name', 'Frank')// 使用keys()遍历for (let key of map.keys()) { console.log(key, map.get(key))}// 'age' 21// 'name' 'Frank'// 使用values()遍历for (let value of map.values()) { console.log(value) }// 21// 'Frank'// 使用entries()遍历for (let item of map.entries()) { // entries()返回的item是一个数组,结构为:[key, value] console.log(item[0], items[1])}// 'age' 21// 'name' 'Frank'// 使用forEach()遍历map.forEach((value, key) => { console.log(key, value)})// 'age' 21// 'name' 'Frank'
我们还可以使用扩展运算符 ... 来将 Map 展开为数组结构
let map = new Map()map.set('age', 21)map.set('name','Frank')console.log([...map.keys()]) // [ 'age', 'name' ]console.log([...map.values()]) // [ 21, 'Frank' ]console.log([...map.entries()]) // [ ['age', 21], ['name', 'Frank'] ]
练习
自我练习,了解相关内容
变量传递
JS中的数据类型我们可以分为两种,弄清楚两者的区别非常重要:
值类型(基本类型):String、Number、Boolean、null、undefined
引用类型:Array、Object、Set、Map等由多个值构成的复杂类型
基本类型的变量保存的是变量值,引用类型变量保存的是内存地址,基本类型在赋值的时候拷贝值,引用类型在赋值的时候只拷贝内存地址,不拷贝值,多个引用指向同一块内存数据,下面我们通过一些例子来体验一下:
let num1 = 1let num2 = num1 // num1的值会被拷贝给num2,两者互相独立num2 = 2 // 改变num2不会影响num1console.log(num1, num2) // 1 2function add(num) { num += 1 // 这里的num也是按值传递的 return num}let num3 = 3let num4 = add(num3) // 这里传递的是num3的值console.log(num3, num4) // 3 4let arr1 = [1, 2]let arr2 = arr1 // arr1的地址会被拷贝给arr2,两者指向的是同一块内存数据arr1.push(3) // 改变arr1,arr2也会一块改变console.log(arr2) // [ 1, 2, 3 ]arr2 = [] // arr2被指向了一个新的内存地址,对arr1不产生影响console.log(arr2) // [ ]console.log(arr1) // [ 1, 2, 3 ]function update(user) { user.age += 1 // user是一个对象,传进来的是地址,对user修改会影响原数据}let user1 = { age: 18 }update(user1) // 传入的是user1的地址console.log(user1) // { age: 19 }let arr3 = [{ age: 18 }, { age: 24 }]let arr4 = arr3.slice(1)arr4[0].age += 1console.log(arr3) // [{ age: 18 }, { age: 25 }],slice虽然返回的是一个新数组,但是里面的子元素对象指向的仍然是原始的数据地址
异步编程
异步编程模型是JavaScript的精髓,分我们来简单了解一下它的基本原理。
事件循环
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务,这个模型与其他一些常见语言中的模型截然不同,比如C或者Java。
一个JavaScript运行时包含了一个待处理消息的消息队列,每一个消息都关联着一个用以处理这个消息的回调函数。从事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息,被处理的消息会被移除队列,并作为输入参数来调用之前关联的回调函数。函数的处理会一直进行到栈再次为空为止,然后事件循环将会处理队列中的下一个消息。
之所以被称为事件循环,是因为它经常按照类似如下的方式来实现:
while (queue.waitForMessage()) { queue.processNextMessage()}
queue.waitForMessage() 会同步地等待消息到达(如果当前没有任何消息等待被处理)
每一个消息完整地执行后,其他消息才会被执行,这为程序的分析提供了一些优秀的特性,例如当一个函数被执行时,它不会被抢占,只有在运行完毕之后才会去运行其他代码,才能修改这个函数操作的数据。这个模型的另一个缺点在于当一个消息需要太长时间处理完毕时,js就无法处理其他任务比如浏览器用户的交互,此时浏览器一般会弹出“脚本运行时间过长”的对话框,一个良好的编程习惯是缩短单个消息的处理时间。
从上面的描述我们可以得知,JavaScript中的调用会被推入消息队列等待事件循环来处理,setTimeout 可以让我们实现延迟的效果,它接受两个参数,待加入队列的消息和一个时间值,这个时间代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟事件过去之后,消息会被马上处理,但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完,因此第二个参数仅仅表示最小的延迟时间,而并非准确的等待时间,看下面的例子:
let start = Date.now() console.log('start') setTimeout(() => { console.log(Date.now() - start) }, 500) while(true) { if (Date.now() - start >= 2000) { console.log('looped for 2 seconds') break } }
通过下面的例子来体验 setTimeout 的执行机制
console.log('1.这是开始')setTimeout(() => { console.log('2.这是来自第一个回调的消息');})console.log('3.这是一条消息')setTimeout(() => { console.log('4.这是来自第二个回调的消息')}, 0)console.log('5.这是结束')
1
JavaScript的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个数据库查询返回或者一个网络请求返回时,它仍然可以处理其它事情,比如用户输入。这种特点也带来一些麻烦,那就是正常的代码执行顺序被打乱,如果后续的代码需要依赖异步请求的结果,那只能将逻辑放到回调函数中,如果有多层依赖,那么就会出现回调嵌套,造成回调地狱,类似下面的代码,我们伪造一个用来异步计算面积的方法:
console.log(1) asyncArea(data_1, result_1 => { console.log(2) asyncArea(result_1, result_2 => { console.log(3) asyncArea(result_2, result_3 => { console.log(4) console.log(result_3) }) }) }) console.log(5)
Promise
Promise 就是用来解决异步回调问题而出现的解决方案。它代表了一个异步操作的最终完成或者失败,本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。它让你能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。
一个 Promise 必然处于以下几种状态之一:
待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
已兑现(fulfilled): 意味着操作成功完成。
已拒绝(rejected): 意味着操作失败。
下面我们用 Promise 和 setTimeout 来封装之前演示用的异步请求面积的方法
function asyncArea(length) { return new Promise((resolve, reject) => { // 模拟异步请求 setTimeout(() => { if (length > 0) resolve(length * length) else reject(new Error(`invalid length: ${length}`)) }, 500) }) }
上面这个方法接受一个数字类型的参数 length,通过 setTimeout 来模拟异步请求,500ms后,如果 length大于等于0,则 resolve 返回 length 的平方,否则 reject 返回错误信息,通过下面的例子我们来看一下具体的调用方法。
// 正常调用 asyncArea(1).then(result => { console.log(result) }).catch(error => { console.trace(error) }) // 触发异常 asyncArea(-1).then(result => { console.log(result) }).catch(error => { console.trace(error) }) // 链式调用 let start = Date.now() asyncArea(1) // 返回了一个新的Promise,可以在下一个then中获取,result === 1 .then(result => asyncArea(result + 1)) // result === 4 .then(result => asyncArea(result + 1)) .then(result => { // 前一个then返回的新Promise,result === 25 console.log(result) // 耗时是三个请求的累加 console.log(`cost ${Date.now() - start}ms`) })
通过上面的例子我们可以看到,前面回调嵌套的问题,通过 Promise 的链式调用写法得到很大的缓解。
上面的链式调用适用于需要串行计算的场景,下一步的请求需要依赖上一步的结果,总的耗时是每个请求的累加。有时候我们的多个异步请求是没有相互依赖的,此时如果串行计算的话会增加无谓的耗时,Promise 有一个 all 方法,可以批量并行执行异步请求,等所有的请求都结束后再统一返回,可以简单认为总的耗时时间是所有请求中耗时最大的那一个。
let start = Date.now() Promise.all([ asyncArea(1), asyncArea(2), asyncArea(3), asyncArea(4), asyncArea(5) ]).then(result => { console.log(result) console.log(`cost ${Date.now() - start}ms`) })
async/await
Promise 通过 then 来进行异步请求虽然改善了回调的问题,但还是不够优雅,好在现在我们可以通过 async/await 语法,使用串行的语法进行异步调用,下面我们来改写一下上面的例子:
async function test_1() { let start = Date.now() let result_1 = await asyncArea(1) let result_2 = await asyncArea(result_1 + 1) let result_3 = await asyncArea(result_2 + 1) console.log('test_1', result_3) console.log(`cost ${Date.now() - start}ms`) } test_1() async function test_2() { let start = Date.now() let result = await Promise.all([ asyncArea(1), asyncArea(2), asyncArea(3), asyncArea(4), asyncArea(5) ]) console.log('test_2', result) console.log(`cost ${Date.now() - start}ms`) } test_2()
使用 async/await 改写之后,我们的异步请求更加优雅,变得更接近符合我们习惯的串行代码,它有以下特点需要注意。
await 只能出现在 async 修饰的函数中,普通函数中无效 async 函数隐式返回一个 Promise 对象,最后 return 的返回值,相当于 Promise 中 resolve 的值,所以可以认为 async 函数是 Promise 的语法糖 await 后面的函数请求需要返回 Promise,因为 async 返回的也是 Promise,所有也可以 await 一个 async 函数 await 需要等待后面的 Promise 返回结果(resolve)之后,才会继续执行后面的代码 async 会将一个普通函数变成异步函数,类似 setTimeout 的效果,下面我们来对比一下差异 async function test() { let result = await Promise.all([ asyncArea(1), asyncArea(2), asyncArea(3), asyncArea(4), asyncArea(5) ]) return result } async function run() { console.log(1) // async函数返回的是一个Promise,可以直接用then来获取结果 test().then(result => console.log(2, result)) console.log(3) // await后面可以跟一个async函数 let result = await test() console.log(4) } run()
async异常捕获
我们推荐在异步请求中尽可能都使用 async/await,await 等待的 Promise 如果 reject 了一个错误的话,可以被 try/catch 捕获到,看下面的例子:
async function run() { try { // await异步请求抛出错误,会被catch住 let result = await asyncArea(-1) console.log(result) } catch (error) { console.log(error.message) } try { // 没有await的异步请求,异常无法被catch asyncArea(-1) } catch (error) { console.log(error.message) } try { // 没有await的异步请求,通过Promise的catch也可以捕获异常,不会继续抛出 asyncArea(-1).catch(error => { console.log(1, error.message) }) } catch (error) { console.log(2, error.message) } } run()