对象转原始类型,会调用内置的 [ToPrimitive] 函数,对于该函数而言,其逻辑如下
- 如果 Symbol.toPrimitive () 方法,优先调用再返回
- 调用 valueOf (),如果转换为原始类型,则返回
- 调用 toString (),如果转换为原始类型,则返回
- 如果都没有返回原始类型,会报错
防抖
js
function debounce(func, wait = 50) {
let timer
return (...args) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
节流
js
function throttle(func, wait) {
let previous = 0
return (...args) => {
let now = Date.now()
if (now - previous > wait) {
func.apply(this, args)
previous = now
}
}
}
EventLoop
概念
- 每个“线程”都有自己的事件循环,因此每个 Web Worker 都有自己的事件循环,因此它可以独立执行,而同一源上的所有窗口共享一个事件循环,因为它们可以同步通信。Tasks, microtasks, queues and schedules
- nestTick
浏览器端
- 一开始整段脚本作为第一个宏任务执行 =>
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列 =>
- 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列(包括 v8 垃圾回收)为空 =>
- 执行浏览器 UI 线程的渲染工作 =>
- 检查是否有 Web worker 任务,有则执行
- 执行队首新的宏任务,回到 2,依此循环,直到宏任务和微任务队列都为空
宏任务包含
- script(整体代码)
- setTimeout
- setInterval
- I/O
- UI 交互事件(requestAnimationFrame)
- postMessage
- MessageChannel
- setImmediate(Node.js 环境)
微任务包括
- Promise.then
- Object.observe(已废弃)
- MutationObserver
- process.nextTick(Node.js 环境,优先级高于 Promise)
- v8 垃圾回收
队列说明
- 浏览器端,宏任务队列,微任务队列
- Nodejs 端,宏任务队列,setImmediate 队列(通常比 timer 队列慢执行),微任务队列,process.nextTick 微任务队列(优先普通微任务队列执行)
服务器端
- timer 阶段 Node.js 事件循环的发起点有 4 个: Node.js 启动后; setTimeout 回调函数; setInterval 回调函数;也可能是一次 I/O 后的回调函数。
- pending callbacks:本阶段执行某些系统操作(如 TCP 错误类型)的回调函数
- idle、prepare:仅系统内部使用,空闲、预备状态 (第 2 阶段结束,poll 未触发之前) 该阶段只供 libuv 内部调用
- poll 轮询 阶段: 检索新的 I/O 事件,执行与 I/O 相关的回调,在 node 代码中难免会有异步操作,比如文件 I/O,网络 I/O 等等,那么当这些异步操作做完了,就会来通知 JS 主线程,怎么通知呢?就是通过 'data'、'connect' 等事件使得事件循环到达 poll 阶段。到达了这个阶段后: 如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timer 阶段。如果没有则继续等待,相当于阻塞了一段时间 (阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段。
- check 阶段 setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分
- close callbacks 关闭事件的回调阶段:执行一些关闭的回调函数,如 socket.on('close', ...)、socket.destroy ()。
::: primary 拓展阅读 多图生动详解浏览器与 Node 环境下的 Event Loop 上多图生动详解浏览器与 Node 环境下的 Event Loop 下 :::
实现继承
js
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
Parent5.call(this)
this.type = 'child5'
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
手动实现一下 instanceof 的功能
js
function myInstanceof(left, right) {
//基本数据类型直接返回false
if (typeof left !== 'object' || left === null) return false
//getPrototypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left)
// 也可通过这个获取获得对象的原型
// proto = left.__proto__
while (true) {
//查找到尽头,还没找到
if (proto == null) return false
//找到相同的原型对象
if (proto == right.prototype) return true
proto = Object.getPrototypeOf(proto)
//proto = proto.__proto__
}
}
new 被调用后做了四件事情
- 创建一个新对象
- 为这个新对象添加构造函数原型 (constructor.prototype) 所在原型链上的属性
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 根据构造函数返回的结果判断返回引用类型还是值类型
javascript
function newOperator(ctor, ...args) {
if (typeof ctor !== 'function') {
throw 'newOperator function the first param must be a function'
}
// 链接到原型
let obj = Object.create(ctor.prototype)
//// 绑定 this,执行构造函数
let res = ctor.apply(obj, args)
let isObject = typeof res === 'object' && res !== null
let isFunction = typeof res === 'function'
return isObject || isFunction ? res : obj
}
function newOperator(ctor, ...args) {
if (typeof ctor !== 'function') {
throw 'the first param must be a function'
}
let obj = Object.create(ctor.prototype)
let res = ctor.apply(obj, args)
let isObject = typeof res === 'object' && res !== null
let isFunction = typeof res === 'function'
return isObject || isFunction ? res : obj
}
模拟实现一个 bind 的效果
- 对于普通函数,绑定 this 指向
- 对于构造函数,要保证原函数的原型对象上的属性不能丢失
js
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
const args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat([...arguments]))
}
}
Function.prototype.myBind = function(context){
if(typeof context!=='function'){
throw new TypeError('error')
}
const _this = this
const args = [...arguments].slice(1)
return function F(){
if(this instanceof F){
return new _this(...args,...arguments)
}
return _this.apply(context,args.concat([...arguments]))
}
模拟实现 call 函数
- 不传入第一个参数,那么上下文默认为 window
- 改变了 this 指向,让新的对象可以执行该函数,并能接受参数
js
Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
context.fn = this
const args = [...arguments].slice(1)
const result = context.fn(...args)
delete context.fn
return result
}
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('is not a function')
}
context = context || window
context.fn = this
const result = context.fn(...args)
delete context.fn
return result
}
模拟实现 apply 函数
- 与 call 区别只是参数不同
- 在参数少的情况下,call 的性能优于 apply,反之 apply 的性能更好。因此在执行回调时候可以根据情况调用 call 或者 apply。
js
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
context.fn = this
let result
// 处理参数和 call 有区别
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
Function.prototype.myApply = function(context = window) {
if (typeof this !== 'function') {
throw new TypeError('error')
}
context.fn = this
let args = arguments[1] || []
const result = context.fn(...args)
delete context.fn
return result
}
实现对象深拷贝
二维数组扁平化
js
function flat1(arr) {
return arr.flat(Infinity)
}
function flat2(arr) {
return arr.reduce((prev, next) => prev.concat(Array.isArray(next) ? flat2(next) : next), [])
}
function flat3(arr){
white(arr.some(Array.isArray)){
arr = [].concat(...arr)
}
return arr
}
简易版及问题
js
JSON.parse(JSON.stringify())
// 1. 无法解决循环引用的问题。如拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况。:
const a = { val: 2 }
a.target = a
// 无法拷贝一写特殊的对象,诸如 RegExp, Date, Set, Map 等。
// 无法拷贝函数 (划重点)。
详细版利用递归
js
function deepClone(obj) {
// 如果是 值类型 或 null,则直接return
if (typeof obj !== 'object' || obj === null) {
return obj
}
// 定义结果对象
let copy = {}
// 如果对象是数组,则定义结果数组
if (obj.constructor === Array) {
copy = []
}
// 遍历对象的key
for (let key in obj) {
// 如果key是对象的自有属性
if (obj.hasOwnProperty(key)) {
// 递归调用深拷贝方法
copy[key] = deepClone(obj[key])
}
}
return copy
}
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj
}
let copy = {}
if (obj.constructor === Array) {
copy = []
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key])
}
}
return copy
}
函数与箭头函数
- 在 JS 种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那么如何来区分两者呢?答案是:利用原型。箭头函数是不存在原型的。
Promise
Promise 利用了三大技术手段来解决回调地狱
- 回调函数延迟绑定。
- 返回值穿透。
- 错误冒泡。
为什么 Promise 要引入微任务?
- 如何处理回调的问题。总结起来有三种方式:
- 使用同步回调,直到异步任务进行完,再进行后面的任务。
- 使用异步回调,将回调函数放在进行宏任务队列的队尾。
- 使用异步回调,将回调函数放到当前宏任务中的最后面。
- 第一种方式显然不可取,因为同步的问题非常明显,会让整个脚本阻塞住,当前任务等待,后面的任务都无法得到执行,而这部分等待的时间是可以拿来完成其他事情的,导致 CPU 的利用率非常低,而且还有另外一个致命的问题,就是无法实现延迟绑定的效果。如果采用第二种方式,那么执行回调 (resolve/reject) 的时机应该是在前面所有的宏任务完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿。为了解决上述方案的问题,另外也考虑到延迟绑定的需求,Promise 采取第三种方式,即引入微任务 , 即把 resolve (reject) 回调的执行放在当前宏任务的末尾。这样
- 利用微任务解决了两大痛点:
- 采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
- 放到当前宏任务最后执行,解决了回调执行的实时性问题。
简易版 Promise
js
// 三个常量用于表示状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
function MyPromise(fn) {
const that = this
this.state = PENDING
// value 变量用于保存 resolve 或者 reject 中传入的值
this.value = null
// 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
that.resolvedCallbacks = []
that.rejectedCallbacks = []
function resolve(value) {
// 首先两个函数都得判断当前状态是否为等待中
if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
// 遍历回调数组并执行
that.resolvedCallbacks.map(cb => cb(that.value))
}
}
function reject(value) {
if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}
// 完成以上两个函数以后,我们就该实现如何执行 Promise 中传入的函数了
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
}
// 最后我们来实现较为复杂的 then 函数
MyPromise.prototype.then = function(onFulfilled, onRejected) {
const that = this
// 判断两个参数是否为函数类型,因为这两个参数是可选参数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : e => throw e
// 当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数
if (this.state === PENDING) {
this.resolvedCallbacks.push(onFulfilled)
this.rejectedCallbacks.push(onRejected)
}
if (this.state === RESOLVED) {
onFulfilled(that.value)
}
if (this.state === REJECTED) {
onRejected(that.value)
}
return this
}
const PENDING = 'pending'
const RESOLVE = 'resolve'
const REJECT = 'reject'
function Promise(fn) {
const _this = this
this.state = PENDING
this.value = null
this.resolveCallbacks = []
this.rejectCallbacks = []
function resolve(value) {
if (_this.state === PENDING) {
_this.state = RESOLVE
_this.value = value
_this.resolvedCallbacks.map(cb => cb(_this.value))
}
}
function reject(value) {
if (_this.state === PENDING) {
_this.state = REJECT
_this.value = value
_this.rejectCallbacks.map(cb => cb(_this.value))
}
}
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : e => throw e
if (this.state === PENDING) {
this.resolveCallbacks.push(onFulfilled)
this.rejectCallbacks.push(onRejected)
}
if (this.state === RESOLVE) {
onFulfilled(this.value)
}
if (this.state === REJECT) {
onRejected(this.value)
}
return this
}
实现 Promise.all
js
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let result = []
let index = 0
let len = promises.length
if (len === 0) {
resolve(result)
return
}
for (let i = 0; i < len; i++) {
// 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
Promise.resolve(promise[i])
.then(data => {
result[i] = data
index++
if (index === len) resolve(result)
})
.catch(err => {
reject(err)
})
}
})
}
实现 Promise.race
js
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
let len = promises.length
if (len === 0) return
for (let i = 0; i < len; i++) {
Promise.resolve(promise[i])
.then(data => {
resolve(data)
return
})
.catch(err => {
reject(err)
return
})
}
})
}
Promise 并发控制
题目参数
js
let urls = ['bytedance.com','tencent.com','alibaba.com','microsoft.com','apple.com','hulu.com','amazon.com'] // 请求地址
//自定义请求函数
const request = url => {
return new Promise(resolve => {
setTimeout(() => {
resolve(`任务${url}完成`)
}, 1000)
}).then(res => {
console.log('外部逻辑', res);
})
}
- 简易递归版本
js
const run = (urls, limit = 3) => {
const pool = []
const addTask = () => {
const url = request(urls.shift())
url.finally(() => {
const idx = pool.indexOf(url)
pool.splice(idx, 1)
if(urls.length) {
addTask()
}
})
pool.push(url)
}
// 首次调用填满并发池
while (pool.length < limit) {
addTask()
}
}
run(urls, 3)
- 利用 Promise.race()
JavaScript
//添加任务
function addTask(url){
let task = request(url);
pool.push(task);
task.then(res => {
//请求结束后将该Promise任务从并发池中移除
pool.splice(pool.indexOf(task), 1);
console.log(`${url} 结束,当前并发数:${pool.length}`);
})
}
//每当并发池跑完一个任务,就再塞入一个任务
function run(race){
race.then(res => {
let url = urls.shift();
if(!url) return '请求完成'
// 还有任务,继续请求
addTask(url);
run(Promise.race(pool));
})
}
let pool = []//并发池
let max = 3 //最大并发量
//先循环把并发池塞满
while (pool.length < max) {
let url = urls.shift();
addTask(url)
}
//利用Promise.race方法来获得并发池中某任务完成的信号
let race = Promise.race(pool)
run(race)
拓展:通过 Iterator 控制 Promise.all 的并发数
forEach 中用 await
- 对于异步代码,forEach 并不能保证按顺序执行。 forEach 底层使用的 for...in...直接遍历进行回调导致它无法保证异步任务的执行顺序。比如后面的任务用时短,那么就又可能抢在前面的任务之前执行。
- for...of for...of 并不像 forEach 那么简单粗暴的方式去遍历执行,而是采用一种特别的手段 —— 迭代器去遍历。
- 原生具有 [Symbol.iterator] 属性数据类型为可迭代数据类型。如数组、类数组(如 arguments、NodeList)、Set 和 Map。
js
let arr = [4, 2, 1]
// 这就是迭代器
let iterator = arr[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
// {value: 4, done: false}
// {value: 2, done: false}
// {value: 1, done: false}
// {value: undefined, done: true}
//斐波那契数列 (50 以内)
function* fibonacci() {
let [prev, cur] = [0, 1]
console.log(cur)
while (true) {
;[prev, cur] = [cur, prev + cur]
yield cur
}
}
for (let item of fibonacci()) {
if (item > 50) break
console.log(item)
}
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
ES5 去写一个能够生成迭代器对象的迭代器生成函数
js
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {
// idx记录当前访问的索引
var idx = 0
// len记录传入集合的长度
var len = list.length
return {
// 自定义next方法
next: function() {
// 如果索引还没有超出集合长度,done为false
var done = idx >= len
// 如果done为false,则可以继续取值
var value = !done ? list[idx++] : undefined
// 将当前值与遍历是否完毕(done)返回
return {
done: done,
value: value,
}
},
}
}
var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()
react-thunk
js
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => Action => {
if (typeof Action === 'function') {
return Action(dispatch, getState, extraArgument)
}
return next(Action)
}
}
const thunk = createThunkMiddleware()
thunk.withExtraArgument = createThunkMiddleware
export default thunk
DOM
获取页面所有标签名称
const set = new Set()
function collectDom(el){
for(const item of el.children){
set.add(item.tagName)
collect(item)
}
}
collect(document.documentElement)
console.log(set)
原生 API 实现
map
Array.prototype.MyMap = function(fn, context){
let arr = Array.prototype.slice.call(this);//由于是ES5所以就不用...展开符了
let mappedArr = [];
for (let i = 0; i < arr.length; i++ ){
mappedArr.push(fn.call(context, arr[i], i, this));
}
return mappedArr;
}
reduce
Array.prototype.myReduce = function(fn, initialValue) {
let arr = Array.prototype.slice.call(this);
let res, startIndex;
res = initialValue ? initialValue : arr[0];
startIndex = initialValue ? 0 : 1;
for(let i = startIndex; i < arr.length; i++) {
res = fn.call(null, res, arr[i], i, this);
}
return res;
}
TS 类型体操实现 TrimLeft
typescript
type TrimLeft<Str extends string> = Str extends `${' '}${infer Rest}` ? TrimLeft<Rest> : Str
type res = TrimLeft<' abc'> //'abc'
::: primary 拓展阅读 TS 类型体操 :::