面试会遇到的手写 Pollyfill 都在这里了

最近会把前阵子自己复盘归类整理的这次跳槽面试遇到的所有题目发布到公众号,这是第一篇。不要惊讶,上次跳槽考的也基本是这些题目,时间长了会忘,你只是需要一个清单!

new

测试用例:

function Fn (name) {
  this.name = name
}
console.log(myNew(Fn('lulu')))

实现:

function myNew () {
  const obj = {}
  const Fn = Array.prototype.shift.call(arguments)
  // eslint-disable-next-line no-proto
  obj.__proto__ = Fn.prototype
  const returnVal = Fn.apply(obj, arguments)
  return typeof returnVal === 'object' ? returnVal : obj
}

bind

测试用例:

this.x = 9
const obj = {
  x: 81,
  getX: function () {
    return this.x
  }
}
console.log(obj.getX()) // 81

const retrieveX = obj.getX
console.log(retrieveX()) // 9

const boundGetX = retrieveX.mybind(obj)
console.log(boundGetX()) // 81

实现:

Function.prototype.mybind = function () {
  const outerArgs = Array.from(arguments)
  const ctx = outerArgs.shift()
  const self = this
  return function () {
    const innerArgs = Array.from(arguments)
    return self.apply(ctx, [...outerArgs, ...innerArgs])
  }
}

instanceof

测试用例:

console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true

实现:

function myInstanceof(left, right) {
    //基本数据类型直接返回false
    if(typeof left !== 'object' || left === null) return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {
        //查找到尽头,还没找到
        if(proto == null) return false;
        //找到相同的原型对象
        if(proto == right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

debounce

在规定时间内函数只会触发一次,如果再次触发,会重新计算时间。

/*** 
 * @description 防抖函数
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate 是否立即执行
 * */
function debouncing(func, wait = 1000, immediate = true) {
    let timer = null;
    return function () {
        let args = arguments;
        let context = this;
        if (timer) {
            clearTimeout(timer);
        }
        if (!immediate) {
            //第一种:n秒之后执行,n秒内再次触发会重新计算时间
            timer = setTimeout(() => {
                //确保this指向不会改变
                func.apply(context, [...args]);
            }, wait);
        } else {
            //第二种:立即执行,n秒内不可再次触发
            let callnew = !timer;
            timer = setTimeout(() => {
                timer = null;
                console.log('kaka')
            }, wait);
            if (callnew) func.apply(context, [...args])
        }
    }
}

function fn() {
    console.log('debluncing')
}

let f1 = debouncing(fn, 1000);

setInterval(() => {
    f1()
}, 1000);

throttle

节流指的是函数一定时间内不会再次执行,用作稀释函数的执行频率。

/**
 * @description 节流函数
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1:时间戳版本 2: 定时器版本
 *  */
function throttle(func, wait = 1000, type = 1) {
    if (type === 1) {
        let timeout = null;
        return function () {
            const context = this;
            const args = arguments;
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, [...args]);
                }, wait);
            }
        }
    } else {
        let previous = 0;
        return function () {
            const context = this;
            const args = arguments;
            let newDate = new Date().getTime();
            if (newDate - previous > wait) {
                func.apply(context, [...args]);
                previous = newDate;
            }
        }
    }

}

function fn() {
    console.log('throttle')
}

const f1 = throttle(fn);

setInterval(() => {
    f1()
}, 100);

deepClone

测试用例:

const map = new Map()
map.set('key', 'value')
map.set('name', 'kaka')

const set = new Set()
set.add('11').add('12')

const target = {
  field1: 1,
  field2: undefined,
  field3: {
    child: 'child'
  },
  field4: [
    2, 8
  ],
  empty: null,
  map,
  set
}
target.target = target
const target1 = deepClone(target)
target1.a = 'a'
console.log('????', target)
console.log('????', target1)

实现:

// 判断类型
function getType (target) {
  return Object.prototype.toString.call(target).slice(8, -1)
}
// 判断是否是原始类型类型.
// 对应可引用的数据类型,需要递归遍历;对应不可引用的数据类型,直接复制即可
function isReferenceType (target) {
  let type = typeof target
  return (target !== null && (type === 'object' || type === 'function'))
}
// 获取原型上的方法
function getInit (target) {
  let ClassNames = target.constructor
  return new ClassNames()
}
// 引用类型
const mapTag = 'Map'
const setTag = 'Set'
const arrayTag = 'Array'
const objectTag = 'Object'

// 不可引用类型
const boolTag = 'Boolean'
const dateTag = 'Date'
const errorTag = 'Error'
const numberTag = 'Number'
const regexpTag = 'RegExp'
const stringTag = 'String'
const symbolTag = 'Symbol'
const bufferTag = 'Uint8Array'

let deepTag = [mapTag, setTag, arrayTag, objectTag]
function deepClone (target, map = new WeakMap()) {
  let type = getType(target)
  let isOriginType = isReferenceType(target)
  if (!isOriginType) { return target } // 对于不可引用的数据类型,直接复制即可

  let cloneTarget
  if (deepTag.includes(type)) {
    cloneTarget = getInit(target)
  }

  // 防止循环引用
  if (map.get(target)) {
    return map.get(target)
  }
  map.set(target, cloneTarget)

  // 如果是 mapTag 类型
  if (type === mapTag) {
    console.log(target, cloneTarget, 'target')
    target.forEach((v, key) => {
      cloneTarget.set(key, deepClone(v, map))
    })
    return cloneTarget
  }

  // 如果是 setTag 类型
  if (type === setTag) {
    target.forEach((v) => {
      cloneTarget.add(deepClone(v, map))
    })
    return cloneTarget
  }

  // 如果是 arrayTag 类型
  if (type === arrayTag) {
    target.forEach((v, i) => {
      cloneTarget[i] = deepClone(v, map)
    })
    return cloneTarget
  }

  // 如果是 objectTag 类型
  if (type === objectTag) {
    let array = Object.keys(target)
    array.forEach((i, v) => {
      cloneTarget[i] = deepClone(target[i], map)
    })
    return cloneTarget
  }
}

reduce

测试用例:

console.log([1, 2, 3, 4].myReduce((total, cur) => total + cur, 0))

实现:

/* eslint-disable no-extend-native */
Array.prototype.myReduce = function (callback, initialVal) {
  const arr = this
  let base = initialVal == null ? 0 : initialVal
  let startPoint = initialVal == null ? 0 : 1
  for (let i = 0; i < arr.length; i++) {
    base = callback(base, arr[i], i + startPoint, arr)
  }
  return base
}

promise

// Promise 是一个可以 new 的类
class Promise {
  constructor (executor) {
    this.status = 'PENDING' // promise 默认是pending态
    this.reason = this.val = undefined // val 用于储存 resolve 函数的参数,reason 用于储存 reject 函数的参数
    /**
         * 这里用数组进行回调函数的存储 是因为一种场景,即同一个 promisee 多次调用 then 函数
         * let p = new Promise((resolve)=>resolve())
         * p.then()...
         * p.then()...
         * p.then()...
         * 这里数组就应储存三个函数 当状态从 pending 改变时,数组遍历执行
         *  */
    this.onResolvedCallbacks = []
    this.onRejectedCallbacks = []
    const resolve = (val) => {
      // 如果一个promise resolve 了一个新的 promise
      // 则会等到内部的 promise 执行完, 获取它返回的结果
      if (val instanceof Promise) {
        return val.then(resolve, reject)
      }
      // 这里必须进行一次状态判断, 因为一个 promise 只能变一次状态
      // 当在调用 resolve 之前调用了 reject, 则 status 已经改变,这里应不再执行
      if (this.status === 'PENDING') {
        this.status = 'FULLFILLD'
        this.val = val
        this.onResolvedCallbacks.forEach(cb => cb())
      }
    }
    const reject = (reason) => {
      // 如果是 reject 的, 不用考虑 reason 是不是 promise 了,直接错误跑出
      if (this.status === 'PENDING') {
        this.status = 'REJECTED'
        this.reason = reason
        this.onRejectedCallbacks.forEach(cb => cb())
      }
    }
    // promise 必定会执行函数参数, 也算是一个缺点
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  static resolve (value) {
    return new Promise((resolve, reject) => {
      resolve(value)
    })
  }
  static reject (reason) {
    return new Promise((resolve, reject) => {
      reject(reason)
    })
  }
  static all (promises) {
    return new Promise((resolve, reject) => {
      let resolvedResult = []
      let resolvedCounter = 0
      for (let i = 0; i < promises.length; i++) {
        promises[i].then((val) => {
          resolvedCounter++
          resolvedResult[i] = val
          if (resolvedCounter === promises.length) {
            return resolve(resolvedResult)
          }
        }, e => reject(e))
      }
    })
  }
  static race (promises) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        // 只要有一个成功就成功,或者只要一个失败就失败
        promises[i].then(resolve, reject)
      }
    })
  }
  then (onFullFilled, onRejected) {
    // 可选参数需要为函数,如果不传或不是函数 则给出默认参数
    onFullFilled = typeof onFullFilled === 'function' ? onFullFilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
    // then 方法调用要返回 promise,即支持链式调用
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === 'FULLFILLD') {
        // 当前的onFulfilled, onRejected不能在这个上下文执行,要确保promise2存在,所以使用setTimeout
        setTimeout(() => {
          try {
            let x = onFullFilled(this.value)
            // 当前的onFulfilled, onRejected不能在这个上下文执行,要确保promise2存在,所以使用setTimeout
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.status === 'REJECTED') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
      // pending 状态就将函数收集到数组中去
      if (this.status === 'PENDING') {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFullFilled(this.value)
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
      }
    })
    return promise2
  }
}

// 在Promise的静态方法上加如下方法可以通过一个npm模块测试是否符合A+规范
// https://github.com/promises-aplus/promises-tests 首先全局安装promises-aplus-tests
// -> npm i promises-aplus-tests -g 再进行测试 promises-aplus-tests myPromise.js
Promise.deferred = function () {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}
// promise 返回值处理函数
// 处理成功回调和失败回调返回的 x 的类型
// 返回类型有3种情况 1、普通值 2、普通对象 3、promise对象
function resolvePromise (promise2, x, resolve, reject) {
  // 返回值不能是promise2,自己等待自己完成
  // 比如 p = new Promise((resolve)=> resolve()).then(()=>p);
  if (promise2 === x) {
    return new TypeError('返回自身会导致死循环')
  }
  if ((typeof x === 'object' && x != null) || typeof x === 'function') {
    let called // 控制 resolve 和 reject 只执行一次,多次调用没有任何作用
    try {
      let then = x.then
      if (typeof then === 'function') { // x 返回的是一个 promise 对象
        /**
          * 这里不用x.then的原因 是x.then会取then方法的get
          * 如 Object.defineProperty(x,'then',{
          *     get () {
          *        throw new Error()
          *    }
          * })
          *
          *  */
        then.call(x,
          y => { // 此处 y 还有可能返回一个 promise 所以用递归直到返回值为一个普通值为止
            if (called) return
            called = true // 没有调用过则 called 赋值为 true 来终止下次的调用
            resolvePromise(promise2, y, resolve, reject)
          },
          r => {
            if (called) return
            called = true
            reject(r) // 直接用 reject 处理错误就会直接断掉 promise 的执行
          }
        )
      } else {
        resolve(x) // x 可能是一个普通的对象而非promise对象直接resolve
      }
    } catch (e) {
      if (called) return // 防止多次调用
      called = true
      reject(e)
    }
  } else {
    resolve(x) // x 可能是一个普通的值直接resolve
  }
}
/**
 * 创建promise
 * @param {Number} value
 */
function makePromise (value) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(value)
    }, Math.random() * 1000)
  })
}
/**
 * 打印结果
 * @param {Number} value
 */
function print (value) {
  console.log(value)
  return value
}

let promises = [1, 3, 4, 5, 6].map((item, index) => {
  return makePromise(item)
})

// 并行执行
Promise
  .all(promises)
  .then(() => {
    console.log('done')
  })
  .catch(() => {
    console.log('error')
  })

// 串行执行
let parallelPromises = promises.reduce((total, currentValue) => total.then(() => currentValue.then(print)), Promise.resolve())

parallelPromises.then(() => {
  console.log('done')
}).catch(() => {
  console.log('done')
})

compose

  • compose 的参数是函数数组,返回的也是一个函数

  • compose 的参数是任意长度的,所有的参数都是函数,执行方向是自右向左的,因此初始函数一定放到参数的最右面

  • compose 执行后返回的函数可以接收参数,这个参数将作为初始函数的参数,所以初始函数的参数是多元的,初始函数的返回结果将作为下一个函数的参数,以此类推。因此除了初始函数之外,其他函数的接收值是一元的。

  • compose 和 pipe 的区别在于调用顺序的不同

let funcs = [fn1, fn2, fn3, fn4]
let composeFunc = compose(...funcs)
let pipeFunc = pipe(...funcs)

// compose
fn1(fn2(fn3(fn4(args))))
// pipe
fn4(fn3(fn2(fn1(args))))
function reduceFunc (prev, curr) {
  return (...args) => {
    curr.call(this, prev.apply(this, args))
  }
}

function compose (...args) {
  return args.reverse().reduce(reduceFunc, args.shift())
}

eventEmitter

const wrapCb = (fn, once = false) => ({ callback: fn, once })
class EventEmitter {
  constructor () {
    this.events = new Map()
  }
  // 绑定事件
  on (event, listener, once = false) {
    let handler = this.events.get(event)
    if (!handler) {
      this.events.set(event, wrapCb(listener, once))
    } else if (handler && typeof handler.callback === 'function') {
      // 如果只绑定一个回调
      this.events.set(event, [handler, wrapCb(listener, once)])
    } else {
      // 绑定了多个回调
      this.events.set(event, [...handler, wrapCb(listener, once)])
    }
  }
  // 解绑事件
  off (event, listener) {
    let handler = this.events.get(event)
    if (!handler) return
    if (!Array.isArray(handler)) {
      // 简单比较回调函数是否相同
      if (String(handler.callback) === String(listener)) {
        this.events.delete(event)
      } else {

      }
    } else {
      // 循环函数回调队列
      for (let i = 0; i < handler.length; i++) {
        const item = handler[i]
        if (String(item.callback) === String(listener)) {
          handler.splice(i, 1)
          i--
        }
      }
    }
  }
  // 注册一个单次监听器
  once (event, listener) {
    this.on(event, listener, true)
  }
  // 触发事件,按监听器的顺序执行执行每个监听器
  emit (event, ...args) {
    let handler = this.events.get(event)
    if (Array.isArray(handler)) {
      // 拷贝到一个数组中,防止后续数组长度出现变化,对数组的访问出错
      let eventsArr = []
      for (let i = 0; i < handler.length; i++) {
        eventsArr.push(handler[i])
      }
      // 遍历队列,触发每一个回调队列
      for (let i = 0; i < eventsArr.length; i++) {
        const item = eventsArr[i]
        item.callback.apply(this, args)
        if (item.once) {
          // 如果回调函数只运行一次,则删除该回调函数
          this.off(event, item.callback)
        }
      }
    } else {
      // 否则直接执行即可
      handler.callback.apply(this, args)
    }
    return true
  }
}

const myEvent = new EventEmitter()

const listener1 = (name) => {
  if (name) {
    console.log(`监听器 ${name} 执行。`)
  } else {
    console.log('监听器 listener1 执行。')
  }
}

const listener2 = () => {
  console.log('监听器 listener2 执行。')
}

const listener3 = () => {
  console.log('监听器 listener3 执行。')
}
myEvent.on('load', listener1)
myEvent.on('load', listener2)
myEvent.once('load', listener3)
myEvent.emit('load')
myEvent.off('load', listener2)
myEvent.emit('load', 'custom')
// 执行结果如下:
// 监听器 listener1 执行。
// 监听器 listener2 执行。
// 监听器 listener3 执行。
// 监听器 custom 执行。

offset


getBoundingClientRect通过 DOM React 来描述一个元素的具体位置。

const offset = ele => {
    let result = {
        top: 0,
        left: 0
    }
    // 当前为 IE11 以下,直接返回 {top: 0, left: 0}
    if (!ele.getClientRects().length) {
        return result
    }

    // 当前 DOM 节点的 display === 'none' 时,直接返回 {top: 0, left: 0}
    if (window.getComputedStyle(ele)['display'] === 'none') {
        return result
    }

    result = ele.getBoundingClientRect()
    // ownerDocument 返回当前节点的顶层的 document 对象。
    // ownerDocument 是文档,documentElement 是根节点
    var docElement = ele.ownerDocument.documentElement 
    return {
        top: result.top + window.pageYOffset - docElement.clientTop,
        left: result.left + window.pageXOffset - docElement.clientLeft
    }
}

Scheduler

class Scheduler {
  constructor (num) {
    this.num = num // 允许同时运行的异步函数的最大个数
    this.list = [] // 用来承载还未执行的异步
    this.count = 0 // 用来计数
  }

  async add (fn) {
    if (this.count >= this.num) {
      // 通过 await 阻塞 Promise 但是又不执行 resolve ,
      // 而是将 resolve 保存到数组当中去,
      // 这样就达到了当异步任务超过 max 个时线程就会阻塞在第一行.

      await new Promise((resolve) => { this.list.push(resolve) })
    }
    this.count++
    const result = await fn()
    this.count--
    if (this.list.length > 0) {
      // 每执行完一个异步任务就会去数组中查看一下有没有还处于阻塞当中的异步任务,
      // 如果有的话就执行最前面的那个异步任务.
      this.list.shift()()
    }
    return result
  }
}
const schedule = new Scheduler(2)// 最多同一时间让它执行3个异步函数

const timeout = (time) => new Promise(resolve => setTimeout(resolve, time))
const addTask = (time, order) => {
  schedule.add(() => timeout(time)).then(() => console.log(order))
}
addTask(1000, 1)
addTask(500, 2)
addTask(300, 3)
addTask(400, 4)
console.dir(schedule, 3)
// output: 2,3,1,4
// 一开始1、2 两个任务进入队列
// 500ms 时,2完成,输出2,任务3进队
// 800ms 时,3完成,输出3,任务4进队
// 1000ms 时, 1完成
// 1200ms 时,4完成

useFetch

function useFetch(fetch, params, visible = true) {
  const [data, setData] = useState({});
  const [loading, setLoading] = useState(false); // fetch 数据时页面 loading
  const [newParams, setNewParams] = useState(params);

  const fetchApi = useCallback(async () => {
    console.log('useCallback');
    if (visible) {
      setLoading(true);
      const res = await fetch(newParams);
      if (res.code === 1) {
        setData(res.data);
      }
      setLoading(false);
    }
  }, [fetch, newParams, visible]);

  useEffect(() => {
    console.log('useEffect');
    fetchApi();
  }, [fetchApi]);

  const doFetch = useCallback(rest => {
    setNewParams(rest);
  }, []);

  const reFetch = () => {
    setNewParams(Object.assign({}, newParams)); // 用户点击modal后加载数据,或者当请求参数依赖某个数据的有无才会fetch数据
  };
  return { loading, data, doFetch, reFetch };
}

useReducer

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

combineReducers

/**
 * Description: 组合不同的 Reducer
 * 
 * @param reducers
 * @return {Function} finalReducer
 */

rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
// rootReducer 将返回如下的 state 对象
{
  potato: {
    // ... potatoes, 和一些其他由 potatoReducer 管理的 state 对象 ...
  },
  tomato: {
    // ... tomatoes, 和一些其他由 tomatoReducer 管理的 state 对象,比如说 sauce 属性 ...
  }
}
function combineReducers(reducers) {
  // 返回合并后的新的 reducer 函数
  return function combination(state = {}, action) {
    const newState = {}
    Object.keys(reducers).map((key, i) => {
      const reducer = reducers[key]
      // 执行分 reducer,获得 newState
      newState[key] = reducer(state[key], action) // 这里通过 state[key] 来获取分模块的state,因此可知reducer的模块名字要和state中保持一致
    })
    return newState
  }
}

ErrorBoundary

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
 
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

双向数据绑定

HOC

const Input = props => (
  <>
    <p>{props.value}</p>
    <input placeholder='input' {...props}/>
  </>
)
const HocInput = HocBind(Input)
const App = () => (
    <HocInput initialValue='init' onChange={val => { console.log("HocInput", val) }} />
)
const HocBind = WrapperComponent => {
  return class extends React.Component {
    state = {
      value: this.props.initialValue
    }
    onChange = e => {
      const { onChange } = this.props;
      this.setState({ value: e.target.value });
      if (onChange) {
        onChange(e.target.value);
      }
    }
    render() {
      const newProps = {
        value: this.state.props,
        onChange: this.onChange
      };
      return <WrapperComponent {...newProps} />;
    }
  };
};

Render Props

const App = () => (
  <HocInput initialValue="init" onChange={val => { console.log('HocInput', val) }}>
    {props => (
      <div>
        <p>{props.value}</p>
        <input placeholder="input" {...props} />
      </div>
    )}
  </HocInput>
);
class HocInput extends React.Component {
  state={
    value: this.props.initialValue
  }
  onChange = (e) => {
    const { onChange } = this.props
    this.setState({ value: e.target.value })
    if (onChange) {
      onChange(this.state.value)
    }
  }

  render() {
    const newProps = {
      value: this.state.value,
      onChange: this.onChange
    };
    return (<div>
      {this.props.children({...newProps})}
    </div>)
  }

}

Hook

function useBind(initialValue) {
  const [value, setValue] = useState(initialValue)
  const onChange = e => {
    setValue(e.target.val)
  }
  return { value, onChange }
}


function InputBind() {
  const { value, onChange } = useBind('init')
  return (
    <div>
      <p>{value}</p>
      <input onChange={onChange}/>
    </div>
  )
}

总结:我是一个不太愿意造这种小轮子的人,所以面试的时候是比较排斥的,我觉得能说出基本的思路即可。自己完整整理了一遍,发现细节才是体现水平的地方,是对底层的深入理解。这是一个刻意练习的过程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值