写点什么

社招前端二面必会手写面试题总结

  • 2023-01-06
    浙江
  • 本文字数:11108 字

    阅读完需:约 36 分钟

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。


a='34';b='1234567'; // 返回 2a='35';b='1234567'; // 返回 -1a='355';b='12354355'; // 返回 5isContain(a,b);
复制代码


function isContain(a, b) {  for (let i in b) {    if (a[0] === b[i]) {      let tmp = true;      for (let j in a) {        if (a[j] !== b[~~i + ~~j]) {          tmp = false;        }      }      if (tmp) {        return i;      }    }  }  return -1;}
复制代码

setTimeout 与 setInterval 实现

setTimeout 模拟实现 setInterval

题目描述: setInterval 用来实现循环定时调用 可能会存在一定的问题 能用 setTimeout 解决吗


实现代码如下:


function mySetInterval(fn, t) {  let timerId = null;  function interval() {    fn();    timerId = setTimeout(interval, t); // 递归调用  }  timerId = setTimeout(interval, t); // 首次调用  return {    // 利用闭包的特性 保存timerId    cancel:() => {      clearTimeout(timerId)    }  }}
复制代码


// 测试var a = mySetInterval(()=>{  console.log(111);},1000)var b = mySetInterval(() => {  console.log(222)}, 1000)
// 终止定时器a.cancel()b.cancel()
复制代码


为什么要用 setTimeout 模拟实现 setIntervalsetInterval 的缺陷是什么?


setInterval(fn(), N);
复制代码


上面这句代码的意思其实是fn()将会在 N 秒之后被推入任务队列。在 setInterval 被推入任务队列时,如果在它前面有很多任务或者某个任务等待时间较长比如网络请求等,那么这个定时器的执行时间和我们预定它执行的时间可能并不一致


// 最常见的出现的就是,当我们需要使用 ajax 轮询服务器是否有新数据时,必定会有一些人会使用 setInterval,然而无论网络状况如何,它都会去一遍又一遍的发送请求,最后的间隔时间可能和原定的时间有很大的出入
// 做一个网络轮询,每一秒查询一次数据。let startTime = new Date().getTime();let count = 0;
setInterval(() => { let i = 0; while (i++ < 10000000); // 假设的网络延迟 count++; console.log( "与原设定的间隔时差了:", new Date().getTime() - (startTime + count * 1000), "毫秒" );}, 1000)
// 输出:// 与原设定的间隔时差了: 567 毫秒// 与原设定的间隔时差了: 552 毫秒// 与原设定的间隔时差了: 563 毫秒// 与原设定的间隔时差了: 554 毫秒(2次)// 与原设定的间隔时差了: 564 毫秒// 与原设定的间隔时差了: 602 毫秒// 与原设定的间隔时差了: 573 毫秒// 与原设定的间隔时差了: 633 毫秒
复制代码


再次强调 ,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。


setInterval(function, N)//即:每隔N秒把function事件推到消息队列中
复制代码



上图可见,setInterval 每隔 100ms 往队列中添加一个事件;100ms 后,添加 T1 定时器代码至队列中,主线程中还有任务在执行,所以等待,some event 执行结束后执行 T1定时器代码;又过了 100msT2 定时器被添加到队列中,主线程还在执行 T1 代码,所以等待;又过了 100ms,理论上又要往队列里推一个定时器代码,但由于此时 T2 还在队列中,所以 T3 不会被添加(T3 被跳过),结果就是此时被跳过;这里我们可以看到,T1 定时器执行结束后马上执行了 T2 代码,所以并没有达到定时器的效果


setInterval 有两个缺点


  • 使用setInterval时,某些间隔会被跳过

  • 可能多个定时器会连续执行


可以这么理解 :每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点

setInterval 模拟实现 setTimeout

const mySetTimeout = (fn, t) => {  const timer = setInterval(() => {    clearInterval(timer);    fn();  }, t);};
复制代码


// 测试// mySetTimeout(()=>{//   console.log(1);// },1000)
复制代码

数组中的数据根据 key 去重

给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:


const dedup = (data, getKey = () => {} ) => {  // todo}let data = [  { id: 1, v: 1 },  { id: 2, v: 2 },  { id: 1, v: 1 },];
// 以 id 作为排重 key,执行函数得到结果// data = [// { id: 1, v: 1 },// { id: 2, v: 2 },// ];
复制代码


实现


const dedup = (data, getKey = () => { }) => {    const dateMap = data.reduce((pre, cur) => {        const key = getKey(cur)        if (!pre[key]) {            pre[key] = cur        }        return pre    }, {})    return Object.values(dateMap)}
复制代码


使用


let data = [    { id: 1, v: 1 },    { id: 2, v: 2 },    { id: 1, v: 1 },];console.log(dedup(data, (item) => item.id))
// 以 id 作为排重 key,执行函数得到结果// data = [// { id: 1, v: 1 },// { id: 2, v: 2 },// ];
复制代码

实现 Array.isArray 方法

Array.myIsArray = function(o) {  return Object.prototype.toString.call(Object(o)) === '[object Array]';};
console.log(Array.myIsArray([])); // true
复制代码

实现一个简易的 MVVM

实现一个简易的MVVM我会分为这么几步来:


  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据

  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时 Dep 类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。

  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新

  4. 完成这些之后,我们还需要一个compile方法来将 HTML 模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用 compile 方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。

  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)

  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了


coding :


需要注意的点:


  • childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)

  • firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配

  • 这里面的正则真的不难,大家可以看一下


完整代码如下:


<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <meta http-equiv="X-UA-Compatible" content="ie=edge" />    <title>MVVM</title>  </head>  <body>    <div id="app">      <h3>姓名</h3>      <p>{{name}}</p>      <h3>年龄</h3>      <p>{{age}}</p>    </div>  </body></html><script>  document.addEventListener(    "DOMContentLoaded",    function () {      let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };      let vm = new Vue(opt);      setTimeout(() => {        opt.data.name = "jing";      }, 2000);    },    false  );  class Vue {    constructor(opt) {      this.opt = opt;      this.observer(opt.data);      let root = document.querySelector(opt.el);      this.compile(root);    }    observer(data) {      Object.keys(data).forEach((key) => {        let obv = new Dep();        data["_" + key] = data[key];
Object.defineProperty(data, key, { get() { Dep.target && obv.addSubNode(Dep.target); return data["_" + key]; }, set(newVal) { obv.update(newVal); data["_" + key] = newVal; }, }); }); } compile(node) { [].forEach.call(node.childNodes, (child) => { if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) { let key = RegExp.$1.trim(); child.innerHTML = child.innerHTML.replace( new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"), this.opt.data[key] ); Dep.target = child; this.opt.data[key]; Dep.target = null; } else if (child.firstElementChild) this.compile(child); }); } }
class Dep { constructor() { this.subNode = []; } addSubNode(node) { this.subNode.push(node); } update(newVal) { this.subNode.forEach((node) => { node.innerHTML = newVal; }); } }</script>
复制代码


简化版 2


function update(){  console.log('数据变化~~~ mock update view')}let obj = [1,2,3]// 变异方法 push shift unshfit reverse sort splice pop// Object.definePropertylet oldProto = Array.prototype;let proto = Object.create(oldProto); // 克隆了一分['push','shift'].forEach(item=>{  proto[item] = function(){    update();    oldProto[item].apply(this,arguments);  }})function observer(value){ // proxy reflect  if(Array.isArray(value)){    // AOP    return value.__proto__ = proto;    // 重写 这个数组里的push shift unshfit reverse sort splice pop  }  if(typeof value !== 'object'){    return value;  }  for(let key in value){    defineReactive(value,key,value[key]);  }}function defineReactive(obj,key,value){  observer(value); // 如果是对象 继续增加getter和setter  Object.defineProperty(obj,key,{    get(){        return value;    },    set(newValue){        if(newValue !== value){            observer(newValue);            value = newValue;            update();        }    }  })}observer(obj); // AOP// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控// obj.name.n = 100;obj.push(123);obj.push(456);console.log(obj);
复制代码

实现 Object.freeze

Object.freeze冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象


function myFreeze(obj){  // 判断参数是否为Object类型,如果是就封闭对象,循环遍历对象。去掉原型属性,将其writable特性设置为false  if(obj instanceof Object){    Object.seal(obj);  // 封闭对象    for(let key in obj){      if(obj.hasOwnProperty(key)){        Object.defineProperty(obj,key,{          writable:false   // 设置只读        })        // 如果属性值依然为对象,要通过递归来进行进一步的冻结        myFreeze(obj[key]);        }    }  }}
复制代码


参考 前端进阶面试题详细解答

实现 JSONP 方法

利用<script>标签不受跨域限制的特点,缺点是只能支持 get 请求


  • 创建script标签

  • 设置script标签的src属性,以问号传递参数,设置好回调函数callback名称

  • 插入到html文本中

  • 调用回调函数,res参数就是获取的数据


function jsonp({url,params,callback}) {  return new Promise((resolve,reject)=>{  let script = document.createElement('script')
window[callback] = function (data) { resolve(data) document.body.removeChild(script) } var arr = [] for(var key in params) { arr.push(`${key}=${params[key]}`) } script.type = 'text/javascript' script.src = `${url}?callback=${callback}&${arr.join('&')}` document.body.appendChild(script) })}
复制代码


// 测试用例jsonp({  url: 'http://suggest.taobao.com/sug',  callback: 'getData',  params: {    q: 'iphone手机',    code: 'utf-8'  },}).then(data=>{console.log(data)})
复制代码


  • 设置 CORS: Access-Control-Allow-Origin:*

  • postMessage

类数组转化为数组的方法

const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符[...arrayLike]// 2.Array.fromArray.from(arrayLike)// 3.Array.prototype.sliceArray.prototype.slice.call(arrayLike)// 4.Array.applyArray.apply(null, arrayLike)// 5.Array.prototype.concatArray.prototype.concat.apply([], arrayLike)
复制代码

用 Promise 实现图片的异步加载

let imageAsync=(url)=>{            return new Promise((resolve,reject)=>{                let img = new Image();                img.src = url;                img.οnlοad=()=>{                    console.log(`图片请求成功,此处进行通用操作`);                    resolve(image);                }                img.οnerrοr=(err)=>{                    console.log(`失败,此处进行失败的通用操作`);                    reject(err);                }            })        }
imageAsync("url").then(()=>{ console.log("加载成功");}).catch((error)=>{ console.log("加载失败");})
复制代码

原生实现

function ajax() {  let xhr = new XMLHttpRequest() //实例化,以调用方法  xhr.open('get', 'https://www.google.com')  //参数2,url。参数三:异步  xhr.onreadystatechange = () => {  //每当 readyState 属性改变时,就会调用该函数。    if (xhr.readyState === 4) {  //XMLHttpRequest 代理当前所处状态。      if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功        let string = request.responseText        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象        let object = JSON.parse(string)      }    }  }  request.send() //用于实际发出 HTTP 请求。不带参数为GET请求}
复制代码

手写 Promise.then

then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给 then 的函数暂存起来,等状态改变时再调用。


**那么,怎么保证后一个 **then** 里的方法在前一个 ****then**(可能是异步)结束之后再执行呢? 我们可以将传给 then 的函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的效果:


  • 承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了

  • 启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promiseresolve,让其状态变更,这又会依次调用新 promisecallbacks 数组里的方法,循环往复。。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve


then(onFulfilled, onReject){    // 保存前一个promise的this    const self = this;     return new MyPromise((resolve, reject) => {      // 封装前一个promise成功时执行的函数      let fulfilled = () => {        try{          const result = onFulfilled(self.value); // 承前          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后        }catch(err){          reject(err)        }      }      // 封装前一个promise失败时执行的函数      let rejected = () => {        try{          const result = onReject(self.reason);          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);        }catch(err){          reject(err)        }      }      switch(self.status){        case PENDING:           self.onFulfilledCallbacks.push(fulfilled);          self.onRejectedCallbacks.push(rejected);          break;        case FULFILLED:          fulfilled();          break;        case REJECT:          rejected();          break;      }    })   }
复制代码


注意:


  • 连续多个 then 里的回调方法是同步注册的,但注册到了不同的 callbacks 数组中,因为每次 then 都返回新的 promise 实例(参考上面的例子和图)

  • 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用 callbacks 数组中提前注册的回调

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。


a='34';b='1234567'; // 返回 2a='35';b='1234567'; // 返回 -1a='355';b='12354355'; // 返回 5isContain(a,b);

复制代码


function isContain(a, b) {  for (let i in b) {    if (a[0] === b[i]) {      let tmp = true;      for (let j in a) {        if (a[j] !== b[~~i + ~~j]) {          tmp = false;        }      }      if (tmp) {        return i;      }    }  }  return -1;}

复制代码

实现数组的 push 方法

let arr = [];Array.prototype.push = function() {    for( let i = 0 ; i < arguments.length ; i++){        this[this.length] = arguments[i] ;    }    return this.length;}
复制代码

debounce(防抖)

触发高频时间后 n 秒内函数只会执行一次,如果 n 秒内高频时间再次触发,则重新计算时间。


const debounce = (fn, time) => {  let timeout = null;  return function() {    clearTimeout(timeout)    timeout = setTimeout(() => {      fn.apply(this, arguments);    }, time);  }};
复制代码


防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

将 VirtualDom 转化为真实 DOM 结构

这是当前 SPA 应用的核心概念之一


// vnode结构:// {//   tag,//   attrs,//   children,// }
//Virtual DOM => DOMfunction render(vnode, container) { container.appendChild(_render(vnode));}function _render(vnode) { // 如果是数字类型转化为字符串 if (typeof vnode === 'number') { vnode = String(vnode); } // 字符串类型直接就是文本节点 if (typeof vnode === 'string') { return document.createTextNode(vnode); } // 普通DOM const dom = document.createElement(vnode.tag); if (vnode.attrs) { // 遍历属性 Object.keys(vnode.attrs).forEach(key => { const value = vnode.attrs[key]; dom.setAttribute(key, value); }) } // 子数组进行递归操作 vnode.children.forEach(child => render(child, dom)); return dom;}
复制代码

实现类的继承

实现类的继承-简版

类的继承在几年前是重点内容,有 n 种继承方式各有优劣,es6 普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。


// 寄生组合继承function Parent(name) {  this.name = name}Parent.prototype.say = function() {  console.log(this.name + ` say`);}Parent.prototype.play = function() {  console.log(this.name + ` play`);}
function Child(name, parent) { // 将父类的构造函数绑定在子类上 Parent.call(this, parent) this.name = name}
/** 1. 这一步不用Child.prototype = Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性3. Object.create是创建了父类原型的副本,与父类原型完全隔离*/Child.prototype = Object.create(Parent.prototype);Child.prototype.say = function() { console.log(this.name + ` say`);}
// 注意记得把子类的构造指向子类本身Child.prototype.constructor = Child;
复制代码


// 测试var parent = new Parent('parent');parent.say() 
var child = new Child('child');child.say() child.play(); // 继承父类的方法
复制代码

ES5 实现继承-详细

第一种方式是借助 call 实现继承


function Parent1(){    this.name = 'parent1';}function Child1(){    Parent1.call(this);    this.type = 'child1'    }console.log(new Child1);
复制代码


这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类中一旦存在方法那么子类无法继承。那么引出下面的方法


第二种方式借助原型链实现继承:


function Parent2() {    this.name = 'parent2';    this.play = [1, 2, 3]  }  function Child2() {    this.type = 'child2';  }  Child2.prototype = new Parent2();
console.log(new Child2());
复制代码


看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:


var s1 = new Child2();  var s2 = new Child2();  s1.play.push(4);  console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]
复制代码


明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象


第三种方式:将前两种组合:


function Parent3 () {    this.name = 'parent3';    this.play = [1, 2, 3];  }  function Child3() {    Parent3.call(this);    this.type = 'child3';  }  Child3.prototype = new Parent3();  var s3 = new Child3();  var s4 = new Child3();  s3.play.push(4);  console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]
复制代码


之前的问题都得以解决。但是这里又徒增了一个新问题,那就是 Parent3 的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?


第四种方式: 组合继承的优化 1


function Parent4 () {    this.name = 'parent4';    this.play = [1, 2, 3];  }  function Child4() {    Parent4.call(this);    this.type = 'child4';  }  Child4.prototype = Parent4.prototype;
复制代码


这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下


var s3 = new Child4();  var s4 = new Child4();  console.log(s3)
复制代码


子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4。


第五种方式(最推荐使用):优化 2


function Parent5 () {    this.name = 'parent5';    this.play = [1, 2, 3];  }  function Child5() {    Parent5.call(this);    this.type = 'child5';  }  Child5.prototype = Object.create(Parent5.prototype);  Child5.prototype.constructor = Child5;
复制代码


这是最推荐的一种方式,接近完美的继承。

Promise.race

Promise.race = function(promiseArr) {  return new Promise((resolve, reject) => {    promiseArr.forEach(p => {      // 如果不是Promise实例需要转化为Promise实例      Promise.resolve(p).then(        val => resolve(val),        err => reject(err),      )    })  })}
复制代码

实现一下 hash 路由

基础的html代码:


<html>  <style>    html, body {      margin: 0;      height: 100%;    }    ul {      list-style: none;      margin: 0;      padding: 0;      display: flex;      justify-content: center;    }    .box {      width: 100%;      height: 100%;      background-color: red;    }  </style>  <body>  <ul>    <li>      <a href="#red">红色</a>    </li>    <li>      <a href="#green">绿色</a>    </li>    <li>      <a href="#purple">紫色</a>    </li>  </ul>  </body></html>
复制代码


简单实现:


<script>  const box = document.getElementsByClassName('box')[0];  const hash = location.hash  window.onhashchange = function (e) {    const color = hash.slice(1)    box.style.background = color  }</script>
复制代码


封装成一个 class:


<script>  const box = document.getElementsByClassName('box')[0];  const hash = location.hash  class HashRouter {    constructor (hashStr, cb) {      this.hashStr = hashStr      this.cb = cb      this.watchHash()      this.watch = this.watchHash.bind(this)      window.addEventListener('hashchange', this.watch)    }    watchHash () {      let hash = window.location.hash.slice(1)      this.hashStr = hash      this.cb(hash)    }  }  new HashRouter('red', (color) => {    box.style.background = color  })</script>
复制代码

实现 findIndex 方法

var users = [  {id: 1, name: '张三'},  {id: 2, name: '张三'},  {id: 3, name: '张三'},  {id: 4, name: '张三'}]
Array.prototype.myFindIndex = function (callback) { // var callback = function (item, index) { return item.id === 4 } for (var i = 0; i < this.length; i++) { if (callback(this[i], i)) { // 这里返回 return i } }}
var ret = users.myFind(function (item, index) { return item.id === 2})
console.log(ret)
复制代码

throttle(节流)

高频时间触发,但 n 秒内只会执行一次,所以节流会稀释函数的执行频率。


const throttle = (fn, time) => {  let flag = true;  return function() {    if (!flag) return;    flag = false;    setTimeout(() => {      fn.apply(this, arguments);      flag = true;    }, time);  }}
复制代码


节流常应用于鼠标不断点击触发、监听滚动事件。


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
社招前端二面必会手写面试题总结_JavaScript_helloworld1024fd_InfoQ写作社区