JavaScript手写面试题2

手写 JSONP

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
let jsonpFn = (url, params, callbackName) => {
let getUrl = (params) => {
let temp = ''
let isParamsExist = url.indexOf('?') > 0
for (let key in params) {
if (params.hasOwnProperty(key)) {
temp = isParamsExist ? `&${key}=${params[key]}` : `?${key}=${params[key]}`
isParamsExist = true
}
}
return temp + (url.indexOf('?') < 0 ? `?callback=${callbackName}`: `&callback=${callbackName}`)
}

return new Promise((resolve, reject) => {
var script = document.createElement('script')
script.src = getUrl(params)
document.body.appendChild(script)
window[callbackName] = (data) => {
resolve(data)
document.body.removeChild(script)
}
})
}

jsonpFn('http://127.0.0.1:3000', {}, 'fn').then(res => {
console.log(res)
})

事件循环

浏览器是多线程的, 但是js 是异步单线程

  • GUI 渲染线程(给浏览器画画的 DOM / BOM)
  • JS 引擎线程(web worker)
  • 浏览器事件线程(onclick)
  • 定时器触发线程
  • http 异步线程
  • eventLoop(时间循环)处理线程

前三个是常驻线程,后三个是有需求时才会工作

执行顺序先同后异先微后宏

eventloop

关于 setTimeout 的挂载时机需要注意

macro-task

  • 主线程代码(script中的代码
  • setTimeout setInterval setImmediate
  • requestAnimationFrame
  • UI render
  • I / O流
  • ajax请求

micro-task

  • process.nextTick

  • promise,准确的说是promise的then方法

  • async/await 实际就是promise

  • MutationObserve(这是h5的新特性)

图解完整eventloop

eventloop

易错题

  1. 浏览器点击这个按钮后 问控制台的输出
1
2
3
4
5
6
7
8
9
const btn = document.getElementById('button')
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('M1'))
console.log('L1')
})
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('M2'))
console.log('L2')
})

答案不是 L1 L2 M1 M2,而是 L1 M1 L2 M2

  1. 不用浏览器点击,而是js 模拟点击,btn.click(),此时答案变回 L1 L2 M1 M2

这是因为在不同的代码作用域(嵌套结构的代码)内,也是遵循先同后异,先微后宏的原则。因此第一个情况,可以理解为有两个宏任务,后者只有一个宏任务(或者理解为主线程任务)

关于 web worker

深入理解JavaScript中的Web Worker

Promise中的then、catch、finally

额,可能你看到下面👇这么多的 1,2,3 脾气就上来了,不是说好了本篇文章没什么屁话嘛,怎么还是 这么多一二三四。

😂,你要理解我的用心良苦啊,我这是帮你把知识点都列举出来,做个总结而已。当然,你也可以先不 看,先去做后面的题,然后再回过头来看这些,你就觉得这些点都好好懂啊,甚至都不需要记。

总结:

  1. Promise 的状态一经改变就不能再改变。(见3.1)
  2. .then 和 .catch 都会返回一个新的 Promise 。(上面的👆1.4证明了)
  3. catch 不管被连接到哪里,都能捕获上层未捕捉过的错误。(见3.2)
  4. 在 Promise 中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如 return 2 会 被包装为 return Promise.resolve(2) 。
  5. Promise 的 .then 或者 .catch 可以被调用多次, 但如果 Promise 内部的状态一经改变,并且有 了一个值,那么后续每次调用 .then 或者 .catch 的时候都会直接拿到该值。(见3.5)
  6. .then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。(见3.6)
  7. .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。(见3.7)
  8. .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。(见3.8)
  9. .then 方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时 候你可以认为 catch 是 .then 第二个参数的简便写法。(见3.9)
  10. .finally 方法也是返回一个 Promise ,他在 Promise 结束的时候,无论结果为 resolved 还是 rejected ,都会执行里面的回调函数。

节流和防抖

用一句话总结防抖和节流的区别:防抖是将多次执行变为最后一次执行,节流是将多次执行变为每隔一段时间执行

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
// 节流
const throttle = (fn, delay) => {
let start = 0;
return function () {
let now = new Date()
let context = this
if (now - start >= delay) {
fn.apply(context, arguments)
start = now
}
}

}

// 防抖
const debounce = (fn, delay) => {
let timer
return function () {
let args = arguments
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}

单例模式

单例模式 (Singleton) 的实现在于保证一个特定类只有一个实例,第二次使用同一个类创建新对象的时候,应该得到与第一次创建对象完全相同的对象

可以看看这个文章,讲的很详细: js 设计模式之单例模式

简单单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let Singleton = function (name) {
this.name = name
}
Singleton.instance = null
Singleton.prototype.getName = () {
return this.name
}
Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}

实现思想:将第一次创建的实例进行保存,之后再次创建前判断是否已经创建,如果之前创建过则返回已经保存的实例,否则创建一个实例,将实例创建和判断封装到了一个 getInstance 函数中,这种方式相对简单,但增加了类的“不透明性”,用一个函数来获取一个实例,而不是以往通过 new 来创建。

透明单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let CreateDiv = function () {
let instance = null;
let CreateDiv = function ( html ) {
if(instance) {
return instance
}
this.html = html
this.init()
instance = this
return instance
}
CreateDiv.prototype.init = function () {
let div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
return CreateDiv
}

let a = new CreateDiv('hello')
let b = new CreateDiv('world')
console.log(a === b) // true

实现思想:该方式与前一实现不同的在于用 new 来创建实例,运用了闭包来保存实例标识,从而达到只能创建唯一实例,但是如果有一天想创建多个 div 实例,该代码就不实用了。并且也不满足单一职责的要求

代理单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Creatediv {
constructor(html) {
this.init(html);
}
init(html) {
const divObj = document.createElement("div");
divObj.innerHTML = html;
document.body.appendChild(divObj);
}
}
const SingletonProxy = (function () {
let instance = null;
return function (html) {
if (!instance) {
instance = new Creatediv(html);
}
return instance;
};
})();
const a = new SingletonProxy("<span>我是一个span标签1</span>");
const b = new SingletonProxy("<span>我是一个span标签2</span>");
console.log(a === b);// true

功能拆解: 把 CreateDiv 独立出来, 而负责管理单例的逻辑移到代理类中,保证只有一个对象。 这样一来,CreateDiv 就变成了一个普通的类,它跟 代理 组合起来可以达到单例模式的效果。

惰性单例模式

例如创建遮罩层,多个页面共同调用同一个

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
34
35
36
37
38
39
40
41
var singleton = function() {
var instance;
return function(fn) {
return instance || (instance = fn.apply(this, arguments));
}
}();
// 创建遮罩层
var createMask = function(){
// 创建div元素
var mask = document.createElement('div');
// 设置样式
mask.style.position = 'fixed';
// ...
document.body.appendChild(mask);
// 单击隐藏遮罩层
mask.onclick = function(){
this.style.display = 'none';
}
return mask;
};

// 创建登陆窗口
var createLogin = function() {
// 创建div元素
var login = document.createElement('div');
// 设置样式
login.style.position = 'fixed';
// ...
login.innerHTML = 'login it';
document.body.appendChild(login);
return login;
};

document.getElementById('btn').onclick = function() {
var oMask = singleton(createMask);
oMask.style.display = 'block';
var oLogin = singleton(createLogin);
oLogin.style.display = 'block';
var w = parseInt(oLogin.clientWidth);
var h = parseInt(oLogin.clientHeight);
}

又例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 写一个通用的获取单例的函数 , fn 为执行单次的函数
let getSingle = function ( fn ) {
let result;
return function () {
return result || ( result = fn.apply(this, arguments))
}
}()
// 绑定事件
let bindEvent = function () {
document.getElementById('div').addEventListener('click',()=>{
console.log('我出现了')
});
}
// 无论渲染多少次,事件只绑定一次
let render = function () {
console.log('开始渲染!');
getSingle(bindEvent)
}
render();
render();
render();

该方式是 js 中常用的单例实现方法,再页面重复渲染三次时,监听事件只绑定了一次,减少了开销。

惰性体现:惰性体现在实例实在需要时创建,并不会在页面加载好就创建。

实现思想:首先利用闭包和高阶函数封装了一个返回单一实例的函数,其参数就是一个只执行一次的函数,可以随时改变单例的作用,大大增加了代码的可复用性。

new 的优先级问题

https://www.jianshu.com/p/412ccd8c386e


JavaScript手写面试题2
http://example.com/2022/07/29/JavaScript手写面试题2/
作者
lyric
发布于
2022年7月29日
许可协议