补充:慕课 前端技术面 第七章 笔记总结
异步与单线程
同步和异步的区别,分别举例子
- 异步是基于JS是单线程语言而出现的
- 同步任务会阻塞代码执行,而异步不会
- alert是同步,setTimeout是异步
手写Promise加载一张图片
1 | // 加载一张图片 |
Promise加载2张图片(解决回调地域)
1 | // 加载2张图片,即使用Promise解决回调地域 |
async/await彻底解决回调地狱
- 背景:
- 异步回调会造成callback hell(回调地狱)
- Promise then catch链式调用可解决回调地狱,但也是基于回调函数
- async/await是同步语法,彻底消灭回调函数
- 用同步的方式编写异步:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// 依旧是用loadImg()返回Promise实例
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`))
}
img.src = src
})
return promise
}
// 注意:await 必须放在 async 函数中,否则会报错
(async function () {
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
try {
// 加载第一张图片
const img1 = await loadImg(src1)
console.log(img1.width, img1.height) // 252,144
// 加载第二张图片
const img2 = await loadImg(src2)
console.log(img2)
// <img src="https://avatars3.githubusercontent.com/u/9583120">
} catch (ex) {
console.error(ex)
}
})()
关于setTimeout的笔试题1
连续打印1 3 5 2
一秒后打印4
1 | console.log(1); |
注意所有的同步任务在主线程上执行,形成一个执行栈。异步任务有了 运行结果 才会在任务队列中放置一个事件。脚本运行时先依次运行执行栈中的所有同步任务,然后会从任务队列里提取事件,选择需要首先执行的异步任务然后执行。
4先有结果,所以4先出现在任务队列了
前端使用异步的场景有哪些
- 定时任务:setTimeout,setInterval(可参考笔记“setTimeout()与setInterval()”)
- 网络请求:ajax请求,动态
<img>
加载 - 事件绑定:事件函数绑定以后并不会等触发事件函数才继续执行程序,程序照常执行,等需要触发的时候才会执行事件绑定函数。
ajax请求的例子
可参考ajax请求完整笔记
打印“start”、“end”,然后等./data1.json
中的数据请求成功后执行函数-打印获取到的数据data1
。(关于$.get()
可参考笔记)
动态<img>
加载的例子
关于createElement()
可以参考笔记“JS中修改/创建/移动/删除 HTML DOM元素”onload事件
就是callback的一种形式
执行顺序:
打印“start”=》创建一个图片元素img(可参考笔记)=》给img的src赋值,src 属性值更新时浏览器会自动加载并显示出新图像。(可参考笔记) =》图片加载过程中打印“end” =》 等img加载完成才会执行onload绑定的函数,打印loaded
(可参考笔记)
很明显src赋值的过程(图片动态加载)是异步的,不会阻塞end的打印
事件绑定的例子
addEventListener()相关笔记
或者通过object.onclick=function(){SomeJavaScriptCode};
绑定onclick事件
也是一样的。
异步编程的方法
- 回调函数,这是异步编程最基本的方法。上面提到的定时器、ajax请求、图片的动态加载、事件绑定 实际上都是回调函数的形式。
- 接下来的Promisee和async await其实就是回调函数的优化,也是建立在回调函数的基础上实现的异步编程。他们都可以使用链式来解决回调地狱的问题。
- Promises对象,Promises 对象是CommonJS 工作组提出的一种规范,目的是为异步编程提供统一接口。
- 简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。回调函数(回调地狱)变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。
- async,async函数返回的是一个 Promise 对象,可以使用 then 方法添加回调函数,async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。当函数执行的时候,一旦遇到await就会先执行await的相关异步操作,等到异步操作完成,再接着执行函数体内后面的语句(包括同步任务也先等着await执行)。
- 比起Promise,await则不需要then,
- 事件监听,任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
- 例:为f1绑定一个事件,当f1发生click事件,就执行f2。
- 发布/订阅,与”事件监听”类似。
- 我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)。
- 这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
事件循环(JS的执行机制)
- 注意:异步任务被挂起时会由浏览器的引擎去执行他们,而同步任务则是由JS的引擎去执行。
- 可参考笔记从输入url到页面显示都经历了什么:
- 用户输入url
- 浏览器进行DNS解析,将域名解析为IP地址,查找顺序:浏览器自身的DNS缓存中查找=》操作系统hosts文件=》本地DNS服务器=》向根服务器发出请求进行递归查询
- TCP连接,三次握手(请求头中Connection:keep-alive可以使一次TCP连接重复使用):TCP连接请求报文=》TCP连接响应报文=》HTTP请求+TCP确认 报文
- 根据服务端返回的响应报文判断是否有 强缓存(响应头中有Expires或有不为no-cache/no-store的Cache-Control)且在有效期(max-age)内,如有则直接读取缓存,如无则通过服务端返回的响应报文判断是否有 协商缓存(Last-Modified/Etag),如有则带上If-Modified-Since/If-None-Match发出http请求,服务器比对后判断缓存是否可用,如可用则返回304状态码并读取客户端缓存;如比对失败则返回状态码200并向服务端请求资源。如无Last-Modified/Etag则直接向服务端请求资源,最后返回给客户端。【提高性能就要看缓存】
- 浏览器边解析html文档边构建DOM树=》遇到css则同时解析css与html,根据css构建CSSOM(css对象模型)=》合并为渲染树=》回流(浏览器计算元素大小、位置)和重绘(元素的大小、颜色等属性确定后浏览器的绘制过程) 显示在屏幕上。
- 所以css建议放在head中,在DOM树生成之前生成CSSOM,这样DOM树生成时可直接和CSSOM合并成渲染树,不会有存在用户看到状态变化的过程。
- 回流和重绘非常耗性能,尽量减少。
- 解析/渲染过程中如遇JS脚本/连接,则停止渲染(因为JS有可能修改DOM结构)来执行和下载相应的代码,这会造成阻塞,故推荐JS代码应该放在html代码的后面。
- js解析【事件循环】:同步任务放在执行栈中,异步任务有了结果后放在任务队列中,其中宏任务会先于微任务,script也属于宏任务,脚本运行时先清空执行栈,再从任务队列中提取事件依次执行。
- 事件循环:解决JS单线程运行阻塞
- 同步任务和微任务是由js引擎执行,script外的宏任务挂起时是浏览器引擎执行的,因为js引擎处理速度比浏览器引擎快,所以宏任务比微任务执行的更早
- 宏任务:script(整体代码)、setTimeout、setInterval、ajax、DOM 事件
- 除script,其他都是浏览器引擎来执行,因为宏任务都是Web API,不属于ES语法
- 微任务:
- Promise.then()。(注意:Promise的函数体是定义时就立即执行的,then()是微任务)
- async是同步的,调用就立马执行,await这行代码也同步,但其后的代码会被放入 微任务队列 中。(例子)
Date、Math、数组API、对象API
Date、Math、数组API、对象API 的相关题目。
数组API、对象API可以理解为数组、对象他们自带的那些方法。
获取2017-06-10格式的日期
- 1-18 函数 formatDate 接受一个Date对象,返回一个格式化后的日期
- 2-4 为防止报错,如果没有传入参数,就新建一个Date对象作为参数dt
- 5
getFullYear()
获取年份 - 6
getMonth()
获取月份,由于获取到的是0-11,所以需要+1 - 7
getDate()
获取日期 - 8-15 由于我们需要格式化的日期是两位数的,所以当
month<10
的时候我们需要在前面添加0。日期也是同理。- 这里涉及到之前讲的字符串拼接,字符串
"0"
和数字month拼接后相当于**强制类型转换**,month将成为字符串。
- 这里涉及到之前讲的字符串拼接,字符串
- 17 通过
-
连接,将数字拼接字符串 得到 字符串
- 19 新建一个Date对象dt
- 20 将dt传入函数formatDate,得到格式化后的日期formatDate
- 21 打印格式化后的日期formatDate
获取长度一致的字符串随机数
题目:获取随机数,要求是长度一致的字符串格式
注意:字符串也有slice(),和数组的slice()类似,用于截取字符串
1 获取随机数random,小数部分长度不确定
2 随机数后加上10个0,保证截取的时候有10位(保证不报错)。数字拼接字符串后等于强制类型转换,random 变为字符串
3 使用字符串的slice()截取random的前10位(注意:10不在截取范围)
遍历对象和数组的通用forEach函数
题目:写一个能遍历对象和数组的通用forEach函数
(jquery中有现成的方法,数组本身就有forEach())
- 1-14 定义forEach函数
- 参数1:对象/数组。参数2:函数。
- 注意:我们这里定义的forEach函数的index和item的顺序和数组自己的forEach()是反的
- 3-7 判断传入的是对象/数组,如果是数组,就用数组自己的forEach(),遍历数组后将index和item传入 参数2(fn函数),通过fn函数将index与item的位置调换一下后打印。
- 8-13 如果是对象,就通过
for in
遍历对象属性,并将 属性名称key
和 属性值obj[key]
传给 参数2(fn函数),被调用后会被打印。- 注意:in运算符在 数组 中是判断有无 数组下标 的,不是数组元素。在 对象 中判断有无 属性。返回值是boolean。
- 15 定义一个数组arr
- 17-19 调用我们定义的forEach函数,传入参数1为数组arr,参数2为一个函数,该函数接收两个参数并打印他们。
- 注意:这里的index和item的顺序和数组的forEach()是反的,主要是为了照顾后面对象的
key
、value
- 注意:这里的index和item的顺序和数组的forEach()是反的,主要是为了照顾后面对象的
- 21 定义一个对象obj
- 22-24 让对象obj调用我们定义的forEach函数
- key:属性名称。value:属性值。
如果把后端的计算逻辑放到前端/移动端需要注意什么?
- 性能, 数据一致性, 尤其是同步下来 / 同步上去的数据, 用什么规则合并, 丢弃 / 更新的原则是什么.
- 安全性问题,需要甄别哪些能放哪些不能放,web 前端 js 文件可以修改,而且网络请求还能被抓包和伪造,一些没什么影响的计算逻辑放在前端可以减轻服务器压力,但有些比较重要的计算逻辑还是不要放在前端了
setTimeout()与let、闭包与作用域
1 | for (var i = 0; i < 10; i++) { |
结果:打印10个10
原因:
因为循环是同步的,setTimeout 是异步的,所以循环结束以后才会执行setTimeout。
而变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。所以每一次循环的i其实都是同一个,循环到最后时前面的i全部被最后的结果覆盖了。
所以最后,每次执行setTimeout时到父级作用域(这里是全局作用域)中找到的i都是10,10次打印的都是最后保存的i。
解决方法:局部变量的话就是每循环一次产生一个新的i,就不会被覆盖。所以我们可以通过闭包(也就是把循环的过程放到一个函数中封闭起来)
闭包:函数a内部定义一个函数b,函数b可以访问函数a的变量,函数b中的变量就是局部变量
如何让他输出自增队列
方法1:将var改为let
1 | for (let i = 0; i < 10; i++) { |
变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量。
方法2:使用闭包(即局部变量)
1 | for (var i = 0; i < 10; i++) { |
【将i传进函数中作为 函数作用域 的变量】
i=0
时生成一个函数,i=1
时又生成另外一个函数,总共通过循环创建了10个函数,循环了10次函数就会产生10个局部变量j,最后每个函数中的setTimeout都对应不同的j值。- 那么setTimeout的参数函数去获取i的值时就会到当前的 父级作用域(函数作用域)中找,自然也不会10个标签都找到同一个i了。
方法3:
删除setTimeout,那么每一次循环就立即输出,也就不存在覆盖的问题了。
深拷贝二维数组
1 | var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }] |
递归拷贝所有层级属性
- 注意:对象和数组都会有遍历时需要避开的继承属性,使用for…in需要手动添加hasOwnProperty()来判断对象是否包含特定的自身(非继承)属性,对数组使用for…of迭代时则不需要判断。
- 不使用for…of的原因: 数组元素内为不可迭代的对象时会报错。
- 复习:
typeof
将引用类型(数组、对象、函数)都判断为object
。for...in
主要用于对 数组元素/对象属性 进行遍历。for ... in
循环中的代码每执行一次,就会对数组的元素或者对象的属性进行一次操作。for(key in obj)
中,当obj为对象时,key是属性名。当obj为数组时,key是数组下标。(不是数组元素,for…of才是数组元素,且of只能迭代有顺序的数组等,不能遍历对象)
- Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/**
* 深拷贝方法1
* @param {Object} obj 要拷贝的对象
*/
var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }]
function deepCopy(obj) {
// 如遇值类型则直接返回,否则继续执行
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 初始化返回结果
let objClone = [];
// 遍历数组元素
for (key in obj) {
// 筛选对象自身属性
if (obj.hasOwnProperty(key)) {
// 递归调用deepClone()
objClone[key] = deepClone(obj[key])
}
}
// 返回结果
return objClone;
}
var objClone = deepCopy(target);
objClone[3][0] = 5;
console.log(target);
console.log(objClone);
1 | /** |
不能通过JSON对象
- 原理:不拷贝引用对象,使用
stringify()
拷贝一个 JSON字符串 会新辟一个新的存储地址,这样就切断了引用对象的指针联系。 - 缺点:无法实现对 数组/对象中 方法 的深拷贝,会显示为undefined。(ES6提供了
object.assign()
可解决这个问题,可参考这篇博客)
1 | var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }] |