# 实现 Object.assign () 和 completeAssign ()
# Object.assign()
Object.assign()
方法用于将所有源对象的可枚举属性,复制到目标对象。它将返回目标对象。
# 语法
Object.assign(target, ...sources)
# 示例
const target = { a: 1, b: 2 }; | |
const source = { b: 4, c: 5 }; | |
const returnedTarget = Object.assign(target, source); | |
console.log(target); // { a: 1, b: 4, c: 5 } |
# 手写实现
使用 for...of
循环遍历 sources
数组,然后使用 Object.keys()
和 Object.getOwnPropertySymbols()
获取 source
对象的可枚举属性和 symbol
类型的属性。
function objectAssign(target, ...sources) { | |
if(target === null || target === undefined) { | |
throw Error() | |
} | |
// your code here | |
target = Object(target) | |
console.log(target); | |
for(let source of sources) { | |
console.log(source, source == null) | |
if(source == null) continue; | |
merge(Object.keys(source), source); | |
merge(Object.getOwnPropertySymbols(source), source); // 获取 symbol 的 key | |
} | |
function merge(keys = [], source) { | |
console.log(keys.length, 111) | |
for(let key of keys) { | |
target[key] = source[key]; | |
if(target[key] !== source[key]) throw Error(); | |
} | |
} | |
return target; | |
} |
# completeAssign()
completeAssign()
处理的是可枚举属性和 symbol
类型的属性, getters
不会被复制,不可枚举属性被忽略。
# 示例
const source = Object.create( | |
{ | |
a: 3 // prototype | |
}, | |
{ | |
b: { | |
value: 4, | |
enumerable: true // 可枚举 data descriptor | |
}, | |
c: { | |
value: 5, // 不可枚举 data descriptor | |
}, | |
d: { // 不可枚举 accessor descriptor | |
get: function() { | |
return this._d; | |
}, | |
set: function(value) { | |
this._d = value | |
} | |
}, | |
e: { // 可枚举 accessor descriptor | |
get: function() { | |
return this._e; | |
}, | |
set: function(value) { | |
this._e = value | |
}, | |
enumerable: true | |
} | |
} | |
) | |
console.log(completeAssign({}, source)) // {b: 4, c: 5} |
# 手写实现
使用 Object.getOwnPropertyDescriptors()
获取 source
对象的所有属性描述符。再使用 Object.defineProperties()
设置 target
对象的属性描述符。最后遍历 source
对象的 symbol
类型的属性,设置 target
对象的属性。
function completeAssign(target, ...sources) { | |
// your code here | |
if(target == null) throw Error('11'); | |
target = Object(target); | |
for(let source of sources) { | |
if(source == null) continue; | |
source = Object(source); | |
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); // 获取到特有的 | |
for(let key of Object.getOwnPropertySymbols(source)) { // Symbol 属性 | |
target[key] = source[key]; | |
} | |
} | |
return target; | |
} |
# 实现 clearAllTimeout ()
clearAllTimeout()
方法用于清除所有的 setTimeout()
定时器。
# 代码实现
开多个定时器,然后依次清除。总能清除所有定时器。
function clearAllTimeout() { | |
// your code here | |
let id = setTimeout(() => {}, 0); | |
while(id) { | |
clearTimeout(id); | |
id --; | |
} | |
} |
# 实现 async helper - sequence()
& parallel()
# parallel
sequence()
方法用于将异步函数串行化,依次执行异步函数,并返回一个函数,该函数接受一个回调函数作为参数。
# 代码实现
- 定义
promisify()
方法,将异步函数转换为 Promise 对象。 - 使用
Promise.resolve()
将输入转换为 Promise 对象。 - 然后开始链式调用,每次返回的值作为下一个异步函数的输入。
- 最后解决时调用回调函数。
/* | |
type Callback = (error: Error, data: any) => void | |
type AsyncFunc = ( | |
callback: Callback, | |
data: any | |
) => void | |
*/ | |
/** | |
* @param {AsyncFunc[]} funcs | |
* @return {(callback: Callback) => void} | |
*/ | |
function sequence(funcs) { | |
const promiseFuncs = funcs.map(promisify) | |
return function (callback, input) { | |
let promise = Promise.resolve(input); | |
promiseFuncs.forEach((promiseFunc) => { | |
promise = promise.then(promiseFunc); | |
}) | |
promise.then((data) => { | |
callback(undefined, data); | |
}).catch(callback) | |
} | |
} | |
function promisify(fun) { | |
return (input) => { | |
return new Promise((resolve, reject) => { | |
fun((err, newData) => { | |
if(err) { | |
reject(err); | |
} else { | |
resolve(newData) | |
} | |
}, input) | |
}) | |
} | |
} |
# parallel()
parallel()
方法用于将异步函数并行化,同时执行异步函数,并返回一个函数,该函数接受一个回调函数作为参数。
# 代码实现
- 定义
promisify()
方法,将异步函数转换为 Promise 对象。 - 使用
Promise.all()
将异步函数并行执行,并返回一个 Promise 对象。 - 最后解决时调用回调函数。
/* | |
type Callback = (error: Error, data: any) => void | |
type AsyncFunc = ( | |
callback: Callback, | |
data: any | |
) => void | |
*/ | |
/** | |
* @param {AsyncFunc[]} funcs | |
* @return {(callback: Callback) => void} | |
*/ | |
function parallel(funcs){ | |
// your code here | |
// const promisiFuncs = funcs.map(promisify); | |
return function(callback, input) { | |
Promise.all(funcs.map(fn => promisify(fn)(input))) | |
.then(data => callback(undefined, data)) | |
.catch(err => callback(err, undefined)) | |
} | |
} | |
function promisify(fun) { | |
return (input) => { | |
return new Promise((resolve, reject) => { | |
fun((err, newData) => { | |
if(err) { | |
reject(err); | |
} else { | |
resolve(newData) | |
} | |
}, input) | |
}) | |
} | |
} |
# 实现 async helper - race()
# race()
race()
方法会在任何一个 function 结束或者产生 error 的时候调用最终的 callback。
# 代码实现
- 为每个 function 都添加一个 flag,如果 flag 为 true,则不再执行后续的 function。
- 使用
forEach()
遍历每个 function,并调用其函数。
/* | |
type Callback = (error: Error, data: any) => void | |
type AsyncFunc = ( | |
callback: Callback, | |
data: any | |
) => void | |
*/ | |
/** | |
* @param {AsyncFunc[]} funcs | |
* @return {(callback: Callback) => void} | |
*/ | |
function race(funcs){ | |
// your code here | |
return (callback, input) => { | |
let flag = false; | |
const fun = (...arg) => { | |
if(flag === true) return ; | |
callback(...arg); | |
flag = true; | |
} | |
for(let func of funcs) { | |
func(fun); | |
} | |
} | |
} |
# 实现 Promise.all()
# Promise.all()
Promise.all()
方法用于将多个 Promise 对象组合为一个新的 Promise 对象,并返回一个 Promise 对象。该 Promise 对象在传入的所有 Promise 对象都成功时才会成功,一旦有一个 Promise 对象失败,则立即返回一个失败的 Promise 对象。
# 代码实现
- 通过一个数组存储每个 Promise 完成后的结果
- 调用
Promise.resolve()
将输入转换为 Promise 对象。 - 使用
.then(onfulfilled, onrejected)
当 Promise 成功时调用 onfulfilled,当 Promise 失败时调用 onrejected。也就直接 reject 出去了
/** | |
* @param {Array<any>} promises - notice input might have non-Promises | |
* @return {Promise<any[]>} | |
*/ | |
function all(promises) { | |
// your code here | |
return new Promise((resolve, reject) => { | |
let arr = []; | |
if(promises.length === 0) resolve([]); | |
for(let i = 0; i < promises.length; i ++) { | |
const promise = promises[i]; | |
Promise.resolve(promise).then((value) => { | |
arr[i] = value; | |
if(i === promises.length - 1) resolve(arr); | |
}, reject); | |
} | |
}) | |
} |
# 实现 Promise.allSettled()
# Promise.allSettled()
Promise.allSettled()
方法返回一个在所有给定的 promise
都已经 fulfilled
或 rejected
后的 promise
,并带有一个对象数组,每个对象表示对应的 promise
结果。
# 示例
Promise.allSettled([ | |
Promise.resolve(33), | |
new Promise((resolve) => setTimeout(() => resolve(66), 0)), | |
99, | |
Promise.reject(new Error("一个错误")), | |
]).then((values) => console.log(values)); | |
// [ | |
// { status: 'fulfilled', value: 33 }, | |
// { status: 'fulfilled', value: 66 }, | |
// { status: 'fulfilled', value: 99 }, | |
// {status: 'rejected', reason: Error: 一个错误} | |
// ] |
# 代码实现
- 通过一个数组存储每个 Promise 完成后的结果
- 注意数组中存储的是
resolve
和reject
后的结果,所以需要分别处理。 - 使用
.finally()
方法,当 Promise 完成时调用,并返回一个 Promise 对象。
/** | |
* @param {Array<any>} promises - notice that input might contains non-promises | |
* @return {Promise<Array<{status: 'fulfilled', value: any} | {status: 'rejected', reason: any}>>} | |
*/ | |
function allSettled(promises) { | |
// your code here | |
let results = []; | |
let idx = 0; | |
return new Promise((resolve, reject) => { | |
if(promises.length === 0) { | |
resolve([]); | |
} | |
for(let i = 0; i < promises.length; i ++) { | |
const promise = promises[i]; | |
Promise.resolve(promise) | |
.then(value => { | |
results[i] = { status: 'fulfilled', value } | |
}) | |
.catch((reason) => { | |
results[i] = { status: 'rejected', reason } | |
}) | |
.finally(() => { | |
idx ++; | |
if(idx === promises.length) resolve(results); | |
}) | |
} | |
}) | |
} |
# 实现 Promise.any()
# Promise.any()
Promise.any()
接收一个 Promise
可迭代对象,只要其中的一个 promise
成功,就返回那个已经成功的 promise
。如果可迭代对象中没有一个 promise
成功(即所有的 promises
都失败 / 拒绝),就返回一个失败的 promise
和 AggregateError
类型的实例,它是 Error
的一个子类,用于把单一的错误集合在一起。本质上,这个方法和 Promise.all()
是相反的。
# 代码实现
- 数组存储每个
promise
失败的原因。 - 使用
.catch()
方法,当 Promise 全部失败时调用,并返回一个 Promise 对象包含AggregateError
。 - 当有一个
promise
成功时直接resolve
掉
/** | |
* @param {Array<Promise>} promises | |
* @return {Promise} | |
*/ | |
function any(promises) { | |
// your code here | |
return new Promise((resolve, reject) => { | |
let results = []; | |
let idx = 0; | |
for(let i = 0; i < promises.length;i ++) { | |
const promise = promises[i]; | |
promise | |
.then((value) => resolve(value)) | |
.catch((err) => { | |
results[i] = err; | |
idx ++; | |
if(idx === promises.length) reject(new AggregateError('error', results)); | |
}) | |
} | |
}) | |
} |
# 手写 _.cloneDeep()
实现深拷贝支持
- 基础数据类型及其 wrapper object
- 简单 Object(可枚举属性)
- 数组 Array
- 注意循环引用
# 代码实现
什么是 WeakMap
?WeakMap
是一个键值对集合,键必须是对象,值可以是任意类型。 WeakMap
对键的引用是弱引用
通过 WeakMap
记录已经拷贝的对象,避免循环引用
let visited = new WeakMap(); | |
function cloneDeep(data) { | |
// your code here | |
if(data === null || data === undefined || typeof data === "number" || typeof data === 'string' || typeof data === 'boolean' || typeof data === 'symbol' || typeof data === 'bigint') { | |
return data; | |
} | |
if(visited.has(data)) { | |
return visited.get(data); | |
} | |
let res = Array.isArray(data) ? [] : {}; | |
visited.set(data, res); | |
if(Array.isArray(data)) { | |
for(let i = 0; i < data.length; i ++) { | |
res[i] = cloneDeep(data[i]); | |
} | |
return res; | |
} else if(typeof data === 'object') { | |
for(key of Reflect.ownKeys(data)) { | |
res[key] = cloneDeep(data[key]); | |
} | |
return res; | |
} | |
return data; | |
} |
# Promise reject 的时候自动 retry
实现一个 fetchWithAutoRetry (fetcher, count),当出错的时候会自动重试,直到最大的重试次数。
# 代码实现
- Promise 链的链接
fetcher
返回一个 Promise
如果fetcher
成功后,就会返回成功结果
如果fetcher
失败后,.catch()
被触发
在.catch()
中,如果次数用完了直接返回失败结果,否则将继续执行fetcher
- 递归工作
每次递归减少次数
次数为 0 时停止,抛出错误
每次递归调用都会返回一个新的 Promise,这个 Promise 会被上一级的.catch () 链接起来。
/** | |
* @param {() => Promise<any>} fetcher | |
* @param {number} maximumRetryCount | |
* @return {Promise<any>} | |
*/ | |
function fetchWithAutoRetry(fetcher, maximumRetryCount) { | |
// your code here | |
return fetcher().catch((err) => { | |
if(maximumRetryCount === 0) { | |
throw err; | |
} else { | |
return fetchWithAutoRetry(fetcher, maximumRetryCount - 1); | |
} | |
}) | |
} |
# 实现负索引
# 实现思路
通过 Proxy 代理实现,设置 get
方法通过 Reflect.get
获取属性值,并返回结果。设置 set
方法通过 Reflect.set
设置属性值,并返回结果。
# 代码实现
注意判断索引还是方法
function wrap(arr) { | |
// your code here | |
const isNumber = (prop) => typeof prop !== 'symbol' && !isNaN(prop); | |
const normalizeProp = (prop, arrLength) => prop >= 0 ? prop : prop + arrLength; | |
return new Proxy(arr, { | |
get(target, prop, reciever) { | |
console.log(2); | |
if(isNumber(prop)) { | |
prop = normalizeProp(+prop, target.length); | |
} | |
return Reflect.get(target, prop, reciever); | |
}, | |
set(target, prop, value, reciever) { | |
console.log(1); | |
if(isNumber(prop)) { | |
prop = normalizeProp(+prop, target.length); | |
} | |
if(prop < 0) throw new Error('error'); | |
return Reflect.set(target, prop, value, reciever); | |
} | |
}) | |
} |
# 实现 DOM 的序列化与反序列化
将 DOM 序列化为对象,对象反序列化为 DOM。
# 实现思路
- 获取 DOM 节点,通过
tagName
可以得到节点类型,通过attributes
可以得到节点的属性,再通过childNodes
去遍历子节点。递归添加到对象中。 - 对于每个节点,通过
createElement
创建节点,直接设置element[key] = value
可以将属性设置到节点上。递归渲染子节点。
# 代码实现
/** | |
* @param {HTMLElement} | |
* @return {object} object literal presentation | |
*/ | |
function virtualize(element) { | |
// your code here | |
const result = { | |
type: element.tagName.toLowerCase(), // 标签名 | |
props: {} | |
} | |
for(let attr of element.attributes) { // 获取属性 | |
const name = attr.name === 'class' ? 'className' : attr.name; | |
result.props[name] = attr.value; | |
} | |
const children = []; | |
for(let node of element.childNodes) { | |
if(node.nodeType === 3) { // 文本节点 | |
children.push(node.textContent); | |
} else { // 其他节点 | |
children.push(virtualize(node)); | |
} | |
} | |
result.props.children = children.length === 1 ? children[0] : children; | |
return result; | |
} | |
/** | |
* @param {object} valid object literal presentation | |
* @return {HTMLElement} | |
*/ | |
function render(json) { | |
// create the top level emlement | |
// recursively append the children | |
// textnode | |
if(typeof json === 'string') { // 文本节点 | |
return document.createTextNode(json); | |
} | |
const {type, props: {children, ...attrs}} = json; | |
const element = document.createElement(type); | |
for(let [key, value] of Object.entries(attrs)) { | |
element[key] = value; // 设置属性 | |
} | |
const childrenArr = Array.isArray(children) ? children : [children]; | |
for(let child of childrenArr) { | |
element.append(render(child)); // 递归渲染子节点 | |
} | |
return element; | |
} |
# 手写 Promise.prototype.finally()
可以再 promise
被 settle
的时候执行一个 callback
,无论是否被 fulfill
或者 reject
,都会执行 callback
。
# 实现思路
返回一个 Promise
当前 promise
被 settle
的时候执行 callback
,并返回 promise
。注意回调函数可能返回一个 promise
,如果返回的是一个 promise
,则返回的 promise
的状态和 callback
的返回值一致。
# 代码实现
设置无论当 promise
被 fulfill
或者 reject
的时候执行 onFinally
,如果 onFinally
返回的是一个 promise
,则返回的 promise
的状态和 onFinally
的返回值一致。当 await
一个被 reject
的 promise
相当于抛出异常
/** | |
* @param {Promise<any>} promise | |
* @param {() => void} onFinally | |
* @returns {Promise<any>} | |
*/ | |
function myFinally(promise, onFinally) { | |
// your code here | |
return promise.then(async (data) => { | |
// console.log(1); | |
await onFinally(); | |
return Promise.resolve(data); | |
}, async (reason) => { | |
await onFinally(); | |
return Promise.reject(reason); | |
}) | |
} | |
const res = [] | |
const promise = Promise.reject(100) | |
myFinally(promise, () => { | |
res.push(1) | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
res.push(2) | |
reject(4) | |
}, 100) | |
}) | |
}).then((value) => { | |
res.push(3) | |
}).catch((reason) => { | |
console.log(reason); | |
}) // 4 |
# 创建 LazyMan
# 实现思路
- 创建一个操作对象
- 在对象中修改函数中的属性,并返回对象本身,实现链式调用
- 通过
setTimeout
来将任务加入延时队列,只有当这次任务执行完毕,才会执行下一次任务,也就是这次链式调用时,setTimeout
任务处理函数并不会执行,等到链式调用结束,才会执行setTimeout
任务处理函数。
# 代码实现
// interface Laziness { | |
// sleep: (time: number) => Laziness | |
// sleepFirst: (time: number) => Laziness | |
// eat: (food: string) => Laziness | |
// } | |
/** | |
* @param {string} name | |
* @param {(log: string) => void} logFn | |
* @returns {Laziness} | |
*/ | |
function LazyMan(name, logFn) { | |
// your code here | |
let queue = [() => {logFn(`Hi, I'm ${name}.`)}]; | |
const Man = { | |
eat: function(name) { | |
queue.push(() => {logFn(`Eat ${name}.`)}); | |
return this; | |
}, | |
sleep: function(delay) { | |
queue.push(() => new Promise((resolve, reject) => {setTimeout(() => {logFn(`Wake up after ${delay} ${delay > 1 ? 'seconds' : 'second'}.`); resolve()}, delay * 1000)})) | |
return this; | |
}, | |
sleepFirst: function(delay) { | |
queue.unshift(() => new Promise((resolve, reject) => {setTimeout(() => {logFn(`Wake up after ${delay} ${delay > 1 ? 'seconds' : 'second'}.`); resolve()}, delay * 1000)})) | |
return this; | |
} | |
} | |
setTimeout(async() => { | |
for (task of queue) { | |
console.log(task); | |
await task(); | |
} | |
}, 0); | |
return Man; | |
} | |
LazyMan('Jack', console.log) | |
.eat('banana') | |
.eat('apple') | |
.sleepFirst(2) | |
// Wake up after 2 seconds. | |
// Hi, I'm Jack. | |
// Eat banana. | |
// Eat apple. |
# 数组扁平化的多种写法
# 目前六种
# 代码
let a = [[1, 2, 3], [[4]], [5, 6]]; | |
// 1. Array.prototype.flat() | |
console.log(a.flat(Infinity)) | |
// 2. 手写递归 flat | |
function myFlat(arrs) { | |
let res = []; | |
for (const arr of arrs) { | |
if (Array.isArray(arr)) { | |
res = res.concat(myFlat(arr)); | |
} else { | |
res.push(arr); | |
} | |
} | |
return res; | |
} | |
console.log(myFlat(a)); | |
// 3. reduce | |
function myFlat2(arrs) { | |
return arrs.reduce((pre, cur) => { | |
return pre.concat(Array.isArray(cur) ? myFlat2(cur) : cur); | |
}, []); | |
} | |
console.log(myFlat2(a)); | |
// 4. 扩展运算符 | |
function myFlat3(arrs) { | |
while (arrs.some(item => Array.isArray(item))) { | |
arrs = [].concat(...arrs); | |
} | |
return arrs; | |
} | |
console.log(myFlat3(a)); | |
// 5. toString + split (仅限于数字数组) | |
function myFlat4(arrs) { | |
return arrs.toString().split(',').map(item => Number(item)); | |
} | |
console.log(myFlat4(a)); | |
// 6. JSON.stringify + parse | |
function myFlat5(arrs) { | |
let str = JSON.stringify(arrs); | |
str = str.replace(/(\[|\])/g, '') | |
str = '[' + str + ']'; | |
return JSON.parse(str); | |
} | |
console.log(myFlat5(a)); |
# 带标签的模板
带标签的模板是模板字面量的一种更高级的形式,它允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串数组,其余的参数与表达式相关。你可以用标签函数对这些参数执行任何操作,并返回被操作过的字符串(或者,也可返回完全不同的内容,见下面的示例)。用作标签的函数名没有限制。
const person = "Mike"; | |
const age = 28; | |
function myTag(strings, personExp, ageExp) { | |
const str0 = strings[0]; // "That " | |
const str1 = strings[1]; // " is a " | |
const str2 = strings[2]; // "." | |
const ageStr = ageExp > 99 ? "centenarian" : "youngster"; | |
// 我们甚至可以返回使用模板字面量构建的字符串 | |
return `${str0}${personExp}${str1}${ageStr}${str2}`; | |
} | |
const output = myTag`That ${person} is a ${age}.`; | |
console.log(output); | |
// That Mike is a youngster. |
function p(strings, ...values) { | |
console.log(strings); | |
console.log(values); | |
} | |
p`h${1}, h${2}, h${3}`; | |
// => [ 'h', ', h', ', h' ] | |
// => [ 1, 2, 3 ] |