前端一面常考手写面试题整理
- 2023-02-13  浙江
- 本文字数:11181 字 - 阅读完需:约 37 分钟 
请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式
<div>  <span>    <a></a>  </span>  <span>    <a></a>    <a></a>  </span></div>
把上面dom结构转成下面的JSON格式
{  tag: 'DIV',  children: [    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] }      ]    },    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] },        { tag: 'A', children: [] }      ]    }  ]}
实现代码如下:
function dom2Json(domtree) {  let obj = {};  obj.name = domtree.tagName;  obj.children = [];  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));  return obj;}
转化为驼峰命名
var s1 = "get-element-by-id"
// 转化为 getElementById
var f = function(s) {    return s.replace(/-\w/g, function(x) {        return x.slice(1).toUpperCase();    })}
修改嵌套层级很深对象的 key
// 有一个嵌套层次很深的对象,key 都是 a_b 形式 ,需要改成 ab 的形式,注意不能用递归。
const a = {  a_y: {    a_z: {      y_x: 6    },    b_c: 1  }}// {//   ay: {//     az: {//       yx: 6//     },//     bc: 1//   }// }
方法 1:序列化 JSON.stringify + 正则匹配
const regularExpress = (obj) => {  try {    const str = JSON.stringify(obj).replace(/_/g, "");    return JSON.parse(str);  } catch (error) {    return obj;  }};;
方法 2:递归
const recursion = (obj) => {  const keys = Object.keys(obj);  keys.forEach((key) => {    const newKey = key.replace(/_/g, "");    obj[newKey] = recursion(obj[key]);    delete obj[key];  });  return obj;};
reduce 用法汇总
语法
array.reduce(function(total, currentValue, currentIndex, arr), initialValue);/*  total: 必需。初始值, 或者计算结束后的返回值。  currentValue: 必需。当前元素。  currentIndex: 可选。当前元素的索引;                       arr: 可选。当前元素所属的数组对象。  initialValue: 可选。传递给函数的初始值,相当于total的初始值。*/
reduceRight()该方法用法与reduce()其实是相同的,只是遍历的顺序相反,它是从数组的最后一项开始,向前遍历到第一项
1. 数组求和
const arr = [12, 34, 23];const sum = arr.reduce((total, num) => total + num);
// 设定初始值求和const arr = [12, 34, 23];const sum = arr.reduce((total, num) => total + num, 10);  // 以10为初始值求和
// 对象数组求和var result = [  { subject: 'math', score: 88 },  { subject: 'chinese', score: 95 },  { subject: 'english', score: 80 }];const sum = result.reduce((accumulator, cur) => accumulator + cur.score, 0); const sum = result.reduce((accumulator, cur) => accumulator + cur.score, -10);  // 总分扣除10分
2. 数组最大值
const a = [23,123,342,12];const max = a.reduce((pre,next)=>pre>cur?pre:cur,0); // 342
3. 数组转对象
var streams = [{name: '技术', id: 1}, {name: '设计', id: 2}];var obj = streams.reduce((accumulator, cur) => {accumulator[cur.id] = cur; return accumulator;}, {});
4. 扁平一个二维数组
var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];var res = arr.reduce((x, y) => x.concat(y), []);
5. 数组去重
实现的基本原理如下:
① 初始化一个空数组② 将需要去重处理的数组中的第1项在初始化数组中查找,如果找不到(空数组中肯定找不到),就将该项添加到初始化数组中③ 将需要去重处理的数组中的第2项在初始化数组中查找,如果找不到,就将该项继续添加到初始化数组中④ ……⑤ 将需要去重处理的数组中的第n项在初始化数组中查找,如果找不到,就将该项继续添加到初始化数组中⑥ 将这个初始化数组返回
var newArr = arr.reduce(function (prev, cur) {    prev.indexOf(cur) === -1 && prev.push(cur);    return prev;},[]);
6. 对象数组去重
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)}
7. 求字符串中字母出现的次数
const str = 'sfhjasfjgfasjuwqrqadqeiqsajsdaiwqdaklldflas-cmxzmnha';
const res = str.split('').reduce((pre,next)=>{ pre[next] ? pre[next]++ : pre[next] = 1 return pre },{})
// 结果-: 1a: 8c: 1d: 4e: 1f: 4g: 1h: 2i: 2j: 4k: 1l: 3m: 2n: 1q: 5r: 1s: 6u: 1w: 2x: 1z: 1
8. compose 函数
redux compose源码实现
function compose(...funs) {    if (funs.length === 0) {        return arg => arg;    }    if (funs.length === 1) {       return funs[0];    }    return funs.reduce((a, b) => (...arg) => a(b(...arg)))}
对象数组列表转成树形结构(处理菜单)
[    {        id: 1,        text: '节点1',        parentId: 0 //这里用0表示为顶级节点    },    {        id: 2,        text: '节点1_1',        parentId: 1 //通过这个字段来确定子父级    }    ...]
转成[    {        id: 1,        text: '节点1',        parentId: 0,        children: [            {                id:2,                text: '节点1_1',                parentId:1            }        ]    }]
实现代码如下:
function listToTree(data) {  let temp = {};  let treeData = [];  for (let i = 0; i < data.length; i++) {    temp[data[i].id] = data[i];  }  for (let i in temp) {    if (+temp[i].parentId != 0) {      if (!temp[temp[i].parentId].children) {        temp[temp[i].parentId].children = [];      }      temp[temp[i].parentId].children.push(temp[i]);    } else {      treeData.push(temp[i]);    }  }  return treeData;}
怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少(字节)
第一版 时间复杂度为 O(n^2)
function getTenNum(testArray, n) {  let result = [];  for (let i = 0; i < n; ++i) {    const random = Math.floor(Math.random() * testArray.length);    const cur = testArray[random];    if (result.includes(cur)) {      i--;      break;    }    result.push(cur);  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
第二版 标记法 / 自定义属性法 时间复杂度为 O(n)
function getTenNum(testArray, n) {  let hash = {};  let result = [];  let ranNum = n;  while (ranNum > 0) {    const ran = Math.floor(Math.random() * testArray.length);    if (!hash[ran]) {      hash[ran] = true;      result.push(ran);      ranNum--;    }  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
第三版 交换法 时间复杂度为 O(n)
function getTenNum(testArray, n) {  const cloneArr = [...testArray];  let result = [];  for (let i = 0; i < n; i++) {    debugger;    const ran = Math.floor(Math.random() * (cloneArr.length - i));    result.push(cloneArr[ran]);    cloneArr[ran] = cloneArr[cloneArr.length - i - 1];  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
值得一提的是操作数组的时候使用交换法 这种思路在算法里面很常见
最终版 边遍历边删除 时间复杂度为 O(n)
function getTenNum(testArray, n) {  const cloneArr = [...testArray];  let result = [];  for (let i = 0; i < n; ++i) {    const random = Math.floor(Math.random() * cloneArr.length);    const cur = cloneArr[random];    result.push(cur);    cloneArr.splice(random, 1);  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
参考 前端进阶面试题详细解答
判断括号字符串是否有效(小米)
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:- 左括号必须用相同类型的右括号闭合。- 左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"输出:true
示例 2:
输入:s = "()[]{}"输出:true
示例 3:
输入:s = "(]"输出:false
答案
const isValid = function (s) {  if (s.length % 2 === 1) {    return false;  }  const regObj = {    "{": "}",    "(": ")",    "[": "]",  };  let stack = [];  for (let i = 0; i < s.length; i++) {    if (s[i] === "{" || s[i] === "(" || s[i] === "[") {      stack.push(s[i]);    } else {      const cur = stack.pop();      if (s[i] !== regObj[cur]) {        return false;      }    }  }
  if (stack.length) {    return false;  }
  return true;};
实现有并行限制的 Promise 调度器
题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个
addTask(1000,"1"); addTask(500,"2"); addTask(300,"3"); addTask(400,"4"); 的输出顺序是:2 3 1 4
 整个的完整执行流程:
一开始1、2两个任务开始执行500ms时,2任务执行完毕,输出2,任务3开始执行800ms时,3任务执行完毕,输出3,任务4开始执行1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行1200ms时,4任务执行完毕,输出4
实现代码如下:
class Scheduler {  constructor(limit) {    this.queue = [];    this.maxCount = limit;    this.runCounts = 0;  }  add(time, order) {    const promiseCreator = () => {      return new Promise((resolve, reject) => {        setTimeout(() => {          console.log(order);          resolve();        }, time);      });    };    this.queue.push(promiseCreator);  }  taskStart() {    for (let i = 0; i < this.maxCount; i++) {      this.request();    }  }  request() {    if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {      return;    }    this.runCounts++;    this.queue      .shift()()      .then(() => {        this.runCounts--;        this.request();      });  }}const scheduler = new Scheduler(2);const addTask = (time, order) => {  scheduler.add(time, order);};addTask(1000, "1");addTask(500, "2");addTask(300, "3");addTask(400, "4");scheduler.taskStart();
实现一个队列
基于链表结构实现队列
const LinkedList = require('./实现一个链表结构')
// 用链表默认使用数组来模拟队列,性能更佳class Queue {  constructor() {    this.ll = new LinkedList()  }  // 向队列中添加  offer(elem) {    this.ll.add(elem)  }  // 查看第一个  peek() {    return this.ll.get(0)  }  // 队列只能从头部删除  remove() {    return this.ll.remove(0)  }}
var queue = new Queue()
queue.offer(1)queue.offer(2)queue.offer(3)var removeVal = queue.remove(3)
console.log(queue.ll,'queue.ll')console.log(removeVal,'queue.remove')console.log(queue.peek(),'queue.peek')
树形结构转成列表(处理菜单)
[    {        id: 1,        text: '节点1',        parentId: 0,        children: [            {                id:2,                text: '节点1_1',                parentId:1            }        ]    }]转成[    {        id: 1,        text: '节点1',        parentId: 0 //这里用0表示为顶级节点    },    {        id: 2,        text: '节点1_1',        parentId: 1 //通过这个字段来确定子父级    }    ...]
实现代码如下:
function treeToList(data) {  let res = [];  const dfs = (tree) => {    tree.forEach((item) => {      if (item.children) {        dfs(item.children);        delete item.children;      }      res.push(item);    });  };  dfs(data);  return res;}
实现千位分隔符
// 保留三位小数parseToMoney(1234.56); // return '1,234.56'parseToMoney(123456789); // return '123,456,789'parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {  num = parseFloat(num.toFixed(3));  let [integer, decimal] = String.prototype.split.call(num, '.');  integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');  return integer + '.' + (decimal ? decimal : '');}
字符串最长的不重复子串
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"输出: 3解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"输出: 1解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"输出: 3解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""输出: 0
答案
const lengthOfLongestSubstring = function (s) {  if (s.length === 0) {    return 0;  }
  let left = 0;  let right = 1;  let max = 0;  while (right <= s.length) {    let lr = s.slice(left, right);    const index = lr.indexOf(s[right]);
    if (index > -1) {      left = index + left + 1;    } else {      lr = s.slice(left, right + 1);      max = Math.max(max, lr.length);    }    right++;  }  return max;};
解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';parseParam(url)/* 结果{ user: 'anonymous',  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型  city: '北京', // 中文需解码  enabled: true, // 未指定值得 key 约定为 true}*/
function parseParam(url) {  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来  const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中  let paramsObj = {};  // 将 params 存到对象中  paramsArr.forEach(param => {    if (/=/.test(param)) { // 处理有 value 的参数      let [key, val] = param.split('='); // 分割 key 和 value      val = decodeURIComponent(val); // 解码      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值        paramsObj[key] = [].concat(paramsObj[key], val);      } else { // 如果对象没有这个 key,创建 key 并设置值        paramsObj[key] = val;      }    } else { // 处理没有 value 的参数      paramsObj[param] = true;    }  })
  return paramsObj;}
请实现一个 add 函数,满足以下功能
add(1);             // 1add(1)(2);      // 3add(1)(2)(3);// 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
function add(...args) {  // 在内部声明一个函数,利用闭包的特性保存并收集所有的参数值  let fn = function(...newArgs) {   return add.apply(null, args.concat(newArgs))  }
  // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回  fn.toString = function() {    return args.reduce((total,curr)=> total + curr)  }
  return fn}
考点:
- 使用闭包, 同时要对 JavaScript 的作用域链(原型链)有深入的理解 
- 重写函数的 - toSting()方法
// 测试,调用toString方法触发求值
add(1).toString();             // 1add(1)(2).toString();      // 3add(1)(2)(3).toString();// 6add(1)(2, 3).toString(); // 6add(1, 2)(3).toString(); // 6add(1, 2, 3).toString(); // 6
实现 lodash 的 chunk 方法--数组按指定长度拆分
题目
/** * @param input * @param size * @returns {Array} */_.chunk(['a', 'b', 'c', 'd'], 2)// => [['a', 'b'], ['c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 3)// => [['a', 'b', 'c'], ['d']]
_.chunk(['a', 'b', 'c', 'd'], 5)// => [['a', 'b', 'c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 0)// => []
实现
function chunk(arr, length) {  let newArr = [];  for (let i = 0; i < arr.length; i += length) {    newArr.push(arr.slice(i, i + length));  }  return newArr;}
判断是否是电话号码
function isPhone(tel) {    var regx = /^1[34578]\d{9}$/;    return regx.test(tel);}
实现 getValue/setValue 函数来获取 path 对应的值
// 示例var object = { a: [{ b: { c: 3 } }] }; // path: 'a[0].b.c'var array = [{ a: { b: [1] } }]; // path: '[0].a.b[0]'
function getValue(target, valuePath, defaultValue) {}
console.log(getValue(object, "a[0].b.c", 0)); // 输出3console.log(getValue(array, "[0].a.b[0]", 12)); // 输出 1console.log(getValue(array, "[0].a.b[0].c", 12)); // 输出 12
实现
/** * 测试属性是否匹配 */export function testPropTypes(value, type, dev) {  const sEnums = ['number', 'string', 'boolean', 'undefined', 'function']; // NaN  const oEnums = ['Null', 'Object', 'Array', 'Date', 'RegExp', 'Error'];  const nEnums = [    '[object Number]',    '[object String]',    '[object Boolean]',    '[object Undefined]',    '[object Function]',    '[object Null]',    '[object Object]',    '[object Array]',    '[object Date]',    '[object RegExp]',    '[object Error]',  ];  const reg = new RegExp('\\[object (.*?)\\]');
  // 完全匹配模式,type应该传递类似格式[object Window] [object HTMLDocument] ...  if (reg.test(type)) {    // 排除nEnums的12种    if (~nEnums.indexOf(type)) {      if (dev === true) {        console.warn(value, 'The parameter type belongs to one of 12 types:number string boolean undefined Null Object Array Date RegExp function Error NaN');      }    }
    if (Object.prototype.toString.call(value) === type) {      return true;    }
    return false;  }}
const syncVarIterator = {  getter: function (obj, key, defaultValue) {    // 结果变量    const defaultResult = defaultValue === undefined ? undefined : defaultValue;
    if (testPropTypes(obj, 'Object') === false && testPropTypes(obj, 'Array') === false) {      return defaultResult;    }
    // 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改    let result = obj;
    // 得到知道值    try {      // 解析属性层次序列      const keyArr = key.split('.');
      // 迭代obj对象属性      for (let i = 0; i < keyArr.length; i++) {        // 如果第 i 层属性存在对应的值则迭代该属性值        if (result[keyArr[i]] !== undefined) {          result = result[keyArr[i]];
          // 如果不存在则返回未定义        } else {          return defaultResult;        }      }    } catch (e) {      return defaultResult;    }
    // 返回获取的结果    return result;  },  setter: function (obj, key, val) {    // 如果不存在obj则返回未定义    if (testPropTypes(obj, 'Object') === false) {      return false;    }
    // 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改    let result = obj;
    try {      // 解析属性层次序列      const keyArr = key.split('.');
      let i = 0;
      // 迭代obj对象属性      for (; i < keyArr.length - 1; i++) {        // 如果第 i 层属性对应的值不存在,则定义为对象        if (result[keyArr[i]] === undefined) {          result[keyArr[i]] = {};        }
        // 如果第 i 层属性对应的值不是对象(Object)的一个实例,则抛出错误        if (!(result[keyArr[i]] instanceof Object)) {          throw new Error('obj.' + keyArr.splice(0, i + 1).join('.') + 'is not Object');        }
        // 迭代该层属性值        result = result[keyArr[i]];      }
      // 设置属性值      result[keyArr[i]] = val;
      return true;    } catch (e) {      return false;    }  },};
使用 promise 来实现
创建 enhancedObject 函数
const enhancedObject = (target) =>  new Proxy(target, {    get(target, property) {      if (property in target) {        return target[property];      } else {        return searchFor(property, target); //实际使用时要对value值进行复位      }    },  });
let value = null;function searchFor(property, target) {  for (const key of Object.keys(target)) {    if (typeof target[key] === "object") {      searchFor(property, target[key]);    } else if (typeof target[property] !== "undefined") {      value = target[property];      break;    }  }  return value;}
使用 enhancedObject 函数
const data = enhancedObject({  user: {    name: "test",    settings: {      theme: "dark",    },  },});
console.log(data.user.settings.theme); // darkconsole.log(data.theme); // dark
以上代码运行后,控制台会输出以下代码:
darkdark
通过观察以上的输出结果可知,使用
enhancedObject函数处理过的对象,我们就可以方便地访问普通对象内部的深层属性。
实现 bind
实现 bind 要做什么
- 返回一个函数,绑定 this,传递预置参数 
- bind 返回的函数可以作为构造函数使用。故作为构造函数时应使得 this 失效,但是传入的参数依然有效 
// mdn的实现if (!Function.prototype.bind) {  Function.prototype.bind = function(oThis) {    if (typeof this !== 'function') {      // closest thing possible to the ECMAScript 5      // internal IsCallable function      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');    }
    var aArgs   = Array.prototype.slice.call(arguments, 1),        fToBind = this,        fNOP    = function() {},        fBound  = function() {          // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用          return fToBind.apply(this instanceof fBound                 ? this                 : oThis,                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的                 aArgs.concat(Array.prototype.slice.call(arguments)));        };
    // 维护原型关系    if (this.prototype) {      // Function.prototype doesn't have a prototype property      fNOP.prototype = this.prototype;     }    // 下行的代码使fBound.prototype是fNOP的实例,因此    // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例    fBound.prototype = new fNOP();
    return fBound;  };}
渲染几万条数据不卡住页面
渲染大数据时,合理使用 createDocumentFragment 和 requestAnimationFrame,将操作切分为一小段一小段执行。
setTimeout(() => {  // 插入十万条数据  const total = 100000;  // 一次插入的数据  const once = 20;  // 插入数据需要的次数  const loopCount = Math.ceil(total / once);  let countOfRender = 0;  const ul = document.querySelector('ul');  // 添加数据的方法  function add() {    const fragment = document.createDocumentFragment();    for(let i = 0; i < once; i++) {      const li = document.createElement('li');      li.innerText = Math.floor(Math.random() * total);      fragment.appendChild(li);    }    ul.appendChild(fragment);    countOfRender += 1;    loop();  }  function loop() {    if(countOfRender < loopCount) {      window.requestAnimationFrame(add);    }  }  loop();}, 0)
实现一个 JSON.parse
JSON.parse(text[, reviver])
用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)
第一种:直接调用 eval
function jsonParse(opt) {    return eval('(' + opt + ')');}jsonParse(jsonStringify({x : 5}))// Object { x: 5}jsonParse(jsonStringify([1, "false", false]))// [1, "false", falsr]jsonParse(jsonStringify({b: undefined}))// Object { b: "undefined"}
避免在不必要的情况下使用
eval,eval()是一个危险的函数,他执行的代码拥有着执行者的权利。如果你用eval()运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。它会执行 JS 代码,有 XSS 漏洞。
如果你只想记这个方法,就得对参数 json 做校验。
var rx_one = /^[\],:{}\s]*$/;var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rx_four = /(?:^|:|,)(?:\s*\[)+/g;if (    rx_one.test(        json            .replace(rx_two, "@")            .replace(rx_three, "]")            .replace(rx_four, "")    )) {    var obj = eval("(" +json + ")");}
第二种:Function
核心:Function 与 eval 有相同的字符串参数特性
var func = new Function(arg1, arg2, ..., functionBody);
在转换 JSON 的实际应用中,只需要这么做
var jsonStr = '{ "age": 20, "name": "jack" }'var json = (new Function('return ' + jsonStr))();
eval与Function都有着动态编译 js 代码的作用,但是在实际的编程中并不推荐使用

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










 
    
评论