一些常见的js手写代码题

实现call、apply、bind

定义一个函数与一个对象

  function Person(a, b, c) {
        return {
          name: this.name,
          a: a,
          b: b,
          c: c,
        };
      }
      var chain = {
        name: "柴柴",
      };

call

//原生js实现call函数,es6的解构写法,rest参数
      Function.prototype.myCall = function (obj, ...params) {
        var obj = obj || window;
        obj.p = this; //形参对象添加p属性,里面是Person这个函数
        let result = obj.p(...params); //执行这个函数
        delete obj.p; //把这个方法删除,因为不能改写对象
        return result;
      };
      const res = Person.myCall(chain, 1, 2, 3);
      console.log("res", res);

在这里插入图片描述

apply

apply跟call类似,只是apply的参数放在一个数组里面,params表示数组里的所有参数

 Function.prototype.myApply = function (obj, params) {
        var obj = obj || window;
        obj.p = this; //形参对象添加p属性,里面是Person这个函数
        let result;
        if (!params) {
          //执行这个函数,
          result = obj.p();
        } else {
          result = obj.p(...params); //...params展开数组里的参数
        }
        delete obj.p; //把这个方法删除,因为不能改写对象
        return result;
      };
      const res = Person.myApply(chain, [1, 2, 3]);
      console.log("res", res);

bind

bind与call和apply有点不一样,bind会返回一个函数,在函数里面执行call的方法


      Function.prototype.myBind = function (obj, ...params) {
        var _this = this; //this执行的是Person函数
        var obj = obj || window;
        return function () {
          //容易造成this的丢失,要提前将this保存,在返回的函数里执行call和apply的功能
          return _this.call(obj, ...params); //Person.call(obj)
        };
      };
      const res = Person.myBind(chain, 1, 2, 3)();
      console.log("res", res);

实现new

1.创建一个空对象
2.将obj的隐形原型指向构造函数的显示原型
3.将步骤1新创建的对象作为this的上下文
4.如果该函数没有返回对象(即result不是一个对象),则返回this(即obj)

//实现new
function myNew(fn, ...arg) {
  //1.创建空对象
  var obj = {};
  //2.将obj的隐形原型指向构造函数的显示原型
  obj.__proto__ = fn.prototype;
  //3.将步骤1新创建的对象作为this的上下文
  var result = fn.call(obj, ...arg);
  //4.如果该函数没有返回对象(即result不是一个对象),则返回this(即obj)
  return result instanceof Object ? result : obj;
}
function Person(name, age) {
  this.name = name;
  this.age = age;
}
var p = myNew(Person, "柴柴", 12);

实现instanceof

instanceof运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置
instanceof运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。
1.左边是对象,右边是构造函数(可以查看原型链,其实Object也是一个构造函数)
2.迭代。左侧对象的__proto__不等于构造函数的prototype时,沿着原型链重新赋值左侧

  //判断构造函数的prototype是否在对象的原型链上
      //左边是对象,右边是构造函数(可以查看原型链,其实object也是一个构造函数)
function myInstanceof(L, R) {
  //L是实例, R是构造函数
  //构造函数的显示原型在实例对象的隐式原型链上
  //1.首先判断类型,如果数据是基本类型,直接返回false
  if (typeof L === "object" || typeof L === "function") {
    //2.如果是引用类型,取出R的显示原型
    let RP = R.prototype;
    L = L.__proto__;
    //递归遍历
    while (L) {
      if (L === RP) {
        return true;
      }
      L = L.__proto__;
    }
    return false;
  } else {
    return false;
  }
}
const res = myInstanceof(Function, Object);
console.log(res);

组合继承

js高级教程博客中有详细介绍
组合继承=原型链继承+构造函数继承

function Person(name, age) {
        this.name = name;
        this.age = age;
        console.log(this.name, this.age);
      }
      Person.prototype.setName = function (name) {
        this.name = name;
      };
      function Student(name, age, price) {
        Person.call(this, name, age);
        this.price = price;
      }
      //只需要两个语句
      Student.prototype = new Person();
      Student.prototype.constructor = Student; //手动修正Student.prototype.constructor
      let result = new Student("柴柴", 5, 33);
      result.setName("肥肥");
      console.log(result);

浅拷贝、深拷贝、深度比较

浅拷贝:只考虑对象类型。
浅拷贝: 用 = 号赋值引用地址
js对于浅拷贝,改了一个对象的某个值以后,另外一个对象对应的属性值也会跟着改变

      const oldObj = {
        name: "柴柴",
        age: 20,
        colors: ["red", "pink", "white"],
        frineds: {
          name: "肥肥",
        },
      };
      const newObj = oldObj;
      newObj.name = "哈哈";
      console.log("oldObj", oldObj);
      console.log("newObj", newObj);

在这里插入图片描述

function shallowCopy(obj) {
  if (typeof obj !== "object") return;
  let newObj = obj instanceof Array ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key];
    }
  }
  return obj;
}
let obj = { name: "柴柴" };
let res = shallowCopy(obj);
console.log(res);

深拷贝:

 //js对于浅拷贝,改了一个对象的某个值以后,另外一个对象对应的属性值也会跟着改变
      const oldObj = {
        name: "柴柴",
        age: 20,
        colors: ["red", "pink", "white"],
        frineds: {
          name: "肥肥",
        },
      };

      //定义一个深拷贝函数
      function deepClone(obj) {
        //这个obj要么是对象,要么是数组
        if (typeof obj !== "object" || obj === null) {
          return obj;
        }
        let result;
        //深拷贝过程
        //1.定义格式 是数组还是对象
        if (obj instanceof Array) {
          result = [];
        } else {
          result = {};
        }
        for (let key in obj) {
          //遍历这个obj的键名
          if (obj.hasOwnProperty(key)) {
            //只拷贝对象自身的属性
            //result[key] = obj[key];不递归调用的话只会拷贝最外层
            result[key] = deepClone(obj[key]);
          }
        }
        return result;
      }
      const newObj = deepClone(oldObj);
      newObj.name = "哈哈";
      console.log("oldObj", oldObj);
      console.log("newObj", newObj);

深拷贝JSON版本:

  function deepClone(obj) {
        let cloneObj = JSON.stringify(obj);
        cloneObj = JSON.parse(cloneObj);
        return cloneObj;
      }

深度比较:判断两个对象是否相同

   function isEqual(obj1, obj2) {
        //如果有一个或都不是对象
        if (typeof obj1 != "object" || typeof obj2 != "object") {
          return obj1 === obj2;
        } else if (obj1 === obj2) {
          return true;
        }
        //不是同一个对象
        const obj1Keys = Object.keys(obj1);
        const obj2Keys = Object.keys(obj2);
        if (obj1Keys.length != obj2Keys.length) {
          return false;
        }
        for (let key in obj1) {
          //键名一样
          const res = isEqual(obj1[key], obj2[key]);
          if (!res) {
            return false;
          }
        }
        //都比较完了,并且没有false
        return true;
      }

防抖和节流

防抖:函数防抖,这里的抖动就是执行的意思,而一般的抖动都是持续的,多次的。假设函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。重点就是规定时间内一直按的话不会执行,冷静下来后才会执行。
1.持续触发不执行
2.在规定时间内未触发第二次,则执行

 <body>
    <input type="text" id="hInput" />
    <script>
      const inputDOM = document.getElementById("hInput");
      //当用户输入完毕时,才发送一次http请求
      function debounce(fn, delay) {
        let timer = null;
        return function () {
          if (timer) {
            clearTimeout(timer); //清除当前定时器
          }
          timer = setTimeout(() => {
            fn(); //执行传入的函数
          }, delay);
        };
      }
      inputDOM.addEventListener(
        "input",
        debounce(() => {
          console.log("发送请求");
        }, 1000)
      );
    </script>
  </body>

节流(一段时间只执行一个操作):
节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。一直按的话会每隔给定时间执行。

1.持续触发并不会执行多次,在规定时间内只触发一次
2.到一定时间再去执行

  <body>
    <div class="box" draggable="true"></div>
    <!-- 节流:一段时间内,只执行一个某操作,过了这段时间,还有操作的话,继续执行新的操作 -->
    <script>
    const boxDom = document.querySelector(".box");
      function throttle(fn,delay) {
        let timer = null;
        return function () {
          if (timer) {
            return;
          }
          //设置一个定时器
          timer = setTimeout(() => {
            fn();
            timer = null;
          }, delay);
        };
      }
      boxDom.addEventListener(
        "drag",
        throttle(function (e) {
          console.log("test");
        },200)
      );
    </script>
  </body>

在这里插入图片描述

事件代理与事件绑定

  <body>
    <ul id="list">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <button id="btn">增加一项</button>
    </ul>
    <script>
      //事件绑定
      const list = document.getElementById("list");
      const lis = list.getElementsByTagName("li");
      const btn = document.getElementById("btn");
      listArray = Array.prototype.slice.call(lis);
      function bindEvent(elem, type, fn, selector) {
        elem.addEventListener(type, (event) => {
          const target = event.target;
          if (selector === undefined) {
            //普通绑定,直接执行fn
            fn(event);
          } else {
            //事件代理,判断当前点击函数是否匹配
            if (target.matches(selector)) {
              fn(event);
            }
          }
        });
      }
      //两种情况,第一种是事件代理,第二种是事件绑定
      bindEvent(
        list,
        "click",
        function (e) {
          console.log(e.target.innerHTML);
        },
        "li"
      );
        bindEvent(list, "click", function (e) {
          console.log(e.target.innerHTML);
        });
      btn.addEventListener("click", () => {
        const li = document.createElement("li");
        li.innerHTML = "新增";
        list.insertBefore(li, btn);
      });
    </script>
  </body>

手写promise

 async function send() {
        const result = await sendTo();
        console.log("result", result);
      }
      function sendTo() {
        let promise = new Promise((resolve, reject) => {
          let data = { name: "柴柴" };
          resolve(data);
        });
        promise
          .then((res) => {
            console.log(res.name);
          })
          .catch((err) => {
            console.log(err);
          });
        return promise;
      }
      send();

js中的拷贝函数

1.直接赋值(引用地址):obj2 = obj1,obj2 = Object(obj1)
2.Object.assign():拷贝的也是引用地址,且不可枚举属性和继承属性不拷贝。
语法: Object.assign(target, …sources) target: 目标对象,sources: 源对象
用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, source);

   let f = [1, 2, [3, 4, 5]];
   let copy = Object.assign([], f);

3.String.prototype.slice():浅拷贝,二级属性还有联系
其实就相当于把String看成一个字符数组,然后调用Array.prototype.slice()方法

let f = [1, 2, [3, 4, 5]];
let copy = f.slice(0);

4.String.prototype.concat():浅拷贝,二级属性还有联系、

      let f = [1, 2, [3, 4, 5]];
      let copy = [].concat(f);

5.Array.from():浅拷贝

let f = [1, 2, [3,4,5]]
let g = Array.from(f)
g[2][0] = 0
console.log(f)  // [ 1, 2, [ 0, 4, 5 ] ]

数组扁平化

1.直接使用flat(),当我们要明确知道它有多少层

[1, [2, [3]]].flat(2)  // [1, 2, 3]

2.ES6实现
结合some、Array.isArray、…
some():方法用于检测数组中的元素是否有满足指定条件的,若满足返回true,否则返回false

var arr = [1, [2, [3]]];
//用es6实现
function flatArr(arr) {
  //Array.isArray(item)判断当前这一项是不是数组
  while (arr.some((item) => Array.isArray(item))) {
    console.log(arr);
    arr = [].concat(...arr);
  }
  return arr;
}
let res = flatArr(arr);
console.log(res);

数组去重

es5


function unique(arr) {
  let res = arr.filter(function (item, index) {
    //若当前值在数组中的下标的该元素的下标,因为后一个重复的元素,他查找到的元素下标返回的是第一个的
    return arr.indexOf(item) === index;
  });
  return res;
}
let arr = [1, 2, 3, 4, 2];
let res = unique(arr);
console.log(res);

es6:

let arr = [1, 2, 3, 4, 2];
let res = [...new Set(arr)];

获取url参数并转化成对象

重点:了解正则表达式用法

//获取url参数并转化成对象
function getQueryParams() {
  const res = {};
  const queryString = "?from=search&seid=133&spm=333";
  //获取相对应的key和value保存到res中
  const reg = /[?&][^?&]+=[^?&]/g;
  const found = queryString.match(reg);
  if (found) {
    found.forEach((item) => {
      //substring(1)去掉第一个字符
      let temp = item.substring(1).split("=");
      let key = temp[0];
      let value = temp[1];
      res[key] = value;
    });
  }
  return res;
}
let url = "https://wwwvideo/?from=search&seid=133&spm=333";
let res = getQueryParams(url);
console.log(res);

函数柯里化

把接收多个参数的函数变换成接收一个单一参数的函数(单一参数为多个参数中的第一个)

//因为参数不确定,所以不设置形参
function add() {
  //并且把保存参数的arguments赋值给args变量保存起来,也就是第一个括号的函数
  //定义一个数组用来存储所有的参数,将arguments对象转化成数组
  let args = Array.prototype.slice.call(arguments);
  let inner = function () {
    //接收第二次传入的参数,也就是第二个括号,然后把第二个括号的参数加入到第一个括号的参数里面
    args.push(...arguments);
    //实际上,每次增加一个括号,我们都需要增加一个内部函数,用递归实现
    //只需要在内部函数里面进行自己调用自己即可实现
    return inner;
  };
  inner.toString = function () {
    return args.reduce(function (prev, cur) {
      return prev + cur;
    });
  };
  return inner;
}
/*
执行第一个括号,返回inner函数(但返回的是字符串,原本的函数呗转换为字符串显示,原因是发生隐式转换调用toString)
执行第二个括号,返回inner函数
执行第三个括号,返回inner函数
。。。。
*/
alert(add(1)(2)(4));
console.log(add(1)(2)(4));

object.create()(原型式继承)

该方法的原理是创建一个构造函数,构造函数的原型指向对象,然后调用 new 操作符创建实例,并返回这个实例,本质是一个浅拷贝。

function objcet (obj) {
    function F () {};
    F.prototype = obj;
    return new F();
}
Object.createCopy = function (proto, prototypeObject = "undefined") {
  //当proto不是函数并且不是对象的时候抛出错误
  if (typeof proto !== "object" && typeof proto !== "function") {
    throw new TypeError("Object prototype may only be an Object or null.");
  }
  function F() {}
  F.prototype = proto;
  const obj = new F();
  if (prototypeObject != "undefined") {
    Object.defineProperties(obj, prototypeObject);
  }
  if (proto == null) {
    //创建一个没有原型对象的对象,object.create(null)
    obj.__proto__ = null;
  }
  return obj;
};
var obj = { name: "柴柴" };
var fun = function () {};
var obj2 = Object.createCopy(fun);
console.log(obj2);

proxy代理

Proxy 也就是代理,可以帮助我们完成很多事情,例如对数据的处理,对构造函数的处理,对数据的验证,
说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。
proxy官网文档、handler用法
语法:

  let p = new Proxy(target, handler);

target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。
例子1:

 let obj = { name: "柴柴", age: 18 };
  obj = new Proxy(obj , {
    get(target, key) {
      console.log('获取了getter属性');
      return target[key];
    }
  });
  console.log(obj.name);

在这里插入图片描述
上方的案例,我们首先创建了一个obj对象,里面有name属性,然后我们使用Proxy将其包装起来,再返回给obj,此时的obj已经成为了一个Proxy实例,我们对其的操作,都会被Proxy拦截。
Proxy有两个参数:
第一个是target,也就是我们传入的obj对象,
另一个则是handler,也就是我们传入的第二个参数,一个匿名对象。
在handler中定义了一个名叫get的函数,当我们获取 obj的属性时,则会触发此函数。
咱们再来试试使用set来拦截一些操作,并将get返回值更改
Reflect官方文档

obj = new Proxy(obj, {
  get(target, key) {
    let result = target[key];
    //如果是获取 年龄 属性,则添加 岁字
    if (key === "age") result += "岁";
    return result;
  },
  set(target, key, value) {
    if (key === "age" && typeof value != "number") {
      //抛出错误
      throw Error("age字段必须为Number类型");
    }
    return Reflect.set(target, key, value);
  },
});
console.log(`我叫${obj.name}  我今年${obj.age}了`);

在这里插入图片描述

class SingleObject {
  constructor(name) {
    this.name = name;
  }
  single() {
    console.log("单例");
  }
}
function proxy(func) {
  let instance;
  let handler = {
    construct(target, args) {
      if (!instance) {
        instance = Reflect.construct(func, args);
      }
      return instance;
    },
  };
  return new Proxy(func, handler);
}

const SingleA = proxy(SingleObject);
const a1 = new SingleA("柴柴");
const a2 = new SingleA("肥肥");//返回的是a1
console.log(a1 === a2, a1, a2);

在这里插入图片描述

图片懒加载

如果用户还没看到网页下面的内容,在某种程度上我们就没必要这么快加载看不见的图片
滚动到网页下面才能预览到看不见的图片

/**
 * 将img标签的src属性改成data-src:浏览器碰到这个属性不会像默认属性那样进行属性处理,相当于不知道在哪里加载这些图片
 * 监听scroll这个事件,鼠标滚动就触发(但该方法非常)
 * 知道两个高度-窗口显示区高度:window.innerHeight
 *            -图片到视窗上的距离:getBoundingClientRect().top
 * 推荐IntersectionObserve:浏览器提供的构造函数,目标元素会和可视窗口会产生交叉区域
 *
 */
const images = document.querySelectorAll("img");
//方法一:监听scroll
window.addEventListener("scroll", (e) => {
  //遍历每一张图片,判断它图片到视窗上的距离与窗口显示区高度的关系
  images.forEach((image) => {
    const imageTop = image.getBoundingClientRect().top;
    //如果图片距离视窗顶部的距离小于窗口显示区高度,就代表该图片可以看见了,使图片开始加载
    if (imageTop < window.innerHeight) {
      //获取刚刚取得的自定义属性,再把这个自定义属性赋值给原本的src属性
      const data_src = image.getAttribute("data-src");
      image.setAttribute("src", data_src);
    }
    console.log("scroll触发");
  });
});
//方法二:IntersectionObserve,target指的是目标元素
const callback = (entries) => {
  console.log("回调函数接收一个参数,是一个数组", entries);
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const image = entry.target;
      const data_src = image.getAttribute("data-src");
      image.setAttribute("src", data_src);
      observe.unobserve(image);
      console.log("触发");
    }
  });
};
const observe = new IntersectionObserver(callback);
//给每一个图片绑定一个观察构造函数
images.forEach((image) => {
  observe.observe(image);
});

滚动加载

原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .content {
        height: 200px;
      }
      .one {
        background-color: pink;
      }
      .two {
        background-color: rgb(236, 236, 120);
      }
      .there {
        background-color: rgb(154, 184, 105);
      }
      .four {
        background-color: rgb(74, 162, 168);
      }
    </style>
  </head>
  <body>
    <div class="main">
      <div class="content one"></div>
      <div class="content two"></div>
      <div class="content there"></div>
      <div class="content four"></div>
    </div>
  </body>
  <script>
    window.addEventListener(
      "scroll",
      function () {
        const clientHeight = document.documentElement.clientHeight;
        const scrollTop = document.documentElement.scrollTop;
        const scrollHeight = document.documentElement.scrollHeight;
        if (clientHeight + scrollTop >= scrollHeight) {
          //监听滚动到最底部
          const one = document.createElement("div");
          one.setAttribute("class", "content one");
          const two = document.createElement("div");
          two.setAttribute("class", "content two");
          const there = document.createElement("div");
          there.setAttribute("class", "content there");
          const four = document.createElement("div");
          four.setAttribute("class", "content four");
          const mainEle = document.querySelector(".main");
          const fragment = document.createDocumentFragment();
          fragment.append(one, two, there, four);
          mainEle.appendChild(fragment);
        }
      },
      false
    );
  </script>
</html>

插入几万条数据不卡页面

渲染大数据时,合理使用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);
      setTimeout(() => {
        const total = 100000;
        const once = 20;
        const loopCount = Math.ceil(total / once);
        const ul = document.querySelector("ul");
        let startCount = 0;
        function loop() {
          //先判断数据插入完成没有
          if (startCount < loopCount) {
            //插入数据
            window.requestAnimationFrame(add);
          }
        }
        loop();
        function add() {
          const fragment = document.createDocumentFragment();

          for (let i = 0; i < once; i++) {
            let li = document.createElement("li");
            li.innerHTML = Math.floor(Math.random() * total);
            fragment.append(li);
          }
          ul.appendChild(fragment);
          startCount++;
          loop();
        }
      }, 0);

字符串解析

主要用replace和eval函数
eval() 函数计算 JavaScript 字符串,并把它作为脚本代码来执行。

replaceMDN详解

replace语法:str.replace(regexp|substr, newSubStr|function)

参数:
regexp :正则表达式 substr :字符串。仅第一个匹配项会被替换。 newSubStr
(replacement):用于替换掉第一个参数匹配到的字符串。 function
(replacement):一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。
返回值:一个部分或全部匹配由替代模式所取代的新的字符串。

在这里插入图片描述

var a = {
  b: "123",
  c: "456",
  e: "789",
};
var strs = `a{a.b}aa{a.c}aa {a.e}aaaa`;
function str(str) {
  const reg = /{(\w\.\w)}/g;
  str = str.replace(reg, (x, y) => eval(y));
  return str;
}
const res = str(strs);
console.log(res);

lookeup

      function lookup(dataObj, keyName) {
        // 看看keyName中有没有点符号,但是不能是.本身
        if (keyName.indexOf(".") != -1 && keyName != ".") {
          // 如果有点符号,那么拆开
          var keys = keyName.split(".");
          // 设置一个临时变量,这个临时变量用于周转,一层一层找下去。
          var temp = dataObj;
          // 每找一层,就把它设置为新的临时变量
          for (let i = 0; i < keys.length; i++) {
            temp = temp[keys[i]];
          }
          return temp;
        }
        // 如果这里面没有点符号
        return dataObj[keyName];
      }
      var res = lookup(
        {
          m: {
            n: {
              p: 100,
            },
          },
        },
        "m.n.p"
      );
      console.log(res);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值