问题集合(1)

问题1 为什么async1 success async1 end没有打印

async function async1() { console.log('async1 start') await new Promise(resolve => { console.log('promise1') }) console.log('async1 success') return 'async1 end' } console.log('srcipt start') async1().then(res => console.log(res)) console.log('srcipt end')

打印结果问题集合(1)
文章图片

分析:有些容易让人产生疑惑的代码,这里的resolve没有执行,promise状态一直保持在pending状态
问题2 实现Scheduler类及add方法
class Scheduler { add(promiseCreator) { // 补充··· } } const timeout = (time) => new Promise((resolve) => { setTimeout(resolve, time); }); const scheduler = new Scheduler(); const addTask = (time, order) => { scheduler.add(() => timeout(time)).then(() => console.log(order)); }; addTask(1000, "1"); addTask(500, "2"); addTask(300, "3"); addTask(400, "4"); // output: 2 3 1 4 // 一开始,1、2两个任务进入队列 // 500ms时,2完成,输出2,任务3进队 // 800ms时,3完成,输出3,任务4进队 // 1000ms时,1完成,输出1 // 1200ms时,4完成,输出4

分析:要实现一个带并发限制的异步调度器,保证同时最多运行2个任务。这里的timeout方法和add方法都返回的是promise。
需要两个队列,一个存储待执行任务,一个存储正在运行的任务。当执行队列小于2就直接执行任务,否则加入待执行队列,执行函数中执行任务后要从执行队列移除。
class Scheduler { constructor() { this.pending = []; // 待执行 this.excute = []; // 正在运行 }add(promiseCreator) { return new Promise((resolve, reject) => { promiseCreator.resolve = resolve; if (this.excute.length < 2) { this.run(promiseCreator); } else { this.pending.push(promiseCreator); } }); }run(promiseCreator) { this.excute.push(promiseCreator); promiseCreator().then(() => { promiseCreator.resolve(); this.remove(promiseCreator); if (this.pending.length) { this.run(this.pending.shift()); } }); } remove(promiseCreator) { let index = this.excute.findIndex((item) => item === promiseCreator); index !== -1 && this.excute.splice(index, 1); } }

问题3 require查找包过程
分析:require加载文件分几种类型
1,内置模块(http,fs)
2,./ ../打头文件绝对路径文件,
3,第三方包
问题集合(1)
文章图片

查找包时缓存优先,优先重缓存加载,第三方包名不会和核心模块名重复
1,判断是否是原生模块
2,非原生模块会判断是否有路径标识
3,第三方包先找到当前文件所在目录中的node_module文件夹(npm出来的第三方),找到node_module文件夹中对应加载模块名的文件夹,找到package.json文件中的 main 这个属性(main属性记录了 第三方包的入口模块)。如果文件package.json不存在或者main属性没有值,require会默认加载该包中的 index 文件(index 为默认被选项),如果加载文件中的当前目录中没有node_module文件夹的话,require就会查找上一级目录中是否有node_module文件夹,如果有,继续执行以上操作,如果没有在到上上一级查找,直到找到磁盘根目录好找不到就报错
can not find module xxx.
问题4 如何取消异步请求
分析:
1,ajax取消请求使用abort()方法
2,fetch请求
JavaScript 规范中添加了新的 AbortController,允许开发人员使用信号中止一个或多个 fetch 调用。
  • 创建一个 AbortController 实例
  • 该实例具有 signal 属性
  • 将 signal 传递给 fetch option 的 signal
  • 调用 AbortController 的 abort 属性来取消所有使用该信号的 fetch。
    const abortController = new AbortController(); const { signal } = abortController; fetch(url, { signal }) .then((res) => res.json()) .then((res) => { // handle }) .catch((e) => { if (e.name === "AbortError") { // abort } }); setTimeout(() => { abortController.abort(); }, 1000);

问题5 箭头函数不适用场景
1,对象方法
2,原型挂载方法
3,构造函数(实例化时会提示不是构造函数)
4,动态上下文中的回调函数。(如addEventListener中回调要用到this)
5,Vue生命周期和method
问题6 HTMLCollection和NodeList区别
  • HTMLCollection是Element的集合,NodeList是的节点集合
  • 获取Node和Element返回结果可能不一样(elem.childNodes和elem.children不一样)
  • 前者会包含Text和comment节点,后者不会
问题7 Js严格模式特点
  • 全局变量必须先声明
  • 禁止使用with
  • 创建eval作用域
  • 禁止this指向window
  • 函数参数不能重名
问题8 for和forEach谁更快
for更快,forEach每次都要创建一个函数来调用,for不会,函数需要独立作用域,会有额外的开销
问题9 Node.js如何创建子进程
开启子进程child_process.forkcluster.fork
使用sendon传递消息
child_process.fork
--compute.js function getSum() { let sum = 0 for (let i = 0; i < 10000; i++) { sum += i } return sum }process.on('message', data => { console.log('子进程 id', process.pid) console.log('子进程接受到的信息: ', data)const sum = getSum()// 发送消息给主进程 process.send(sum) })

---process-fork const http = require('http') const fork = require('child_process').forkconst server = http.createServer((req, res) => { if (req.url === '/get-sum') { console.info('主进程 id', process.pid)// 开启子进程 const computeProcess = fork('./compute.js') computeProcess.send('开始计算')computeProcess.on('message', data => { console.info('主进程接受到的信息:', data) res.end('sum is ' + data) })computeProcess.on('close', () => { console.info('子进程因报错而退出') computeProcess.kill() res.end('error') }) } }) server.listen(3000, () => { console.info('localhost: 3000') })

cluster.fork
问题10 js-bridge实现原理
实现方式
1,注册全局变量(适合获取简单信息,不适合异步)
2,URL Scheme(适合所有情况)
封装一个简易的js-bridge
const sdk = { invoke(url, data = https://www.it610.com/article/{}, onSuccess, onErr) { const iframe = document.createElement("iframe"); iframe.style.visibility = "hidden"; document.body.append(iframe); iframe.onload = () => { const content = iframe.contentWindow.document.body.innerHTML; onSuccess(JSON.parse(content)); iframe.remove(); }; iframe.onErr = () => { onErr(); iframe.remove(); }; iframe.src = https://www.it610.com/article/`my-app-name://${url}?data=${JSON.stringify(data)}`; }, // 能力一 fn1(data, onSuccess, onErr) { this.invoke("api/fn1", data, onSuccess, onErr); }, // 能力二 fn1(data, onSuccess, onErr) { this.invoke("api/fn1", data, onSuccess, onErr); }, // 能力三 fn1(data, onSuccess, onErr) { this.invoke("api/fn1", data, onSuccess, onErr); }, };

问题11 requestAnimationFrame requestIdleCallback 是宏任务还是微任务?
宏任务,需要等待DOM渲染完成才执行
问题12 移动端H5点击有300ms延迟,如何解决?
早期使用了FastClick库,监听touchend事件(touchstart touchend 会先于click触发)
使用自定义DOM事件模拟一个click事件,把默认click事件(300ms之后触发)禁止掉
window.addEventListener('load',function(){ FastClick.attach(document.body) },false)

后期meta标签进行了改进,width=device-width属性解决了问题

问题13 token和cookie区别?
cookie
  • HTTP无状态,每次请求都要带cookie,以帮助识别身份
  • 服务端也可以向客户端set-cookie,cookie大小限制为4kb
  • 默认有跨域限制:不可跨域共享,传递cookie(ajax请求配置withCredentials,var xhr = new XMLHttpRequest() xhr.withCredentials = true 可以设置允许跨域携带cookie。fetch配置credentials。omit: 默认值,忽略cookie的发送 same-origin: 表示cookie只能同域发送,不能跨域发送。include: cookie既可以同域发送,也可以跨域发送)
  • HTML5之前cookie常被用于本地存储,HTML5之后常用localStorage和sessionStorage
session优缺点
  • 用户信息存储在服务端,可快速封禁某个用户
  • 占用服务端内存,硬件成本高
  • 多进程,多服务器时,不好同步——需要用到第三方如Redis缓存
  • 默认有跨域限制
cookie和session
  • cookie用于登录验证,存储用户标识(如userId)
  • session存在服务端,存储用户相信信息,和cookie信息一一对应
  • cookie+session是常见登录验证解决方案
token vs cookie
  • cookie是HTTP规范,跨域限制,配合session使用,token是自定义传递
  • cookie默认会被浏览器存储,token需要自己存储
  • token默认也有跨域限制,用于JWT(Json Web Token)
JWT优缺点
  • 不占用服务端内存
  • 多进程,多服务器不受影响
  • 用户信息存储在客户端,无法快速封禁某用户
  • 服务端秘钥被泄露,则用户信息全部丢失
  • token体积一般大于cookie,会增加请求数据量
问题14 HTTP1.0 1.1 2.0区别
HTTP1.0
最基础HTTP请求,支持基本GET POST方法
HTTP1.1
  • 缓存策略cache-control E-tag等,支持长连接Connection:keep-alive,一次TCP连接多次请求
  • 断点续传,状态码206
  • 支持PUT DELETE等,可用于Restful API
HTTP2.0
  • 可压缩header,减少体积
  • 多路复用,一次TCP连接中可以多个HTTP并行请求
  • 服务端推送
问题15 script中defer和async区别
默认情况下HTML暂停解析,下载js,执行js,再继续解析HTML
defer:HTML继续解析,并行下载JS,HTML解析完之后再执行JS
async:HTML继续解析,并行下载JS,执行JS,再解析HTML
问题集合(1)
文章图片


问题16 prefetch和dns-prefetch有什么区别
preload资源在当前页面使用,会优先加载
prefetch资源在未来页面使用,空闲时加载
dns-prefetch即DNS预查询
preconnect即DNS预连接

问题17 从URL输入到页面显示的完整过程
网络请求
  • DNS查询(得到IP),建立TCP连接(三次握手)
  • 浏览器发起HTTP请求
  • 收到请求回应,得到HTML源码
  • 解析HTML过程中,遇到静态资源还会继续发起网络请求
  • JS CSS 图片 视频等(静态资源如果有强缓存,此时不必请求)
    解析
  • 字符串->结构化数据
  • HTML构建DOM树
  • CSS构建CSSDOM树(style tree)
  • 两者结合,形成render tree
  • 解析过程很复杂
  • CSS可能来自
  • js可能内嵌或者外链,还有defer async逻辑
  • img可能内嵌(base64),可能外链
渲染
  • 计算各个DOM的尺寸,定位,最后绘制到页面
  • 遇到JS可能会执行(参考defer async)
  • 异步CSS,图片加载,可能会触发重新渲染
问题17 重绘repaint和重拍reflow区别?
重绘,元素外观改变,如颜色,背景,但是位置不变,重排会重新计算尺寸和布局,可能会影响其他元素位置
问题18 如何实现网页多标签tab通讯
localStorage实现
-- a.html const btn1 = document.getElementById('btn1') btn1.addEventListener('click', () => { const newInfo = { id: 100, name: '标题' + Date.now() } localStorage.setItem('changeInfo', JSON.stringify(newInfo)) })-- b.html监听 window.addEventListener('storage', event => { console.info('key', event.key) console.info('value', event.newValue) })

SharedWorker WebSocket也可以。SharedWorker 兼容性不是很好,比如safari,IE兼容性不太好
问题19 遍历DOM树
深度优先遍历,递归和非递归版
function visitNode(n: Node) { if (n instanceof Comment) { console.log("注释节点", n.textContent); } if (n instanceof Text) { const t = n.textContent?.trim(); if (t) { console.log("文本节点", t); } } if (n instanceof HTMLElement) { console.log("元素节点", `<${n.tagName.toLowerCase()}>`); } }function depthFirstTraverse(root: Node) { visitNode(root); const childNodes = root.childNodes; // .childNodes .chldren不一样,.chldren只获取元素,.childNodes有text和comment if (childNodes.length) { childNodes.forEach((child) => { depthFirstTraverse(child); }); } } -- 非递归 function depthStackTraverse(root: Node) { const stack: Node[] = []; // root node in stack stack.push(root); while (stack.length > 0) { const curNode = stack.pop(); if (curNode === null) break; visitNode(curNode); // 子节点压栈 const childNodes = curNode.childNodes; if (childNodes.length) { Array.from(childNodes) .reverse() // 反向入栈,先进后出 .forEach((child) => stack.push(child)); } } }

广度优先遍历
function breadthFirstTraverse(root: Node) { const queue: Node[] = []; //inqueue queue.unshift(root); while (queue.length) { const curNode = queue.pop(); if (curNode === null) break; visitNode(curNode); const childNodes = curNode.childNodes; if (childNodes.length) { childNodes.forEach((child) => queue.unshift(child)); } } }

问题20 实现一个LazyMan
class LazyMan { name = ""; tasks = []; constructor(name) { this.name = name; setTimeout(() => { this.next(); }); }next() { const task = this.tasks.shift(); task && task(); }eat(food) { const task = () => { console.info(`${this.name} eat ${food}`); this.next(); // 立刻执行下一个任务 }; this.tasks.push(task); return this; // 链式调用 } sleep(sec) { const task = () => { console.info(`${this.name} 开始睡觉`); setTimeout(() => { console.info(`${this.name} 已经睡完了 ${sec}s,开始执行下一个任务`); this.next(); // xx 秒之后再执行下一个任务 }, sec * 1000); }; this.tasks.push(task); return this; } }const me = new LazyMan("jack"); me.eat("苹果") .eat("香蕉") .sleep(2) .eat("葡萄") .eat("西瓜") .sleep(2) .eat("橘子");

问题集合(1)
文章图片

问题21 手写curry函数,实现科里化
function curry(fn) { const fnArgsLen = fn.length; // 传入函数的参数长度 let args = []; function calc(...newArgs) { args = [...args, ...newArgs]; if (args.length < fnArgsLen) { return calc; } else { return fn.apply(this, args.slice(0, fnArgsLen)); } } return calc; }function add(a, b, c) { return a + b + c; } const curryAdd = curry(add); const res = curryAdd(10)(20)(30); // 60 console.info(res);

问题21 参与到网页生成流程中,有哪些线程?
  • GUI渲染线程
  • JS引擎线程
  • 定时器触发线程
  • 异步HTTP请求线程
  • 事件触发线程
  1. JS分为同步任务和异步任务=>同步异步区分
  2. 同步任务都在主线程上执行,形成一个执行栈(JS引擎线程)=>主线程执行方式
  3. 主线程之外,事件触发线程维护任务队列,异步任务有了回调,就在队列中插入一个事件=>任务队列的调度
  4. 一旦执行栈所有同步任务执行完成,系统就会读取任务队列,并且把可运行的异步任务转移到可执行栈中=>事件调度线程
问题22 实现一个H5图片懒加载SDK?
页面中有很多图片,一次性展示不完,可以设置img标签的data-src属性
问题集合(1)
文章图片

【问题集合(1)】设计逻辑
获取img标签的在页面中距离顶部高度值通过elem.getBoundingClientRect()与innerHeight比较,如果距离顶部值小于window.innerHeight,就可以显示了
function mapImagesAndTryLoad() { const images = document.querySelectorAll('img[data-src]') if (images.length === 0) returnimages.forEach(img => { const rect = img.getBoundingClientRect() if (rect.top < window.innerHeight) { // 显示出来 // console.info('loading img', img.dataset.src) img.src = https://www.it610.com/article/img.dataset.src img.removeAttribute('data-src') // 移除 data-src 属性,为了下次执行时减少计算成本 } }) } // 节流 window.addEventListener('scroll', _.throttle(() => { mapImagesAndTryLoad() }, 100))mapImagesAndTryLoad()

    推荐阅读