💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
一:自动绑定实例方法 题目: 在 JavaScript 的类当中,类实例如果不通过实例进行调用,方法中的 this 就不会指向实例,例如: class Person { constructor (name) { this.name = name } sayHi () { console.log(`I am ${this.name}.`) } } const jerry = new Person('Jerry') const sayHi = jerry.sayHi sayHi() // => 报错 所以在类似于 React.js 的组件的事件监听当中我们总是需要手动地进行 bind(this) 操作。为了简化这样的操作,请你完成一个方法 autoBind,它可以接受一个类作为参数,并且返回一个类。返回的类的实例和原来的类的实例功能上并无差别,只是新的类的实例所有方法都会自动 bind 到实例上。例如: const BoundPerson = autoBind(Person) const jerry = new BoundPerson('Jerry') const sayHi = jerry.sayHi sayHi() // => I am Jerry. const lucy = new BoundPerson('Lucy') const sayHi = lucy.sayHi sayHi() // => I am Lucy. 注意,如果 autoBind 以后给原来的类新增方法,也会自动反映在实例上,例如: Person.prototype.sayGood = function () { console.log(`I am ${this.name}. I am good!`) } const sayGood = lucy.sayGood sayGood() // => I am Lucy. I am good! 请你完成 autoBind 的编写。 答案: ~~~ /* Proxy 和反射的使用 */ const autoBind = (fn) => new Proxy(fn, { construct (_, args) { const obj = new fn(...args) const prop = new Proxy(Reflect.getPrototypeOf(obj), { get (target, key) { const fn = Reflect.get(target, key) return typeof fn === 'function' ? fn.bind(obj) : fn } }) Reflect.setPrototypeOf(obj, prop) return obj } }) ~~~ 二: filter map 请你给原生的 Map 添加方法 filterKeys 和 filterValues,可以类似于于数组方法的 filter。它们分别可以对 Map 的键和值进行筛选,它们会返回一个新的 Map, 是对原有的 Map 的筛选结果,例如: const m = new Map([['Jerry', 12], ['Jimmy', 13], ['Tomy', 14]]) m.filterKeys((key) => key.startsWith('J')) // => Map { Jerry => 12, Jimmy => 13 } m.filterValues((val) => val >= 13) // => Map { Jimmy => 13, Tomy => 14 } // 原有的 map 保持不变 console.log(m) // => Map { Jerry => 12 , Jimmy => 13, Tomy => 14 } 答案: function filterWithIndex (i) { return function (filter) { return new Map([...this.entries()].filter((kv) => filter(kv[i]))) } } Map.prototype.filterKeys = filterWithIndex(0) Map.prototype.filterValues = filterWithIndex(1) 三:单例模式 单例模式(Singleton)是一种常用的软件设计模式,它保证我们系统中的某一个类在任何情况实例化的时候都获得同一个实例。例如: const root1 = new Root() const root2 = new Root() const root3 = new Root() root1 === root2 // true root2 === root3 // true 我们构造一个名为 singletonify 方法,可以传入一个用户自定义的类,可以返回一个新的单例模式的类。例如: class A () {} const SingleA = singletonify(A) const a1 = new SingleA() const a2 = new SingleA() const a3 = new SingleA() a1 === a2 // => true a2 === a3 // => true 注意,你要保证 singletonify 返回的类的实例也是原来的类的实例: a1 instanceof A // => true a1 instanceof SingleA // => true 自定义的类属性也要保持一致,例如: class A () {} A.staticMethod = () => {} const SingleA = singletonify(A) SingleA.staticMethod === A.staticMethod // => true 请你完成 singletonify 的编写。 答案: ~~~ /* 不仅考察 Singleton 模式,还考察了 Proxy 的用法 */ const singletonify = (OriginalClass) => { let i return new Proxy(OriginalClass, { construct () { if (!i) i = new OriginalClass() return i } }) } ~~~ 四: 灵魂交换 有两个不同的人,他们有不同的灵魂(prototype)。 class A { constructor (name) { this.name = name } sayHi () { return `I am ${this.name}.` } } class B { constructor (name) { this.name = name } sayHi () { return `This is ${this.name}.` } } const a = new A('Jerry') const b = new B('Lucy') a.sayHi() // => 'I am Jerry.' b.sayHi() // => 'This is Lucy.' a instanceof B // => false b instanceof A // => false 请你完成 exchange,传入两个对象,可以交换他们的灵魂: exchange(a, b) a.sayHi() // => 'This is Jerry.' b.sayHi() // => 'I am Lucy.' a instanceof B // => true b instanceof A // => true 注意不要触碰到这两个对象原来的类,例如: exchange(a, b) a.sayHi() // => 'This is Jerry.' b.sayHi() // => 'I am Lucy.' const c = new A('Tomy') c.sayHi() // => 应该返回 'I am Tomy.' 你也不能使用 __proto__ 属性。 答案: ~~~ /* 考察的是 Object.getPrototypeOf 和 Object.setPrototypeOf 这两个 API 的使用 */ const exchange = (a, b) => { const getProto = (o) => Object.getPrototypeOf(o) const setProto = (o, p) => Object.setPrototypeOf(o, p) const ap = getProto(a) setProto(a, getProto(b)) setProto(b, ap) } /* or */ /* const exchange = (a, b) => { const protos = [a, b].map(o => Object.getPrototypeOf(o)); //这里的分号是必要的 [b, a].forEach((o, i) => Object.setPrototypeOf(o, protos[i])) } */ ~~~ 六:属性闪烁 完成一个 flikerProps 方法,接受一个对象作为参数。可以把该对象的不可遍历属性变成可遍历属性;把可遍历属性变成不可遍历属性。例如: const obj = {} const config1 = { enumerable: false, configurable: true } const config2 = { enumerable: true, configurable: true } Object.defineProperties(obj, { green: config1, red: config2, blue: config1, yellow: config2 }) console.log(Object.keys(obj)) // => ["red", "yellow"] flikerProps(obj) // 闪烁 console.log(Object.keys(obj)) // => ["green", "blue"] flikerProps(obj) // 闪烁 console.log(Object.keys(obj)) // => ["red", "yellow"] flikerProps(obj) // 闪烁 console.log(Object.keys(obj)) // => ["green", "blue"] 注意不要触碰到传入对象的 prototype。 答案: ~~~ const flikerProps = (obj) => { /* 本题主要考察的是 getOwnPropertyDescriptors 和 defineProperty 方法的使用 */ const descs = Object.getOwnPropertyDescriptors(obj) for (let [key, desc] of Object.entries(descs)) { Reflect.defineProperty(obj, key, { enumerable: !desc.enumerable }) } } ~~~ 七: 数组的空位填充 JavaScript 数组有空位的概念,也就数组的一个位置上没有任何的值。例如: [ , , 'Hello'] // => 0, 1 都是空位, 3 不是空位 空位并不等于 undefined 或者 null。一个位置上如果是 undefined 那么它依然有值,例如 [, , undefined],0 和 1 都是空位,而 2 不是空位。 请你完成一个函数 fillEmpty,它接受一个数组作为参数,可以把数组里面的所有空位都设置为 'Hello',例如: const a = [, , null, undefined, 'OK', ,] fillEmpty(a) // a 变成 ['Hello', 'Hello', null, undefined, 'OK', 'Hello'] 注意,你要原地修改原先的数组,而不是返回一个新的数组。 答案: ~~~ const fillEmpty = (arr) => { for (let i = 0; i < arr.length; i++) { if (i in arr) continue arr[i] = 'Hello' } } ~~~ 八:使用 generator 模拟 async/await 在远古时代,我们使用 callback 进行异步流程控制,但是会有 callback hell 的问题。经过历史的发展,逐渐地使用了不少的工具进行异步流程的改进,例如 Async.js、Promise 等,到后来的 generator + promise,还有最终的方案 async/await。了解以前是用什么方案处理异步流程控制,对我们理解现在的 asyn/await 也是很有好处。 请你实现一个简单的函数 wrapAsync,使用 generator + promise 来模拟 async/await 进行异步流程的控制。wrapAsync 接受一个 generator 函数作为参数,并且返回一个函数。generator 函数内部可以使用关键字 yield 一个 Promise 对象,并且可以类似 async/await 那样获取到 Promise 的返回结果,例如: const getData = (name) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('My name is ' + name) }, 100) // 模拟异步获取数据 }) } const run = wrapAsync(function * (lastName) { const data1 = yield getData('Jerry ' + lastName) const data2 = yield getData('Lucy ' + lastName) return [data1, data2] }) run('Green').then((val) => { console.log(val) // => [ 'My name is Jerry Green', 'My name is Lucy Green' ] }) getData 是一个异步函数并且返回 Promise,我们通过 yield 关键字获取到这个异步函数的 Promise 返回的结果,在代码编写上起来像是同步的,执行上实际是异步的。 请你完成 wrapAsync 的编写,wrapAsync 返回的函数接受的参数和传入的 generator 接受的函数保持一致,并且在调用的时候会传给 generator 函数(正如上面的例子);另外,wrapAsync 返回的函数执行结果是一个 Promise,我们可以通过这个 Promise 获取到 generator 函数执行的结果(正如上面的例子)。 (此简单实现你暂时不需要考虑异常的控制。) 答案: ~~~ const wrapAsync = (fn) => (...args) => new Promise((resolve, reject) => { const gen = fn(...args) let ret = gen.next() const run = () => { const promise = isPromise(ret.value) ? ret.value : Promise.resolve(ret.value) if (ret.done) return promise.then(resolve) promise.then((val) => { ret = gen.next(val) run() }).catch((e) => { try { gen.throw(e) } catch (e) { reject(e) } }) } /* Run generator */ run() }) const isPromise = (obj) => { return obj && typeof obj.then === 'function' } ~~~ 九:不重复数字 编写一个 JavaScript 函数 uniqueNums,该函数有一个参数 n(一个不大 31 的整数),其返回值是一个数组,该数组内是 n 个随机且不重复的整数,且整数取值范围是 [2, 32]。 请你完成 uniqueNums 的编写。 答案: ~~~ // const uniqueNums = (n) => { // const arr = [...Array(31).keys()].map((n) => n + 2) // const newArr = [] // while (newArr.length < n && arr.length) { // const index = Math.floor(arr.length * Math.random()) // newArr.push(arr[index]) // arr.splice(index, 1) // } // return newArr // } const uniqueNums = (n) => [...(new Array(31)).keys()] .map((i) => i + 2) .sort(() => Math.random() - Math.random()) .slice(0, n) ~~~ 十: Math.clz32 的 Polyfill ES6 新增了 Math.clz32 方法,可以让我们获取到一个整数的无符号 32 位的二进制形式有多少位前置的 0。例如: // 1 的 32 位二进制表示:0b00000000000000000000000000000001 // 有 31 位前置的 0 Math.clz32(1) // => 31 请你完成 clz32 来达到和 Math.clz32 的同样的功能。如果输入的是能够转换成数字的,先转换成数字再进行计算: Math.clz32('2') // => 30 如果不能转换成数字的,返回 32: Math.clz32('good') // => 32 总而言之,你的函数的返回结果要和 Math.clz32 保持一致。 答案: ~~~ const clz32 = (x) => { x = (x >>> 0).toString(2) return x * 1 === 0 ? 32 : 32 - x.length } ~~~