HTTPS是如何保障平安的?
先了解两个概念:
- 对称加密:即通信的双⽅都使⽤同⼀个秘钥进⾏加解密,对称加密尽管很简略性能也好,然而⽆法解决⾸次把秘钥发给对⽅的问题,很容易被⿊客拦挡秘钥。
- ⾮对称加密:
- 私钥 + 公钥 = 密钥对
- 即⽤私钥加密的数据, 只有对应的公钥能力解密, ⽤公钥加密的数据, 只有对应的私钥能力解密
- 因为通信双⽅的⼿⾥都有⼀套⾃⼰的密钥对, 通信之前双⽅会先把⾃⼰的公钥都先发给对⽅
- 而后对⽅再拿着这个公钥来加密数据响应给对⽅, 等到到了对⽅那⾥, 对⽅再⽤⾃⼰的私钥进⾏解密
⾮对称加密尽管安全性更⾼,然而带来的问题就是速度很慢,影响性能。
解决⽅案:
联合两种加密⽅式,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,而后发送进来,接管⽅使⽤私钥进⾏解密失去对称加密的密钥,而后双⽅能够使⽤对称加密来进⾏沟通。
此时⼜带来⼀个问题,两头⼈问题:
如果此时在客户端和服务器之间存在⼀个两头⼈, 这个两头⼈只须要把本来双⽅通信互发的公钥, 换成⾃⼰的公钥, 这样两头⼈就能够轻松解密通信双⽅所发送的所有数据。
所以这个时候须要⼀个平安的第三⽅颁发证书(CA),证实身份的身份,防⽌被两头⼈攻打。证书中包含:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的 HASH 算法、证书到期工夫等。
然而问题来了,如果两头⼈篡改了证书,那么身份证明是不是就⽆效了?这个证实就⽩买了,这个时候须要⼀个新的技术,数字签名。
数字签名就是⽤ CA ⾃带的 HASH 算法对证书的内容进⾏ HASH 失去⼀个摘要,再⽤ CA 的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候, 我再⽤同样的 Hash 算法, 再次⽣成音讯摘要,而后⽤ CA 的公钥对数字签名解密, 失去 CA 创立的音讯摘要, 两者⼀⽐, 就晓得两头有没有被⼈篡改了。这个时候就能最⼤水平保障通信的平安了。
Nginx 的概念及其工作原理
Nginx 是一款轻量级的 Web 服务器,也能够用于反向代理、负载平衡和 HTTP 缓存等。Nginx 应用异步事件驱动的办法来解决申请,是一款面向性能设计的 HTTP 服务器。
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于 event-driven 模型的。正是这个次要的区别带给了 Nginx 在性能上的劣势。
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其余的 worker process,这一点和 Apache 十分像,然而 Nginx 的 worker process 能够同时解决大量的 HTTP 申请,而每个 Apache process 只能解决一个。
代码输入后果
function a() {
var temp = 10;
function b() {console.log(temp); // 10
}
b();}
a();
function a() {
var temp = 10;
b();}
function b() {console.log(temp); // 报错 Uncaught ReferenceError: temp is not defined
}
a();
在下面的两段代码中,第一段是能够失常输入,这个应该没啥问题,关键在于第二段代码,它会报错 Uncaught ReferenceError: temp is not defined。这时因为在 b 办法执行时,temp 的值为 undefined。
JS 隐式转换,显示转换
个别非根底类型进行转换时会先调用 valueOf,如果 valueOf 无奈返回根本类型值,就会调用 toString
字符串和数字
- “+” 操作符,如果有一个为字符串,那么都转化到字符串而后执行字符串拼接
- “-” 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
[] + {} 和 {} + []
布尔值到数字
- 1 + true = 2
- 1 + false = 1
转换为布尔值
- for 中第二个
- while
- if
- 三元表达式
- ||(逻辑或)&&(逻辑与)右边的操作数
符号
- 不能被转换为数字
- 能被转换为布尔值(都是 true)
- 能够被转换成字符串 “Symbol(cool)”
宽松相等和严格相等
宽松相等容许进行强制类型转换,而严格相等不容许
字符串与数字
转换为数字而后比拟
其余类型与布尔类型
- 先把布尔类型转换为数字,而后持续进行比拟
对象与非对象
- 执行对象的 ToPrimitive(对象)而后持续进行比拟
假值列表
- undefined
- null
- false
- +0, -0, NaN
- “”
闭包产生的实质
以后环境中存在指向父级作用域的援用
有哪些可能引起前端平安的问题?
- 跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了与 CSS 辨别所以被称作 XSS。晚期常⻅于⽹络论坛, 起因是⽹站没有对⽤户的输⼊进⾏严格的限度, 使得攻击者能够将脚本上传到帖⼦让其余⼈浏览到有歹意脚本的⻚⾯, 其注⼊⽅式很简略包含但不限于 JavaScript / CSS / Flash 等;
- iframe 的滥⽤: iframe 中的内容是由第三⽅来提供的,默认状况下他们不受管制,他们能够在 iframe 中运⾏ JavaScirpt 脚本、Flash 插件、弹出对话框等等,这可能会毁坏前端⽤户体验;
- 跨站点申请伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已实现认证的⽤户进⾏⾮预期的个⼈信息或设定信息等某些状态更新,属于被动攻打
- 歹意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤少数时候都是在借助开发框架和各种类库进⾏疾速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起平安问题。
参考 前端进阶面试题具体解答
写代码:实现函数可能深度克隆根本类型
浅克隆:
function shallowClone(obj) {let cloneObj = {};
for (let i in obj) {cloneObj[i] = obj[i];
}
return cloneObj;
}
深克隆:
- 思考根底类型
-
援用类型
- RegExp、Date、函数 不是 JSON 平安的
- 会失落 constructor,所有的构造函数都指向 Object
- 破解循环援用
function deepCopy(obj) {if (typeof obj === 'object') {var result = obj.constructor === Array ? [] : {};
for (var i in obj) {result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
}
} else {var result = obj;}
return result;
}
代码输入后果
function Dog() {this.name = 'puppy'}
Dog.prototype.bark = () => {console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)
输入后果:true
解析: 因为 constructor 是 prototype 上的属性,所以 dog.constructor 实际上就是指向 Dog.prototype.constructor;constructor 属性指向构造函数。instanceof 而理论检测的是类型是否在实例的原型链上。
constructor 是 prototype 上的属性,这一点很容易被疏忽掉。constructor 和 instanceof 的作用是不同的,理性地来说,constructor 的限度比拟严格,它只能严格比照对象的构造函数是不是指定的值;而 instanceof 比拟涣散,只有检测的类型在原型链上,就会返回 true。
原型链
原型链实际上在下面原型的问题中就有波及到,在原型的继承中,咱们继承来多个原型,这里再提一下实现完满
继承的计划,通过借助寄生组合继承,PersonB.prototype = Object.create(PersonA.prototype)
这是当咱们实例化 PersonB 失去实例化对象,拜访实例化对象的属性时会触发 get 办法,它会先在本身属性上查
找,如果没有这个属性,就会去__proto__中查找,一层层向上直到查找到顶层对象 Object,这个查找的过程
就是原型链来。
PWA 应用过吗?serviceWorker 的应用原理是啥?
渐进式网络应用(PWA)
是谷歌在 2015 年底提出的概念。基本上算是 web 应用程序,但在外观和感觉上与 原生 app
相似。反对 PWA
的网站能够提供脱机工作、推送告诉和设施硬件拜访等性能。
Service Worker
是浏览器在后盾独立于网页运行的脚本,它关上了通向不须要网页或用户交互的性能的大门。当初,它们已包含如推送告诉和后盾同步等性能。未来,Service Worker
将会反对如定期同步或天文围栏等其余性能。本教程探讨的外围性能是拦挡和解决网络申请,包含通过程序来治理缓存中的响应。
如何实现浏览器内多个标签页之间的通信?
实现多个标签页之间的通信,实质上都是通过中介者模式来实现的。因为标签页之间没有方法间接通信,因而咱们能够找一个中介者,让标签页和中介者进行通信,而后让这个中介者来进行音讯的转发。通信办法如下:
- 应用 websocket 协定,因为 websocket 协定能够实现服务器推送,所以服务器就能够用来当做这个中介者。标签页通过向服务器发送数据,而后由服务器向其余标签页推送转发。
- 应用 ShareWorker 的形式,shareWorker 会在页面存在的生命周期内创立一个惟一的线程,并且开启多个页面也只会应用同一个线程。这个时候共享线程就能够充当中介者的角色。标签页间通过共享一个线程,而后通过这个共享的线程来实现数据的替换。
- 应用 localStorage 的形式,咱们能够在一个标签页对 localStorage 的变动事件进行监听,而后当另一个标签页批改数据的时候,咱们就能够通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。
- 应用 postMessage 办法,如果咱们可能取得对应标签页的援用,就能够应用 postMessage 办法,进行通信。
为什么 0.1 + 0.2 != 0.3,请详述理由
因为 JS 采纳 IEEE 754 双精度版本(64 位),并且只有采纳 IEEE 754 的语言都有该问题。
咱们都晓得计算机示意十进制是采纳二进制示意的,所以 0.1
在二进制示意为
// (0011) 示意循环
0.1 = 2^-4 * 1.10011(0011)
那么如何失去这个二进制的呢,咱们能够来演算下
小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且失去的第一位为最高位。所以咱们得出 0.1 = 2^-4 * 1.10011(0011)
,那么 0.2
的演算也根本如上所示,只须要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)
。
回来持续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1
和 0.2
都是有限循环的二进制了,所以在小数位开端处须要判断是否进位(就和十进制的四舍五入一样)。
所以 2^-4 * 1.10011...001
进位后就变成了 2^-4 * 1.10011(0011 * 12 次)010
。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11 次)0100
, 这个值算成十进制就是 0.30000000000000004
上面说一下原生解决办法,如下代码所示
parseFloat((0.1 + 0.2).toFixed(10))
解析 URL 参数为对象
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {if (/=/.test(param)) { // 解决有 value 的参数
let [key, val] = param.split('='); // 宰割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创立 key 并设置值
paramsObj[key] = val;
}
} else { // 解决没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
实现 LazyMan
题目形容:
实现一个 LazyMan,能够依照以下形式调用:
LazyMan(“Hank”)输入:
Hi! This is Hank!
LazyMan(“Hank”).sleep(10).eat(“dinner”)输入
Hi! This is Hank!
// 期待 10 秒..
Wake up after 10
Eat dinner~
LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输入
Hi This is Hank!
Eat dinner~
Eat supper~
LazyMan(“Hank”).eat(“supper”).sleepFirst(5)输入
// 期待 5 秒
Wake up after 5
Hi This is Hank!
Eat supper
实现代码如下:
class _LazyMan {constructor(name) {this.tasks = [];
const task = () => {console.log(`Hi! This is ${name}`);
this.next();};
this.tasks.push(task);
setTimeout(() => {// 把 this.next() 放到调用栈清空之后执行
this.next();}, 0);
}
next() {const task = this.tasks.shift(); // 取第一个工作执行
task && task();}
sleep(time) {this._sleepWrapper(time, false);
return this; // 链式调用
}
sleepFirst(time) {this._sleepWrapper(time, true);
return this;
}
_sleepWrapper(time, first) {const task = () => {setTimeout(() => {console.log(`Wake up after ${time}`);
this.next();}, time * 1000);
};
if (first) {this.tasks.unshift(task); // 放到工作队列顶部
} else {this.tasks.push(task); // 放到工作队列尾部
}
}
eat(name) {const task = () => {console.log(`Eat ${name}`);
this.next();};
this.tasks.push(task);
return this;
}
}
function LazyMan(name) {return new _LazyMan(name);
}
协商缓存和强缓存的区别
(1)强缓存
应用强缓存策略时,如果缓存资源无效,则间接应用缓存资源,不用再向服务器发动申请。
强缓存策略能够通过两种形式来设置,别离是 http 头信息中的 Expires 属性和 Cache-Control 属性。
(1)服务器通过在响应头中增加 Expires 属性,来指定资源的过期工夫。在过期工夫以内,该资源能够被缓存应用,不用再向服务器发送申请。这个工夫是一个相对工夫,它是服务器的工夫,因而可能存在这样的问题,就是客户端的工夫和服务器端的工夫不统一,或者用户能够对客户端工夫进行批改的状况,这样就可能会影响缓存命中的后果。
(2)Expires 是 http1.0 中的形式,因为它的一些毛病,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更准确的管制。它有很多不同的值,
Cache-Control
可设置的字段:
public
:设置了该字段值的资源示意能够被任何对象(包含:发送申请的客户端、代理服务器等等)缓存。这个字段值不罕用,个别还是应用 max-age= 来准确管制;private
:设置了该字段值的资源只能被用户浏览器缓存,不容许任何代理服务器缓存。在理论开发当中,对于一些含有用户信息的 HTML,通常都要设置这个字段值,防止代理服务器 (CDN) 缓存;no-cache
:设置了该字段须要先和服务端确认返回的资源是否产生了变动,如果资源未发生变化,则间接应用缓存好的资源;no-store
:设置了该字段示意禁止任何缓存,每次都会向服务端发动新的申请,拉取最新的资源;max-age=
:设置缓存的最大有效期,单位为秒;s-maxage=
:优先级高于 max-age=,仅实用于共享缓存(CDN),优先级高于 max-age 或者 Expires 头;max-stale[=]
:设置了该字段表明客户端违心接管曾经过期的资源,然而不能超过给定的工夫限度。
一般来说只须要设置其中一种形式就能够实现强缓存策略,当两种形式一起应用时,Cache-Control 的优先级要高于 Expires。
no-cache 和 no-store 很容易混同:
- no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,然而会有协商缓存;
- no-store 是指不应用任何缓存,每次申请都间接从服务器获取资源。
(2)协商缓存
如果命中强制缓存,咱们无需发动新的申请,间接应用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
下面曾经说到了,命中协商缓存的条件有两个:
max-age=xxx
过期了- 值为
no-store
应用协商缓存策略时,会先向服务器发送一个申请,如果资源没有产生批改,则返回一个 304 状态,让浏览器应用本地的缓存正本。如果资源产生了批改,则返回批改后的资源。
协商缓存也能够通过两种形式来设置,别离是 http 头信息中的 Etag 和Last-Modified 属性。
(1)服务器通过在响应头中增加 Last-Modified 属性来指出资源最初一次批改的工夫,当浏览器下一次发动申请时,会在申请头中增加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当申请发送到服务器后服务器会通过这个属性来和资源的最初一次的批改工夫来进行比拟,以此来判断资源是否做了批改。如果资源没有批改,那么返回 304 状态,让客户端应用本地的缓存。如果资源曾经被批改了,则返回批改后的资源。应用这种办法有一个毛病,就是 Last-Modified 标注的最初批改工夫只能准确到秒级,如果某些文件在 1 秒钟以内,被批改屡次的话,那么文件已将扭转了然而 Last-Modified 却没有扭转,这样会造成缓存命中的不精确。
(2)因为 Last-Modified 的这种可能产生的不准确性,http 中提供了另外一种形式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中增加了 Etag 属性,这个属性是资源生成的惟一标识符,当资源产生扭转的时候,这个值也会产生扭转。在下一次资源申请时,浏览器会在申请头中增加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接管到申请后会依据这个值来和资源以后的 Etag 的值来进行比拟,以此来判断资源是否产生扭转,是否须要返回资源。通过这种形式,比 Last-Modified 的形式更加准确。
当 Last-Modified 和 Etag 属性同时呈现的时候,Etag 的优先级更高。应用协商缓存的时候,服务器须要思考负载平衡的问题,因而多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因而在思考负载平衡时,最好不要设置 Etag 属性。
总结:
强缓存策略和协商缓存策略在缓存命中时都会间接应用本地的缓存正本,区别只在于协商缓存会向服务器发送一次申请。它们缓存不命中时,都会向服务器发送申请来获取资源。在理论的缓存机制中,强缓存策略和协商缓存策略是一起单干应用的。浏览器首先会依据申请的信息判断,强缓存是否命中,如果命中则间接应用资源。如果不命中则依据头信息向服务器发动申请,应用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器间接应用本地资源的正本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
call/apply/bind 的实现
call
形容 :应用 一个指定的 this
值 (默认为 window
) 和 一个或多个参数 来调用一个函数。
语法:function.call(thisArg, arg1, arg2, ...)
核心思想:
- 调用 call 的可能不是函数
- this 可能传入 null
- 传入不固定个数的参数
- 给对象绑定函数并调用
- 删除绑定的函数
- 函数可能有返回值
实现:
Function.prototype.call1 = function(context, ...args) {if(typeof this !== "function") {throw new TypeError("this is not a function");
}
context = context || window; // 如果传入的是 null, 则指向 window
let fn = Symbol('fn'); // 发明惟一的 key 值, 作为结构的 context 外部办法名
context[fn] = this; // 为 context 绑定原函数(this)
let res = context[fn](...args); // 调用原函数并传参, 保留返回值用于 call 返回
delete context[fn]; // 删除对象中的函数, 不能批改对象
return res;
}
apply
形容:与 call
相似,惟一的区别就是 call
是传入不固定个数的参数,而 apply
是传入一个参数数组或类数组。
实现:
Function.prototype.apply1 = function(context, arr) {if(typeof this !== "function") {throw new TypeError("this is not a function");
}
context = context || window; // 如果传入的是 null, 则指向 window
let fn = Symbol('fn'); // 发明惟一的 key 值,作为结构的 context 外部办法名
context[fn] = this; // 为 context 绑定原函数(this)
let res;
// 判断是否传入的数组是否为空
if(!arr) {res = context[fn]();}
else {res = context[fn](...arr); // 调用原函数并传参, 保留返回值用于 call 返回
}
delete context[fn]; // 删除对象中的函数, 不能批改对象
return res;
}
bind
形容:bind
办法会创立一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时应用。
核心思想:
- 调用 bind 的可能不是函数
- bind() 除了 this 外,还可传入多个参数
- bind() 创立的新函数可能传入多个参数
- 新函数可能被当做结构函数调用
- 函数可能有返回值
实现:
Function.prototype.bind1 = function(context, ...args) {if (typeof that !== "function") {throw new TypeError("this is not function");
}
let that = this; // 保留原函数(this)return function F(...innerArgs) {
// 判断是否是 new 构造函数
// 因为这里是调用的 call 办法,因而不须要判断 context 是否为空
return that.call(this instanceof F ? this : context, ...args, ...innerArgs);
}
}
new 实现
形容:new
运算符用来创立用户自定义的对象类型的实例或者具备构造函数的内置对象的实例。
核心思想:
- new 会产生一个新对象
- 新对象须要可能拜访到构造函数的属性,所以须要从新指定它的原型
- 构造函数可能会显示返回对象与根本类型的状况(以及 null)
步骤 :应用new
命令时,它前面的函数顺次执行上面的步骤:
- 创立一个空对象,作为将要返回的对象实例。
- 将这个空对象的隐式原型 (
__proto__
),指向构造函数的prototype
属性。 - 让函数外部的
this
关键字指向这个对象。开始执行构造函数外部的代码(为这个新对象增加属性)。 - 判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。
实现:
// 写法一:function myNew() {
// 将 arguments 对象转为数组
let args = [].slice.call(arguments);
// 取出构造函数
let constructor = args.shift();
// 创立一个空对象,继承构造函数的 prototype 属性
let obj = {};
obj.__proto__ = constructor.prototype;
// 执行构造函数并将 this 绑定到新创建的对象上
let res = constructor.call(obj, ...args);
// let res = constructor.apply(obj, args);
// 判断构造函数执行返回的后果。如果返回后果是援用类型,就间接返回,否则返回 obj 对象
return (typeof res === "object" && res !== null) ? res : obj;
}
// 写法二:constructor:构造函数,...args:结构函数参数
function myNew(constructor, ...args) {
// 生成一个空对象, 继承构造函数的 prototype 属性
let obj = Object.create(constructor.prototype);
// 执行构造函数并将 this 绑定到新创建的对象上
let res = constructor.call(obj, ...args);
// let res = constructor.apply(obj, args);
// 判断构造函数执行返回的后果。如果返回后果是援用类型,就间接返回,否则返回 obj 对象
return (typeof res === "object" && res !== null) ? res : obj;
}
如何优化要害渲染门路?
为尽快实现首次渲染,咱们须要最大限度减小以下三种可变因素:
(1)要害资源的数量。
(2)要害门路长度。
(3)关键字节的数量。
要害资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其余资源的占用也就越少。同样,要害门路长度受所有要害资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后能力开始下载,并且资源越大,下载所需的往返次数就越多。最初,浏览器须要下载的关键字节越少,解决内容并让其呈现在屏幕上的速度就越快。要缩小字节数,咱们能够缩小资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
优化要害渲染门路的惯例步骤如下:
(1)对要害门路进行剖析和个性形容:资源数、字节数、长度。
(2)最大限度缩小要害资源的数量:删除它们,提早它们的下载,将它们标记为异步等。
(3)优化要害字节数以缩短下载工夫(往返次数)。
(4)优化其余要害资源的加载程序:您须要尽早下载所有要害资产,以缩短要害门路长度
实现有并行限度的 Promise 调度器
题目形容:JS 实现一个带并发限度的异步调度器 Scheduler,保障同时运行的工作最多有两个
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
的输入程序是:2 3 1 4
整个的残缺执行流程:一开始 1、2 两个工作开始执行
500ms 时,2 工作执行结束,输入 2,工作 3 开始执行
800ms 时,3 工作执行结束,输入 3,工作 4 开始执行
1000ms 时,1 工作执行结束,输入 1,此时只剩下 4 工作在执行
1200ms 时,4 工作执行结束,输入 4
实现代码如下:
class Scheduler {constructor(limit) {this.queue = [];
this.maxCount = limit;
this.runCounts = 0;
}
add(time, order) {const promiseCreator = () => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(order);
resolve();}, time);
});
};
this.queue.push(promiseCreator);
}
taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();
}
}
request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}
this.runCounts++;
this.queue
.shift()()
.then(() => {
this.runCounts--;
this.request();});
}
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
call apply bind
题目形容: 手写 call apply bind 实现
实现代码如下:
Function.prototype.myCall = function (context, ...args) {if (!context || context === null) {context = window;}
// 发明惟一的 key 值 作为咱们结构的 context 外部办法名
let fn = Symbol();
context[fn] = this; //this 指向调用 call 的函数
// 执行函数并返回后果 相当于把本身作为传入的 context 的办法进行调用了
return context[fn](...args);
};
// apply 原理统一 只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {if (!context || context === null) {context = window;}
// 发明惟一的 key 值 作为咱们结构的 context 外部办法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回后果
return context[fn](...args);
};
//bind 实现要简单一点 因为他思考的状况比拟多 还要波及到参数合并(相似函数柯里化)
Function.prototype.myBind = function (context, ...args) {if (!context || context === null) {context = window;}
// 发明惟一的 key 值 作为咱们结构的 context 外部办法名
let fn = Symbol();
context[fn] = this;
let _this = this;
// bind 状况要简单一点
const result = function (...innerArgs) {
// 第一种状况 : 若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符应用,则不绑定传入的 this,而是将 this 指向实例化进去的对象
// 此时因为 new 操作符作用 this 指向 result 实例对象 而 result 又继承自传入的_this 依据原型链常识可得出以下论断
// this.__proto__ === result.prototype //this instanceof result =>true
// this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
if (this instanceof _this === true) {
// 此时 this 指向指向 result 的实例 这时候不须要扭转 this 指向
this[fn] = _this;
this[fn](...[...args, ...innerArgs]); // 这里应用 es6 的办法让 bind 反对参数合并
} else {
// 如果只是作为一般函数调用 那就很简略了 间接扭转 this 指向为传入的 context
context[fn](...[...args, ...innerArgs]);
}
};
// 如果绑定的是构造函数 那么须要继承构造函数原型属性和办法
// 实现继承的形式: 应用 Object.create
result.prototype = Object.create(this.prototype);
return result;
};
// 用法如下
// function Person(name, age) {// console.log(name); //'我是参数传进来的 name'
// console.log(age); //'我是参数传进来的 age'
// console.log(this); // 构造函数 this 指向实例对象
// }
// // 构造函数原型的办法
// Person.prototype.say = function() {// console.log(123);
// }
// let obj = {
// objName: '我是 obj 传进来的 name',
// objAge: '我是 obj 传进来的 age'
// }
// // 一般函数
// function normalFun(name, age) {// console.log(name); //'我是参数传进来的 name'
// console.log(age); //'我是参数传进来的 age'
// console.log(this); // 一般函数 this 指向绑定 bind 的第一个参数 也就是例子中的 obj
// console.log(this.objName); //'我是 obj 传进来的 name'
// console.log(this.objAge); //'我是 obj 传进来的 age'
// }
// 先测试作为结构函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的 name')
// let a = new bindFun('我是参数传进来的 age')
// a.say() //123
// 再测试作为一般函数调用
// let bindFun = normalFun.myBind(obj, '我是参数传进来的 name')
// bindFun('我是参数传进来的 age')
深拷贝
实现一:不思考 Symbol
function deepClone(obj) {if(!isObject(obj)) return obj;
let newObj = Array.isArray(obj) ? [] : {};
// for...in 只会遍历对象本身的和继承的可枚举的属性(不含 Symbol 属性)for(let key in obj) {// obj.hasOwnProperty() 办法只思考对象本身的属性
if(obj.hasOwnProperty(key)) {newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
实现二:思考 Symbol
// hash 作为一个查看器,防止对象深拷贝中呈现环援用,导致爆栈
function deepClone(obj, hash = new WeakMap()) {if(!isObject(obj)) return obj;
// 查看是有存在雷同的对象在之前拷贝过,有则返回之前拷贝后存于 hash 中的对象
if(hash.has(obj)) return hash.get(obj);
let newObj = Array.isArray(obj) ? [] : {};
// 备份存在 hash 中,newObj 目前是空对象、数组。前面会对属性进行追加,这里存的值是对象的栈
hash.set(obj, newObj);
// Reflect.ownKeys 返回一个数组,蕴含对象本身的(不含继承的)所有键名,不论键名是 Symbol 或字符串,也不论是否可枚举。Reflect.ownKeys(obj).forEach(key => {
// 属性值如果是对象,则进行递归深拷贝,否则间接拷贝
newObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];
});
return newObj;
}