当前位置:  开发笔记 > 编程语言 > 正文

如何动态获取函数参数名称/值?

如何解决《如何动态获取函数参数名称/值?》经验,为你挑选了11个好方法。

有没有办法动态获取函数的函数参数名称?

假设我的函数看起来像这样:

function doSomething(param1, param2, .... paramN){
   // fill an array with the parameter name and value
   // some other code 
}

现在,我如何从函数内部获取参数名称及其值的列表到数组中?



1> Jack Allan..:

以下函数将返回传入的任何函数的参数名称数组.

var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
function getParamNames(func) {
  var fnStr = func.toString().replace(STRIP_COMMENTS, '');
  var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
  if(result === null)
     result = [];
  return result;
}

用法示例:

getParamNames(getParamNames) // returns ['func']
getParamNames(function (a,b,c,d){}) // returns ['a','b','c','d']
getParamNames(function (a,/*b,c,*/d){}) // returns ['a','d']
getParamNames(function (){}) // returns []

编辑:

随着ES6的发明,这个功能可以通过默认参数跳闸.这是一个快速的黑客,在大多数情况下应该工作:

var STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;

我说大多数情况都是因为有些事情会让它绊倒

function (a=4*(5/3), b) {} // returns ['a']

编辑:我也注意到vikasde也希望数组中的参数值.这已在名为arguments的局部变量中提供.

摘录自https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments:

arguments对象不是Array.它类似于Array,但除了length之外没有任何Array属性.例如,它没有pop方法.但是它可以转换为真正的数组:

var args = Array.prototype.slice.call(arguments);

如果Array泛型可用,则可以使用以下代码:

var args = Array.slice(arguments);


请注意,由于注释和空格,此解决方案可能会失败 - 例如:`var fn = function(a/*fooled you)*/,b){};`将导致`["a","/*", "骗了","你"]`
编译正则表达式需要付出代价,因此您希望避免多次编译复杂的正则表达式.这就是它在函数之外完成的原因
更正:是否要使用/ s修饰符修改正则表达式,perl允许这样做'.' 也可以匹配换行符.这对于/**/中的多行注释是必需的.原来Javascript正则表达式不允许使用/ s修饰符.使用[/ s/S]的原始正则表达式确实匹配换行符.SOOO,请忽略之前的评论.

2> Lambder..:

下面是从AngularJS获取的代码,它使用该技术进行依赖注入机制.

以下是对http://docs.angularjs.org/tutorial/step_05的解释

Angular的依赖注入器在构造控制器时为控制器提供服务.依赖注入器还负责创建服务可能具有的任何传递依赖性(服务通常依赖于其他服务).

请注意,参数的名称很重要,因为注入器使用这些参数来查找依赖项.

/**
 * @ngdoc overview
 * @name AUTO
 * @description
 *
 * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}.
 */

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function annotate(fn) {
  var $inject,
      fnText,
      argDecl,
      last;

  if (typeof fn == 'function') {
    if (!($inject = fn.$inject)) {
      $inject = [];
      fnText = fn.toString().replace(STRIP_COMMENTS, '');
      argDecl = fnText.match(FN_ARGS);
      forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
        arg.replace(FN_ARG, function(all, underscore, name){
          $inject.push(name);
        });
      });
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) {
    last = fn.length - 1;
    assertArgFn(fn[last], 'fn')
    $inject = fn.slice(0, last);
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject;
}


@apaidnerd带着恶魔的血和撒旦的产卵,显然.正则表达式?如果在JS中有内置方式会很酷,不会.
为了节省人们的时间,你可以从角度通过```annotate = angular.injector.$$ annotate``获得这个功能.
@apaidnerd,真实!只是想到 - 地狱是如何实施的?其实我想过使用functionName.toString()但我希望有更优雅的东西(也许更快)
我真的在互联网上寻找这个话题,因为我很好奇Angular是怎么做到的......现在我知道了,而且我也知道得太多了!
@ sasha.sochka,来到这里想知道完全相同的事情,在意识到没有内置的方式来获取参数名称与JavaScript
如果你已经在使用Angular,你可以使用它的[`$ injector`服务](https://docs.angularjs.org/api/auto/service/$injector):`$ injector.invoke(function(serviceA) {});`

3> humbletim..:

这是一个更新的解决方案,试图以紧凑的方式解决上面提到的所有边缘情况:

function $args(func) {  
    return (func + '')
      .replace(/[/][/].*$/mg,'') // strip single-line comments
      .replace(/\s+/g, '') // strip white space
      .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  
      .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters  
      .replace(/=[^,]+/g, '') // strip any ES6 defaults  
      .split(',').filter(Boolean); // split & filter [""]
}  

缩略测试输出(完整的测试用例如下):

'function (a,b,c)...' // returns ["a","b","c"]
'function ()...' // returns []
'function named(a, b, c) ...' // returns ["a","b","c"]
'function (a /* = 1 */, b /* = true */) ...' // returns ["a","b"]
'function fprintf(handle, fmt /*, ...*/) ...' // returns ["handle","fmt"]
'function( a, b = 1, c )...' // returns ["a","b","c"]
'function (a=4*(5/3), b) ...' // returns ["a","b"]
'function (a, // single-line comment xjunk) ...' // returns ["a","b"]
'function (a /* fooled you...' // returns ["a","b"]
'function (a /* function() yes */, \n /* no, */b)/* omg! */...' // returns ["a","b"]
'function ( A, b \n,c ,d \n ) \n ...' // returns ["A","b","c","d"]
'function (a,b)...' // returns ["a","b"]
'function $args(func) ...' // returns ["func"]
'null...' // returns ["null"]
'function Object() ...' // returns []

function $args(func) {  
    return (func + '')
      .replace(/[/][/].*$/mg,'') // strip single-line comments
      .replace(/\s+/g, '') // strip white space
      .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  
      .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters  
      .replace(/=[^,]+/g, '') // strip any ES6 defaults  
      .split(',').filter(Boolean); // split & filter [""]
}  

// test cases  
document.getElementById('console_info').innerHTML = (
[  
  // formatting -- typical  
  function(a,b,c){},  
  function(){},  
  function named(a, b,  c) {  
/* multiline body */  
  },  
    
  // default values -- conventional  
  function(a /* = 1 */, b /* = true */) { a = a||1; b=b||true; },  
  function fprintf(handle, fmt /*, ...*/) { },  
  
  // default values -- ES6  
  "function( a, b = 1, c ){}",  
  "function (a=4*(5/3), b) {}",  
  
  // embedded comments -- sardonic  
  function(a, // single-line comment xjunk) {}
    b //,c,d
  ) // single-line comment
  {},  
  function(a /* fooled you{*/,b){},  
  function /* are you kidding me? (){} */(a /* function() yes */,  
   /* no, */b)/* omg! */{/*}}*/},  
  
  // formatting -- sardonic  
  function  (  A,  b  
,c  ,d  
  )  
  {  
  },  
  
  // by reference  
  this.jQuery || function (a,b){return new e.fn.init(a,b,h)},
  $args,  
  
  // inadvertent non-function values  
  null,  
  Object  
].map(function(f) {
    var abbr = (f + '').replace(/\n/g, '\\n').replace(/\s+|[{]+$/g, ' ').split("{", 1)[0] + "...";
    return "    '" + abbr + "' // returns " + JSON.stringify($args(f));
  }).join("\n") + "\n"); // output for copy and paste as a markdown snippet


4> bubersson..:

不太容易出现空格和注释的解决方案是:

var fn = function(/* whoa) */ hi, you){};

fn.toString()
  .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg,'')
  .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
  .split(/,/)

["hi", "you"]


不适用于ES6胖箭头功能:)

5> James Drew..:

这里的很多答案都使用了正则表达式,这很好但是它并没有很好地处理语言的新增功能(比如箭头函数和类).另外值得注意的是,如果你在缩小代码上使用这些函数中的任何一个,它就会发生.它将使用任何缩小的名称.Angular通过允许您在向DI容器注册它们时传入与参数顺序匹配的有序字符串数组来解决这个问题.所以解决方案:

var esprima = require('esprima');
var _ = require('lodash');

const parseFunctionArguments = (func) => {
    // allows us to access properties that may or may not exist without throwing 
    // TypeError: Cannot set property 'x' of undefined
    const maybe = (x) => (x || {});

    // handle conversion to string and then to JSON AST
    const functionAsString = func.toString();
    const tree = esprima.parse(functionAsString);
    console.log(JSON.stringify(tree, null, 4))
    // We need to figure out where the main params are. Stupid arrow functions 
    const isArrowExpression = (maybe(_.first(tree.body)).type == 'ExpressionStatement');
    const params = isArrowExpression ? maybe(maybe(_.first(tree.body)).expression).params 
                                     : maybe(_.first(tree.body)).params;

    // extract out the param names from the JSON AST
    return _.map(params, 'name');
};

这处理原始解析问题和一些其他函数类型(例如箭头函数).以下是它能够和不能处理的概念:

// I usually use mocha as the test runner and chai as the assertion library
describe('Extracts argument names from function signature. ', () => {
    const test = (func) => {
        const expectation = ['it', 'parses', 'me'];
        const result = parseFunctionArguments(toBeParsed);
        result.should.equal(expectation);
    } 

    it('Parses a function declaration.', () => {
        function toBeParsed(it, parses, me){};
        test(toBeParsed);
    });

    it('Parses a functional expression.', () => {
        const toBeParsed = function(it, parses, me){};
        test(toBeParsed);
    });

    it('Parses an arrow function', () => {
        const toBeParsed = (it, parses, me) => {};
        test(toBeParsed);
    });

    // ================= cases not currently handled ========================

    // It blows up on this type of messing. TBH if you do this it deserves to 
    // fail  On a tech note the params are pulled down in the function similar 
    // to how destructuring is handled by the ast.
    it('Parses complex default params', () => {
        function toBeParsed(it=4*(5/3), parses, me) {}
        test(toBeParsed);
    });

    // This passes back ['_ref'] as the params of the function. The _ref is a 
    // pointer to an VariableDeclarator where the ? happens.
    it('Parses object destructuring param definitions.' () => {
        function toBeParsed ({it, parses, me}){}
        test(toBeParsed);
    });

    it('Parses object destructuring param definitions.' () => {
        function toBeParsed ([it, parses, me]){}
        test(toBeParsed);
    });

    // Classes while similar from an end result point of view to function
    // declarations are handled completely differently in the JS AST. 
    it('Parses a class constructor when passed through', () => {
        class ToBeParsed {
            constructor(it, parses, me) {}
        }
        test(ToBeParsed);
    });
});

根据您想要使用它的ES6代理和解构可能是您最好的选择.例如,如果您想将它用于依赖注入(使用参数的名称),那么您可以按如下方式执行:

class GuiceJs {
    constructor() {
        this.modules = {}
    }
    resolve(name) {
        return this.getInjector()(this.modules[name]);
    }
    addModule(name, module) {
        this.modules[name] = module;
    }
    getInjector() {
        var container = this;

        return (klass) => {
            console.log(klass);
            var paramParser = new Proxy({}, {
                // The `get` handler is invoked whenever a get-call for
                // `injector.*` is made. We make a call to an external service
                // to actually hand back in the configured service. The proxy
                // allows us to bypass parsing the function params using
                // taditional regex or even the newer parser.
                get: (target, name) => container.resolve(name),

                // You shouldn't be able to set values on the injector.
                set: (target, name, value) => {
                    throw new Error(`Don't try to set ${name}! `);
                }
            })
            return new klass(paramParser);
        }
    }
}

它不是那里最先进的解析器,但如果你想使用args解析器进行简单的DI,它会让你知道如何使用代理来处理它.然而,这种方法有一点需要注意.我们需要使用解构赋值而不是普通参数.当我们传入注入器代理时,解构与调用对象上的getter相同.

class App {
   constructor({tweeter, timeline}) {
        this.tweeter = tweeter;
        this.timeline = timeline;
    }
}

class HttpClient {}

class TwitterApi {
    constructor({client}) {
        this.client = client;
    }
}

class Timeline {
    constructor({api}) {
        this.api = api;
    }
}

class Tweeter {
    constructor({api}) {
        this.api = api;
    }
}

// Ok so now for the business end of the injector!
const di = new GuiceJs();

di.addModule('client', HttpClient);
di.addModule('api', TwitterApi);
di.addModule('tweeter', Tweeter);
di.addModule('timeline', Timeline);
di.addModule('app', App);

var app = di.resolve('app');
console.log(JSON.stringify(app, null, 4));

这输出如下:

{
    "tweeter": {
        "api": {
            "client": {}
        }
    },
    "timeline": {
        "api": {
            "client": {}
        }
    }
}

它连接整个应用程序.最好的一点是应用程序很容易测试(你可以实例化每个类并传入模拟/存根/等).此外,如果您需要交换实现,您可以从一个地方执行此操作.由于JS代理对象,这一切都是可能的.

注意:在准备好用于生产之前,需要做很多工作,但它确实会让人知道它的外观.

答案有点晚,但它可能有助于其他想到同样事情的人.



6> Domino..:

我知道这是一个古老的问题,但是初学者一直在进行复制,好像这是任何代码中的好习惯.大多数情况下,必须解析函数的字符串表示以使用其参数名称只是隐藏了代码逻辑中的缺陷.

函数的参数实际上存储在一个类似于数组的对象中arguments,其中第一个参数是arguments[0],第二个是arguments[1],依此类推.在括号中写入参数名称可以看作是一种简写语法.这个:

function doSomething(foo, bar) {
    console.log("does something");
}

...是相同的:

function doSomething() {
    var foo = arguments[0];
    var bar = arguments[1];

    console.log("does something");
}

变量本身存储在函数的作用域中,而不是作为对象中的属性.无法通过代码检索参数名称,因为它只是表示人类语言变量的符号.

我总是将函数的字符串表示视为用于调试目的的工具,尤其是因为这个arguments类似于数组的对象.您不需要首先为参数命名.如果您尝试解析字符串化函数,它实际上并不会告诉您可能需要的额外未命名参数.

这是一个更糟糕,更常见的情况.如果函数具有3个或4个以上的参数,则将其传递给对象可能是合乎逻辑的,这更容易使用.

function saySomething(obj) {
  if(obj.message) console.log((obj.sender || "Anon") + ": " + obj.message);
}

saySomething({sender: "user123", message: "Hello world"});

在这种情况下,函数本身将能够读取它接收的对象并查找其属性并获取它们的名称和值,但是尝试解析函数的字符串表示只会给出参数的"obj",这根本没用.



7> Zack Morris..:

由于JavaScript是一种脚本语言,我觉得它的内省应该支持获取函数参数名称.对该功能进行修改违反了第一原则,因此我决定进一步探讨该问题.

这让我想到了这个问题,但没有内置的解决方案.这让我得到了这个解释,这个解释arguments只是在函数之外被弃用,所以我们不能再使用myFunction.arguments或得到:

TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

是时候卷起袖子开始工作了:

⭐检索函数参数需要解析器,因为复杂的表达式4*(5/3)可以用作默认值.因此,Gaafar的答案或James Drew的答案是迄今为止最好的方法.

我尝试了巴比伦和esprima解析器但不幸的是他们无法解析独立的匿名函数,正如Mateusz Charytoniuk的回答所指出的那样.我想通过围绕括号中的代码找出另一种解决方法,以免改变逻辑:

const ast = parser.parse("(\n" + func.toString() + "\n)")

换行符可防止//(单行注释)出现问题.

⭐如果解析器不可用,那么下一个最佳选择是使用像Angular.js的依赖注入器正则表达式这样经过验证的技术.我将Lambder的答案的功能版本与humbletim的答案结合起来,并添加了一个可选的ARROW布尔值来控制正则表达式是否允许ES6胖箭头函数.


这是我放在一起的两个解决方案.请注意,它们没有逻辑来检测函数是否具有有效语法,它们只提取参数.这通常是好的,因为我们通常将解析的函数传递给getArguments()它们,因此它们的语法已经有效.

我将尝试尽可能地策划这些解决方案,但是如果没有JavaScript维护者的努力,这将仍然是一个悬而未决的问题.

Node.js版本(在StackOverflow支持Node.js之前不可运行):

const parserName = 'babylon';
// const parserName = 'esprima';
const parser = require(parserName);

function getArguments(func) {
    const maybe = function (x) {
        return x || {}; // optionals support
    }

    try {
        const ast = parser.parse("(\n" + func.toString() + "\n)");
        const program = parserName == 'babylon' ? ast.program : ast;

        return program
            .body[0]
            .expression
            .params
            .map(function(node) {
                return node.name || maybe(node.left).name || '...' + maybe(node.argument).name;
            });
    } catch (e) {
        return []; // could also return null
    }
};

////////// TESTS //////////

function logArgs(func) {
	let object = {};

	object[func] = getArguments(func);

	console.log(object);
// 	console.log(/*JSON.stringify(*/getArguments(func)/*)*/);
}

console.log('');
console.log('////////// MISC //////////');

logArgs((a, b) => {});
logArgs((a, b = 1) => {});
logArgs((a, b, ...args) => {});
logArgs(function(a, b, ...args) {});
logArgs(function(a, b = 1, c = 4 * (5 / 3), d = 2) {});
logArgs(async function(a, b, ...args) {});
logArgs(function async(a, b, ...args) {});

console.log('');
console.log('////////// FUNCTIONS //////////');

logArgs(function(a, b, c) {});
logArgs(function() {});
logArgs(function named(a, b, c) {});
logArgs(function(a /* = 1 */, b /* = true */) {});
logArgs(function fprintf(handle, fmt /*, ...*/) {});
logArgs(function(a, b = 1, c) {});
logArgs(function(a = 4 * (5 / 3), b) {});
// logArgs(function (a, // single-line comment xjunk) {});
// logArgs(function (a /* fooled you {});
// logArgs(function (a /* function() yes */, \n /* no, */b)/* omg! */ {});
// logArgs(function ( A, b \n,c ,d \n ) \n {});
logArgs(function(a, b) {});
logArgs(function $args(func) {});
logArgs(null);
logArgs(function Object() {});

console.log('');
console.log('////////// STRINGS //////////');

logArgs('function (a,b,c) {}');
logArgs('function () {}');
logArgs('function named(a, b, c) {}');
logArgs('function (a /* = 1 */, b /* = true */) {}');
logArgs('function fprintf(handle, fmt /*, ...*/) {}');
logArgs('function( a, b = 1, c ) {}');
logArgs('function (a=4*(5/3), b) {}');
logArgs('function (a, // single-line comment xjunk) {}');
logArgs('function (a /* fooled you {}');
logArgs('function (a /* function() yes */, \n /* no, */b)/* omg! */ {}');
logArgs('function ( A, b \n,c ,d \n ) \n {}');
logArgs('function (a,b) {}');
logArgs('function $args(func) {}');
logArgs('null');
logArgs('function Object() {}');


8> Will..:
(function(a,b,c){}).toString().replace(/.*\(|\).*/ig,"").split(',')

=> ["a","b","c"]



9> Mateusz Char..:

您还可以使用"esprima"解析器来避免参数列表中的注释,空格和其他内容的许多问题.

function getParameters(yourFunction) {
    var i,
        // safetyValve is necessary, because sole "function () {...}"
        // is not a valid syntax
        parsed = esprima.parse("safetyValve = " + yourFunction.toString()),
        params = parsed.body[0].expression.right.params,
        ret = [];

    for (i = 0; i < params.length; i += 1) {
        // Handle default params. Exe: function defaults(a = 0,b = 2,c = 3){}
        if (params[i].type == 'AssignmentPattern') {
            ret.push(params[i].left.name)
        } else {
            ret.push(params[i].name);
        }
    }

    return ret;
}

它甚至可以使用这样的代码:

getParameters(function (hello /*, foo ),* /bar* { */,world) {}); // ["hello", "world"]



10> Hugoware..:

我之前尝试过这样做,但从来没有找到一种完成它的实用方法.我最终传入了一个对象,然后循环遍历它.

//define like
function test(args) {
    for(var item in args) {
        alert(item);
        alert(args[item]);
    }
}

//then used like
test({
    name:"Joe",
    age:40,
    admin:bool
});



11> Jaketr00..:

我已经在这里阅读了大多数答案,并且我想添加我的单行代码。

new RegExp('(?:'+Function.name+'\\s*|^)\\((.*?)\\)').exec(Function.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '')

要么

function getParameters(func) {
  return new RegExp('(?:'+func.name+'\\s*|^)\\s*\\((.*?)\\)').exec(func.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '');
}

或ECMA6中的单线功能

var getParameters = func => new RegExp('(?:'+func.name+'\\s*|^)\\s*\\((.*?)\\)').exec(func.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '');

__

假设您有一个功能

function foo(abc, def, ghi, jkl) {
  //code
}

下面的代码将返回 "abc,def,ghi,jkl"

该代码还将与Camilo Martin提供的功能一起使用:

function  (  A,  b
,c      ,d
){}

另外,还有Bubersson对Jack Allan的回答的评论:

function(a /* fooled you)*/,b){}

__

说明

new RegExp('(?:'+Function.name+'\\s*|^)\\s*\\((.*?)\\)')

这就产生了一个正则表达式用new RegExp('(?:'+Function.name+'\\s*|^)\\s*\\((.*?)\\)')。我必须使用,new RegExp因为我正在向Function.nameRegExp中注入变量(,即目标函数的名称)。

示例如果函数名称为“ foo”(function foo()),则RegExp将为/foo\s*\((.*?)\)/

Function.toString().replace(/\n/g, '')

然后,它将整个函数转换为字符串,并删除所有换行符。删除换行符有助于Camilo Martin提供的功能设置。

.exec(...)[1]

这就是RegExp.prototype.exec功能。它基本上将正则指数(new RegExp())与字符串(Function.toString())相匹配。然后,[1]它将返回在正指数()中找到的第一个捕获组(.*?)

.replace(/\/\*.*?\*\//g, '').replace(/ /g, '')

这将删除所有的注释中/**/,并删除所有空格。


现在,这还支持阅读和理解arrow(=>)函数,例如f = (a, b) => void 0;,该函数Function.toString()将返回(a, b) => void 0而不是常规函数function f(a, b) { return void 0; }。原始正则表达式可能会引起混乱,但现在可以解决了。

变化是从new RegExp(Function.name+'\\s*\\((.*?)\\)')/Function\s*\((.*?)\)/)到new RegExp('(?:'+Function.name+'\\s*|^)\\((.*?)\\)')/(?:Function\s*|^)\((.*?)\)/


如果要将所有参数放入一个Array中,而不是用逗号分隔的String中,请最后添加.split(',')

推荐阅读
惬听风吟jyy_802
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有