zoukankan      html  css  js  c++  java
  • lodash源码学习curry,curryRight

    _.curry(func, [arity=func.length])

    柯里化函数

    关于函数柯里化,说简单点就是将接受多个参数的方法,转化为接受单一参数的方法,具体分析以及它的作用可以查看这篇文章http://www.zhangxinxu.com/wordpress/2013/02/js-currying/

    先看lodash中curry方法的源码

    //curry.js
    
    var createWrap = require('./_createWrap');//函数包装方法
    
    var WRAP_CURRY_FLAG = 8;
    
    /**
     * 
     *
     * _.curry.placeholder的值默认为`_`
     *
     * @param {Function} func 需要柯里化的函数.
     * @param {number} [arity=func.length] 参数数量.
     * @param- {Object} [guard] 是否能作为遍历参数被像_.map这样的方法调用.
     * @returns {Function} 返回柯里化之后的函数.
     * @example
     *
     * var abc = function(a, b, c) {
     *   return [a, b, c];
     * };
     *
     * var curried = _.curry(abc);
     *
     * curried(1)(2)(3);
     * // => [1, 2, 3]
     *
     * curried(1, 2)(3);
     * // => [1, 2, 3]
     *
     * curried(1, 2, 3);
     * // => [1, 2, 3]
     *
     * // Curried with placeholders.
     * curried(1)(_, 3)(2);
     * // => [1, 2, 3]
     */
    function curry(func, arity, guard) {
      arity = guard ? undefined : arity;
      //调用createWrap方法,传入位掩码标识为curry
      var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
      result.placeholder = curry.placeholder;
      return result;//返回包装后的方法
    }
    
    // 默认占位符赋值.
    curry.placeholder = {};
    
    module.exports = curry;

    这个方法依赖于createWrap方法,由于这个方法是很多方法的基础包装方法,所有依然只分析对应部分。

    //_createWrap.js
    
    var baseSetData = require('./_baseSetData'),
        createBind = require('./_createBind'),
        createCurry = require('./_createCurry'),
        createHybrid = require('./_createHybrid'),
        createPartial = require('./_createPartial'),
        getData = require('./_getData'),
        mergeData = require('./_mergeData'),
        setData = require('./_setData'),
        setWrapToString = require('./_setWrapToString'),
        toInteger = require('./toInteger');
    
    var FUNC_ERROR_TEXT = 'Expected a function';
    
    //各种方法的位掩码标识
    var WRAP_BIND_FLAG = 1,
        WRAP_BIND_KEY_FLAG = 2,
        WRAP_CURRY_FLAG = 8,
        WRAP_CURRY_RIGHT_FLAG = 16,
        WRAP_PARTIAL_FLAG = 32,
        WRAP_PARTIAL_RIGHT_FLAG = 64;
    
    var nativeMax = Math.max;//原生最大值方法
    
    /**
     * 创建一个函数,该函数可以创建或调用func用可选的this和部分应用的参数.
     *
     * @param {Function|string} func 需要包装的函数.
     * @param {number} bitmask 位掩码标识
     * @param {*} [thisArg] func的this对象
     * @param {Array} [partials] 应用的参数
     * @param {Array} [holders] 占位符的索引
     * @param {Array} [argPos] .
     * @param {number} [ary] .
     * @param {number} [arity] 可用参数数量.
     * @returns {Function} 返回包装之后的函数.
     */
     //lodash使用BitMask来进行各种方法的表示,BitMask使用方法可以看 http://geek.csdn.net/news/detail/73343
    
    function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
      var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;//是否是bindKey方法(不是)
      if (!isBindKey && typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      var length = partials ? partials.length : 0;//应用的参数个数,为0
      if (!length) {
        bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG);//清除WRAP_PARTIAL_FLAG和WRAP_PARTIAL_RIGHT_FLAG
        partials = holders = undefined;//部分参数和占位符索引都为undefined
      }
      ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);
      arity = arity === undefined ? arity : toInteger(arity);//将arity转为整形
      length -= holders ? holders.length : 0;//跳过
    
      if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) {//是否是WRAP_PARTIAL_RIGHT_FLAG(不是,跳过)
        var partialsRight = partials,
            holdersRight = holders;
    
        partials = holders = undefined;
      }
      var data = isBindKey ? undefined : getData(func);//得到func的元数据
    
      var newData = [
        func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
        argPos, ary, arity
      ];//将所有参数赋值给newData
    
      if (data) {
        mergeData(newData, data);
      }
      func = newData[0];
      bitmask = newData[1];
      thisArg = newData[2];
      partials = newData[3];
      holders = newData[4];
      arity = newData[9] = newData[9] === undefined
        ? (isBindKey ? 0 : func.length)
        : nativeMax(newData[9] - length, 0);//处理一下arity,默认为func的长度
    
      if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) {//如果没有arity,清除curry和curryRight标识
        bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG);
      }
      if (!bitmask || bitmask == WRAP_BIND_FLAG) {
        var result = createBind(func, bitmask, thisArg);
      } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) {//如果传了arity,进入这里,调用createCurry方法
        result = createCurry(func, bitmask, arity);
      } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) {
        result = createPartial(func, bitmask, thisArg, partials);
      } else {
        result = createHybrid.apply(undefined, newData);
      }
      var setter = data ? baseSetData : setData;//设置data的元数据(暂不分析)
      return setWrapToString(setter(result, newData), func, bitmask);//给包装之后的方法添加元数据(用于优化),添加toStirng方法,并返回func(具体实现暂不分析)
    }
    
    module.exports = createWrap;

    这个方法依赖于createCurry方法

    //_createCurry.js
    
    var apply = require('./_apply'),//同Function.apply
        createCtor = require('./_createCtor'),//创建一个可以创建函数实例的方法
        createHybrid = require('./_createHybrid'),//混合包装方法
        createRecurry = require('./_createRecurry'),//createRecurry方法
        getHolder = require('./_getHolder'),//得到占位符
        replaceHolders = require('./_replaceHolders'),//替换占位符
        root = require('./_root');//根元素,浏览器为window,node为global
    
    /**
     * 创建一个包装func,使其实现柯里化的方法.
     *
     * @private
     * @param {Function} func The function to wrap.
     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.
     * @param {number} arity The arity of `func`.
     * @returns {Function} Returns the new wrapped function.
     */
    function createCurry(func, bitmask, arity) {
      var Ctor = createCtor(func);//创建实例生成器
    
      function wrapper() {
        var length = arguments.length,//参数个数
            args = Array(length),//参数数组
            index = length,//参数索引
            placeholder = getHolder(wrapper);//得到占位符
    
        while (index--) {//遍历参数,将参数转化为args数组
          args[index] = arguments[index];
        }
        var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)
          ? []
          : replaceHolders(args, placeholder);//得到占位符索引
    
        length -= holders.length;//参数个数减去占位符的个数
        if (length < arity) {//如果参数个数少于arity,调用createRecurry,并返回得到的新的函数
          return createRecurry(
            func, bitmask, createHybrid, wrapper.placeholder, undefined,
            args, holders, undefined, undefined, arity - length);
        }
        var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
        return apply(fn, this, args);//调用fn
      }
      return wrapper;
    }
    
    module.exports = createCurry;

    如果参数未传完,调用createRecurry方法

    //_createRecurry.js
    
    var isLaziable = require('./_isLaziable'),//是否是一个惰性函数(在缓存中是否存在)
        setData = require('./_setData'),//设置元数据
        setWrapToString = require('./_setWrapToString');//添加toString方法
    
    //各种位掩码
    var WRAP_BIND_FLAG = 1,
        WRAP_BIND_KEY_FLAG = 2,
        WRAP_CURRY_BOUND_FLAG = 4,
        WRAP_CURRY_FLAG = 8,
        WRAP_PARTIAL_FLAG = 32,
        WRAP_PARTIAL_RIGHT_FLAG = 64;
    
    /**
     * 创建一个包装func使其持续柯里化的方法.
     *
     * @private
     * @param {Function} func 需要包装的方法.
     * @param {number} bitmask 位掩码标识.
     * @param {Function} wrapFunc 包装函数.
     * @param {*} placeholder 占位符的值.
     * @param {*} [thisArg] .
     * @param {Array} [partials] 提前传入的参数.
     * @param {Array} [holders] 占位符索引.
     * @param {Array} [argPos] The argument positions of the new function.
     * @param {number} [ary] The arity cap of `func`.
     * @param {number} [arity] 参数个数.
     * @returns {Function} 返回新的包装方法.
     */
    function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
      var isCurry = bitmask & WRAP_CURRY_FLAG,//是curry
          newHolders = isCurry ? holders : undefined,
          newHoldersRight = isCurry ? undefined : holders,
          newPartials = isCurry ? partials : undefined,
          newPartialsRight = isCurry ? undefined : partials;
    
      bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG);//添加partial标识
      bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG);//清除partialRight标识
    
      if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) {
        bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG);
      }
      var newData = [
        func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,
        newHoldersRight, argPos, ary, arity
      ];
    
      var result = wrapFunc.apply(undefined, newData);//调用wrapFunc方法
      if (isLaziable(func)) {//如果在缓存中存在,设置元数据
        setData(result, newData);
      }
      result.placeholder = placeholder;
      return setWrapToString(result, func, bitmask);//添加toString方法并返回包装函数
    }
    
    module.exports = createRecurry;

    这个方法的wrapfunc也就是createHybrid

    //_createHybrid.js
    
    var composeArgs = require('./_composeArgs'),//组合参数方法,见源码学习(7)
        composeArgsRight = require('./_composeArgsRight'),
        countHolders = require('./_countHolders'),
        createCtor = require('./_createCtor'),
        createRecurry = require('./_createRecurry'),
        getHolder = require('./_getHolder'),
        reorder = require('./_reorder'),
        replaceHolders = require('./_replaceHolders'),
        root = require('./_root');
    
    //位掩码标识
    var WRAP_BIND_FLAG = 1,
        WRAP_BIND_KEY_FLAG = 2,
        WRAP_CURRY_FLAG = 8,
        WRAP_CURRY_RIGHT_FLAG = 16,
        WRAP_ARY_FLAG = 128,
        WRAP_FLIP_FLAG = 512;
    
    /**
     * 创建一个包装函数,调用func使用可选的thisArg,应用部分参数和柯里化.
     *
     * @param {Function|string} func 需要包装的方法.
     * @param {number} bitmask 位掩码标识
     * @param {*} [thisArg] .
     * @param {Array} [partials] 实现传入的参数.
     * @param {Array} [holders] 占位符.
     * @param {Array} [partialsRight] .
     * @param {Array} [holdersRight] .
     * @param {Array} [argPos] .
     * @param {number} [ary] .
     * @param {number} [arity] 可用函数参数数量.
     * @returns {Function} 返回新的包装函数.
     */
    function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
      var isAry = bitmask & WRAP_ARY_FLAG,
          isBind = bitmask & WRAP_BIND_FLAG,
          isBindKey = bitmask & WRAP_BIND_KEY_FLAG,
          isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG),//是curry方法
          isFlip = bitmask & WRAP_FLIP_FLAG,
          Ctor = isBindKey ? undefined : createCtor(func);
    
      function wrapper() {
        var length = arguments.length,//参数个数
            args = Array(length),//保存所有参数
            index = length;//参数数组索引
    
        while (index--) {//遍历参数,将所有参数存入args
          args[index] = arguments[index];
        }
        if (isCurried) {
          var placeholder = getHolder(wrapper),//得到占位符标识
              holdersCount = countHolders(args, placeholder);//占位符数量
        }
        if (partials) {//执行composeArgs,对参数进行组合
          args = composeArgs(args, partials, holders, isCurried);
        }
        if (partialsRight) {
          args = composeArgsRight(args, partialsRight, holdersRight, isCurried);
        }
        length -= holdersCount;//参数个数减去占位符数量
        if (isCurried && length < arity) {//如果个数比arity小
          var newHolders = replaceHolders(args, placeholder);//得到新的占位符索引位置
          return createRecurry(
            func, bitmask, createHybrid, wrapper.placeholder, thisArg,
            args, newHolders, argPos, ary, arity - length
          );//再次调用createRecurry方法,继续柯里化(会一直循环调用createRecurry和createHybrid方法,直到参数传完整)
        }
        var thisBinding = isBind ? thisArg : this,
            fn = isBindKey ? thisBinding[func] : func;
    
        length = args.length;//参数长度
        if (argPos) {
          args = reorder(args, argPos);
        } else if (isFlip && length > 1) {
          args.reverse();
        }
        if (isAry && ary < length) {
          args.length = ary;
        }
        if (this && this !== root && this instanceof wrapper) {
          fn = Ctor || createCtor(fn);
        }
        return fn.apply(thisBinding, args);//调用fn,并且传入args
      }
      return wrapper;//返回包装方法
    }
    module.exports = createHybrid;

    _.curryRight(func, [arity=func.length])

     很_.curry很像,除了它接受参数从func的最后一个参数开始

    这个方法和curry基本上实现是差不多的,只有细微的差别

    //curryRight.js
    
    var createWrap = require('./_createWrap');//包装方法
    
    var WRAP_CURRY_RIGHT_FLAG = 16;//curryRight位掩码标识
    
    /**
     *
     *
     * _.curryRight.placeholder默认值为`_`
     *
     * @param {Function} func 需要柯里化的函数.
     * @param {number} [arity=func.length] 参数数量.
     * @param- {Object} [guard] 是否能作为遍历参数被像_.map这样的方法调用.
     * @returns {Function} 返回柯里化之后的函数.
     * @example
     *
     * var abc = function(a, b, c) {
     *   return [a, b, c];
     * };
     *
     * var curried = _.curryRight(abc);
     *
     * curried(3)(2)(1);
     * // => [1, 2, 3]
     *
     * curried(2, 3)(1);
     * // => [1, 2, 3]
     *
     * curried(1, 2, 3);
     * // => [1, 2, 3]
     *
     * // Curried with placeholders.
     * curried(3)(1, _)(2);
     * // => [1, 2, 3]
     */
    function curryRight(func, arity, guard) {//除了传入位掩码的不同和curry一模一样
      arity = guard ? undefined : arity;
      var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
      result.placeholder = curryRight.placeholder;
      return result;
    }
    
    // Assign default placeholders.
    curryRight.placeholder = {};
    
    module.exports = curryRight;

    调用步骤和curry类似,看不同的部分

    createRecurry

    //_createRecurry.js
    
    var isLaziable = require('./_isLaziable'),//是否是一个惰性函数(在缓存中是否存在)
        setData = require('./_setData'),//设置元数据
        setWrapToString = require('./_setWrapToString');//添加toString方法
    
    //各种位掩码
    var WRAP_BIND_FLAG = 1,
        WRAP_BIND_KEY_FLAG = 2,
        WRAP_CURRY_BOUND_FLAG = 4,
        WRAP_CURRY_FLAG = 8,
        WRAP_PARTIAL_FLAG = 32,
        WRAP_PARTIAL_RIGHT_FLAG = 64;
    
    /**
     * 创建一个包装func使其持续柯里化的方法.
     *
     * @private
     * @param {Function} func 需要包装的方法.
     * @param {number} bitmask 位掩码标识.
     * @param {Function} wrapFunc 包装函数.
     * @param {*} placeholder 占位符的值.
     * @param {*} [thisArg] .
     * @param {Array} [partials] 提前传入的参数.
     * @param {Array} [holders] 占位符索引.
     * @param {Array} [argPos] The argument positions of the new function.
     * @param {number} [ary] The arity cap of `func`.
     * @param {number} [arity] 参数个数.
     * @returns {Function} 返回新的包装方法.
     */
    function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
      var isCurry = bitmask & WRAP_CURRY_FLAG,//不是curry
          newHolders = isCurry ? holders : undefined,
          newHoldersRight = isCurry ? undefined : holders,//设置newHoldersRight
          newPartials = isCurry ? partials : undefined,
          newPartialsRight = isCurry ? undefined : partials;//设置newPartialsRight
    
      bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG);//添加partialRight标识
      bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG);//清除partial标识
    
      if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) {
        bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG);
      }
      var newData = [
        func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,
        newHoldersRight, argPos, ary, arity
      ];
    
      var result = wrapFunc.apply(undefined, newData);//调用wrapFunc方法
      if (isLaziable(func)) {//如果在缓存中存在,设置元数据
        setData(result, newData);
      }
      result.placeholder = placeholder;
      return setWrapToString(result, func, bitmask);//添加toString方法并返回包装函数
    }
    
    module.exports = createRecurry;

    createHybrid

    //_createHybrid.js
    
    var composeArgs = require('./_composeArgs'),//组合参数方法
        composeArgsRight = require('./_composeArgsRight'),//从右向左组合参数
        countHolders = require('./_countHolders'),
        createCtor = require('./_createCtor'),
        createRecurry = require('./_createRecurry'),
        getHolder = require('./_getHolder'),
        reorder = require('./_reorder'),
        replaceHolders = require('./_replaceHolders'),
        root = require('./_root');
    
    //位掩码标识
    var WRAP_BIND_FLAG = 1,
        WRAP_BIND_KEY_FLAG = 2,
        WRAP_CURRY_FLAG = 8,
        WRAP_CURRY_RIGHT_FLAG = 16,
        WRAP_ARY_FLAG = 128,
        WRAP_FLIP_FLAG = 512;
    
    /**
     * 创建一个包装函数,调用func使用可选的thisArg,应用部分参数和柯里化.
     *
     * @param {Function|string} func 需要包装的方法.
     * @param {number} bitmask 位掩码标识
     * @param {*} [thisArg] .
     * @param {Array} [partials] 实现传入的参数.
     * @param {Array} [holders] 占位符.
     * @param {Array} [partialsRight] .
     * @param {Array} [holdersRight] .
     * @param {Array} [argPos] .
     * @param {number} [ary] .
     * @param {number} [arity] 可用函数参数数量.
     * @returns {Function} 返回新的包装函数.
     */
    function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
      var isAry = bitmask & WRAP_ARY_FLAG,
          isBind = bitmask & WRAP_BIND_FLAG,
          isBindKey = bitmask & WRAP_BIND_KEY_FLAG,
          isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG),//是curry方法
          isFlip = bitmask & WRAP_FLIP_FLAG,
          Ctor = isBindKey ? undefined : createCtor(func);
    
      function wrapper() {
        var length = arguments.length,//参数个数
            args = Array(length),//保存所有参数
            index = length;//参数数组索引
    
        while (index--) {//遍历参数,将所有参数存入args
          args[index] = arguments[index];
        }
        if (isCurried) {
          var placeholder = getHolder(wrapper),//得到占位符标识
              holdersCount = countHolders(args, placeholder);//占位符数量
        }
        if (partials) {
          args = composeArgs(args, partials, holders, isCurried);
        }
        if (partialsRight) {//执行composeArgsRight方法,组合参数
          args = composeArgsRight(args, partialsRight, holdersRight, isCurried);
        }
        length -= holdersCount;//参数个数减去占位符数量
        if (isCurried && length < arity) {//如果个数比arity小
          var newHolders = replaceHolders(args, placeholder);//得到新的占位符索引位置
          return createRecurry(
            func, bitmask, createHybrid, wrapper.placeholder, thisArg,
            args, newHolders, argPos, ary, arity - length
          );//再次调用createRecurry方法,继续柯里化(会一直循环调用createRecurry和createHybrid方法,直到参数传完整)
        }
        var thisBinding = isBind ? thisArg : this,
            fn = isBindKey ? thisBinding[func] : func;
    
        length = args.length;//参数长度
        if (argPos) {
          args = reorder(args, argPos);
        } else if (isFlip && length > 1) {
          args.reverse();
        }
        if (isAry && ary < length) {
          args.length = ary;
        }
        if (this && this !== root && this instanceof wrapper) {
          fn = Ctor || createCtor(fn);
        }
        return fn.apply(thisBinding, args);//调用fn,并且传入args
      }
      return wrapper;//返回包装方法
    }
    module.exports = createHybrid;

    composeArgsRight

    //_composeArgsRight.js
    
    var nativeMax = Math.max;//原生求最大值方法
    
    /**
     * 和composeArgs很像,只是组合参数的方向相反
     *
    * @private
     * @param {Array} args 提供的参数.
     * @param {Array} partials 提前传入的参数.
     * @param {Array} holders 提前传入参数中的占位符索引.
     * @params {boolean} [isCurried] 指定是否组成一个柯里化函数.
     * @returns {Array} 返回新的参数集合.
     */
    function composeArgsRight(args, partials, holders, isCurried) {
      var argsIndex = -1,//参数索引
          argsLength = args.length,//传入参数个数
          holdersIndex = -1,//占位符索引
          holdersLength = holders.length,//占位符个数
          rightIndex = -1,//提前传入参数索引
          rightLength = partials.length,//提前传入参数个数
          rangeLength = nativeMax(argsLength - holdersLength, 0),//实际传入参数个数
          result = Array(rangeLength + rightLength),//结果参数数组
          isUncurried = !isCurried;
    
      while (++argsIndex < rangeLength) {//遍历传入参数,将其传入result
        result[argsIndex] = args[argsIndex];
      }
      var offset = argsIndex;
      while (++rightIndex < rightLength) {//遍历partials,将其传入result
        result[offset + rightIndex] = partials[rightIndex];
      }
      while (++holdersIndex < holdersLength) {//遍历占位符索引,将result中对应索引的值,换成传入的参数对应的值
        if (isUncurried || argsIndex < argsLength) {
          result[offset + holders[holdersIndex]] = args[argsIndex++];
        }
      }
      return result;//返回生成的完整参数数组
    }
    
    module.exports = composeArgsRight;

    至此lodash中的函数柯里化分析完毕。

  • 相关阅读:
    ural1018(树形dp)
    hdu1011(树形dp)
    poj1463(树形dp)
    poj1655(树形dp)
    poj1155(树形dp)
    hdu2196(树形dp)
    hdu1520(树形dp)
    hdu2126(求方案数的01背包)
    运用bootstrap框架的时候 引入文件的问题
    动态的改变标签内的src属性
  • 原文地址:https://www.cnblogs.com/wandiao/p/7188588.html
Copyright © 2011-2022 走看看