Day1-18

js

1 作用域

变量或者函数的作用范围

全局 局部 ES6中的作用域

es6之前,js中没有块级作用域

可以通过let 和 const声明形成块级作用域

2 作用域链底层执行过程

3 预编译

预编译是上下文创建之后, js代码执行前的一段时期, 在这个时期, 会对js代码进行预处理

全局上下文创建后,

会生成变量对象VO

VO首先寻找变量声明,将var声明的变量作为VO对象的属性名,值为undefined。然后寻找函数声明,属性值为函数本身,如果函数名与变量名冲突,函数声明会将变量声明覆盖。

全局预编译
console.log(a)  
var a = 100;
function a () { }
console.log(a)
​
开始预编译
生成VO
寻找变量声明  VO {a:undefined}
寻找函数声明  重名 覆盖 VO {a:undefined(×) function (√)}
预编译完成,开始输出:
console.log(a)  // functionA
a被赋值  VO {a:undefined(×) function (×) 100(√)}
console.log(a)  // 100 
所以,输出为 functionA  100
函数预编译

函数调用的时候,函数上下文创建后,会生成变量对象AO

  • 寻找var声明的变量, 变量名作为AO对象的属性名, 属性值置为 undefined

  • 寻找形参, 形参名作为AO对象的属性名, 属性值置为 undefined

  • 将实参的值赋予形参, 即替换 AO对象中形参的属性值

  • 寻找函数声明, 函数名作为AO对象的属性名, 属性值为函数本身

  • 如果函数名与变量名冲突, 函数声明会将变量声明覆盖

function a(b, c) {
  console.log(b);           
  var b = 0
  console.log(b);       
  var b = function () {
    console.log('bbbb')
  } 
  console.log(c);       
  console.log(b);               
}
  a(1)
开始预编译
生成AO
寻找变量声明  AO { b:undefined }、
寻找形参,b已有,所以只加c  AO { b:undefined  c:undefined }
将实参的值赋予形参  AO { b:undefined(×) 1(√)  c:undefined }
没有寻找函数声明
预编译完成,开始输出:
console.log(b)  // 1
b被赋值0  AO { b:undefined(×) 1(×)0(√) c:undefined }
console.log(b)  // 0
b被赋值func AO { b:undefined(×) 1(×)0(×)func(√) c:undefined }
console.log(c); // undefined
console.log(b); // function() { console.log('bbbb'); }

4 内存和数据类型

内存

js运行在内存中

js在函数声明(页面运行期间持续存在),和执行(临时占用,运行结束释放内存)时都会占用内存

内存的声明周期

1 分配内存(声明变量)
var obj ={
    b:2
}
2 使用内存
console。log(obj)
3 回收(有回收机制)

栈内存和堆内存

栈:用于记录函数的执行顺序,包括函数的基本数据类型。

堆:存储引用数据类型(对象),特性:堆内存中的变量只有在所有对它的引用结束后才能被回收。

数据类型

基本数据类型:

string number boolean null undefined biglnt symbol

存在在栈内存中

特性:

1 按值访问,比较的是值 2 声明之后值不可被修改

引用数据类型:

obj array function 存放在堆内存中

特性:

1 按引用访问,比较的是地址 2 声明之后值可被修改

浅拷贝与深拷贝

浅拷贝

浅拷贝,在拷贝的过程中创建一个对象,对原对象的属性进行拷贝。

当你更改源或副本时,可能导致其他对象也发生更改

若属性为基本数据类型,则浅拷贝会再单独开辟一块内存空间。

若属性为引用数据类型,则新对象和源对象的属性指向同一块。

三种方法

===================Object.assign()
var obj1={
    a:1,
    b:{
        b1:1
    }
}
var obj2 = Object.assign({},obj1);
// 修改obj2的值
obj2.a =3
obj2.b.b1 =2
​
则此时,修改引用数据类型 两个都受到了影像。
obj1
{ a:1
  b:{b1:2}
} 
obj2
{ a:3
  b:{b1:2}
} 
===============================Object.create()
var obj1={ a:1}
var obj2 = {Object.create(obj1)}
===============================扩展运算符
var obj1={
    a:1,
    b:{
        b1:1
    }
}
var obj2 ={...obj1} // 把obj1所有的属性展开,赋值给obj2
深拷贝

深拷贝是 新生成对象的属性与其拷贝的源对象的属性不共享相同的引用,当你更改源或副本时,可以确保不会导致其他对象也发生更改

两种方法

============================ JSON.parse(JSON.stringify(obj))
var obj = {
    a: 1,
    b: {
        b1: 1
    }
}
var obj1 = JSON.parse(JSON.stringify(obj))
obj1.a = 2
obj1.b.b1 = 2
有弊端,有些数据不能序列字符串化 例如,函数,symbol,正则,undefined
===============================借助组件库提供的clone方法
Lodash.cloneDeep()
// cdn地址
// https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
​
var obj = {
    a: 1,
    b: {
        b1: 1
    }
}
var obj1 = _.cloneDeep(obj)
obj1.a = 2
obj1.b.b1 = 2
​

深拷贝的原理

  1. 数据类型的划分

  2. 递归处理 (把old对象的属性循环付给new对象)

  3. 循环引用的处理

手写一个方法实现深拷贝。
function deepClone (obj, map = new WeakMap()) {
    // 对于不需要递归的类型,直接返回(null 基本数据类型 日期 正则)
    if (obj === null || typeof obj !== "object" || obj instanceof Date || obj instanceof RegExp) {
        return obj
    }
    // 循环引入
    if (map.get(obj)) {
        return map.get(obj)
    }
    
    if (obj instanceof Map) { // 对于Map数据结构的处理
        const newObj = new Map()
        map.set(obj, newObj)
        for (let val of obj) {
          newObj.set(val[0], deepClone(val[1], map))
        }
        return newObj
    } else if (obj instanceof Set) { // 对于Set数据结构的处理
        const newObj = new Set()
        map.set(obj, newObj)
        for (let val of obj) {
          newObj.add(deepClone(val, map))
        }
        return newObj
    } else if (obj instanceof Array) { // 对于Array数据结构的处理
        const newObj = []
        map.set(obj, newObj)
        obj.forEach(function (item) {
            newObj.push(deepClone(item, map))
        })
        return newObj
    } else if (obj instanceof Object) { // 对于Object数据结构的处理
        const newObj = {}
        map.set(obj, newObj)
        for (let key in obj) {
            newObj[key] = deepClone(obj[key], map)
        }
        return newObj
    }
}

闭包

在外部访问到函数作用域内的变量

1 能访问到其他函数的作用域 2 是个函数

常见写法

================================嵌套函数
function fn(){
    var a = 1
    function fn1 (){
        c.log(a+1)
    }
    fn1()
}
fn()
输出为2
================================= 回调函数
function fn (cb){
    var a =1
    cb(a)
}
function callback(a){
    c.log(a+1)
}
fn(callback)

面试题

function fn() {
  var arr = []; // 创建一个空数组
​
  for (var i = 0; i < 10; i++) {
    arr[i] = function () {
      return i;
    };
  }
  return arr; // 返回包含函数的数组
}
该函数的目的是创建一个包含了 10 个函数的数组,每个函数都返回相应的索引值 i。由于 JavaScript 中的变量作用域是函数作用域,而不是块级作用域,因此在循环中创建的匿名函数都共享同一个变量 i。当循环结束后,i 的值为 10。因此,当调用数组中的函数时,无论索引是多少,都会返回 10。
​
解决方案
for (let i = 0; i < 10; i++) 

this的基本概念

this的指向问题

1 一般函数调用时,指向window

var fn1 = function(){
	c.log(this)
}
fn1()

2 作为对象的方法调用时,指向对象

var obj = {
	fn:function(){
		console.log(this)
	}
}
var fn = obj.fn
fn()
	当直接通过 obj.fn() 的方式调用函数时,函数内部的 this 将指向对象 obj,因为函数是作为对象 obj 的方法被调用的。
	然而,在 var fn = obj.fn; 这行代码中,将对象 obj 中的 fn 属性赋值给了变量 fn。此时,函数 fn 与对象 obj 的关联已经断开,因此在调用 fn() 时,函数内部的 this 将不再指向对象 obj,而是指向全局对象(在浏览器环境下通常是 window 对象)。
	因此,当执行 fn() 时,将打印全局对象( window )的信息,而不是对象 obj。

3 在构造函数中,指向实例化对象

function Fn(){
	c.log(this)
}
c.log(new Fn())
输出是一样的

4 call 和 apply,this指向第一个调用的对象

var x = 2
function fn () {
    console.log(this.x)
}

var obj1 = {
    x: 1
}

var obj2 = {}

obj2.fn = fn

obj2.fn.call(obj1) //1
obj2.fn.call() //2

09 apply call bind

call、bind和apply的使用

call方法:

语法: function.call(thisArg, arg1, arg2, ...)。 其中thisArg是要设置为函数执行上下文的对象,也就是this要指向的对象,从第二个参数开始,arg1, arg2, ... 是传递给函数的参数。通过使用call方法,可以将一个对象的方法应用到另一个对象上。

// 如果省略第一个 thisArg 参数,则默认为 undefined。在非严格模式下,this 值将被替换为 globalThis(类似于全局对象)
"use strict"
const person1 = {
    fn: function () {
        console.log(this)
    }
}

const person2 = {
    name: 'person2'
}
person1.fn.call() // 此时为为 undefined
apply

语法:function.apply(thisArg, [argsArray])。 其中thisArg是要设置为函数执行上下文的对象,也就是this要指向的对象,argsArray是一个包含参数的数组。通过使用apply方法,可以将一个对象的方法应用到另一个对象上,并使用数组作为参数。

bind

语法:function.bind(thisArg, arg1, arg2, ...)。 其中thisArg是要绑定到函数执行上下文的对象,也就是this要指向的对象,从第二个参数开始,arg1, arg2, ...是传递给函数的参数。与call和apply方法不同,bind方法并不会立即执行函数,而是返回一个新函数,可以稍后调用。这对于事件处理程序和setTimeout函数等场景非常有用。

const person1 = {
    name: 'allen',
    fn: function () {
        console.log(this.name)
    }
}

const fn = person1.fn
fn() // undefined

// 改写成,this指向person1
// const fn = person1.fn.bind(person1)

对于解决定时器参数的问题

function fn(name) {
    console.log("Hello, " + name);
}

const delayFn = fn.bind(null, 'kevin')
setTimeout(delayFn, 2000)

call、bind、apply的区别

  1. call和apply都是直接调用函数,bind不会立即调用

  2. call和bind的参数为参数列表,apply的参数需要以数组的形式传递

Event loop(事件循环)

javascript是一门单线程的语言,它的异步和多线程都是通过Event Loop实现的

主线程在执行任务时,如果遇到异步事件,先将其放到事件循环队列中,等主线程任务执行完毕再执行异步任务

宏任务:setTimeout setInterval Ajax DOM事件 微任务:Promise

优先级:微任务 > 宏任务

面试题

var btn = document.getElementById('button');
btn.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(1));
  console.log('listener 1');
});
btn.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(2));
  console.log('listener 2');
});
当按钮被点击时,会触发两个点击事件监听器的执行。执行顺序如下:

    用户点击按钮。
    第一个点击事件监听器执行:
        Promise.resolve() 创建一个已解析的 Promise 对象。
        注册一个微任务,将其添加到微任务队列中。
        执行 console.log('listener 1'),输出 'listener 1'。
        调用栈为空,事件循环从微任务队列中取出微任务并执行,输出数字 1。
    第二个点击事件监听器执行:
        Promise.resolve() 创建一个已解析的 Promise 对象。
        注册一个微任务,将其添加到微任务队列中。
        执行 console.log('listener 2'),输出 'listener 2'。
        调用栈为空,事件循环从微任务队列中取出微任务并执行,输出数字 2。
        
 输出:listener1  1   listener2  2

总结:在每个点击事件监听器中,都使用了 Promise 对象并注册了一个微任务。微任务会在当前任务执行完毕后立即执行。因此,在点击事件的回调函数执行完后,会先执行微任务队列中的任务,然后再执行下一个点击事件的回调函数。
========================================================================
var btn = document.getElementById('button');
btn.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(1));
  console.log('listener 1');
});
btn.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(2));
  console.log('listener 2');
});
btn.click()
当执行 btn.click() 时,会模拟用户点击按钮的操作,从而触发按钮的点击事件。
 btn.click() 执行,js代码模拟用户点击按钮的操作。
 
 执行过程:
  btn.click()被送入,同时触发两个点击事件,
  第一个,注册一个微任务,将其添加到微任务队列中。
  第一个输出 listener 1
如果是代码A,此时会执行微任务输出1(工作站里的代码已经执行完毕)
但是在代码B里,btn.click()还在工作站里没有执行完毕,所以不会执行微任务1,
接着往下走,
  第二个,注册一个微任务,将其添加到微任务队列中。
  第二个输出 listener 2
此时,btn.click()执行完毕,从工作站移除、
开始依次执行微任务,输出 1 2 
输入如下,listener1  listener2  1  2
==========================================================
    解释如下:

    执行第一个点击事件监听器的回调函数:
        console.log('listener 1') 输出 'listener 1'。
        创建一个已解析的 Promise 对象,并注册一个微任务。
        因为微任务是异步的,所以会先执行下一行代码。

    执行第二个点击事件监听器的回调函数:
        console.log('listener 2') 输出 'listener 2'。
        创建一个已解析的 Promise 对象,并注册一个微任务。
        因为微任务是异步的,所以会先执行下一行代码。

    执行微任务队列中的第一个微任务:
        console.log(1) 输出数字 1。

    执行微任务队列中的第二个微任务:
        console.log(2) 输出数字 2。

总结:由于 Promise 的 then 方法中的回调函数是微任务,微任务会在当前任务完成后立即执行。在这个例子中,两个点击事件监听器的回调函数会先执行,然后根据微任务队列中的顺序执行微任务。因此,输出的顺序是先输出两个监听器的文本,然后按照微任务的顺序输出数字 1 和数字 2。
===========================================================
不同点:当执行 btn.click() 模拟按钮点击事件时,会依次执行两个点击事件监听器的回调函数。由于 Promise 对象的 then 方法中的回调函数是微任务,会先被添加到微任务队列中,等待当前任务完成后执行。

因此,两个点击事件监听器的回调函数的执行顺序不仅取决于它们被添加到按钮的顺序,还取决于微任务队列中的微任务执行顺序。

在不同的 JavaScript 环境中,微任务队列的执行机制可能会有细微的差异,导致微任务的执行顺序有所变化。这可能是导致两次输出顺序发生变化的原因。

所以,虽然代码中的两个点击事件监听器被添加的顺序是固定的,但微任务的执行顺序可能会有变化,从而导致输出顺序的变化。

14 Ajax的实现原理

ajax的使用优点

网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面

Ajax的实现原理

  1. 创建Ajax对象

  2. 传入请求方式和请求地址

  3. 发送请求

  4. 获取服务器与客户端的响应数据

    <script>
        // 1. 创建Ajax对象
        var xhr = new XMLHttpRequest()
        // 2. 传入请求方式和请求地址
        xhr.open('get', 'http://127.0.0.1:3000/getData')
        // 3. 发送请求
        xhr.send()
        // 4. 获取服务器与客户端的响应数据
        xhr.onload = function () {
          console.log(xhr.responseText)
        }
      </script>

14 同源策略

浏览器引入的一种安全机制,即 端口,域名,协议,三者相同

内嵌资源:在页面中通过 <script><link><iframe> 等标签引入的资源,其内容由浏览器加载,不会受到同源策略的限制。

有一些操作并不会产生跨域问题。跨域写操作

<body>
    <!-- <img src="https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF" /> -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.5.0/axios.min.js"></script>
    <script>
        axios({
            methods: 'get',
            url: 'http://localhost:3000/api/getData',
            headers: {
                token: 1
            }
        })
    </script>
</body>

15 跨域解决方式

JSONP解决同源限制问题

原理:利用script标签的src属性可以跨域加载资源的特性,通过动态创建script标签来实现跨域请求。

内嵌资源:在页面中通过 <script><link><iframe> 等标签引入的资源,其内容由浏览器加载,不会受到同源策略的限制。

<body>
    <!-- <script src="http://127.0.0.1:3000/api/getData?cb=callback"></script> -->
    <script>
        // 动态创建script标签
         const script = document.createElement('script')
         // 设置src属性
       script.src = 'http://127.0.0.1:3000/api/getData?cb=callback'
         // 接收后端的回调函数
         function callback(res) {
             console.log(res, 'res')
         }
         // 将script挂载到页面上
         document.getElementsByTagName('body')[0].appendChild(script)
        
    </script>
</body>

跨源网络访问的三种方式:跨域写操作(不会产生),跨域资源嵌入,跨域读操作(会产生)

CORS(跨域资源共享)

原理:需要服务器实现CORS接口,在服务器端设置响应头,允许指定的域名访问资源。

给服务器端添加响应头,浏览器在接收这个标识时,会默认这个请求是没有跨域问题的。

res.header('Access-Control-Allow-Origin', '*') //允许所有的请求源
res.header('Access-Control-Allow-Headers', '*') //允许所有的请求源 X-Token
res.header('Access-Control-Allow-Methods', '*') //允许所有的请求源 get post put delete
代理服务器

仅限于开发过程(本地开发)

原理:在同源策略限制下,可以通过在服务器端创建一个代理,将跨域请求发送到代理服务器,然后由代理服务器转发请求并将响应返回给客户端。

16 面向对象

三大对象,封装,继承,多态

封装:

工厂模式:由工厂类创建对象,将对象的创建与使用分离。

工厂函数创建的对象没有标记,和createCar 函数没有关系

function createCar (color, style) {
     var obj = new Object()
     obj.color = color
      obj.style = style
      return obj
    }

 var Cayenne = createCar('red', '卡宴')
var C911 = createCar('blue', '911')
构造函数模式:使用构造函数创建对象,通过 new 关键字实例化。
function createCar (color, style) {
     this.color = color
     this.style = style
 }
   var Cayenne = new CreateCar('red', '卡宴')
   var C911 = new CreateCar('blue', '911')
原型模式:通过克隆已有对象来创建新对象,避免重复创建相似对象。
构造函数与工厂模式的区别(显示)
  1. 没有显示的创建对象(创建的对象和createCar 函数有没有关系)

  2. 直接将属性和方法赋值给了this对象

  3. 没有return语句

20 原型和原型链

在面向对象编程中,原型(Prototype)是一个对象,其他对象可以通过它实现继承和共享属性。

在 JavaScript 中,每个对象都有一个原型。对象可以通过 __proto__ 属性访问其原型对象。原型对象本身也是一个对象,也有自己的原型,形成了原型链。

原型链是一种对象之间通过原型关联的机制。当访问一个对象的属性时,如果该对象自身没有该属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端(一般是 Object.prototype)。

例如,考虑以下代码:

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

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

var john = new Person("John");
john.greet(); // 输出: Hello, my name is John

在这个例子中,我们创建了一个构造函数 Person,它有一个 name 属性和一个 greet 方法。greet 方法被添加到 Person.prototype 对象上。

当我们使用 new Person("John") 创建一个 john 对象时,john 对象的原型会指向 Person.prototype。因此,john 对象可以访问 greet 方法,即 john.greet()

在这个例子中,原型链的关系如下:

john --> Person.prototype --> Object.prototype --> null

john 对象的原型是 Person.prototypePerson.prototype 的原型是 Object.prototype,最终的原型是 null。如果在 john 对象上没有找到属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到或者到达原型链的顶端。

通过原型链,JavaScript 实现了对象之间的继承和共享属性的机制,使得代码可以更加简洁和灵活。

image-20231118214300486

function fn(){}  // fn构造函数
var obj = new fn() // obj实例化对象
var f1 = fn.prototype // f1 原型
obj.__proto__ === f1  // true
obj.constuctor === fn //true
f1.constuctor === fn //true

原型继承

 function CreateCarA (color, style) {
        this.color = color
        this.style = style
      }

      function CreateCarB (color, style) {
        this.color = color
        this.style = style
      }

      CreateCarA.prototype.configure = '顶配'
      CreateCarB.prototype = CreateCarA.prototype // A的属性和方法(color,style,configure)都会继承B的实例化对象上
      
      var Cayenne = new CreateCarA('red', '卡宴')
      var C911 = new CreateCarA('blue', '911')

      var AOOOO = new CreateCarB('red', '520')
      var BMW = new CreateCarB('blue', '320')

习题:

什么是作用域,作用域有哪些?

变量或者函数的作用范围,分为全局作用域、局部作用域和ES6中的作用域

预编译的过程有哪些?

创建变量对象,寻找变量声明和函数声明,对变量和函数进行初始化。

下面几题的代码的执行结果分别是什么?
var foo = 1;
function bar() {
    console.log(foo); 
    if (!foo) {        	
        var foo = 10;  
    }
    console.log(foo); 
}

bar();

// 在变量声明之前

输出为:
1
10
function fn () {
    func()
    var func = function () {
        console.log('表达式')
    }
    function func() {
        console.log('声明式')
    }
    func()
}
fn()
========================
输出:
声明式
表达式
function test(d) {
    console.log(b);
    if (a) {
        b = 100;
    }
    console.log(b);
    c = 4;
    console.log(d);
    var d = 20;
    console.log(d);
}
var a = 10;
var b = 10;
test(3);
console.log(c);
=====================
输出:
10
100
3
20
4
 
// 由于变量 c 是在函数内部没有使用 var 关键字声明的,它变成了一个全局变量。
js的数据类型有哪些?

基本数据类型:

  • Number

  • String

  • Boolean

  • Undefined

  • null

  • symbol

引用数据类型:

  • Object

  • Array

  • Function

什么是深拷贝 深拷贝有哪些方式?

深拷贝创建一个新对象,两个对象属性完成相同,但是对应两个不同的地址,修改其中一个对象的属性,不会改变另一个对象的属性

方式:

1 _.cloneDeep()

2 JSON.stringify()

const obj2=JSON.parse(JSON.stringify(obj1));

3 手写循环递归

什么是闭包?

可以在一个内层函数中访问到其外层函数的作用域

下面几题的代码的执行结果分别是什么?
var obj = { 
    a: 1, 
    foo() {
        console.log(this.a);
    } 
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }

obj.foo(); // 指向对象obj 输出为1
foo();     // 指向window 输出为undefined
obj2.foo(); // 指向obj2 输出为3
有 在当前作用域
function Foo(){
    getName = function(){ console.log(1); };
    return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName() { console.log(5) };

Foo.getName();         
getName();        
Foo().getName();
getName();        
new Foo.getName(); 2
new Foo().getName(); 3 
new new Foo().getName(); 3
// 不懂
 
getName { console.log(5)     console.log(4); };
Foo

静态属性
什么是EventLoop事件循环?

事件循环(Event Loop)是 JavaScript 中用于处理异步操作的一种机制。它负责协调和处理事件、回调函数以及其他异步任务的执行顺序。

什么是同源策略?图片资源是否存在跨域?

是浏览器引入的一种安全机制,即 端口,域名,协议,三者相同。图片资源不存在跨域

解决跨域的方式有哪些?

JSP CORS(跨域资源共享) 代理服务器

js设计模式有哪些?

工厂模式,构造函数模式和原型模式

什么是原型,原型链?

在js中,每个对象(包括函数)都有一个prototype属性,这个属性是一个对象,就是其原型。

原型对象也拥有原型,当我们在一个对象上找不到某个属性或者方法时,就会向其原型对象上寻找,如果找不到,就去原型对象的原型上寻找,直到找到或者返回null。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值