nodejs核心模块–util

util的字面意思就是“工具”,“实用工具”,从这个地方就可以想象一下util模块大概会包含一些什么样的功能了,主要就是我们最常用的一些公用方法了,比如在JS中最初学习到的alert方法,这里我们就先对这个util的模块,看下他所包含的所有的方法,以及简单的应用。

1:方法总汇

这里依然使用最简单的方法,来看下util模块中,包含了哪些个方法


var util = require("util");

for(var i in util){
    console.log(i);
}


控制台的显示,util的方法分为以下几种:


format
deprecate
print
puts
debug
error
inspect
isArray
isRegExp
isDate
isError
p
log
exec
pump
inherits
_extend


上述util的方法,是nodejs的版本为v0.10.28下获取。

2:分别说明

inspect

它完成的功能就是,把任意对象转换成字符串的方法,常用语调试和错误输出。

使用方法是:util.inspect(object, [options])

  • object是必须的
  • showHidden:取值结果为true/false,默认为false。如果设置结果为true,那么会把那些不可枚举的方法和属性,也获取到,并转换成字符串输出。
  • depth:告诉inspect方法,对该对象获取的深度的处理。该方法对于处理那些较为复杂的对象有很大的好处。默认深度取值为2层,如果设置为null,则表示将不限制递归层数的完整遍历对象。
  • colors:取值为true/false,默认为false,如果值为true,则输出格式将会以ANSI颜色编码,可以有更好的展示效果。
  • customInspect:取值true/false,默认取值为true,如果值为false,则默认定义在目标对象(inspect的第一个参数)上的传统的inspect()方法将不会被调用(有点模糊,下面在源代码中进行分析)。

使用方法的话,就非常简单了,例如我们想要把util模块的所有的方法或者属性转换成一个字符串,那么就可以使用下面的方法:


var util = require("util");
console.log(util.inspect(util, { showHidden: true,depth:2,colors:true,customInspect:false}));


修改inspect方法的第二个参数对象中的各个属性,可以获取到不同的结果。

接下来,我们继续看看inspect方法的内部实现,想象一下,如果我们自己去写这个方法的话,会去怎么样实现这个功能呢,其实架构的话,很简单,可以参考之前的一篇文章:Nodejs中的module简介,看下该文中关于exports的那块简介即可。

所以,这里我们先看下util模块的源文件中,是怎么架构的:查看源文件


var formatRegExp = /%[sdj%]/g;
exports.format = function(f) {
  
};
exports.deprecate = function(fn, msg) {
  
};

function inspect(obj, opts) {

}
exports.inspect = inspect;


我这里只给这几个简单的例子,如果想要查看源码,请查看上上面地址给出的连接。OK,下面我们具体看一下,inspect函数的具体实现方法。下面的源码中,也包含了inspect方法中,必须的几个其他的功能模块,为了能让我们在读代码时,有个更清晰的认识。


function stylizeNoColor(str, styleType) {
//当在inspect中,不设置colors值或者设置为false时,使用该函数
  return str;
}
function stylizeWithColor(str, styleType) {
//当在inspect中,把colors的值,设置为true时,会使用到该函数
  var style = inspect.styles[styleType];

  if (style) {
    return '\u001b[' + inspect.colors[style][0] + 'm' + str +
           '\u001b[' + inspect.colors[style][1] + 'm';
  } else {
    return str;
  }
}
function isUndefined(arg) {
  return arg === void 0;
}
function isBoolean(arg) {
  return typeof arg === 'boolean';
}

function inspect(obj, opts) {
  // 该函数其实也是在设置默认参数的功能,其主要的inspect的实现功能,在该函数尾部的formatValue中,这个下面继续来看。
  var ctx = {
    seen: [],
    stylize: stylizeNoColor
	//初始化colors设置为false时的处理函数
  };
  //ctx就是一个默认的属性集合,比如我们会在第二个参数中设置的showHidden,depth,colors,customInspect这几个属性,都会添加到ctx对象中,供以后使用。
  //下面的话,就是在支持如果控制项的输入,不是以对象的形式输入,而只是以参数的形式输入的话,是怎么样处理的。
  //所以,看到这里,我们也就知道了一点,那就是:
  //util.inspect(util, { showHidden: true,depth:2,colors:true,customInspect:false})
  //util.inspect(util,true,2,true,false),这两种写法,有相同的效果。
  //有兴趣的可以试试哦,只是后面的写法,可读性太差了。。而且万一写错顺序,那不是很扯。
  if (arguments.length >= 3) ctx.depth = arguments[2];
  if (arguments.length >= 4) ctx.colors = arguments[3];
  if (isBoolean(opts)) {
    // 这里的if语句,就是在设置showHidden的属性了,如果opts是一个布尔逻辑型,则只表示为showHidden,如果不是那么就说明opts是一个对象了,就使用else if中的_extend方法,把opts中的属性,添加到ctx中
    ctx.showHidden = opts;
  } else if (opts) {
    // got an "options" object
    exports._extend(ctx, opts);
  }
  // 通过上述的,把输入的控制项的属性放入ctx,接下来对还缺少的进行初始化了。
  if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
  if (isUndefined(ctx.depth)) ctx.depth = 2;
  if (isUndefined(ctx.colors)) ctx.colors = false;
  if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
  if (ctx.colors) ctx.stylize = stylizeWithColor;
  //如果colors的值为true,则处理函数更改为:stylizeWithColor
  
  //根据上述的初始化内容,调用formatValue方法,返回需要的指定的字符串
  return formatValue(ctx, obj, ctx.depth);
}


说到这里,算是结束了么?可是最重要的formatValue方法的实现还是没有说明呢,这里要说明这些,就有些复杂了,因为formatValue内部还有更多的处理,过于占用篇幅,所以这里就不再说了,现在只说明一个问题,请看下面的代码:


function formatValue(ctx, value, recurseTimes) {
  // Provide a hook for user-specified inspect functions.
  // Check that value is an object with an inspect function on it
  if (ctx.customInspect &&
	//ctx.customInspect,默认值为false,如果为true时,执行
      value &&
	  //value,即为待转换的对象,只有存在才执行
      isFunction(value.inspect) &&
	  //value对象中,是否有inspect函数,如果有,继续执行
      value.inspect !== exports.inspect &&
	  //value中的inspect和exports对象中的inspect是否为同一个方法,如果不是,继续执行
      //下面这个判断,虽然可以看出它在做些什么判断,但是我并没有确认,什么时候,value.constructor.prototype === value才会成立,说明我对constructor的认识,还不够。源文件中这么注释的
	  //filter out any prototype objects using the circular check.
      !(value.constructor && value.constructor.prototype === value)) {
    var ret = value.inspect(recurseTimes, ctx);
	//如果都成立,则使用value中自带的inspect方法,获取返回值
    if (!isString(ret)) {
	//如果返回值不是字符串,则继续调用formatValue方法,获取字符串
      ret = formatValue(ctx, ret, recurseTimes);
    }
	//返回该字符串
    return ret;
  }
  
  //其他部分,省略
}


记得之前,也说到了一个属性,就是customInspect属性,它的默认值是true,看下在源代码中,是怎么处理这个属性的区别的吧。

既然我们在源码中,知道了对于customInspect的处理方法,那么我们就可以知道,这个属性到底在实现一个怎么样的功能了。所以我们可以做以下的测试:在app.js中,编写如下代码


var util = require("util");
var P = {
    name:"zhangyunling",
    inspect:function(){
	return this.name;
    }
}

console.log(util.inspect(P));
//zhangyunling
console.log(util.inspect(P,{customInspect:false}));
//{name:"zhangyunling",inspect:[Function]}


OK,是不是这样就可以了解到了一些呢,如果在我们需要转换的对象中(上述代码中的P对象),有inspect方法,那么在不设置其他控制参数(customInspect)为false的情况下,就会优先使用P中的inspect方法,当社长customInspect=false时,表示不使用目标对象中的inspect方法,则会按照我们常用的方法进行处理了。

format

字面意思,格式化,也就是获取一种特定格式的数据。说到这里,我就很明显的想到了一个我们常用的方法,比如在C语言中的printf方法,按照指定的格式,输出一些东西,也想到了前端中的模板化方法template(JS实现简单的template),所以首先我们先看看,在Nodejs中,util模块的formatAPI介绍中,对该方法,是怎么介绍的吧:源地址查看

根据format的第一个参数(该参数的写法类似printf),返回一个特定格式的字符串。

第一个参数的字符串串中,包含0或者多个替换符,每一个替换符,将会被参数中,对应的属性值替换,第一个参数中的替换符包含以下几种:

  • %s – 字符串.
  • %d – 数字(整型或者浮点型).
  • %j – JSON格式.
  • % – 这个不是替换符,它就是代表本身的意思,百分号.

当再执行替换时,如果占位符,和之后的待匹配的数据的个数不同,那么就会出现不同的匹配结果。

1,如果占位符多于待匹配的数据,比如下面这样:


util.format('%s:%s', 'foo'); // 'foo:%s'


这里在format的第一个参数中,占位符有两个,可是待匹配的数据只有一个,那么第二个则不进行替换。

2,如果待匹配的数据个数,多于占位符的个数呢,那么多出来的待匹配数据,将会执行inspect的方法,会连接到第一个参数的尾部,并且以空格为间隔。如下:


util.format('%s:%s', 'foo', 'bar', 'baz'); // 'foo:bar baz'


3,在最初,我们也说了,占位符的个数是0或者是多个,那么当占位符为0的时候,呢,那么执行的结果也就是我上面的情况中的,多出来的待匹配数据的操作相同了,使用inspect方法,连接转换成字符串,以空格为间隔,实例如下:


util.format(1, 2, 3); // '1 2 3'


OK,看起来很简单的,然后我们去看下,在util的源码中,是怎么样处理format的呢:


var formatRegExp = /%[sdj%]/g;
//该正则表达式很重要啊,它在匹配我们占位符的格式,所以,最后的匹配结果就是%s,%d,%j,%%这四种。
//最后一个的虽然是两个百分号,但是在处理时,会被处理成一个百分号的。
exports.format = function(f) {
  if (!isString(f)) {
	//因为该方法,就是在处理字符串的匹配,所以,如果输入的值,并不是一个字符串的话,那么就使用inspect的方法,把每一个都转换一个字符串,然后使用空格把所有的再连接起来,输出。
    var objects = [];
    for (var i = 0; i < arguments.length; i++) {
      objects.push(inspect(arguments[i]));
    }
    return objects.join(' ');
  }

  //如果是字符串,那么就进行下面的处理。
  var i = 1;
  var args = arguments;
  var len = args.length;
  var str = String(f).replace(formatRegExp, function(x) {
  //核心方法,也就是这段正则的匹配了。
    if (x === '%%') return '%';
    if (i >= len) return x;
    switch (x) {
      case '%s': return String(args[i++]);
      case '%d': return Number(args[i++]);
      case '%j':
        try {
          return JSON.stringify(args[i++]);
        } catch (_) {
          return '[Circular]';
        }
      default:
        return x;
    }
  });
  
  //这里呢,就是处理占位符小于待匹配数据多时,进行的处理了。
  for (var x = args[i]; i < len; x = args[++i]) {
    if (isNull(x) || !isObject(x)) {
      str += ' ' + x;
    } else {
      str += ' ' + inspect(x);
    }
  }
  return str;
};


Ok,看到这里,也算是能对format的实现,有了简单的认识了吧。

如果您对正则的replace方法,没有认识更多,请参考:字符串的replace方法基础简介强大的replace方法JS实现简单的template

输出调试信息,debug,error,puts,print,log

这几种方法,看到它们的名称也就知道了它们的意义了,所以这里也没有什么好说的。
其实关键是,像这种比较接近,但是有不同的方法,一般比较难有个清晰又准确的认识,所以这里暂且不对这个做评论。留待以后对nodejs有更深层次的认识之后,再做说明。

格式判断,isArray,isRegExp,isDate,isError

这里对外公开的格式判断的接口也就这么几种,还有更多的格式判断的接口是没有对外公开的,比如我们之前使用的isFunctionisUndefined方法,都是只有在util模块内部可以调用,而在外部却无法使用的方法。

这四种方法,返回值都是Boolean型的值。代表的意思,应该也不需要多说了吧。

对象扩展,_extend

JS中,对象的继承等,有很多的方法,比如工厂继承,比如原型链继承等,这其实也叫做扩展,比如这里的_extend方法。

举个简单的例子吧:


var util = require("util");

var a = {a:"a"},
    b = {b:"b"};

a = util._extend(a,b);
console.log(a);
//{a:"a",b:"b"}


对于JS有了解话,对于这个也应该没有什么疑问了,下面看下它的源码实现,


exports._extend = function(origin, add) {
  // Don't do anything if add isn't an object
  if (!add || !isObject(add)) return origin;

  var keys = Object.keys(add);
  var i = keys.length;
  while (i--) {
	//遍历整个add对象,把add对象的方法,添加到origin方法上。
    origin[keys[i]] = add[keys[i]];
  }
  return origin;
};


从上面的源码中,我们也看到了一个问题,就是,如果add对象和origin对象中,有相同的属性,那么add中的属性值,就会覆盖origin中的属性值。这在我看来,并不是最好的,因为有时候,我们希望只继承自身没有的属性,而且,这跟我们继承时,优先考虑本身的属性,只有当本身不包含目标属性时,才去查找继承来的属性的思想有些区别的。

至于如何去实现上面的这个效果,这里就不进行说明了,有兴趣的可以自己尝试的去写一下哦。

继承,inherits

前面刚看了_extend的方法,这里就出现了inherits的方法,看看他们的区别呢。

使用方法:util.inherits(constructor, superConstructor)inherits是一个实现对象间原型继承的函数,即constructor只会继承superConstructor的原型链上的属性和方法。

举个例子:


var util = require("util");

function A(){
    this.name = "zhang";
}

A.prototype.sayName = function(){
    return this.name;
}

function B(){
    this.name = "ling";
}

B.prototype.sayAge = function(){
    return "123";
}

util.inherits(B,A);

var a = new A();
console.log(a.sayName());  //zhang
var b = new B();

console.log(a.prototype == b.prototype);   //true

console.log(b.sayName());  //undefined
console.log(b.sayAge());   //throw error,no this method


从上面这个例子中,我们分别来说明一下,都证明了一个什么问题。

  • B构造函数,只继承了A构造函数的原型链中的属性。
  • 确切的说,是inherits方法,只是把B原型链的指向了A的原型链。从a.prototype == b.prototype的结果为true就可以证明。
  • 这是我的想法,看到这里,我觉得这个突然很是没有意义,因为这样A,B两个构造函数,就会互相影响,修改一个就会影响另外的一个。

继续看下源码中,怎么实现该功能的。


exports.inherits = function(ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
};


使用的Object.create方法,其实也是对prototype进行了重新赋值,但是这么做,同时也会导致ctor本来的原型链的断链。在JS,这是一个很严重的问题,但是这里却是这么实现的,突然对于Nodejs有些失望。关于原型链断链的原因,请查看:原型链断链的原因

可能还有其他的一些地方会解决这些问题吧,只是现在还没有看到,没有了解到,只是希望nodejs的维护开发者们,对这些都更认真一点吧。

其他

在我们文章的最初,还有几种方法,比如pexecpump,这些方法在v0.10.29版本,已经移到了其他的模块下,所以这里也不在做说明。

总结

总的来说,我突然对util的这些基本方法有些失望,_extendinherits的实现,直接就是以最简单的方法在做这些处理,可是这些最简单的方法,却并不是我们想要的最好的方法。期待nodejs中,在下一个版本中对util模块进一步优化吧。

本文地址:http://www.zhangyunling.com/?p=192

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>