• 技术文章 >头条

    前端面试常考的各种重点手写汇总!

    藏色散人藏色散人2021-01-12 14:29:38转载1283
    本文涵盖了前端面试常考的各种重点手写。

    推荐:
    2021年PHP面试题大汇总(收藏)
    2021年大前端面试题汇总(收藏)

    建议优先掌握:

    1. 手写instanceof

    instanceof作用:

    判断一个实例是否是其父类或者祖先类型的实例。

    instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

     let myInstanceof = (target,origin) => {
         while(target) {
             if(target.__proto__===origin.prototype) {
                return true
             }
             target = target.__proto__
         }
         return false
     }
     let a = [1,2,3]
     console.log(myInstanceof(a,Array));  // true
     console.log(myInstanceof(a,Object));  // true

    2. 实现数组的map方法

    数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。

    用法:

    const a = [1, 2, 3, 4];
    const b = array1.map(x => x * 2);
    console.log(b);   // Array [2, 4, 6, 8]

    原生实现:

     Array.prototype.myMap = function(fn, thisValue) {
         let res = []
         thisValue = thisValue||[]
         let arr = this
         for(let i in arr) {
            res.push(fn(arr[i]))
         }
         return res
     }

    3. reduce实现数组的map方法

    利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握

    Array.prototype.myMap = function(fn,thisValue){
         var res = [];
         thisValue = thisValue||[];
         this.reduce(function(pre,cur,index,arr){
             return res.push(fn.call(thisValue,cur,index,arr));
         },[]);
         return res;
    }
    
    var arr = [2,3,1,5];
    arr.myMap(function(item,index,arr){
     console.log(item,index,arr);
    })

    4. 手写数组的reduce方法

    reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

    参数:

     function reduce(arr, cb, initialValue){
         var num = initValue == undefined? num = arr[0]: initValue;
         var i = initValue == undefined? 1: 0
         for (i; i< arr.length; i++){
            num = cb(num,arr[i],i)
         }
         return num
     }
     
     function fn(result, currentValue, index){
         return result + currentValue
     }
     
     var arr = [2,3,4,5]
     var b = reduce(arr, fn,10) 
     var c = reduce(arr, fn)
     console.log(b)   // 24

    5. 数组扁平化

    数组扁平化就是把多维数组转化成一维数组

    1. es6提供的新方法 flat(depth)

    let a = [1,[2,3]]; 
    a.flat(); // [1,2,3] 
    a.flat(1); //[1,2,3]

    其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

    let a = [1,[2,3,[4,[5]]]]; 
    a.flat(Infinity); // [1,2,3,4,5]  a是4维数组

    2. 利用cancat

    function flatten(arr) {
         var res = [];
         for (let i = 0, length = arr.length; i < length; i++) {
         if (Array.isArray(arr[i])) {
         res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
         //res.push(...flatten(arr[i])); //或者用扩展运算符 
         } else {
             res.push(arr[i]);
           }
         }
         return res;
     }
     let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
    flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

    6. 函数柯里化

    柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

    当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

    有两种思路:

    将这两点结合一下,实现一个简单 curry 函数:

    /**
     * 将函数柯里化
     * @param fn    待柯里化的原函数
     * @param len   所需的参数个数,默认为原函数的形参个数
     */
    function curry(fn,len = fn.length) {
     return _curry.call(this,fn,len)
    }
    
    /**
     * 中转函数
     * @param fn    待柯里化的原函数
     * @param len   所需的参数个数
     * @param args  已接收的参数列表
     */
    function _curry(fn,len,...args) {
        return function (...params) {
             let _args = [...args,...params];
             if(_args.length >= len){
                 return fn.apply(this,_args);
             }else{
              return _curry.call(this,fn,len,..._args)
             }
        }
    }

    我们来验证一下:

    let _fn = curry(function(a,b,c,d,e){
     console.log(a,b,c,d,e)
    });
    
    _fn(1,2,3,4,5);     // print: 1,2,3,4,5
    _fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
    _fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
    _fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

    我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。

    比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:

    直接看一下官网的例子:

    839d3bce45351ff73f4cb368b221afa.png

    接下来我们来思考,如何实现占位符的功能。

    对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。

    而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符

    使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。

    直接上代码:

    /**
     * @param  fn           待柯里化的函数
     * @param  length       需要的参数个数,默认为函数的形参个数
     * @param  holder       占位符,默认当前柯里化函数
     * @return {Function}   柯里化后的函数
     */
    function curry(fn,length = fn.length,holder = curry){
     return _curry.call(this,fn,length,holder,[],[])
    }
    /**
     * 中转函数
     * @param fn            柯里化的原函数
     * @param length        原函数需要的参数个数
     * @param holder        接收的占位符
     * @param args          已接收的参数列表
     * @param holders       已接收的占位符位置列表
     * @return {Function}   继续柯里化的函数 或 最终结果
     */
    function _curry(fn,length,holder,args,holders){
     return function(..._args){
     //将参数复制一份,避免多次操作同一函数导致参数混乱
     let params = args.slice();
     //将占位符位置列表复制一份,新增加的占位符增加至此
     let _holders = holders.slice();
     //循环入参,追加参数 或 替换占位符
     _args.forEach((arg,i)=>{
     //真实参数 之前存在占位符 将占位符替换为真实参数
     if (arg !== holder && holders.length) {
         let index = holders.shift();
         _holders.splice(_holders.indexOf(index),1);
         params[index] = arg;
     }
     //真实参数 之前不存在占位符 将参数追加到参数列表中
     else if(arg !== holder && !holders.length){
         params.push(arg);
     }
     //传入的是占位符,之前不存在占位符 记录占位符的位置
     else if(arg === holder && !holders.length){
         params.push(arg);
         _holders.push(params.length - 1);
     }
     //传入的是占位符,之前存在占位符 删除原占位符位置
     else if(arg === holder && holders.length){
        holders.shift();
     }
     });
     // params 中前 length 条记录中不包含占位符,执行函数
     if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
     return fn.apply(this,params);
     }else{
     return _curry.call(this,fn,length,holder,params,_holders)
     }
     }
    }

    验证一下:;

    let fn = function(a, b, c, d, e) {
     console.log([a, b, c, d, e]);
    }
    
    let _ = {}; // 定义占位符
    let _fn = curry(fn,5,_);  // 将函数柯里化,指定所需的参数个数,指定所需的占位符
    
    _fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
    _fn(_, 2, 3, 4, 5)(1);              // print: 1,2,3,4,5
    _fn(1, _, 3, 4, 5)(2);              // print: 1,2,3,4,5
    _fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
    _fn(1, _, _, 4)(_, 3)(2)(5);        // print: 1,2,3,4,5
    _fn(_, 2)(_, _, 4)(1)(3)(5);        // print: 1,2,3,4,5

    至此,我们已经完整实现了一个 curry 函数~~

    7. 实现深拷贝

    浅拷贝和深拷贝的区别:

    浅拷贝:只拷贝一层,更深层的对象级别的只拷贝引用

    深拷贝:拷贝多层,每一级别的数据都会拷贝。这样更改拷贝值就不影响另外的对象

    ES6浅拷贝方法:Object.assign(target,...sources)

    let obj={
     id:1,
     name:'Tom',
     msg:{
     age:18
     }
    }
    let o={}
    //实现深拷贝  递归    可以用于生命游戏那个题对二维数组的拷贝,
    //但比较麻烦,因为已知元素都是值,直接复制就行,无需判断
    function deepCopy(newObj,oldObj){
         for(var k in oldObj){
             let item=oldObj[k]
             //判断是数组?对象?简单类型?
             if(item instanceof Array){
                 newObj[k]=[]
                 deepCopy(newObj[k],item)
             }else if(item instanceof Object){
                 newObj[k]={}
                 deepCopy(newObj[k],item)
             }else{  //简单数据类型,直接赋值
                 newObj[k]=item
             }
         }
    }

    8. 手写call, apply, bind

    手写call

    Function.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上
     if(typeof this !=="function"){   // 这里if其实没必要,会自动抛出错误
        throw new Error("不是函数")
     }
     const obj=context||window   //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
     obj.fn=this      //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
     const arg=[...arguments].slice(1)   //第一个为obj所以删除,伪数组转为数组
     res=obj.fn(...arg)
     delete obj.fn   // 不删除会导致context属性越来越多
     return res
    }
    //用法:f.call(obj,arg1)
    function f(a,b){
     console.log(a+b)
     console.log(this.name)
    }
    let obj={
     name:1
    }
    f.myCall(obj,1,2) //否则this指向window
    
    obj.greet.call({name: 'Spike'}) //打出来的是 Spike

    手写apply(arguments[this, [参数1,参数2.....] ])

    Function.prototype.myApply=function(context){  // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
     let obj=context||window
     obj.fn=this
     const arg=arguments[1]||[]    //若有参数,得到的是数组
     let res=obj.fn(...arg)
     delete obj.fn
     return res
    } 
    function f(a,b){
     console.log(a,b)
     console.log(this.name)
    }
    let obj={
     name:'张三'
    }
    f.myApply(obj,[1,2])  //arguments[1]

    手写bind

    this.value = 2
    var foo = {
     value: 1
    };
    var bar = function(name, age, school){
     console.log(name) // 'An'
     console.log(age) // 22
     console.log(school) // '家里蹲大学'
    }
    var result = bar.bind(foo, 'An') //预置了部分参数'An'
    result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中

    简单版本

    Function.prototype.bind = function(context, ...outerArgs) {
     var fn = this;
     return function(...innerArgs) {   //返回了一个函数,...rest为实际调用时传入的参数
     return fn.apply(context,[...outerArgs, ...innerArgs]);  //返回改变了this的函数,
     //参数合并
     }
    }

    new失败的原因:

    例:

    // 声明一个上下文
    let thovino = {
     name: 'thovino'
    }
    
    // 声明一个构造函数
    let eat = function (food) {
     this.food = food
     console.log(`${this.name} eat ${this.food}`)
    }
    eat.prototype.sayFuncName = function () {
     console.log('func name : eat')
    }
    
    // bind一下
    let thovinoEat = eat.bind(thovino)
    let instance = new thovinoEat('orange')  //实际上orange放到了thovino里面
    console.log('instance:', instance) // {}

    生成的实例是个空对象

    new操作符执行时,我们的thovinoEat函数可以看作是这样:

    function thovinoEat (...innerArgs) {
     eat.call(thovino, ...outerArgs, ...innerArgs)
    }

    在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)时,这里的obj是new操作符自己创建的那个简单空对象{},但它其实并没有替换掉thovinoEat函数内部的那个上下文对象thovino。这已经超出了call的能力范围,因为这个时候要替换的已经不是thovinoEat函数内部的this指向,而应该是thovino对象。

    换句话说,我们希望的是new操作符将eat内的this指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功

    可new可继承版本

    Function.prototype.bind = function (context, ...outerArgs) {
     let that = this;
    
    function res (...innerArgs) {
         if (this instanceof res) {
             // new操作符执行时
             // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
             that.call(this, ...outerArgs, ...innerArgs)
         } else {
             // 普通bind
             that.call(context, ...outerArgs, ...innerArgs)
         }
         }
         res.prototype = this.prototype //!!!
         return res
    }

    9. 手动实现new

    new的过程文字描述:

    function Person(name,age){
     this.name=name
     this.age=age
    }
    Person.prototype.sayHi=function(){
     console.log('Hi!我是'+this.name)
    }
    let p1=new Person('张三',18)
    
    ////手动实现new
    function create(){
     let obj={}
     //获取构造函数
     let fn=[].shift.call(arguments)  //将arguments对象提出来转化为数组,arguments并不是数组而是对象    !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果      或者let arg = [].slice.call(arguments,1)
     obj.__proto__=fn.prototype
     let res=fn.apply(obj,arguments)    //改变this指向,为实例添加方法和属性
     //确保返回的是一个对象(万一fn不是构造函数)
     return typeof res==='object'?res:obj
    }
    
    let p2=create(Person,'李四',19)
    p2.sayHi()

    细节:

    [].shift.call(arguments)  也可写成:
     let arg=[...arguments]
     let fn=arg.shift()  //使得arguments能调用数组方法,第一个参数为构造函数
     obj.__proto__=fn.prototype
     //改变this指向,为实例添加方法和属性
     let res=fn.apply(obj,arg)

    10. 手写promise(常见promise.all, promise.race)

    // Promise/A+ 规范规定的三种状态
    const STATUS = {
     PENDING: 'pending',
     FULFILLED: 'fulfilled',
     REJECTED: 'rejected'
    }
    
    class MyPromise {
     // 构造函数接收一个执行回调
     constructor(executor) {
         this._status = STATUS.PENDING // Promise初始状态
         this._value = undefined // then回调的值
         this._resolveQueue = [] // resolve时触发的成功队列
         this._rejectQueue = [] // reject时触发的失败队列
        
     // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
     const resolve = value => {
         const run = () => {
             // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
             if (this._status === STATUS.PENDING) {
                 this._status = STATUS.FULFILLED // 更改状态
                 this._value = value // 储存当前值,用于then回调
                
                 // 执行resolve回调
                 while (this._resolveQueue.length) {
                     const callback = this._resolveQueue.shift()
                     callback(value)
                 }
             }
         }
         //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
         setTimeout(run)
     }
    
     // 同 resolve
     const reject = value => {
         const run = () => {
             if (this._status === STATUS.PENDING) {
             this._status = STATUS.REJECTED
             this._value = value
            
             while (this._rejectQueue.length) {
                 const callback = this._rejectQueue.shift()
                 callback(value)
             }
         }
     }
         setTimeout(run)
     }
    
         // new Promise()时立即执行executor,并传入resolve和reject
         executor(resolve, reject)
     }
    
     // then方法,接收一个成功的回调和一个失败的回调
     function then(onFulfilled, onRejected) {
      // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
      typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
      typeof onRejected !== 'function' ? onRejected = error => error : null
    
      // then 返回一个新的promise
      return new MyPromise((resolve, reject) => {
        const resolveFn = value => {
          try {
            const x = onFulfilled(value)
            // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          } catch (error) {
            reject(error)
          }
        }
      }
    }
    
      const rejectFn = error => {
          try {
            const x = onRejected(error)
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
          } catch (error) {
            reject(error)
          }
        }
    
        switch (this._status) {
          case STATUS.PENDING:
            this._resolveQueue.push(resolveFn)
            this._rejectQueue.push(rejectFn)
            break;
          case STATUS.FULFILLED:
            resolveFn(this._value)
            break;
          case STATUS.REJECTED:
            rejectFn(this._value)
            break;
        }
     })
     }
     catch (rejectFn) {
      return this.then(undefined, rejectFn)
    }
    // promise.finally方法
    finally(callback) {
      return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
        MyPromise.resolve(callback()).then(() => error)
      })
    }
    
     // 静态resolve方法
     static resolve(value) {
          return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
      }
    
     // 静态reject方法
     static reject(error) {
          return new MyPromise((resolve, reject) => reject(error))
        }
    
     // 静态all方法
     static all(promiseArr) {
          let count = 0
          let result = []
          return new MyPromise((resolve, reject) =>       {
            if (!promiseArr.length) {
              return resolve(result)
            }
            promiseArr.forEach((p, i) => {
              MyPromise.resolve(p).then(value => {
                count++
                result[i] = value
                if (count === promiseArr.length) {
                  resolve(result)
                }
              }, error => {
                reject(error)
              })
            })
          })
        }
    
     // 静态race方法
     static race(promiseArr) {
          return new MyPromise((resolve, reject) => {
            promiseArr.forEach(p => {
              MyPromise.resolve(p).then(value => {
                resolve(value)
              }, error => {
                reject(error)
              })
            })
          })
        }
    }

    11. 手写原生AJAX

    步骤

    不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。

    了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

    version 1.0:

    myButton.addEventListener('click', function () {
      ajax()
    })
    
    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实现

    function ajax(url) {
      const p = new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest()
        xhr.open('get', url)
        xhr.onreadystatechange = () => {
          if (xhr.readyState == 4) {
            if (xhr.status >= 200 && xhr.status <= 300) {
              resolve(JSON.parse(xhr.responseText))
            } else {
              reject('请求出错')
            }
          }
        }
        xhr.send()  //发送hppt请求
      })
      return p
    }
    let url = '/data.json'
    ajax(url).then(res => console.log(res))
      .catch(reason => console.log(reason))

    12. 手写节流防抖函数

    函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。

    节流:连续触发事件但是在 n 秒中只执行一次函数

    例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

    防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

    例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

    防抖的实现:

    function debounce(fn, delay) {
         if(typeof fn!=='function') {
            throw new TypeError('fn不是函数')
         }
         let timer; // 维护一个 timer
         return function () {
             var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
             var args = arguments;
             if (timer) {
                clearTimeout(timer);
             }
             timer = setTimeout(function () {
                fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
             }, delay);
         };
    }
    
    // 调用
    input1.addEventListener('keyup', debounce(() => {
     console.log(input1.value)
    }), 600)

    节流的实现:

    function throttle(fn, delay) {
      let timer;
      return function () {
        var _this = this;
        var args = arguments;
        if (timer) {
          return;
        }
        timer = setTimeout(function () {
          fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
          // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
          timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
        }, delay)
      }
    }
    
    p1.addEventListener('drag', throttle((e) => {
      console.log(e.offsetX, e.offsetY)
    }, 100))

    13. 手写Promise加载图片

    function getData(url) {
      return new Promise((resolve, reject) => {
        $.ajax({
          url,
          success(data) {
            resolve(data)
          },
          error(err) {
            reject(err)
          }
        })
      })
    }
    const url1 = './data1.json'
    const url2 = './data2.json'
    const url3 = './data3.json'
    getData(url1).then(data1 => {
      console.log(data1)
      return getData(url2)
    }).then(data2 => {
      console.log(data2)
      return getData(url3)
    }).then(data3 =>
      console.log(data3)
    ).catch(err =>
      console.error(err)
    )

    14. 函数实现一秒钟输出一个数

    for(let i=0;i<=10;i++){   //用var打印的都是11
     setTimeout(()=>{
        console.log(i);
     },1000*i)
    }

    15. 创建10个标签,点击的时候弹出来对应的序号?

    var a
    for(let i=0;i<10;i++){
     a=document.createElement('a')
     a.innerHTML=i+'<br>'
     a.addEventListener('click',function(e){
         console.log(this)  //this为当前点击的<a>
         e.preventDefault()  //如果调用这个方法,默认事件行为将不再触发。
         //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
         alert(i)
     })
     const d=document.querySelector('p')
     d.appendChild(a)  //append向一个已存在的元素追加该元素。
    }
    声明:本文转载于:segmentfault,如有侵犯,请联系admin@php.cn删除
    专题推荐:javascript 前端
    上一篇:五款漂亮的2021倒计时动态特效源码(免费下载) 下一篇:推荐10个Chrome开发工具和技巧
    大前端线上培训班

    相关文章推荐

    • 20个常见jQuery面试题和答案(分享)• 15个Vue.js高级面试问题• 2021最新SSH框架面试题汇总• 分享一次腾讯Go开发岗位面试经过

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网