带正确行号的console.log的适当封装?

189

我正在开发一个应用程序,并放置了一个全局的isDebug开关。我想要包装console.log以便更方便地使用。

//isDebug controls the entire site.
var isDebug = true;

//debug.js
function debug(msg, level){
    var Global = this;
    if(!(Global.isDebug && Global.console && Global.console.log)){
        return;
    }
    level = level||'info';
    Global.console.log(level + ': '+ msg);
}

//main.js
debug('Here is a msg.');

然后我在Firefox控制台中得到了这个结果。

info: Here is a msg.                       debug.js (line 8)

如果我想在调用debug()的行号处记录日志,例如info: 这是一条消息。main.js(第2行),该怎么办?


你可以使用 console.log 来输出信息,使用 console.warn 来输出警告,使用 console.error 来输出错误,而不是通过包装函数在 console.log 中添加内容。 - Alvin Wong
2
@AlvinWong 是的,我知道这一点,但问题是我需要一个全局调试开关来控制是否需要使用 console。为了实现这个目标,似乎只有使用包装器才是唯一的方法? - OpenGG
对于Google Chrome,请参见https://dev59.com/ZWgu5IYBdhLWcg3wdG2o#25729203在您的情况下,模式将是debug.js。 - Frison Alexander
你应该重新表达你的问题或选择另一个答案。你要求一个带有行号的包装器,但接受了一个既不包装也不提供行号的答案。 - JakeDK
@JakeDK 感谢您的提醒。我尝试了Jacob Phillips的答案,它完美地解决了问题。 - OpenGG
30个回答

140

这是一个旧问题,所有提供的答案都过于hackey(即过于 hacky),存在严重的跨浏览器问题,并且没有提供任何超级有用的东西。这个解决方案可以在每个浏览器中工作,并准确地报告所有控制台数据。无需hack,只需要一行代码查看CodePen

var debug = console.log.bind(window.console)

按照以下方式创建开关:

isDebug = true // toggle this to turn on / off for global control

if (isDebug) var debug = console.log.bind(window.console)
else var debug = function(){}

然后只需按以下方式调用:

debug('This is happening.')

你甚至可以使用类似以下方式的开关接管console.log:

if (!isDebug) console.log = function(){}

如果你想要利用这个做些有用的事情,你可以添加所有控制台方法,并将其封装在一个可重复使用的函数中,不仅提供全局控制,还提供类级别的控制:
var Debugger = function(gState, klass) {
  
  this.debug = {}

  if (gState && klass.isDebug) {
    for (var m in console)
      if (typeof console[m] == 'function')
        this.debug[m] = console[m].bind(window.console, klass.toString()+": ")
  } else {
    for (var m in console)
      if (typeof console[m] == 'function')
        this.debug[m] = function(){}
  }
  return this.debug
}

isDebug = true //global debug state

debug = Debugger(isDebug, this)

debug.log('Hello log!')
debug.trace('Hello trace!')

现在你可以将它添加到你的类中:

var MyClass = function() {
  this.isDebug = true //local state
  this.debug = Debugger(isDebug, this)
  this.debug.warn('It works in classses')
}

29
如果我说错了,请纠正我,但这并不允许您添加任何其他功能,对吗?您实际上只是为控制台对象设置别名?举个简单例子,没有办法在每次debug.log()时将事件记录两次以console.log()的方式输出,对吗? - A.B. Carroll
4
通过绑定一个自定义的log()函数包含两个对console.log的调用,您可以通过console.log两次。但是行号将反映实际存在console.log的行,而不是调用debug.log的行。 但是,您可以添加动态前缀/后缀等内容。还有方法来补偿行号问题,但我认为这是另一个问题。请查看此项目以获取示例:https://github.com/arctelix/iDebugConsole/blob/master/README.md - Arctelix
4
这种方法在Firefox 47到49版本中不起作用,并且仅在50.0a2版本中得到修复。虽然FF50将在两周内发布,但我花了几个小时才意识到为什么它不起作用。因此,我认为这些信息可能对某些人有所帮助。[链接](https://bugzilla.mozilla.org/show_bug.cgi?id=1280818) - Vladimir Liubimov
14
这不回答问题:“如果我想记录调用debug()的行号怎么办?” - technomage
2
https://totallynoob.com/javascript-prefix-console-log-without-affecting-the-line-number/ 这个解决方案允许在自定义控制台日志中添加额外的前缀文本。 - Russell Chisholm
显示剩余4条评论

31
您可以通过巧妙使用Function.prototype.bind来保持行号并输出日志级别。
function setDebug(isDebug) {
  if (isDebug) {
    window.debug = window.console.log.bind(window.console, '%s: %s');
  } else {
    window.debug = function() {};
  }
}

setDebug(true);

// ...

debug('level', 'This is my message.'); // --> level: This is my message. (line X)

更进一步,你可以利用console的错误/警告/信息区分,并且仍然拥有自定义级别。试一试!
function setDebug(isDebug) {
  if (isDebug) {
    window.debug = {
      log: window.console.log.bind(window.console, 'log: %s'),
      error: window.console.error.bind(window.console, 'error: %s'),
      info: window.console.info.bind(window.console, 'info: %s'),
      warn: window.console.warn.bind(window.console, 'warn: %s')
    };
  } else {
    var __no_op = function() {};

    window.debug = {
      log: __no_op,
      error: __no_op,
      warn: __no_op,
      info: __no_op
    }
  }
}

setDebug(true);

// ...

debug.log('wat', 'Yay custom levels.'); // -> log: wat: Yay custom levels.    (line X)
debug.info('This is info.');            // -> info: This is info.        (line Y)
debug.error('Bad stuff happened.');     // -> error: Bad stuff happened. (line Z)

2
我一直在尝试自动给 console.debug(...) 的输出加上 函数名参数 前缀,你有什么想法吗? - Daniel Sokolowski
3
我一直在看各种控制台包装器/替身等,这是我遇到的第一个能够保留行号并自定义输出的工具。它聪明地利用了 .bind 的一些柯里化特性,因此除了上下文,您还可以绑定一个或多个参数。您甚至可以进一步传递一个具有 .toString 方法的 noop 函数,以在调用日志方法时运行代码!请参见此 jsfiddle。 - Sam Hasler
2
也许不是所有浏览器都支持(我没有深入研究),但在Chrome中用%o替换%s将以你期望的方式打印参数(对象可展开,数字和字符串有颜色等)。 - anson
我喜欢这个解决方案。我对它进行了一些改动,以更好地适应我的应用程序,但大部分仍然完整且运行良好。谢谢。 - Ward
我喜欢“更进一步”的解决方案。我想自动在输出中加上时间戳。有人能指点我正确的方向吗?我无法弄清楚如何做到这一点(除了在函数初始化时输出生成的时间戳,这不是我想要的)。 - zıəs uɐɟəʇs
有时候,仅仅是询问就能找到解决方案: https://jsfiddle.net/2gs3rq4x/ - zıəs uɐɟəʇs

27

我喜欢@fredrik的回答,所以我将它与另一个分割Webkit堆栈跟踪的回答合并,并与@PaulIrish的安全console.log包装器

在fiddle中测试:http://jsfiddle.net/drzaus/pWe6W/

_log = (function (undefined) {
    var Log = Error; // does this do anything?  proper inheritance...?
    Log.prototype.write = function (args) {
        /// <summary>
        /// Paulirish-like console.log wrapper.  Includes stack trace via @fredrik SO suggestion (see remarks for sources).
        /// </summary>
        /// <param name="args" type="Array">list of details to log, as provided by `arguments`</param>
        /// <remarks>Includes line numbers by calling Error object -- see
        /// * http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
        /// * https://dev59.com/qGYr5IYBdhLWcg3wQYJa
        /// * https://dev59.com/-HM_5IYBdhLWcg3whznx#3806596
        /// </remarks>

        // via @fredrik SO trace suggestion; wrapping in special construct so it stands out
        var suffix = {
            "@": (this.lineNumber
                    ? this.fileName + ':' + this.lineNumber + ":1" // add arbitrary column value for chrome linking
                    : extractLineNumberFromStack(this.stack)
            )
        };

        args = args.concat([suffix]);
        // via @paulirish console wrapper
        if (console && console.log) {
            if (console.log.apply) { console.log.apply(console, args); } else { console.log(args); } // nicer display in some browsers
        }
    };
    var extractLineNumberFromStack = function (stack) {
        /// <summary>
        /// Get the line/filename detail from a Webkit stack trace.  See https://dev59.com/-HM_5IYBdhLWcg3whznx#3806596
        /// </summary>
        /// <param name="stack" type="String">the stack string</param>

        if(!stack) return '?'; // fix undefined issue reported by @sigod

        // correct line number according to how Log().write implemented
        var line = stack.split('\n')[2];
        // fix for various display text
        line = (line.indexOf(' (') >= 0
            ? line.split(' (')[1].substring(0, line.length - 1)
            : line.split('at ')[1]
            );
        return line;
    };

    return function (params) {
        /// <summary>
        /// Paulirish-like console.log wrapper
        /// </summary>
        /// <param name="params" type="[...]">list your logging parameters</param>

        // only if explicitly true somewhere
        if (typeof DEBUGMODE === typeof undefined || !DEBUGMODE) return;

        // call handler extension which provides stack trace
        Log().write(Array.prototype.slice.call(arguments, 0)); // turn into proper array
    };//--  fn  returned

})();//--- _log
这个方法同样适用于Node环境,你可以使用以下代码进行测试:
// no debug mode
_log('this should not appear');

// turn it on
DEBUGMODE = true;

_log('you should', 'see this', {a:1, b:2, c:3});
console.log('--- regular log ---');
_log('you should', 'also see this', {a:4, b:8, c:16});

// turn it off
DEBUGMODE = false;

_log('disabled, should not appear');
console.log('--- regular log2 ---');

1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - sigod
@sigod -- 可能取决于浏览器,或者是因为我写这段代码已经有两年了,而浏览器(们)已经发生了变化。你的具体情况是什么? - drzaus
1
我的一个同事将你的代码复制粘贴到我们的项目中。这导致在IE11和Safari 5中网站崩溃了。不确定其他版本的浏览器是否也会出现此问题。或许你可以为未来的复制粘贴者添加一个检查? - sigod
1
@sigod现在怎么样?将if(!stack) return '?'添加到失败的方法中,而不是在调用它的地方(因此如果有人使用该方法本身,他们也会受到“保护”) - drzaus
@JesseDupuy 看起来自从我写这篇文章以来,Chrome在5年多的时间里已经改变了其堆栈跟踪格式。请考虑来自Google文章(日期未知)的截图,显示可能更接近我所写的格式,而不是你现在看到的。实际上,我认为这是2与3之间的区别——可能取决于日志函数嵌套的深度? - drzaus
显示剩余4条评论

12

听着,麦克弗莱,这是唯一对我起作用的方法:

let debug = true;
Object.defineProperty(this, "log", {get: function () {
  return debug ? console.log.bind(window.console, '['+Date.now()+']', '[DEBUG]') 
               : function(){};}
});

// usage:
log('Back to the future');
// outputs:
[1624398754679] [DEBUG] Back to the future

美妙之处在于避免像log('xyz')()这样的另一个函数调用,或者创建一个包装对象甚至类。 它也是ES5安全的。

如果您不想要前缀,只需删除参数即可。

更新:将当前时间戳包含在每个日志输出的前缀中。


1
很好,但请纠正我:在使用console.log之前,没有机会操纵log的参数吗?我想像https://www.npmjs.com/package/debug中添加计时器等。 - Holtwick
1
是的,这是可能的。更新答案,前缀为当前时间戳。 - angabriel
3
我花了一些时间才明白为什么你在使用getdefineProperty一起,这样日期就会始终更新而不是静态的... - gotson
请添加如何删除此属性的操作说明。 - SL5net
2
@SL5net:好的,我明白你的意思。由于这不是问题的一部分,并且已经有答案(比如你发布的那个),我想保持这个答案的专注性。 - angabriel
显示剩余3条评论

12
我找到了一个简单的解决方案,可以将已接受的答案(绑定到console.log/error等)与一些外部逻辑组合起来,以过滤实际记录的内容。
// or window.log = {...}
var log = {
  ASSERT: 1, ERROR: 2, WARN: 3, INFO: 4, DEBUG: 5, VERBOSE: 6,
  set level(level) {
    if (level >= this.ASSERT) this.a = console.assert.bind(window.console);
    else this.a = function() {};
    if (level >= this.ERROR) this.e = console.error.bind(window.console);
    else this.e = function() {};
    if (level >= this.WARN) this.w = console.warn.bind(window.console);
    else this.w = function() {};
    if (level >= this.INFO) this.i = console.info.bind(window.console);
    else this.i = function() {};
    if (level >= this.DEBUG) this.d = console.debug.bind(window.console);
    else this.d = function() {};
    if (level >= this.VERBOSE) this.v = console.log.bind(window.console);
    else this.v = function() {};
    this.loggingLevel = level;
  },
  get level() { return this.loggingLevel; }
};
log.level = log.DEBUG;

使用方法:

log.e('Error doing the thing!', e); // console.error
log.w('Bonus feature failed to load.'); // console.warn
log.i('Signed in.'); // console.info
log.d('Is this working as expected?'); // console.debug
log.v('Old debug messages, output dominating messages'); // console.log; ignored because `log.level` is set to `DEBUG`
log.a(someVar == 2) // console.assert
  • 请注意,console.assert 使用条件日志记录。
  • 确保您的浏览器开发工具显示所有的信息级别!

因为它没有给出任何行号,也没有展示日志级别的工作示例。 - not2qubit
行号将与直接使用控制台时相同。我已更新答案,并附有使用示例。它的投票数不多,因为我回答得比较晚 :) - Jacob Phillips

10

使用Chrome Devtools和Blackboxing,您可以实现此目的。您可以创建console.log包装器,它可以具有副作用,调用其他函数等,并仍然保留调用包装器函数的行号。

只需将一个小的console.log包装器放入单独的文件中即可,例如:

(function() {
    var consolelog = console.log
    console.log = function() {
        // you may do something with side effects here.
        // log to a remote server, whatever you want. here
        // for example we append the log message to the DOM
        var p = document.createElement('p')
        var args = Array.prototype.slice.apply(arguments)
        p.innerText = JSON.stringify(args)
        document.body.appendChild(p)

        // call the original console.log function
        consolelog.apply(console,arguments)
    }
})()

将其命名为log-blackbox.js之类的名称。

然后前往Chrome Devtools设置并找到“黑盒化”部分,添加一个用于要黑盒化的文件名的模式,在这种情况下是log-blackbox.js。


注意:确保您不要在同一文件中放置任何您希望出现在堆栈跟踪中的代码,因为它也将从跟踪中删除。 - jamesthollowell
这个还支持吗?链接好像指向错误的位置。 - undefined

9

来源:如何获取JavaScript调用者函数的行号?如何获取JavaScript调用者源URL? 在Firefox中,Error对象具有行号属性。因此,类似于这样的内容应该可以工作:

var err = new Error();
Global.console.log(level + ': '+ msg + 'file: ' + err.fileName + ' line:' + err.lineNumber);

在 Webkit 浏览器中,您可以使用 err.stack 获取表示当前调用堆栈的字符串。它会显示当前行号和更多信息。 更新: 要获取正确的行号,您需要在该行上调用错误。例如:
var Log = Error;
Log.prototype.write = function () {
    var args = Array.prototype.slice.call(arguments, 0),
        suffix = this.lineNumber ? 'line: '  + this.lineNumber : 'stack: ' + this.stack;

    console.log.apply(console, args.concat([suffix]));
};

var a = Log().write('monkey' + 1, 'test: ' + 2);

var b = Log().write('hello' + 3, 'test: ' + 4);

1
new Error(); 给我提供了它所执行的上下文,如果我将其放在 debug.js 中,那么我将得到 info: Here is a msg. file: http://localhost/js/debug.js line:7 - OpenGG
1
“Log = Error”的意义是什么?你仍然在修改Error类,对吗? - drzaus
将您的答案与其他几个答案结合起来——请参见下面的链接:https://dev59.com/qGYr5IYBdhLWcg3wQYJa#14841411 - drzaus

9
这里有一种保留行号的方法:https://gist.github.com/bgrins/5108712。它基本上可以归结为以下内容:
if (Function.prototype.bind) {
    window.log = Function.prototype.bind.call(console.log, console);
}
else {
    window.log = function() { 
        Function.prototype.apply.call(console.log, console, arguments);
    };
}

如果您不需要调试,可以使用isDebug包装并将window.log设置为function() { }


6

堆栈跟踪解决方案显示行号,但不允许点击跳转到源代码,这是一个主要问题。保持此行为的唯一解决方案是绑定到原始函数。

绑定可以防止包含中间逻辑,因为这个逻辑会干扰行号。然而,通过重新定义绑定函数并使用控制台字符串替换,仍然可能实现一些额外的行为。

这个代码片段展示了一个极简的日志框架,提供模块、日志级别、格式化和正确的可点击行号,只需34行。将其用作基础或灵感来满足您自己的需求。

var log = Logger.get("module").level(Logger.WARN);
log.error("An error has occured", errorObject);
log("Always show this.");

编辑:下面包含了gist

/*
 * Copyright 2016, Matthieu Dumas
 * This work is licensed under the Creative Commons Attribution 4.0 International License.
 * To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/
 */

/* Usage : 
 * var log = Logger.get("myModule") // .level(Logger.ALL) implicit
 * log.info("always a string as first argument", then, other, stuff)
 * log.level(Logger.WARN) // or ALL, DEBUG, INFO, WARN, ERROR, OFF
 * log.debug("does not show")
 * log("but this does because direct call on logger is not filtered by level")
 */
var Logger = (function() {
    var levels = {
        ALL:100,
        DEBUG:100,
        INFO:200,
        WARN:300,
        ERROR:400,
        OFF:500
    };
    var loggerCache = {};
    var cons = window.console;
    var noop = function() {};
    var level = function(level) {
        this.error = level<=levels.ERROR ? cons.error.bind(cons, "["+this.id+"] - ERROR - %s") : noop;
        this.warn = level<=levels.WARN ? cons.warn.bind(cons, "["+this.id+"] - WARN - %s") : noop;
        this.info = level<=levels.INFO ? cons.info.bind(cons, "["+this.id+"] - INFO - %s") : noop;
        this.debug = level<=levels.DEBUG ? cons.log.bind(cons, "["+this.id+"] - DEBUG - %s") : noop;
        this.log = cons.log.bind(cons, "["+this.id+"] %s");
        return this;
    };
    levels.get = function(id) {
        var res = loggerCache[id];
        if (!res) {
            var ctx = {id:id,level:level}; // create a context
            ctx.level(Logger.ALL); // apply level
            res = ctx.log; // extract the log function, copy context to it and returns it
            for (var prop in ctx)
                res[prop] = ctx[prop];
            loggerCache[id] = res;
        }
        return res;
    };
    return levels; // return levels augmented with "get"
})();


这个回答只有3个赞,但比页面上其他任何回答都更丰富、更清晰。 - Tom
然而,看起来所有有用的部分都在外部的gist上。 - Ryan Leach

6
您可以将行号传递给您的调试方法,例如:
您的调试方法名(第几行)
//main.js
debug('Here is a msg.', (new Error).lineNumber);

在这里,(new Error).lineNumber会给你提供当前在你的javascript代码中的行数。

5
有点啰嗦,是吧? - OpenGG
2
我认为这已经足以回答你的问题了。 :) - Subodh
2
lineNumber 属性是非标准的,目前仅在 Firefox 上有效,请参见此处 - Matthias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接