前端手写代码

1.手写 Object.create

function customObjectCreate(proto, propertiesObject) {
  // 判断传入的 proto 是否为 null 或者对象
  if (proto !== Object(proto) && proto !== null) {
    throw new TypeError('Object prototype may only be an Object or null.');
  }

  // 创建一个临时的构造函数
  function F() {}

  // 将临时构造函数的原型设置为传入的 proto
  F.prototype = proto;

  // 创建一个继承了 proto 的新对象
  const obj = new F();

  // 如果传入了 propertiesObject,定义其属性
  if (propertiesObject !== undefined) {
    Object.defineProperties(obj, propertiesObject);
  }

  // 返回新创建的对象
  return obj;
}


// 使用方法


// 定义原型对象
const person = {
  isHuman: false,
  printIntroduction: function() {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

// 定义属性描述符对象
const properties = {
  name: {
    value: 'John Doe',
    writable: true,
    enumerable: true,
    configurable: true
  },
  age: {
    value: 30,
    writable: false,
    enumerable: true,
    configurable: false
  },
  greet: {
    value: function() {
      console.log(`Hello, my name is ${this.name}`);
    },
    writable: false,
    enumerable: false,
    configurable: true
  }
};

// 创建新对象
const me = customObjectCreate(person, properties);

// 测试新对象
me.printIntroduction(); // 输出: My name is John Doe. Am I human? false
console.log(me.name); // 输出: John Doe
console.log(me.age); // 输出: 30
me.greet(); // 输出: Hello, my name is John Doe

propertiesObject 参数的格式

propertiesObject 是一个对象,其中每个键代表一个属性名,每个值是一个属性描述符对象。属性描述符对象可以包含以下属性:

  • value: 属性的值。
  • writable: 如果为 true,则属性值可以被修改(默认为 false)。
  • enumerable: 如果为 true,则属性会出现在对象的枚举属性列表中(默认为 false)。
  • configurable: 如果为 true,则属性可以被删除或修改其特性(默认为 false)。
  • get: 一个函数,用于获取属性值(如果有 value,则 getvalue 不能同时存在)。
  • set: 一个函数,用于设置属性值(如果有 value,则 setvalue 不能同时存在)。

2.手写 instanceof 方法

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left); // 获取对象的原型
  let prototype = right.prototype; // 获取构造函数的 prototype 对象

  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false; // 如果原型链结束(proto 为 null),返回 false
    if (proto === prototype) return true; // 如果原型链中找到构造函数的 prototype,返回 true

    proto = Object.getPrototypeOf(proto); // 向上移动到原型链的上一级
  }
}


// 使用示例

function Person(name) {
  this.name = name;
}

const alice = new Person('Alice');

console.log(myInstanceof(alice, Person)); // 输出: true
console.log(myInstanceof(alice, Object)); // 输出: true
console.log(myInstanceof(alice, Array));  // 输出: false

3.手写 new 操作符

function customNew(constructor, ...args) {
  // 1. 创建一个新对象
  const obj = {};

  // 2. 设置新对象的原型为构造函数的 prototype
  Object.setPrototypeOf(obj, constructor.prototype);

  // 3. 将构造函数的 `this` 绑定到新对象上,并执行构造函数
  const result = constructor.apply(obj, args);

  // 4. 如果构造函数返回的是对象,则返回这个对象
  // 否则返回新创建的对象
  return result instanceof Object ? result : obj;
}


// 使用示例

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const alice = customNew(Person, 'Alice', 30);

console.log(alice.name); // 输出: Alice
console.log(alice.age);  // 输出: 30
alice.sayHello();        // 输出: Hello, my name is Alice

result instanceof Objectfalse 的情况很少,但可能发生在以下情况:

  1. 构造函数返回原始值(非对象): 如果构造函数返回的是原始值,如 number, string, boolean, nullundefined,那么 result instanceof Object 会返回 false。例如:

    function MyConstructor() {
      return 42; // 返回原始值
    }
    
    const instance = customNew(MyConstructor);
    console.log(instance instanceof Object); // false
    
  2. 构造函数返回一个不是 Object 的实例: 如果构造函数返回的是一个自定义的特殊类型或构造函数的实例,但它不是一个标准的 Object(例如某些非标准的对象),instanceof Object 也会返回 false。这很少见,因为大多数 JavaScript 对象都是从 Object 构造函数派生的。

    function MyConstructor() {
      return new MySpecialType(); // MySpecialType 不是标准的 Object 类型
    }
    
    function MySpecialType() {}
    
    const instance = customNew(MyConstructor);
    console.log(instance instanceof Object); // false
    
  3. 构造函数返回 nullundefined: 如果构造函数返回 nullundefinedresult instanceof Object 会返回 false

    function MyConstructor() {
      return null; // 返回 null
    }
    
    const instance = customNew(MyConstructor);
    console.log(instance instanceof Object); // false
    

通常情况下,构造函数返回 Object 实例或其子类(如 Array, Function, Date 等),因此 result instanceof Object 多数时间会为 true。确保构造函数按预期返回对象通常是确保自定义 new 实现正确性的关键。

4.使用 Promise 封装 AJAX 请求

function ajax(url, options = {}) {
  return new Promise((resolve, reject) => {
    // 创建 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest();
    
    // 配置请求类型和 URL
    xhr.open(options.method || 'GET', url);

    // 设置请求头
    if (options.headers) {
      Object.keys(options.headers).forEach(key => {
        xhr.setRequestHeader(key, options.headers[key]);
      });
    }

    // 设置请求的响应类型
    if (options.responseType) {
      xhr.responseType = options.responseType;
    }

    // 处理请求状态变化
    xhr.onreadystatechange = () => {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status >= 200 && xhr.status < 300) {
          // 请求成功,解析并返回响应数据
          resolve(xhr.response);
        } else {
          // 请求失败,返回错误信息
          reject(new Error(`HTTP Error: ${xhr.status}`));
        }
      }
    };

    // 处理请求错误
    xhr.onerror = () => {
      reject(new Error('Network Error'));
    };

    // 发送请求
    xhr.send(options.body || null);
  });
}

// 使用示例
ajax('https://jsonplaceholder.typicode.com/posts', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => {
  console.log('Success:', response);
})
.catch(error => {
  console.error('Error:', error);
});

readyState 属性

readyState 属性表示 XMLHttpRequest 对象的当前状态,取值范围是 0 到 4,分别表示不同的请求阶段:

  1. 0 (UNSENT):

    • 请求对象已创建,但尚未调用 open() 方法。请求未被初始化。
  2. 1 (OPENED):

    • open() 方法已被调用,请求已经被初始化,但尚未发送请求。此时可以设置请求头和请求体。
  3. 2 (HEADERS_RECEIVED):

    • 请求已发送,响应头已接收。此时可以访问响应头,但响应体尚未完全接收。
  4. 3 (LOADING):

    • 响应体正在接收中。此时可以访问部分响应体数据,但响应尚未完全接收。
  5. 4 (DONE):

    • 请求已完成,响应体已经完全接收。此时可以访问完整的响应数据和状态码。

status 属性

status 属性表示响应的 HTTP 状态码,通常用于确定请求是否成功。常见的状态码包括:

  • 200 OK:

    • 请求成功,服务器返回请求的数据。
  • 201 Created:

    • 请求成功,并且创建了新的资源。
  • 204 No Content:

    • 请求成功,但没有返回任何内容。
  • 400 Bad Request:

    • 请求无效或存在语法错误,服务器无法理解。
  • 401 Unauthorized:

    • 请求未经授权,需进行身份验证。
  • 403 Forbidden:

    • 服务器拒绝执行请求。
  • 404 Not Found:

    • 请求的资源未找到。
  • 500 Internal Server Error:

    • 服务器内部错误,无法完成请求。
  • 502 Bad Gateway:

    • 网关错误,服务器从上游服务器接收到无效响应。

XMLHttpRequest 常量及其含义

  1. XMLHttpRequest.UNSENT (0)

    • 含义: 请求对象已创建,但尚未调用 open() 方法。此时请求未被初始化。
  2. XMLHttpRequest.OPENED (1)

    • 含义: open() 方法已被调用,表示请求已经初始化,但尚未发送请求。此时可以设置请求头和请求体。
  3. XMLHttpRequest.HEADERS_RECEIVED (2)

    • 含义: 请求已发送,服务器响应头已接收。此时可以访问响应头,但响应体尚未完全接收。
  4. XMLHttpRequest.LOADING (3)

    • 含义: 响应体正在接收中。此时可以访问部分响应体数据,但响应尚未完全接收。
  5. XMLHttpRequest.DONE (4)

    • 含义: 请求已完成,无论请求成功还是失败。此时可以访问完整的响应数据和状态码。

5.类型判断函数

function getType(value) {
  // 判断数据是 null 的情况
  if (value === null) {
    return 'null';
  }

  // 判断数据是引用类型的情况
  if (typeof value === "object") {
    const valueClass = Object.prototype.toString.call(value);
    const type = valueClass.slice(8, -1); // 提取 `[object Type]` 中的 `Type`
    return type.toLowerCase(); // 转为小写
  }
  // 判断数据是基本数据类型的情况和函数的情况
  return typeof value;
}

// 测试
console.log(getType(123));           // 'number'
console.log(getType('Hello'));       // 'string'
console.log(getType(true));          // 'boolean'
console.log(getType(undefined));     // 'undefined'
console.log(getType(null));          // 'null'
console.log(getType([1, 2, 3]));     // 'array'
console.log(getType({ key: 'value' })); // 'object'
console.log(getType(() => {}));      // 'function'
console.log(getType(new Date()));    // 'date'
console.log(getType(/regex/));       // 'regexp'

解释

  1. null 的处理:

    • 如果 valuenull,直接返回 'null'
  2. 引用类型的处理:

    • 使用 Object.prototype.toString.call(value) 返回 [object Type] 形式的字符串。
    • 使用 slice(8, -1) 从字符串中提取类型名称,如 'Array''Date' 等。这里的 8 是因为 "[object " 的长度,-1 去掉了末尾的 "]"
    • 将类型名称转换为小写以保持一致性。
  3. 基本数据类型和函数:

    • 对于基本数据类型(numberstringbooleanundefined)以及 function,直接使用 typeof 进行判断。

总结

这个优化后的 getType 函数更简洁且易于理解,同时保留了 Object.prototype.toString.call 的优点,能够准确识别多种类型,包括 ArrayDateRegExp

6.函数柯里化

函数柯里化(Currying)是将一个接受多个参数的函数转换成一个接受一个参数的函数,并返回一个接受下一个参数的函数,直到所有参数都被接受并且函数执行。

实现科里化函数

我们可以实现一个通用的科里化函数,这个函数会接受一个原始函数,并返回一个可以逐步接受参数的函数,直到所有参数都被提供为止。

//方法一
const add(x) {
  return function (y) {
    return function (z) {
      return x + y + z
    }
  }
}
console.log(add(1)(2)(3));


//方法二
function curry(fn) {
  return function (y) {
    return function (z) {
      return fn(x, y, z);
    };
  };
}
var add = curry((x, y, z) => {
  return x + y + z;
});
console.log(add(1)(2)(3)); // 6
通用科里化函数的实现
function curry(fn) {
  const expectedArgsLength = fn.length; // 期望的参数数量

  // 返回一个内部函数来处理参数
  function curried(...args) {
    // 如果当前参数数量少于期望数量,继续返回一个新的函数
    if (args.length >= expectedArgsLength) {
      // 调用原函数,并传递参数
      return fn(...args);
    }

    // 否则,返回一个新的函数来继续接受参数
    return (...moreArgs) => curried(...args, ...moreArgs);
  }

  return curried;
}
示例:应用科里化

假设我们有一个函数 multiply,它接受多个参数,并对它们进行乘法计算。我们可以使用科里化将其转化为逐步接受参数的形式。

// 定义一个多参数函数
function multiply(a, b, c) {
  return a * b * c;
}

// 使用科里化函数包装 multiply
const curriedMultiply = curry(multiply);

// 使用科里化函数逐步传递参数
console.log(curriedMultiply(2)(3)(4)); // 输出: 24

// 也可以将参数一次性传递
console.log(curriedMultiply(2, 3, 4)); // 输出: 24

科里化应用场景

科里化可以用于许多不同的场景,不仅仅是计算数据和。例如:

  1. 配置函数

    • 对于需要多个配置选项的函数,科里化可以让你逐步设置这些配置。
    function createUser(name, age, address) {
      return { name, age, address };
    }
    
    const curriedCreateUser = curry(createUser);
    
    const user = curriedCreateUser('Alice')(30)('123 Main St');
    console.log(user); // 输出: { name: 'Alice', age: 30, address: '123 Main St' }
    
  2. 事件处理

    • 对于需要多个处理参数的事件处理函数,科里化可以逐步设置这些参数。
    function handleEvent(type, details, callback) {
      console.log(`Event Type: ${type}`);
      console.log(`Event Details: ${details}`);
      callback();
    }
    
    const curriedHandleEvent = curry(handleEvent);
    
    curriedHandleEvent('click')('button clicked')(() => console.log('Event handled'));
    
  3. 函数组合

    • 将多个函数组合成一个管道(pipeline),使得每个函数的输入都是前一个函数的输出。
    function add(a, b) {
      return a + b;
    }
    
    function multiply(a, b) {
      return a * b;
    }
    
    const curriedAdd = curry(add);
    const curriedMultiply = curry(multiply);
    
    const result = curriedMultiply(2)(curriedAdd(3)(4)); // 2 * (3 + 4) = 14
    console.log(result); // 输出: 14
    

总结

  • 通用科里化:使用 curry 函数可以将任何接受多个参数的函数转化为逐步接受参数的形式。
  • 应用场景:科里化不仅限于简单的数据计算,也可以应用于配置、事件处理、函数组合等多个场景。

7.递归实现深拷贝

function deepClone(obj, map = new WeakMap()) {
  // 基本数据类型和函数直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理循环引用
  if (map.has(obj)) {
    return map.get(obj);
  }

  // 创建一个新的对象或数组
  const clone = Array.isArray(obj) ? [] : {};

  // 将当前对象存储在 map 中,以处理循环引用
  map.set(obj, clone);

  // 递归拷贝对象的所有属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }

  return clone;
}

8.数组扁平化

1. 使用递归方法

递归是最直观的方式,通过遍历数组的每个元素,如果元素是数组,则递归地将其展开。

function flattenArray(arr) {
  let result = [];
  
  arr.forEach(item => {
    if (Array.isArray(item)) {
      result = result.concat(flattenArray(item)); // 如果元素是数组,递归处理
    } else {
      result.push(item); // 如果元素不是数组,直接加入结果
    }
  });
  
  return result;
}

const nestedArray = [1, [2, [3, [4]], 5], 6];
console.log(flattenArray(nestedArray)); // 输出: [1, 2, 3, 4, 5, 6]

2. 使用 ES6 reduce() 方法

reduce() 方法可以用来将数组的每个元素逐步处理,并将结果累积起来。

function flattenArray(arr) {
  return arr.reduce((acc, item) => {
    if (Array.isArray(item)) {
      return acc.concat(flattenArray(item)); // 如果元素是数组,递归处理并累积结果
    } else {
      return acc.concat(item); // 如果元素不是数组,直接累积到结果中
    }
  }, []);
}

const nestedArray = [1, [2, [3, [4]], 5], 6];
console.log(flattenArray(nestedArray)); // 输出: [1, 2, 3, 4, 5, 6]

3. 使用 ES6 flat() 方法

从 ECMAScript 2019(ES10)开始,JavaScript 引入了 Array.prototype.flat() 方法,它可以直接将数组扁平化。

const nestedArray = [1, [2, [3, [4]], 5], 6];
const flatArray = nestedArray.flat(Infinity); // 使用 Infinity 展开任意深度的数组
console.log(flatArray); // 输出: [1, 2, 3, 4, 5, 6]

4. 使用 while 循环和扩展运算符(...

这是另一种通过非递归方式实现数组扁平化的方法。

function flattenArray(arr) {
  while (arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr); // 使用扩展运算符将数组展开一层
  }
  return arr;
}

const nestedArray = [1, [2, [3, [4]], 5], 6];
console.log(flattenArray(nestedArray)); // 输出: [1, 2, 3, 4, 5, 6]

9.实现数组去重

1. 使用 Set

Set 是 ES6 引入的一个数据结构,它类似于数组,但不允许有重复的值。可以利用 Set 的这个特性来轻松实现数组去重。

function uniqueArray(arr) {
  return [...new Set(arr)];
}

const array = [1, 2, 3, 4, 3, 2, 1];
console.log(uniqueArray(array)); // 输出: [1, 2, 3, 4]

2. 使用 filterindexOf

filter 方法可以用来筛选数组中的元素,结合 indexOf 可以实现去重功能。

function uniqueArray(arr) {
  return arr.filter((value, index) => arr.indexOf(value) === index);
}

const array = [1, 2, 3, 4, 3, 2, 1];
console.log(uniqueArray(array)); // 输出: [1, 2, 3, 4]

3. 使用 reduce

reduce 方法可以通过累积器(accumulator)来构建一个不包含重复元素的新数组。

function uniqueArray(arr) {
  return arr.reduce((acc, value) => {
    if (!acc.includes(value)) {
      acc.push(value);
    }
    return acc;
  }, []);
}

const array = [1, 2, 3, 4, 3, 2, 1];
console.log(uniqueArray(array)); // 输出: [1, 2, 3, 4]

4. 使用 Map

可以使用 Map 来记录数组中的元素是否出现过,遍历数组并根据 Map 记录进行去重。

function uniqueArray(arr) {
  const map = new Map();
  return arr.filter(value => !map.has(value) && map.set(value, true));
}

const array = [1, 2, 3, 4, 3, 2, 1];
console.log(uniqueArray(array)); // 输出: [1, 2, 3, 4]

5. 使用 forEach 和对象作为哈希表

你可以使用一个对象来记录已经出现过的元素,然后使用 forEach 来遍历数组,筛选出唯一的元素。

function uniqueArray(arr) {
  const result = [];
  const seen = {};

  arr.forEach(value => {
    if (!seen[value]) {
      seen[value] = true;
      result.push(value);
    }
  });

  return result;
}

const array = [1, 2, 3, 4, 3, 2, 1];
console.log(uniqueArray(array)); // 输出: [1, 2, 3, 4]

10.将数字每千分位用逗号隔开

const number = 1234567.89;
const formattedNumber = number.toLocaleString();
console.log(formattedNumber); // 输出: "1,234,567.89" (在大多数英语环境中)

11.使用 reduce 求和

const items = [
  { price: 10 },
  { price: 20 },
  { price: 30 }
];

const totalPrice = items.reduce((acc, item) => acc + item.price, 0);

console.log(totalPrice); // 输出: 60

12.将js对象转化为树形结

const flatData = [
  { id: 1, name: 'A', parentId: null },
  { id: 2, name: 'B', parentId: 1 },
  { id: 3, name: 'C', parentId: 1 },
  { id: 4, name: 'D', parentId: 2 },
  { id: 5, name: 'E', parentId: 2 },
  { id: 6, name: 'F', parentId: 3 }
];
//转换成下面的结构
[
  {
    "id": 1,
    "name": "A",
    "parentId": null,
    "children": [
      {
        "id": 2,
        "name": "B",
        "parentId": 1,
        "children": [
          {
            "id": 4,
            "name": "D",
            "parentId": 2,
            "children": []
          },
          {
            "id": 5,
            "name": "E",
            "parentId": 2,
            "children": []
          }
        ]
      },
      {
        "id": 3,
        "name": "C",
        "parentId": 1,
        "children": [
          {
            "id": 6,
            "name": "F",
            "parentId": 3,
            "children": []
          }
        ]
      }
    ]
  }
]
function buildTree(data) {
  const map = {};
  const tree = [];

  // 创建映射:每个节点的 ID 作为键,节点对象作为值
  data.forEach(item => {
    map[item.id] = { ...item, children: [] };
  });

  // 构建树结构
  data.forEach(item => {
    if (item.parentId === null) {
      // 如果没有父节点,作为根节点
      tree.push(map[item.id]);
    } else {
      // 将子节点添加到其父节点的 children 属性中
      if (map[item.parentId]) {
        map[item.parentId].children.push(map[item.id]);
      }
    }
  });

  return tree;
}

const treeData = buildTree(flatData);
console.log(JSON.stringify(treeData, null, 2));

13.解析 URL Params 为对象

function parseParam(url) {
  const params = new URLSearchParams(new URL(url).search);
  const result = {};

  for (const [key, value] of params.entries()) {
    // 处理重复的键
    if (result.hasOwnProperty(key)) {
      // 如果结果对象中已存在该键,则将其转换为数组或推入数组中
      if (Array.isArray(result[key])) {
        result[key].push(isNaN(value) ? value : Number(value));
      } else {
        result[key] = [result[key], isNaN(value) ? value : Number(value)];
      }
    } else {
      // 处理键的值
      result[key] = isNaN(value) ? decodeURIComponent(value) : Number(value);
    }
  }

  // 处理未指定值的键
  for (const key of new URL(url).searchParams.keys()) {
    if (params.get(key) === null) {
      result[key] = true;
    }
  }

  return result;
}

// 示例使用
const url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
console.log(parseParam(url));

14.循环打印红黄绿

function red(callback) {
    console.log('red');
    setTimeout(callback, 3000); // 红灯亮 3 秒
}

function green(callback) {
    console.log('green');
    setTimeout(callback, 1000); // 绿灯亮 1 秒
}

function yellow(callback) {
    console.log('yellow');
    setTimeout(callback, 2000); // 黄灯亮 2 秒
}

function startCycle() {
    red(() => {
        green(() => {
            yellow(startCycle); // 在黄灯亮完后,递归调用 startCycle 继续循环
        });
    });
}

startCycle();
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function red() {
    console.log('red');
    return delay(3000); // 红灯亮 3 秒
}

function green() {
    console.log('green');
    return delay(1000); // 绿灯亮 1 秒
}

function yellow() {
    console.log('yellow');
    return delay(2000); // 黄灯亮 2 秒
}

async function startCycle() {
    while (true) {
        await red();
        await green();
        await yellow();
    }
}

startCycle();

15.实现每隔一秒打印 1,2,3,4

// 使用闭包实现
for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

16.使用 setTimeout 实现 setInterval

function customSetInterval(callback, interval) {
  function intervalFunction() {
    callback(); // 执行回调函数
    setTimeout(intervalFunction, interval); // 递归调用,设置下一次的超时
  }
  
  setTimeout(intervalFunction, interval); // 启动第一次调用
}

// 使用示例
customSetInterval(() => {
  console.log('This message prints every 1000 milliseconds');
}, 1000);
function customSetInterval(callback, interval) {
  let timerId;
  
  function intervalFunction() {
    callback(); // 执行回调函数
    timerId = setTimeout(intervalFunction, interval); // 递归调用,设置下一次的超时
  }
  
  timerId = setTimeout(intervalFunction, interval); // 启动第一次调用
  
  return function clearCustomInterval() {
    clearTimeout(timerId); // 清除定时器
  };
}

// 使用示例
const stopInterval = customSetInterval(() => {
  console.log('This message prints every 1000 milliseconds');
}, 1000);

// 停止定时器
setTimeout(() => {
  stopInterval(); // 停止自定义的 setInterval
}, 5000);

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值