柯里化问题描述实现实现支持placeholder的curry()问题描述实现实现Array.prototype.flat()问题描述实现手写throttle()问题描述实现手写debounce()问题描述实现shuffle()随机打乱一个数组问题描述实现实现pipe()问题描述实现利用栈(Stack)创建队列(Queue)问题实现实现一个Event Emitter问题实现实现一个DOM element store问题实现检测 data type问题实现手写JSON.stringify()描述实现实现Object.assign()实现实现Promise.all()实现 实现Promise.race()实现实现Promise.any()实现实现Promise.allSettled()实现实现async helper - parallel()实现call实现bind实现new深拷贝MapReduce
柯里化
问题
描述
柯里化是一种函数的转换,它是指将一个函数从可调用的
f(a, b, c)
转换为可调用的 f(a)(b)(c)
,柯里化不会调用函数。它只是对函数进行转换。作用是可以轻松地生成偏函数,便于扩展下面是一个示例
const join = (a, b, c) => { return `${a}_${b}_${c}`; }; const curriedJoin = curry(join); curriedJoin(1, 2, 3); // '1_2_3' curriedJoin(1)(2, 3); // '1_2_3' curriedJoin(1, 2)(3); // '1_2_3'
参考
实现
function curry(fn) { return function curryInner(...args) { if (args.length >= fn.length) return fn(...args); return (...args2) => curryInner(...args, ...args2); }; }
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args) } else { return curried.bind(this, ...args); } } }
关键是通过
bind
方法继续收集剩余参数实现支持placeholder的curry()
问题
描述
下面是一个示例
const join = (a, b, c) => { return `${a}_${b}_${c}` } const curriedJoin = curry(join) const _ = curry.placeholder curriedJoin(1, 2, 3) // '1_2_3' curriedJoin(_, 2)(1, 3) // '1_2_3' curriedJoin(_, _, _)(1)(_, 3)(2) // '1_2_3'
实现
这题需要过滤参数,判断不包含
_
的参数数量满足条件才能调用,并且需要正确进行替换,每次遇到_
符号,需要从nextArgs
中取出第一项替换掉function curry(fn) { return function curried(...args) { const validArgs = args.slice(0, fn.length).filter(i => i !== curry.placeholder); if (validArgs.length >= fn.length) { return fn.apply(this, args); } else { return function(...nextArgs) { const processArgs = args.map(arg => arg === curry.placeholder && nextArgs.length ? nextArgs.shift() : arg) return curried(...processArgs, ...nextArgs); } } } } curry.placeholder = Symbol()
实现Array.prototype.flat()
问题
描述
下面是一个示例
const arr = [1, [2], [3, [4]]]; flat(arr) // [1, 2, 3, [4]] flat(arr, 1) // [1, 2, 3, [4]] flat(arr, 2) // [1, 2, 3, 4]
实现
递归写法
function flat(arr, depth = 1) { return arr.reduce((acc, item) => { if (Array.isArray(item) && depth > 0) { acc.push(...flat(item, depth - 1)) } else { acc.push(item) } return acc }, []) }
function flat(arr, depth = 1) { while (depth-- > 0 && arr.some(Array.isArray)) arr = [].concat(...arr) return arr }
手写throttle()
问题
描述
Throttle
是web
应用中经常用到的技巧,通常情况下你应该使用现有的实现,比如lodash throttle()
。你能够自己实现一个基本的
throttle()
吗?再次说明一下,
throttle(func, delay)
返回一个function
,这个function
无论多么频繁地调用,原始的func的调用也不会超过指定的频率。比如,这是throttle之前的调用
─ A ─ B ─ C ─ ─ D ─ ─ ─ ─ ─ ─ E ─ ─ F ─ G
按照
3
个单位进行throttle
过后─ A ─ ─ ─ C ─ ─ ─D ─ ─ ─ ─ E ─ ─ ─ G
注意到
A
因为不在任何的冷却时间,所以立即被执行
B
被跳过了,因为B
和C
都在A
的冷却时间里。实现
/** * @param {(...args:any[]) => any} func * @param {number} wait * @returns {(...args:any[]) => any} */ function throttle(func, wait) { let timer = 0; let lastArgs; return function throttled(...args) { if (timer) { lastArgs = args; } else { func.apply(this, args) timer = setTimeout(() => { if (lastArgs) { func.apply(this, lastArgs) } timer = null; }, wait) } } }
手写debounce()
问题
描述
Debounce是web应用中经常用到的技巧,通常情况下你应该使用现有的实现,比如lodash debounce() 。
你能够自己实现一个基本的debounce()吗?
比如,在debounce之前如下的调用
─ A ─ B ─ C ─ ─ D ─ ─ ─ ─ ─ ─ E ─ ─ F ─ G
经过3单位的debounce之后变为了
─ ─ ─ ─ ─ ─ ─ ─ D ─ ─ ─ ─ ─ ─ ─ ─ ─ G
实现
function debounce(func, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; }
shuffle()随机打乱一个数组
问题
描述
能否手写一个shuffle() ?
当传入一个数组的时候,shuffle()需要更换元素的顺序,每一种最终的数列都需要被相等的概率生成。
比如
const arr = [1, 2, 3, 4]
以上的数组共有4! = 24 中不同的排列
[1, 2, 3, 4] [1, 2, 4, 3] [1, 3, 2, 4] [1, 3, 4, 2] [1, 4, 2, 3] [1, 4, 3, 2] [2, 1, 3, 4] [2, 1, 4, 3] [2, 3, 1, 4] [2, 3, 4, 1] [2, 4, 1, 3] [2, 4, 3, 1] [3, 1, 2, 4] [3, 1, 4, 2] [3, 2, 1, 4] [3, 2, 4, 1] [3, 4, 1, 2] [3, 4, 2, 1] [4, 1, 2, 3] [4, 1, 3, 2] [4, 2, 1, 3] [4, 2, 3, 1] [4, 3, 1, 2] [4, 3, 2, 1]
实现
function getRandomIndexWithInMax(n) { return Math.floor(Math.random() * n); } function shuffle(arr) { // modify the arr inline to change the order randomly const op = [...arr]; do { const random = getRandomIndexWithInMax(op.length); const item = op[random]; op.splice(random, 1); arr[op.length - 1] = item; } while(op.length); }
复制一个数组
op
,每次从op
中随机获取一个元素按顺序来覆盖arr
数组,然后从op
中删掉,因为op
长度会发生变化,因此获取op
随机元素时需要获取随机索引 Math.floor(Math.random() * n)
实现pipe()
问题
描述
现在需要你自己写一个pipe() 方法。
假设有一些简单的四则运算方法:
const times = (y) => (x) => x * y const plus = (y) => (x) => x + y const subtract = (y) => (x) => x - y const divide = (y) => (x) => x / y
pipe() 可以用来生成新的计算方式
pipe([ times(2), times(3) ]) // x * 2 * 3 pipe([ times(2), plus(3), times(4) ]) // (x * 2 + 3) * 4 pipe([ times(2), subtract(3), divide(4) ])
实现
Composition是函数式编程中的一个概念,指的是将多个函数组合起来形成一个新函数。具体来说,如果有函数A和函数B,那么它们的Composition就是一个新函数C,满足C(x) = B(A(x))。可以把这个过程看做是把一系列单一的操作组合起来形成了一个更复杂的操作。
实现
pipe()
方法其实就是实现Composition的过程。我们可以将要组合的函数存储在一个数组中,然后用reduce()方法将它们组合起来。具体实现如下:const pipe = (fns) => (x) => fns.reduce((y, fn) => fn(y), x)
这里的
pipe()
方法接收一个函数数组fns作为参数,然后返回一个新函数。这个新函数的作用就是把给定的参数进行一系列操作,这一系列操作就是fns数组中所有函数的Composition。具体而言,我们使用reduce()方法,依次将fns数组中的函数作用在传入的参数上,最终得到一个结果。利用栈(Stack)创建队列(Queue)
问题
假设你有Stack,包含如下的方法
class Stack { push(element) { /* 添加元素到stack */ } peek() { /* 获取top 元素 */ } pop() { /* 弹出top 元素 */} size() { /* 获取元素数量 */} }
你能否通过只使用Stack实现一个包含如下方法的Queue?
class Queue { enqueue(element) { /* 添加元素到Queue,类似于Array.prototype.push */ } peek() { /* 获取头元素*/ } dequeue() { /* 弹出头元素,类似于Array.prototype.pop */ } size() { /* 获取元素数量 */ } }
请只使用Stack,不要使用Array。
实现
/* you can use this Class which is bundled together with your code class Stack { push(element) { // add element to stack } peek() { // get the top element } pop() { // remove the top element} size() { // count of element } } */ /* Array is disabled in your code */ // you need to complete the following Class class Queue { constructor() { this.inStack = new Stack(); this.outStack = new Stack(); } enqueue(element) { this.inStack.push(element); } peek() { const temp = this.dequeue(); this.outStack.push(temp); return temp; } size() { return this.outStack.size() + this.inStack.size(); } dequeue() { if (!this.outStack.size()) { while(this.inStack.size()) { this.outStack.push(this.inStack.pop()) } } return this.outStack.pop(); } }
使用
stack
实现队列时需要用到两个stack
,在出队列时为了获取到第一个元素,需要把stack
从一个全部倒出来放入第二个stack
,倒完后再去outStack.pop()
就是队列要出队列的元素实现一个Event Emitter
问题
Node.js中有Event Emitter,Facebook 也曾经有自己的实现 不过已经archive了。
请实现你自己的 Event Emitter
const emitter = new Emitter()
它需要支持事件订阅
const sub1 = emitter.subscribe('event1', callback1) const sub2 = emitter.subscribe('event2', callback2) // 同一个callback可以重复订阅同一个事件 const sub3 = emitter.subscribe('event1', callback1)
emit(eventName, ...args)
可以用来触发callbackemitter.emit('event1', 1, 2); // callback1 会被调用两次
subscribe()
返回一个含有release()
的对象,可以用来取消订阅。sub1.release() sub3.release() // 现在即使'event1'被触发, // callback1 也不会被调用
实现
class EventEmitter { constructor() { this.eventStore = {} } subscribe(eventName, callback) { if (!this.eventStore[eventName]) { this.eventStore[eventName] = [] } this.eventStore[eventName].push(callback); return { release: () => { const index = this.eventStore[eventName].indexOf(callback); this.eventStore[eventName].splice(index, 1); } }; } emit(eventName, ...args) { this.eventStore[eventName].forEach(fn => fn(...args)) } }
实现一个DOM element store
问题
JavaScript中有
Map
,我们可以用任何data做key,即便是DOM元素。const map = new Map() map.set(domNode, somedata)
如果运行的JavaScript不支持Map,我们如何能让上述代码能够工作?
请在不利用Map的条件下实现一个Node Store,支持DOM element作为key。
class NodeStore { set(node, value) { } get(node) { } has(node) { } }
实现
obj[key] = value
实际上是function processingKey(key) { if (key === undefined) { return 'undefined' } else if (key === null) { return 'null' } else { return key.toString() } } obj[processingKey(key)] = value
class NodeStore { /** * @param {Node} node * @param {any} value */ constructor() { this.nodes = {}; } set(node, value) { node.__nodeStoreKey__ = Symbol(); // to store same elements multiple times with uniqueness (as different nodes can have different data) this.nodes[node.__nodeStoreKey__] = value; } /** * @param {Node} node * @return {any} */ get(node) { return this.nodes[node.__nodeStoreKey__]; } /** * @param {Node} node * @return {Boolean} */ has(node) { return !!this.nodes[node.__nodeStoreKey__]; } }
检测 data type
问题
对于JavaScript中的所有基础数据类型,请实现一个方法进行检测。
除了基础数据类型之外,你的方法需要额外支持常见的类型包括
Array
、ArrayBuffer
、Map
、 Set
、Date
和 Function
。该题目的目标并不是想要你列举出所有数据类型,而是想要你证明你能解决该类型的问题。
类型名请返回小写。
detectType(1) // 'number' detectType(new Map()) // 'map' detectType([]) // 'array' detectType(null) // 'null' // judge的时候会有更多
实现
function detectType(data) { return Object.prototype.toString.call(data).toLowerCase().slice(8, -1) }
手写JSON.stringify()
描述
JSON.stringify()
将值转换为相应的 JSON
格式:- 转换值如果有
toJSON()
方法,该方法定义什么值将被序列化。
- 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
undefined
、任意的函数以及symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。函数、undefined
被单独转换时,会返回undefined
,如JSON.stringify(function(){})
orJSON.stringify(undefined)
.
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 所有以
symbol
为属性键的属性都会被完全忽略掉,即便replacer
参数中强制指定包含了它们。
Date
日期调用了toJSON()
将其转换为了string
字符串(同Date.toISOString()
),因此会被当做字符串处理。
NaN
和Infinity
格式的数值及null
都会被当做null
。
- 其他类型的对象,包括
Map/Set/WeakMap/WeakSet
,仅会序列化可枚举的属性。
实现
const isFunOrUndefinedOrSymbol = (e) => typeof e === 'function' || typeof e === 'undefined' || typeof e === 'symbol'; function stringify(data) { if ([NaN, null, Infinity].includes(data)) { return 'null'; } if (isFunOrUndefinedOrSymbol(data)) { return undefined; } const type = typeof data; switch (type) { case 'bigint': throw Error('bigints are not supported'); case 'string': return `"${data}"`; case 'object': { if (data.toJSON && typeof data.toJSON === 'function') { // 对象可能内置toJSON方法来自定义序列化对象 return stringify(data.toJSON()); } if (Array.isArray(data)) { return `[${data.map(e => isFunOrUndefinedOrSymbol(e) ? 'null' : stringify(e)).join()}]`; } return '{' + Object.keys(data).filter(key => !isFunOrUndefinedOrSymbol(data[key])).map(key => `"${key}":${stringify(data[key])}`).join() + '}'; } default: return String(data); } }
实现Object.assign()
主要是将所有可枚举属性的值从一个或多个源对象中复制到目标对象,同时返回目标对象。
语法如下:
Object.assign(target, ...source)
其中
target
是目标对象,...source
是源对象,可以是一个或多个,返回修改后的目标对象。
如果目标对象和源对象具有相同属性,则目标对象的该属性将会被源对象的相同属性覆盖,后来的源对象的属性将会类似地覆盖早先的属性。String
类型和Symbol
类型的属性都会被拷贝,而且不会跳过那些值为null
或undefined
的属性。Object.keys()
静态方法返回一个由给定对象自身的可枚举
的字符串键属性名
组成的数组,不包括Symbol
属性Object.getOwnPropertyNames()
方法返回一个由指定对象的所有自身属性
的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。var a = {}; Object.defineProperties(a, { one: {enumerable: true, value: 1}, two: {enumerable: false, value: 2}, }); Object.keys(a); // ["one"] Object.getOwnPropertyNames(a); // ["one", "two"]
Object.getOwnPropertySymbols()
在给定对象自身上找到的所有 Symbol 属性的数组Object.getOwnPropertyDescriptors()
方法返回给定对象的所有自有属性描述符(属性包括可枚举、不可枚举、symbol
)实现
function objectAssign(target, ...sources) { if (target === undefined || target === null) { throw 'err' } target = Object(target) sources.forEach(source => { if (source === undefined || source === null) { return } Object.defineProperties( target, Object.getOwnPropertyDescriptors(source) ) }) return target }
实现Promise.all()
Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。
source - MDN
promise使用及实现参考该文章
实现
function all(promises) { let count = 0; const res = [] const len = promises.length; if (!len) { return Promise.resolve([]) } return new Promise((resolve, reject) => { promises.forEach((promise, i) => { Promise.resolve(promise) .then((value) => { count++; res[i] = value; if (count === len) { resolve(res) } }) .catch((reson) => { reject(reson) }) }) }) }
实现Promise.race()
Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。source: MDN
实现
function race(promises) { return new Promise((resolve, reject) => { promises.forEach((promise, i) => { Promise.resolve(promise) .then((value) => { resolve(value) }) .catch((reson) => { reject(reson) }) }) }) }
实现Promise.any()
Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和AggregateError类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和Promise.all()是相反的。
- from MDN
AggregateError
暂时还没有被Chrome支持。但是你仍然可以使用它因为我们在judge你的code时候添加了AggregateError。你可以这样:
new AggregateError( 'No Promise in Promise.any was resolved', errors )
实现
function any(promises) { let count = 0; const resons = [] return new Promise((resolve, reject) => { promises.forEach((promise, i) => { Promise.resolve(promise) .then((value) => { resolve(value) }) .catch((reson) => { count++; resons[i] = reson; if (count === promises.length) { reject(new AggregateError('No Promise in Promise.any was resolved', resons)) } }) }) }) }
实现Promise.allSettled()
Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。
- from MDN
和
Promise.all()
不同,Promise.allSettled()
会等待所有的promise直到fulfill或者reject。实现
/** * @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) { let count = 0; const result = [] const len = promises.length; if (!len) { return Promise.resolve([]) } return new Promise((resolve, reject) => { promises.forEach((promise, i) => { Promise.resolve(promise) .then((value) => { result[i] = { status: 'fulfilled', value, }; }) .catch((reason) => { result[i] = { status: 'rejected', reason, }; }) .finally(() => { count++; if (count === len) { resolve(result) } }) }) }) }
实现async helper - parallel()
请实现async helper -
parallel()
有点类似Promise.all()
。异步函数的执行没有先后顺序,在parrallel()
中是同时触发。本题目中的所有异步函数是如下interface。
type Callback = (error: Error, data: any) => void type AsyncFunc = ( callback: Callback, data: any ) => void
你的
parallel()
需要能 接受 AsyncFunc 数组,并且返回一个function,这个function将会在所有的异步函数完成或者error发生的时候被触发。假设我们有如下3个异步函数。
const async1 = (callback) => { callback(undefined, 1) } const async2 = (callback) => { callback(undefined, 2) } const async3 = (callback) => { callback(undefined, 3) }
parallel()
需要使得以下成为可能。const all = parallel( [ async1, async2, async3 ] ) all((error, data) => { console.log(data) // [1, 2, 3] }, 1)
当Error发生的时候,只有第一个error需要被传递到最后,剩下的error和data都被忽略。
实现思路:
- 对于每个异步操作,新建一个Promise对象,并将resolve和reject方法作为参数传给异步操作函数
- 将这些Promise对象放入数组中,使用Promise.all等待它们全部完成
- 如果发生错误,直接reject
- 如果全部完成,将结果数组作为resolve值返回
代码实现如下:
function parallel(asyncFuncs) { return function(callback, data) { const promises = asyncFuncs.map(func => { return new Promise((resolve, reject) => { func((error, result) => { if (error) { reject(error); } else { resolve(result); } }, data); }); }); Promise.all(promises) .then(results => { callback(undefined, results); }) .catch(error => { callback(error, undefined); }); }; }
实现call
实现
myCall
函数可以考虑使用函数的arguments
对象,为thisArg
赋值,然后调用函数即可。具体的实现如下:Function.prototype.myCall = function (thisArg, ...args) { // 判断是否传递了thisArg,如果没有则默认为全局对象 thisArg = thisArg || window // 将当前函数作为thisArg的方法调用 thisArg.fn = this // 调用函数并传递参数 const result = thisArg.fn(...args) // 删除该方法,避免污染thisArg delete thisArg.fn // 返回结果 return result }
在
myCall
函数中,首先判断是否传递了thisArg
,如果没有则默认为全局对象window
。然后将当前函数作为thisArg
的方法调用,并将参数传递给该方法。接着调用该方法并将结果存储在result
变量中,并删除该方法,避免污染thisArg
。最后返回结果即可。实现bind
- 当作为构造函数时,this 指向实例,此时
this instanceof fn
结果为true
,可以让实例获得来自绑定函数的值。
- 当作为普通函数时,this 指向
window
,此时结果为false
,将绑定函数的 this 指向context
Function.prototype.myBind = function (context, ...args) { let that = this; let fn = function (...newArgs) { const theArgs = args.concat(newArgs) return that.apply(this instanceof fn ? this : context, theArgs); }; return fn; };
实现new
说明:
myNew()
接受一个构造函数和任意数量的参数。
- 它首先创建一个空对象
obj
,并将其原型链指向构造函数的原型对象。
- 然后它调用构造函数并传递给它所有的参数。
- 最后,它检查构造函数的返回值类型。如果它是一个对象,则返回该对象;否则返回创建的新对象
obj
。
const myNew = (constructor, ...args) => { // 创建一个新对象 const obj = {}; // 将新对象的原型设置为构造函数的原型 Object.setPrototypeOf(obj, constructor.prototype); // 调用构造函数并传入参数 const result = constructor.apply(obj, args); // 如果构造函数返回一个对象,则返回该对象,否则返回新对象 return (typeof result === 'object' && result !== null) ? result : obj; }
实现`instanceof`
实现
myInstanceOf
,核心思路就是判断对象的 __proto__
链上是否有与构造函数的 prototype
属性相等的值。代码实现如下:
function myInstanceOf(obj, constructor) { if(obj === null || typeof obj !== 'object') return false; while(obj){ if(obj.__proto__ === constructor.prototype) return true; obj = obj.__proto__; } return false; }
执行
myInstanceOf(b, B)
,先获取 b
的原型 proto
,并设置一个循环,每次循环将 proto
赋值为其原型的 __proto__
属性,直到找到 proto === B.prototype
时,返回 true。如果没有找到,最终返回 false。深拷贝
function cloneDeep(data, map = new WeakMap()) { if (data === null || typeof data !== "object") { return data; } //使用map解决循环引用带来的无限递归 //以要克隆的data作为键 clone作为值 //在递归过程中若遇到重复的data(引用值) //则为循环引用,返回之前在map中储存的clone if (map.has(data)) { return map.get(data); } const res = Array.isArray(data) ? [] : {}; map.set(data, res); // const keys = [...Object.keys(data), ...Object.getOwnPropertySymbols(data)]; const keys = Reflect.ownKeys(data); for (let key of keys) { res[key] = cloneDeep(data[key], map); } return res; }
Map
Array.prototype.myMap = function(fn, thisArg) { // your code here const results = new Array(this.length) const length = this.length for (let i = 0; i < length; i++) { if (Object.hasOwn(this, i)) { results[i] = fn.call(thisArg, this[i], i, this) } } return results }
Reduce
Array.prototype.myReduce = function (callback, initialValue) { // your code here // 参数callback initialValue // 1、如果传递了initialValue 第一次调用为 (initialValue, array[0]) // 2、如果未传递initialValue 第一次调用为 (array[0], array[1]) const argsLen = arguments.length; const index = argsLen > 1 ? 0 : 1; let result = argsLen > 1 ? initialValue : this[0] const len = this.length; if (len === 0 && argsLen < 2) { throw Error('') } for (let i = index; i < len; i++) { result = callback(result, this[i], i, this); } return result; }