JS基础知识面试题(2)

补充:慕课 前端技术面 第七章 笔记总结

异步与单线程

同步和异步的区别,分别举例子

  • 异步是基于JS是单线程语言而出现的
  • 同步任务会阻塞代码执行,而异步不会
  • alert是同步,setTimeout是异步

手写Promise加载一张图片

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
// 加载一张图片
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}

const url = 'https://cdn.jsdelivr.net/gh/huanglizhu/huanglizhu.github.io/images/JS_Web_API_DOM_more2one.jpg'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))

Promise加载一张图片的结果

Promise加载2张图片(解决回调地域)

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
33
// 加载2张图片,即使用Promise解决回调地域
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}

const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'

loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 普通对象
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // promise 实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))

Promise加载2张图片的结果

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
2
3
4
5
6
7
8
9
10
console.log(1);
setTimeout(function () {
console.log(2)
}, 2000)
console.log(3);
setTimeout(function () {
console.log(4)
}, 1000)
console.log(5);
// 连续打印1 3 5 ,1秒后4 ,1秒后2

注意所有的同步任务在主线程上执行,形成一个执行栈。异步任务有了 运行结果 才会在任务队列中放置一个事件。脚本运行时先依次运行执行栈中的所有同步任务,然后会从任务队列里提取事件,选择需要首先执行的异步任务然后执行。

4先有结果,所以4先出现在任务队列了

补充一个微任务、宏任务的题目


前端使用异步的场景有哪些

  • 定时任务:setTimeout,setInterval(可参考笔记“setTimeout()与setInterval()”
  • 网络请求:ajax请求,动态<img>加载
  • 事件绑定:事件函数绑定以后并不会等触发事件函数才继续执行程序,程序照常执行,等需要触发的时候才会执行事件绑定函数。

ajax请求的例子

可参考ajax请求完整笔记
ajax请求代码示例
打印“start”、“end”,然后等./data1.json中的数据请求成功后执行函数-打印获取到的数据data1(关于$.get()可参考笔记

动态<img>加载的例子

动态`<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方法,允许指定回调函数。回调函数(回调地狱)变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。Promise的例子
  • async,async函数返回的是一个 Promise 对象,可以使用 then 方法添加回调函数,async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。当函数执行的时候,一旦遇到await就会先执行await的相关异步操作,等到异步操作完成,再接着执行函数体内后面的语句(包括同步任务也先等着await执行)。
    • 比起Promise,await则不需要then,Promise与async对比的例子
  • 事件监听,任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
    • 例:为f1绑定一个事件,当f1发生click事件,就执行f2。
  • 发布/订阅,与”事件监听”类似。
    • 我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)。
    • 这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

事件循环(JS的执行机制)

  • 注意:异步任务被挂起时会由浏览器的引擎去执行他们,而同步任务则是由JS的引擎去执行。
  • 可参考笔记从输入url到页面显示都经历了什么:
    1. 用户输入url
    2. 浏览器进行DNS解析,将域名解析为IP地址,查找顺序:浏览器自身的DNS缓存中查找=》操作系统hosts文件=》本地DNS服务器=》向根服务器发出请求进行递归查询
    3. TCP连接,三次握手(请求头中Connection:keep-alive可以使一次TCP连接重复使用):TCP连接请求报文=》TCP连接响应报文=》HTTP请求+TCP确认 报文
    4. 根据服务端返回的响应报文判断是否有 强缓存(响应头中有Expires或有不为no-cache/no-store的Cache-Control)且在有效期(max-age)内,如有则直接读取缓存,如无则通过服务端返回的响应报文判断是否有 协商缓存(Last-Modified/Etag),如有则带上If-Modified-Since/If-None-Match发出http请求,服务器比对后判断缓存是否可用,如可用则返回304状态码并读取客户端缓存;如比对失败则返回状态码200并向服务端请求资源。如无Last-Modified/Etag则直接向服务端请求资源,最后返回给客户端。【提高性能就要看缓存】http缓存 综述
    5. 浏览器边解析html文档边构建DOM树=》遇到css则同时解析css与html,根据css构建CSSOM(css对象模型)=》合并为渲染树=》回流(浏览器计算元素大小、位置)和重绘(元素的大小、颜色等属性确定后浏览器的绘制过程) 显示在屏幕上。
      • 所以css建议放在head中,在DOM树生成之前生成CSSOM,这样DOM树生成时可直接和CSSOM合并成渲染树,不会有存在用户看到状态变化的过程。
      • 回流和重绘非常耗性能,尽量减少。
    6. 解析/渲染过程中如遇JS脚本/连接,则停止渲染(因为JS有可能修改DOM结构)来执行和下载相应的代码,这会造成阻塞,故推荐JS代码应该放在html代码的后面
    7. 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格式的日期

获取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()
能遍历对象和数组的通用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()是反的,主要是为了照顾后面对象的keyvalue
  • 21 定义一个对象obj
  • 22-24 让对象obj调用我们定义的forEach函数
    • key:属性名称。value:属性值。

如果把后端的计算逻辑放到前端/移动端需要注意什么?

  1. 性能, 数据一致性, 尤其是同步下来 / 同步上去的数据, 用什么规则合并, 丢弃 / 更新的原则是什么.
  2. 安全性问题,需要甄别哪些能放哪些不能放,web 前端 js 文件可以修改,而且网络请求还能被抓包和伪造,一些没什么影响的计算逻辑放在前端可以减轻服务器压力,但有些比较重要的计算逻辑还是不要放在前端了

setTimeout()与let、闭包与作用域

1
2
3
4
5
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 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
2
3
4
5
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, i);
}

变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量

方法2:使用闭包(即局部变量)

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
((i) => {
setTimeout(function () {
console.log(i)//0-9
}, i)
})(i)
}

【将i传进函数中作为 函数作用域 的变量】

  1. i=0时生成一个函数,i=1时又生成另外一个函数,总共通过循环创建了10个函数循环了10次函数就会产生10个局部变量j,最后每个函数中的setTimeout都对应不同的j值。
  2. 那么setTimeout的参数函数去获取i的值时就会到当前的 父级作用域(函数作用域)中找,自然也不会10个标签都找到同一个i了。

方法3:
删除setTimeout,那么每一次循环就立即输出,也就不存在覆盖的问题了。


深拷贝二维数组

1
2
3
4
5
6
var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }]

// 完成下面的深拷贝函数
function deepCopy(src) {
return dest;
}

递归拷贝所有层级属性

  • 注意:对象和数组都会有遍历时需要避开的继承属性使用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
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
/**
* 深拷贝方法2
* @param {Object} obj 要拷贝的对象
*/
var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }]

// 完成下面的深拷贝函数
function deepCopy(obj) {
let objClone = [];
// 遍历 数组元素
for (key in obj) {
//判断 数组元素 是否为引用类型(数组、对象、函数),是则递归复制
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepCopy(obj[key]);
} else {
//如果不是,简单复制
objClone[key] = obj[key];
}
}
return objClone;
}

var objClone = deepCopy(target);
objClone[3][0] = 5;
console.log(target);
console.log(objClone);

结果

不能通过JSON对象

  • 原理:不拷贝引用对象,使用stringify()拷贝一个 JSON字符串 会新辟一个新的存储地址,这样就切断了引用对象的指针联系
  • 缺点无法实现对 数组/对象中 方法 的深拷贝,会显示为undefined。(ES6提供了object.assign()可解决这个问题,可参考这篇博客
1
2
3
4
5
6
7
8
9
10
var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }]

// 完成下面的深拷贝函数
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
var objClone = deepCopy(target);
objClone[4].name = "zzz";
console.log(target);
console.log(objClone);

无法实现对 数组/对象中 方法 的深拷贝


,