ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
### 一: 简单的模版引擎 模版引擎是在前端是非常常用的一种工具。请你完成一个简单的模版引擎的 render 函数,它可以接受模版字符串和一个数据对象作为参数。函数执行返回渲染以后的模版字符串,例如: const templateStr = ` <ul class="users"> <% users.forEach((user) => { %> <li class="user-item"> <%= 'My name is ' + user.name %> </li> <% }) %> </ul> ` const data = { users: [ { name: 'Jerry', age: 12 }, { name: 'Lucy', age: 13 }, { name: 'Tomy', age: 14 } ] } render(templateStr, data) /*返回结果: <ul class="users"> <li class="user-item"> My name is Jerry </li> <li class="user-item"> My name is Lucy </li> <li class="user-item"> My name is Tomy </li> </ul> */ <% 和 %> 之间可以放置任意的 JavaScript 代码,而 <%= 和 %> 之间执行任意的 JavaScript 表达式并且输出在模版上;传入的 data 可以作为模版引擎执行的上下文进行数据的引用,请你完成 render 函数。 (提示:你可以结合 执行任意表达式 来实现) 答案: ~~~ function render(template, data){ const evalExpr = /<%=(.+?)%>/g const expr = /<%([\s\S]+?)%>/g template = template .replace(evalExpr, '`); \n __echo( $1 ); \n __echo(`') .replace(expr, '`); \n $1 \n __echo(`') template = '__echo(`' + template + '`);' const script =` let output = "" const __echo = (html) => output += html ${template} return output ` return new Function(...Object.keys(data), script)(...Object.values(data)) } ~~~ ### 二:字符串居中补全 完成函数 centerPad 可以让一个字符串居中包裹在指定的可重复填充的字符串中间,例如: centerPad('Hello', 13, 'abc') // => 'abcaHelloabca' centerPad('Gook Luck!', 30, '*~') // => '*~*~*~*~*~Gook Luck!*~*~*~*~*~' 第一个参数为被包裹字符串,第二个参数为最终的字符串长度,第三个参数为用来填充的字符。 如果字符串无法完全居中,那么让字符串偏左,例如: centerPad('Hello', 10, 'abc') // => 'abHelloabc' 如果第二个参数传入的字符串长度比原来长度要短,直接返回原有字符串即可,例如: centerPad('Hello', 1, 'abc') // => 'Hello' 请你完成 centerPad 函数。 (提示:可以充分利用 ES6 新增的扩展方法) 答案: ~~~ const centerPad = (str, len, pad) => { const half = Math.floor((len - str.length) / 2) + str.length return str.padStart(half, pad).padEnd(len, pad) } ~~~ ### 三:迷你 MVVM 做一个小型的 MVVM 库,可以做到数据和视图之间的自动同步。 你需要做的就是完成一个函数 bindViewToData,它接受一个 DOM 节点和一个对象 data 作为参数。bindViewToData 会分析这个 DOM 节点下的所有文本节点,并且分析其中被 {{ 和 }} 包裹起来的表达式,然后把其中的内容替换成在 data 上下文执行该表达式的结果。例如: <div id='app'> <p> My name is {{firstName + ' ' + lastName}}, I am {{age}} years old. </p> <div> const appData = { firstName: 'Lucy', lastName: 'Green', age: 13 } bindViewToData(document.getElementById('app'), appData) // div 里面的 p 元素的内容为 // My name is Lucy Green, I am 13 years old. appData.firstName = 'Jerry' appData.age = 16 // div 里面的 p 元素的内容自动变为 // My name is Jerry Green, I am 16 years old. 当数据改变的时候,会自动地把相应的表达式的内容重新计算并且插入文本节点。 答案: ~~~ const bindViewToData = (el, data) => { Object.keys(data).forEach((k) => { let v = data[k] Object.defineProperty(data, k, { get () { return v }, set (nv) { v = nv applyChanges() } }) }) let changes = [] const parse = (el) => { Array.from(el.childNodes).forEach((c) => { if(!(c instanceof Text)) parse(c) else { changes.push(((originExp) => () => { insertExpForTextNode(c, originExp) })(c.nodeValue)) } }) } const insertExpForTextNode = (node, originExp) => { const newValue = originExp.replace(/{{[\s\S]+?}}/g, function (exp) { exp = exp.replace(/[{}]/g, '') return execute(exp) }) if (newValue !== node.nodeValue) node.nodeValue = newValue } const execute = (exp) => { return new Function(...Object.keys(data), `return ${exp}`)(...Object.values(data)) } const applyChanges = () => { setTimeout(() => { changes.forEach((f) => f()) }) } parse(el) applyChanges() } ~~~ ### 四:翻箱倒柜 完成一个类 Box,实例化的时候给它传入一个数组。Box 的实例支持 for...of 操作,可以把初始化的时候传给 Box 的数组内容遍历出来: const box = new Box(['book', 'money', 'toy']) for (let stuff of box) { console.log(stuff) // => 依次打印 'book', 'money', 'toy' } 你不能在 constructor 里面直接返回数组。 请你完成 Box 类。 答案: ~~~ class Box { constructor (stuffs = []) { this.stuffs = stuffs } [Symbol.iterator] () { let i = 0 return { next: () => { if (i >= this.stuffs.length) return { done: true } else return { value: this.stuffs[i++], done: false } } } } } ~~~ ### 五: Symbol 转换 请你完成 convertSymbolToNormalStr 函数,它会把一个键全是 Symbol 的对象转换成键全是 String 的对象,而同时值保持不变。例如: convertSymbolToNormalStr({ [Symbol('name')]: 'Jerry' }) // => { name: 'Jerry' } 答案: ~~~ const convertSymbolToNormalStr = (kv) => { return Object.getOwnPropertySymbols(kv).reduce((o, s) => { o[s.toString().replace(/(^Symbol\()|(\)$)/g, '')] = kv[s] return o }, {}) } ~~~ ### 六:全选和不选 现在页面上有很多 checkbox,有一个 id 为 check-all 的 checkbox 表示全选。check-all 的勾选状态和其他的 checkbox 的状态紧密关联: 勾选了 check-all 表示全部选中其他 checkbox,不勾选 check-all 表示全部不选。 在勾选了 check-all 以后,再把其他任意 checkbox 去掉勾选,那么 check-all 也该也去掉勾选,因为已经不是全部选中了。 在不勾选 check-all 的情况下,其他的 checkbox 被手动一个个选中了,那么 check-all 也应该被选中。 请你完成 initCheckBox 函数,可以初始化上述功能。 HTML <ul> <li>全选:<input type='checkbox' id='check-all'></li> <li>1 选项 <input type='checkbox' class='check-item'></li> <li>2 选项 <input type='checkbox' class='check-item'></li> <li>3 选项 <input type='checkbox' class='check-item'></li> <li>4 选项 <input type='checkbox' class='check-item'></li> <li>5 选项 <input type='checkbox' class='check-item'></li> <li>6 选项 <input type='checkbox' class='check-item'></li> </ul> 答案: js: ~~~ function initCheckBox () { const checkAll = document.getElementById('check-all') const checkItems = document.querySelectorAll('.check-item') checkAll.addEventListener('change', () => { Array.from(checkItems).forEach((item) => { item.checked = checkAll.checked }) }) Array.from(checkItems).forEach((item) => { item.addEventListener('change', () => { if (item.checked) { checkIfCheckedAll() } else { uncheckMaster() } }) }) function checkIfCheckedAll () { let checkedCount = 0 Array.from(checkItems).forEach((item) => { if (item.checked) checkedCount++ }) if (checkedCount === checkItems.length) checkMaster() } function checkMaster () { checkAll.checked = true } function uncheckMaster () { checkAll.checked = false } } function k() { return 'goo' } ~~~ ### 七:监听数组变化 在前端的 MVVM 框架当中,我们经常需要监听数据的变化,而数组是需要监听的重要对象。请你完成 ObserverableArray,它的实例和普通的数组实例功能相同,但是当调用: push pop shift unshift splice sort reverse 这些方法的时候,除了执行相同的操作,还会把方法名打印出来。 例如: const arr = new ObserverableArray() arr.push('Good') // => 打印 'push',a 变成了 ['Good'] 注意,你不能修改 Array 的 prototype。还有函数 return 的值和原生的操作保持一致。 答案: ~~~ class ObserverableArray extends Array { push (...args) { console.log('push') return super.push(...args) } reverse (...args) { console.log('reverse') return super.reverse(...args) } unshift (...args) { console.log('unshift') return super.unshift(...args) } sort (...args) { console.log('sort') return super.sort(...args) } splice (...args) { console.log('splice') return super.splice(...args) } pop (...args) { console.log('pop') return super.pop(...args) } shift (...args) { console.log('shift') return super.shift(...args) } } ~~~ ### 八: shallowEqual 在 React、Redux 当中,经常会用到一种 shallowEqual 来做性能优化。shallowEqual 结合 immutable 的共享数据结构可以帮助我们简单地检测到哪些数据没有发生变化,就不需要做额外的渲染等操作,优化效果拨群。 简单来说,shallowEqual 接受两个参数,如果这两个参数的值相同、或者这两个参数都是对象并且对象的第一层数据相同,那么就返回 true;否则就返回 false。例如: shallowEqual(1, 1) // true shallowEqual(1, 2) // false shallowEqual('foo', 'foo') // true shallowEqual(window, window) // true shallowEqual('foo', 'bar') // false shallowEqual([], []) // true shallowEqual([1, 2, 3], [1, 2, 3]) // true shallowEqual({ name: 'Jerry' }, { name: 'Jerry' }) // true shallowEqual({ age: NaN }, { age: NaN }) // true shallowEqual(null, { age: NaN }) // false var a = { name: 'Jerry' } var b = { age: 12 } shallowEqual({ a, b }, { a, b }) // true shallowEqual({ name: { a, b } }, { name: { a, b } } // false shallowEqual({ a, b }, { a }) // false 请你完成 shallowEqual 函数。 答案: ~~~ function shallowEqual(objA, objB) { const hasOwn = Object.prototype.hasOwnProperty if (Object.is(objA, objB)) return true if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false } const keysA = Object.keys(objA) const keysB = Object.keys(objB) if (keysA.length !== keysB.length) return false for (let i = 0; i < keysA.length; i++) { if (!hasOwn.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) { return false } } return true } ~~~ ### 九: 到底一不一样? 完成 is 函数,它接受两个参数,你返回 true 和 false 来表示这两个参数是否有 相同的值。例如: is('foo', 'foo'); // true is(window, window); // true is('foo', 'bar'); // false is([], []); // false var test = { a: 1 }; is(test, test); // true is(null, null); // true is(0, -0); // false is(-0, -0); // true is(NaN, 0/0); // true 答案: ~~~ function is (x, y) { // SameValue algorithm if (x === y) { // Steps 1-5, 7-10 // Steps 6.b-6.e: +0 != -0 return x !== 0 || 1 / x === 1 / y; } else { // Step 6.a: NaN == NaN return x !== x && y !== y; } }; ~~~ ### 十:你是五年的程序员吗 每天都是快乐的一天,比如看到一个帖子 说了这么一个故事: 面试一个5年的前端,却连原型链也搞不清楚,满口都是Vue,React之类的实现,这样的人该用吗? 最后还是拒绝。还有其他的原因。一个问题,输入m.n参数,获取一个m长度的都是n的数组,不能用循环,他不会写。问他他们公司项目的webpack配置entry有几个,他一会说1个,一会说很多个,不知道他到底懂不懂。 那么,为证明你的实力,请写出一个函数 initArray ,接受两个参数 m 和 n,返回一个数组,它的长度是 m,每个值都是 n。 答案: ~~~ const initArray = (m, n) => { if (m === 0) return [] return (n + ' ').repeat(m).trim().split(' ').map(n => +n) } ~~~