JavaScript专题系列

前言

本文章基于冴羽大佬的专题系列

git地址:https://github.com/mqyqingfeng/Blog

这篇文章还包含了JavaScript 深入系列和ES6 系列,文章很好,也很适合我们这些开发经验1年到3年的初中级前端开发工程师

一、JavaScript专题之从零实现jQuery的extend

jQuery 的 extend 是 jQuery 中应用非常多的一个函数

"Merge the contents of two or more objects together into the first object",这是jQuery 官网对于extend功能的描述,翻译过来就是合并两个或者更多的对象的内容到第一个对象中

简单来说就是复制对象,当两个对象出现相同字段的时候,后者会覆盖前者,而不会进行深层次的覆盖。

例:

var obj1 = {
    a: 1,
    b: { b1: 1, b2: 2 }
};

var obj2 = {
    b: { b1: 3, b3: 4 },
    c: 3
};

var obj3 = {
    d: 4
}

console.log($.extend(obj1, obj2, obj3));

// {
//    a: 1,
//    b: { b1: 3, b3: 4 },
//    c: 3,
//    d: 4
// }

在第一版的extend函数中,采用了arguments然后对target目标进行赋值替换,但是这样写如果替换对象是对象,则会浅拷贝。

在第二版,核心部分还是跟第一版一样,但是额外加入了一个布尔参数,并且在处理对象赋值时采用了递归来处理。并且多了很多细节问题的判断

到了最终版,已经是非常完美了,增加了目标属性值跟要复制的对象的属性值类型是否一致的判断

* 如果待复制对象属性值类型为数组,目标属性值类型不为数组的话,目标属性值就设为 []

* 如果待复制对象属性值类型为对象,目标属性值类型不为对象的话,目标属性值就设为 {}

二、JavaScript专题之递归

定义:程序调用自身的编程技巧称为递归(recursion)。

以阶乘为例:

function factorial(n) {
    if (n == 1) return n;
    return n * factorial(n - 1)
}

console.log(factorial(5)) // 5 * 4 * 3 * 2 * 1 = 120

关于递归,有个很重要的点要遵循:不能无限制地调用本身,须有个出口,化简为非递归状况处理。否则会形成无限递归下去

以阶乘为例,他的出口条件就是n == 1的时候

在文章中提到了子问题须与原始问题为同样的事,还是以阶乘为例,都是在做重复的n乘以阶乘后的结果或者1

一般我们在项目中,运用较多的应该就是对于一些树形层级很深,但结构又相似的数据的递归处理

,其次就是一些算法的问题。

三、JavaScript专题之惰性函数

定义:函数执行的分支只会在函数第一次调用的时候执行

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

最经典的写法,对于浏览器的一些API以及对不同浏览器的兼容判断,对于这种判断,大可不必每次调用该函数都进行一次判断,所以以上函数在做的事情就是,第一次进入会去判断,进入改分支后,则会直接修改他的函数。从而实现第二次调用时进行无必要的判断。

文章还提供另外一种写法

var addEvent = (function(){
    if (window.addEventListener) {
        return function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        return function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
})();

利用的是立即执行函数表达式 (IIFE)

当 IIFE 执行时,它检测浏览器的事件绑定支持情况,并根据检测结果选择适当的事件绑定实现。这个选择过程只在初始化时进行一次。然后在此次函数中刚好返回了一个已经定义好的函数。

在后续调用 addEvent 时,不会再次执行 IIFE 内部的判断逻辑,而是直接调用已经定义好的函数。这是因为 addEvent 已经被赋值为适当的事件绑定函数。

四、JavaScript 专题之函数记忆

先来看一下第一版的代码

function memoize(f) {
    var cache = {};
    return function(){
        var key = arguments.length + Array.prototype.join.call(arguments, ",");
        if (key in cache) {
            return cache[key]
        }
        else return cache[key] = f.apply(this, arguments)
    }
}

代码并不复杂,利用率闭包的特性来进行缓存,声明一个memoize函数,传入你的函数,在内部做一个判断,如果key未出现在cache里,则会去借调并在cache中存值。

在第一版中,他的第四行代码是在生成一个唯一的缓存键。这个键是基于传递给函数的参数生成的。它是通过将参数的长度和参数值(以逗号分隔)连接起来生成的。这种方法可能会导致键的碰撞,但在大多数简单场景中,它能起到一定作用。在文章中也有所提及,因为这一行使用了join方法,假设你的参数是对象,就会自动调用toString方法从而把你的key转成`[Object object]`,会导致第二次传入对象时,即使对象与之前传入的不同,但是会返回第一次调用的结果。

对于这种情况,我尝试着修改了一下

首先我试着用了js内置的new Map方法去进行缓存,并用getset去操作他的键值对,在第一版出现的问题则是直接使用了JSON.stringify转换他的参数做为key值,可以做到很好的避免。

function memorize(f) {
    const cache = new Map();
    return function () {
        const key = JSON.stringify(arguments);
        if (cache.has(key)) {
            return cache.get(key);
        } else {
            const result = f.apply(this, arguments);
            cache.set(key, result);
            return result;
        }
    }
}

const propValue = function (obj) {
    return obj.value;
}

const memoizedPropValue = memorize(propValue);

console.log(memoizedPropValue({ value: 1 })); // 1
console.log(memoizedPropValue({ value: 2 })); // 2

五、JavaScript专题之类型判断

类型判断在 JavaScript 中是一个常见且重要的主题,特别是在处理动态类型的语言时,正确的类型判断可以帮助我们编写更健壮和安全的代码。

一、typeof

在现在包括ES6新增的类型已有八种分别是:

Number、String、Boolean、Undefined、Null、Symbol、Object、Function

分别对应的typeof返回的结果如下:

number、string、boolean、undefined、object、function、object、function

其中,Null是会被检测为object,则Symbol会被检测为function,需typeof(Symbol())才会被检测为symbol

但是,对于Array、Function、Date、RegExp、Error以及new Date()等。typeof返回的则都是function和object类型

二、Obejct.prototype.toString

这个方法的规范可以在 ECMA-262 6th Edition 的第 19.1.3 节找到关于Object.prototype.toString 方法的详细描述。

 以下就是对各个类型以及输出结果的测试

var number = 1;          // [object Number]
var string = '123';      // [object String]
var boolean = true;      // [object Boolean]
var und = undefined;     // [object Undefined]
var nul = null;          // [object Null]
var obj = {a: 1}         // [object Object]
var array = [1, 2, 3];   // [object Array]
var date = new Date();   // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g;          // [object RegExp]
var func = function a(){}; // [object Function]

function checkType() {
    for (var i = 0; i < arguments.length; i++) {
        console.log(Object.prototype.toString.call(arguments[i]))
    }
}

checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func)

// 除了以上 11 种之外,还有:
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]

// 除了以上 13 种之外,还有:
function a() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
a();
三、instanceof操作符以及Array.isArray() 方法

instanceof定义:instanceof是 JavaScript 中的一个二元操作符,用于检查一个对象是否是某个构造函数(或其原型链上的一个构造函数的 prototype 属性)的实例。

// 语法:
object instanceof constructor

// object:要检查的对象实例
// constructor:某个构造函数

[] instanceof Array; // true
new Date() instanceof Date; // true
/regex/ instanceof RegExp; // true
new Error() instanceof Error; // true

function Person(name) {
    this.name = name;
}
let person = new Person('Alice');
person instanceof Person; // true

// 对于一些特定的内置对象,如 Object、Function,它们的实例判断也是有效的,但通常更多用于自定义构 
// 造函数的实例判断。

注意事项:

instanceof 只能用来判断对象是否是某个构造函数的实例,无法判断原始数据类型(如 numberstringboolean)或 nullundefined

在多层继承关系中,instanceof 只会检查原型链上是否存在构造函数的 prototype,而不关心继承链中的实际构造函数。

总结

在文章种,类型判断分成了两篇,由于内容发布很早,第二篇的内容更多的是介绍了 jQuery 的 isPlainObject、isEmptyObject、isWindow、isArrayLike、以及 underscore 的 isElement 实现。

尽管 jQuery 在其兴盛时期为前端开发带来了极大的便利性和跨浏览器兼容性,但随着技术的发展和原生 API 的改进,现在在很多场景下,开发者更倾向于使用原生 JavaScript 来避免 jQuery 的一些潜在问题和限制。所以在此就不对文章的这部分内容进行过多的描写。

六、JavaScript专题之乱序

Array.prototype.sort() 在 JavaScript 中是一个非常灵活的排序方法,他的默认排序方式是按照Unicode编码顺序进行排序。这通常用于按字母表顺序排序字符串数组,但对于数字数组的排序可能会导致意外结果。

例如 [10, 1, 21] 按照Unicode编码排序则结果会是 [1, 10, 21]

sort的自定义排序方式则需提供compareFunction。compareFunction是一个用于定义排序规则的函数,接受两个参数 ab,并决定它们的相对顺序。

例如 a - b则是升序,b - a则是降序

文章种则是使用了Math.random()去生成一个-0.5到0.5之间的任意随机数来决定他的排序规则

然而,以这种方法进行打乱排序,在文章的测试结果种,显而易见是不够随机的。所以在最终版本。引入了一个概念:

"Fisher-Yates随机洗牌算法"

Fisher-Yates随机洗牌算法,也称为Knuth洗牌算法,是一种用来将数组随机排序的有效算法。

我们来逐步解释一下他的工作原理

1、初始化变量:j, x, i 分别用来存储随机索引、交换值的暂存变量以及循环计数。

2、循环过程:

        (1)、for (i = a.length; i; i--):从数组的最后一个元素开始,依次往前处理,直到第一个元素。

        (2)、j = Math.floor(Math.random() * i):随机生成一个介于 0 到 i-1 之间的整数,这里用来作为要交换的元素的索引。

        (3)、x = a[i - 1]:将当前处理的元素暂存在 x 中。

        (4)、a[i - 1] = a[j]a[j] = x:将当前处理的元素与随机选取的元素进行交换。

最后再返回洗牌完后的a

代码如下:

function shuffle(a) {
    var j, x, i;
    for (i = a.length; i; i--) {
        j = Math.floor(Math.random() * i);
        x = a[i - 1];
        a[i - 1] = a[j];
        a[j] = x;
    }
    return a;
}

这种算法之所以称为“彻底”的随机排序,是因为它保证了每个元素被随机地放置在数组的任意位置,且每种排列的概率是相等的。这种性质对于许多需要真正随机性的应用是非常重要的。

因此,这段代码确实实现了一种彻底的随机排序方法,可以在不引入任何偏见的情况下对数组进行重新排列。

七、JavaScript专题之如何判断两个对象相等

首先在我们的常规认知种,NaN 和 NaN 是相等,[1] 和 [1] 是相等,{value: 1} 和 {value: 1} 是相等。但是实则,这三个均不相等(三等对比)。并且1 和 new Number(1)、'Curly' 和 new String('Curly')以及true 和 new Boolean(true)在全等对比也均为false。

但是对于这个等式:+0 === -0 ,结果反而是true。

对于`===`的以上特殊情况,这些真的都为false吗?最后一种又真的是true吗?显然是很有争议的

在文章中,对于相等的这个概念就做出了重新定义。并且在ES6中,也衍生出了一个新的对比方法:“Object.is()”

ES6提出“Same-value equality”(同值相等)算法用于解决JavaScript在所有环境中,只要两个值是一样的,他们的值就应该相等。object.is就是部署这个算法的新方法。

对于三等出现的+0 === -0等于true以及NaN === NaN等于false的两种特殊情况,Object.is()做了很好的解决。


console.log(Object.is(NaN,NaN)) // true
console.log(Object.is(+0,-0)) // false

对于第一种特殊情况,文章中采用了1 / +0以及1 / -0结果不同的特性来处理

a !== 0 || 1 / a === 1 / b

而对于NaN则是有一行这个

if (a !== a) return b !== b;

console.log(eq(NaN, NaN));

在自身不等于自身的情况下做特殊处理。第一版中是已经可进行简单的类型比较,但是对于复杂的对象等,再最后一行则交给了一个deepEq函数做处理。

在写deepEq函数之前,我们得先考虑一个问题。'Curly'如何才能等于new String(''Curly''),如何才能让这个等式成立。这是一个需要考虑的点。

首先,new String是一个构造函数,用于创建一个String对象,String对象是一个对象的包装器,用于将基本的字符串数据类型封装成一个对象。虽然'String'对象提供了字符串的包装和一些额外的方法,但是在大多数情况下,直接使用原始字符串(即'String类型')更为常见和推荐。

在上文提到的额外的方法中,就有一个valueOf()方法,可以利用这个方法来获取其的基本值。

因此,想要实现这个等式成立就很简单了。

console.log('Curly' === new String('Curly').valueOf()) // true

在文中使用了隐式转换的方法,例如toString或者 + ''等方法。

但是在隐式转换对比之前还需要一个前置条件,需要两者的Object.prototype.toString.call结果一致都为'[object String]'。有了这个前置条件才可进行后续判断。

对于更多的对象,也就可以沿用隐式转换的思路进行处理

**Boolean**

var a = true;
var b = new Boolean(true);

console.log(+a === +b) // true

**Date**

var a = new Date(2009, 9, 25);
var b = new Date(2009, 9, 25);

console.log(+a === +b) // true

**RegExp**

var a = /a/i;
var b = new RegExp(/a/i);

console.log('' + a === '' + b) // true

**Number**

var a = 1;
var b = new Number(1);

console.log(+a === +b) // true

有了以上这些处理方法,这样第一版的deepEq就可以开写了!

function deepEq(a, b) {
    var className = toString.call(a);
    if (className !== toString.call(b)) return false;

    switch (className) {
        case '[object RegExp]':
        case '[object String]':
            return '' + a === '' + b;
        case '[object Number]':
            if (+a !== +a) return +b !== +b;
            return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
            return +a === +b;
    }
}

对于文章后构造函数实例、数组相等、循环引用等深入的判断。在此篇中就不作过多深入。

八、JavaScript专题之如何求数组的最大值和最小值

文章的第一个概念:

Math.max

JavaScript 提供了 Math.max 函数返回一组数中的最大值

简单介绍一下这个方法的语法

// Math.max(value1, value2, ..., valueN)

console.log(Math.max(1, 2, 3, 4, 5)); // 输出: 5
console.log(Math.max(-1, -2, -3, -4, -5)); // 输出: -1

console.log(Math.max()); // 输出: -Infinity
console.log(Math.min()); // 输出: Infinity

const numbers = [1, 2, 3, 4, 5];
console.log(Math.max.apply(null, numbers)); // 输出: 5

const numbers = [1, 2, 3, 4, 5];
console.log(Math.max(...numbers)); // 输出: 5

Math.max(true, 0) // 1
Math.max(true, '2', null) // 2
Math.max(1, undefined) // NaN
Math.max(1, {}) // NaN

对于这个方法也有一写注意事项:

1. 如果有任一参数不能被转换为数值,则结果为 NaN。

2. max 是 Math 的静态方法,所以应该像这样使用:Math.max(),而不是作为 Math 实例的方法 (简单的来说,就是不使用 new )

3. 如果没有参数,则结果为 `-Infinity` (注意是负无穷大)

// 原始方法
var arr = [6, 4, 1, 8, 2, 11, 23];

var result = arr[0];
for (var i = 1; i < arr.length; i++) {
    result =  Math.max(result, arr[i]);
}
console.log(result);

// reduce方法
var arr = [6, 4, 1, 8, 2, 11, 23];

function max(prev, next) {
    return Math.max(prev, next);
}
console.log(arr.reduce(max));

两者都是通过遍历数组,来依次比较数组的每一项,从而求出最终值。

当然,遍历数组是可行的,支持传多个参数进入,那么传一整个数组进去呢?

文中提到了三种方法,eval,apply以及es6的扩展运算符,着重了解一下后面两个。

apply是利用了借调,戒掉数组的max方法

例如: var arr = [1,2,3,6,8,13,23,13,9]

Math.max.apply(null,arr) // 23

ES6的拓展运算符...,实际则是将一个数组结构成一个一个的数组项。

例如: var arr = [1,2,3,6,8,13,23,13,9]

Math.max(...arr) // 23

九、JavaScript专题之深浅拷贝

浅拷贝和深拷贝的根本区别就在于储存区域是否指向同一个地址,当你声明一个对象时,计算机会自动分配一个地址,当你把这个对象赋给另一个变量时就会引发指向的是同一个地址的问题。

这同时也引出了两个概念,堆与栈。那么什么是堆什么又是栈呢?

栈(Stack)

定义

 栈是一种后进先出(LIFO, Last In First Out)的数据结构。在程序执行时,栈用于管理函数调用和局部变量的存储。

函数调用:每次函数调用时,JavaScript会将函数的执行上下文(包括局部变量、参数等)压入栈中。当函数执行完成后,相应的执行上下文会从栈中弹出。

局部变量:函数内部定义的变量通常存储在栈中。

堆(Heap)

定义

堆是一种用于动态内存分配的内存区域。与栈不同,堆内存的管理是手动或由垃圾回收机制处理的,堆内存的分配和释放不遵循LIFO原则。

对象和数组:在JavaScript中,所有的对象、数组以及函数等动态数据结构通常存储在堆内存中。

垃圾回收:JavaScript引擎会定期运行垃圾回收机制,以自动回收不再使用的堆内存,以防止内存泄漏。

在日常工作工,最常见的一些浅拷贝有concat、slice、Object.assign()以及扩展运算符

const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

// 浅拷贝,扩展运算符

那么如何去实现一个深拷贝呢?

JSON.Parse(JSON.stringify())就是一个很好的办法,简单粗暴,此外,你还可以尝试着去写一个函数,利用递归去一层层去克隆出来一个全新的对象!

// 深拷贝
function deepClone (target,obj) {
    for(var key in obj) {
        // 判断是否数组
        if(obj[key] instanceof Array) {
            target[key] = []
            // 递归赋值
            deepClone(target[key],obj[key])
        } else if (obj[key] instanceof Object) {
            target[key] = {}
            // 递归赋值
            deepClone(target[key],obj[key])
        } else {
            target[key] = obj[key]
        }
    }
}

十、JavaScript专题之数组扁平化

什么是数组扁平话呢?数组的扁平化,就是将一个嵌套多层的数组 array (嵌套可以是任何层数)转换为只有一层的数组。

假设我们有一个var arr = [1, [2, [3, 4]]];

经扁平化处理之后,arr应为[1, 2, 3, 4]

既然目标有了,那让我们来实现吧!

第一个方法!循环目标数组,如果循环项不为数组,那么直接push,如果为数组,那么就递归contact连接!

// 数组扁平化
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
    var result = []
    for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
            // result.push(...flatten(flatten(arr[i]))也是可以的
        } else {
            result.push(arr[i])
        }
    }
    return result
}

文章中的方法3也与第一种方法大同小异

第二个方法!力推!ES6的'Array.prototype.flat'方法!扁平化操作更加简洁和高效!flat方法可以用来将嵌套数组的元素扁平化成一维数组,支持指定深度参数来控制扁平化的层级。

const arr = [1, [2, [3, 4]]];

// 扁平化到一维
const flattened = arr.flat();
console.log(flattened);  // 输出: [1, 2, [3, 4]]

// 扁平化到指定深度
const deeplyFlattened = arr.flat(2);
console.log(deeplyFlattened);  // 输出: [1, 2, 3, 4]

十一、JavaScript专题之数组去重

数组去重不管是面试中还是平时开发中都是经常遇见的!其去重的方法也是老生常谈。

首先还是最原始的js写法,这里我就直接引用文章中的函数了!

function unique(array) {
    // res用来存储结果
    var res = [];
    for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
        for (var j = 0, resLen = res.length; j < resLen; j++ ) {
            if (array[i] === res[j]) {
                break;
            }
        }
        // 如果array[i]是唯一的,那么执行完循环,j等于resLen
        if (j === resLen) {
            res.push(array[i])
        }
    }
    return res;
}

双层for循环进行对比!如果是索引对应且在res中未出现!咋就不会进入break,也同时会进入push的判断!

除了最原始的方法可以做到,我再介绍三种较有代表性的去重方法。

1、new Set去重

Set是一种es6新增的数据结构,结构类似数组,但他的一大特性就是所有的元素都是唯一的,没有重复的值,因此可以利用这一特性进行简单的数组去重

例:let arrs = [1,2,3,4,5,5,5,2]

let setArrs = Array.from(new Set(arrs)) // [1,2,3,4,5]

2、ES6的filter去重
var newArr = arr.filter(function(item, index, arr) {
            // return 出的结果是一个数组 filter方法的返回值是一个数组
            // indexOf方法的结果是索引 0 1 2 3 会和当前的遍历索引比较 
            // 如果相等就将结果返回 就会将这一项数组元素放到新数组中
            return arr.indexOf(item) === index;
        })
3、Object 键值对去重

利用对象的key值,每次循环进入时区判断这个key键值,如果有,返回false,没有则返回true并且在对象中添加这个key键值。

var array = [1, 2, 1, 1, '1'];

function unique(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        return obj.hasOwnProperty(item) ? false : (obj[item] = true)
    })
}

console.log(unique(array)); // [1, 2]

十二、JavaScript专题之在数组中查找指定元素

在开发中,我们经常会遇到在数组中查找指定元素的需求。

ES6是新增了一个findIndex方法去查找指定元素,它会返回数组中满足提供的函数的第一个元素的索引,否则返回 -1。那么对于我们来说,去实现findIndex以及一个findLastIndex就足够了。

// 正常情况
function findIndex(array, predicate, context) {
    for (var i = 0; i < array.length; i++) {
        if (predicate.call(context, array[i], i, array)) return i;
    }
    return -1;
}

console.log(findIndex([1, 2, 3, 4], function(item, i, array){
    if (item == 3) return true;
})) // 2


// 配置context情况
function findIndex(array, predicate, context) {
    for (var i = 0; i < array.length; i++) {
        if (predicate.call(context, array[i], i, array)) return i;
    }
    return -1;
}

const context = { threshold: 3 };
console.log(findIndex([1, 2, 3, 4], function (item, i, array) {
   return item === this.threshold;
},context)) // 2

代码差不多就是这样,这里可能会有一个疑问点,context的作用是什么?

其实在这段代码中,context是指定 this 的值,如果 predicate 函数内部使用了 this,需要正确设置。也就是以上代码的第二种情况。

然后对于实现findLastIndex,有了findIndex的基础后实现起来就非常简单了

无非就是循环的过程从原来的从第一位到最后一位修改为从后往前前去寻找

也就是这一句:var i = 0; i < array.length; i++,修改为 var i = length; i >= 0; i-- 即可。

总结

那么到此为止,该篇文章的所有专题均都以完结。共十二个专题系列。其中有很多专题是在开发中是很常见的,也很重要!包括数组去重、深浅拷贝、判断两个对象是否相等、类型判断等。在这些专题中,其实也能衍生出更很多关联的问题,就好比在new Set去重时为何需要包括一个Array.from。如果不包括,那么这个数据能否使用数组的全部方法呢......... 还好比在深浅拷贝专题系列中,衍生出来的堆和栈的概念,并且提出了一个垃圾回收机制。那什么是js的垃圾回收机制呢?

虽然在现实工作时,我们可能会用到一些第三方库来帮助我们更快捷的开发。在有些时候,我们也确实能够通过这些第三方库完成一些需求,但是我觉得,对于一个需求的实现,有一个好的开发思维是至关重要的,而不是上来就库库敲,从而引发完工之后陆陆续续出现的隐性问题。

此外,扎实的js基础也是不可缺少的,在文章中的深入系列文章系列也是需要我们去熟练掌握的!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值