第21章 错误处理与调试
第21章 错误处理与调试
try-catch语句
ECMA-262,第三版,引入了try-catch语句作为处理JavaScript异常的一种方式。基本语法如下,与Java中的try-catch语句相同:
try{
// 可能引发错误的代码
}catch(error){
// 错误发生时该干什么
}
如果在语句的try部分中的任何一点发生错误,代码执行将立即退出并在catch部分继续执行。语句的catch部分接收一个对象,其中包含发生的错误的信息。与其他语言不同,即使不打算使用它,也必须为错误对象定义一个名称。此对象上可用的确切信息因浏览器而异,但至少包含保存错误消息的message属性。 ECMA-262 还指定了一个name属性,用于定义错误类型;此属性在所有浏览器中可用。因此,可以在必要时显示消息,如下所示:
try {
console.log(a);
} catch (b) {
console.log(b.message); // a is not defined
}
本示例在向用户显示错误消息时使用message属性。即使每个浏览器都添加了其他信息,message属性是唯一可以保证在 IE、Firefox、Safari、Chrome 和 Opera 上都存在的属性。 IE添加了一个description属性,它总是等于message,以及一个number属性,它给出一个内部错误号。 Firefox添加了fileName, lineNumber和stack(其中包含堆栈跟踪)。 Safari 添加 line(用于行号)、sourceId(内部错误代码)和 sourceURL。再次强调,最好仅依赖message属性来实现跨浏览器兼容性。
finally语句
try-catch语句的finally子句总是运行其代码,无论出现错误与否。在语句的try或catch部分实际上没有什么可以阻止finally中的代码执行,包括使用return语句。如下所示:
function testFinally() {
try {
return 2;
} catch (error) {
return 1;
} finally {
return 0;
}
}
console.log(testFinally()); //0
该函数在try-catch语句的每一部分放置一个return语句。看起来该函数应该返回 2 ,因为它在try部分并不会导致错误。然而,finally子句的存在导致返回被忽略。无论如何,该函数在调用时都返回 0 。如果删除finally子句,函数将返回 2 。如果提供finally,则catch变为可选(仅需二者之一)。
注意:如果代码中包含finally子句,那么了解try或catch部分中的任何return语句都将被忽略非常重要。确保在使用 finally时仔细检查代码的预期行为。
错误类型
在代码执行过程中可能会发生几种不同类型的错误。每个错误类型都有一个对应的对象类型,在发生错误时抛出。 ECMA-262 定义了以下八种错误类型:
Error
InternalError
EvalError
RangeError
RenfrenceError
SyntaxError
TypeError
URIError
Error类型是其他错误类型的基类。因此,所有错误类型共享相同的属性。 Error类型的错误很少,它主要提供给开发人员抛出自定义错误。
InternalError类型是底层引擎异常。例如,当由于递归过多而发生堆栈溢出时。如果抛出这个错误,很可能代码正在做一些不正确或危险的事情。
当使用eval()函数时发生异常时抛出EvalError。如:
new eval(); // throws EvalError
eval = foo; // throws EvalError
当数字超出其范围时会发生RangeError。例如,当尝试使用不受支持的数(例如–20或Number. MAX_VALUE)定义数组时,可能会发生此错误,如下所示:
let items1 = new Array(-20); // throws RangeError
let items2 = new Array(Number.MAX_VALUE); // throws RangeError
ReferenceError类型。这种类型的错误通常发生在尝试访问不存在的变量时,如下例所示:
let obj = x; //ReferenceError: x is not defined
当传递给 eval()的JavaScript字符串中存在语法错误时,会抛出SyntaxError对象,如下例所示:
eval("a ++ b"); //SyntaxError: Unexpected identifier
eval() 之外很少使用SyntaxError类型,因为JavaScript代码中发生的语法错误会立即停止执行。
TypeError类型是JavaScript中最常用的类型,当变量属于非预期类型或尝试访问不存在的方法时会发生。发生这种情况的原因有很多,最常见的是将特定类型的操作与错误的变量类型一起使用时:
let o = new 10; // throws TypeError
console.log("name" in true); // throws TypeError
Function.prototype.toString.call("name"); // throws TypeError
最后一种错误类型是URIError,它仅在使用具有错误格式的URI的encodeURI()或decodeURI()时发生。这个错误可能是JavaScript中最不常见的错误,因为这些函数非常健壮。
不同的错误类型可用于提供有关异常的具体信息,从而进行适当的错误处理。可以使用instanceof运算符确定try-catch语句的catch部分中抛出的错误类型:
try {
someFunction();
} catch (error) {
if (error instanceof TypeError) {
// handle type error
} else if (error instanceof ReferenceError) {
// handle reference error
} else {
// handle all other error types
}
}
try-catch的用法
当try-catch语句中发生错误时,浏览器认为该错误已被处理,因此不会使用本章前面讨论的机制报告它。这对于那些不具备技术倾向并且无法理解何时发生错误的用户的Web应用程序来说是理想的选择。 try-catch语句允许为特定的错误类型实现自己的错误处理机制。
try-catch语句最适用于可能发生超出控制的错误的情况。例如,如果使用的函数是较大JavaScript库的一部分,则该函数可能有意或无意地抛出错误。因为不能修改库的代码,所以最好在try-catch语句中包含调用,以防发生错误,然后适当地处理错误。
如果知道特定代码会发生错误,则使用try-catch语句是不合适的。例如,如果传入字符串而不是数字时函数会失败,则应检查参数的数据类型并采取相应措施;在这种情况下不需要使用try-catch语句。
抛出错误
同try-catch语句的一起使用的throw运算符,可用于在任何时间点抛出自定义错误。 throw运算符必须与值一起使用,但对值的类型没有限制。以下所有行都是合法的:
// 每行需单独测试
throw 12345;
throw "Hello world!";
throw true;
throw {
name: "JavaScript"
};
使用throw运算符时,代码执行会立即停止并仅在try-catch语句捕获到抛出的值时继续执行。
使用内置错误类型可以更准确地模拟浏览器错误。每个错误类型的构造函数都接受一个确切的错误消息的参数:
throw new Error("Something bad happened.");
此代码抛出一个带有自定义错误消息的一般错误。该错误由浏览器处理,就好像它是由浏览器本身生成的一样,这意味着浏览器以平常的方式报告它并显示自定义错误消息。可以使用其他错误类型获得相同的结果:
throw new SyntaxError("I don't like your syntax.");
throw new InternalError("I can't do that, Dave.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don't have the range.");
throw new EvalError("That doesn't evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn't cite your references properly.");
自定义错误消息最常用的错误类型是 Error、RangeError、ReferenceError 和 TypeError。
可以通过从Error继承来创建自定义错误类型。并在该错误类型上同时提供name属性和message属性:
class CustomError extends Error {
constructor(message) {
super(message);
this.name = "CustomError";
this.message = message;
}
}
throw new CustomError("My message");
从Error继承的自定义错误类型被浏览器视为与任何其他错误类型一样。当要捕获抛出的错误并需要从浏览器生成的错误中解密它们时,创建自定义错误类型会很有帮助。
何时抛出错误
抛出自定义错误是提供有关函数失败原因的好方法。当存在不允许函数正确执行的特定已知错误条件时,应抛出错误。也就是说,浏览器会在给定条件下执行此函数时抛出错误。例如,如果参数不是数组,以下函数将失败:
function process(values) {
values.sort();
for (let value of values) {
if (value > 100) {
return value;
}
}
return -1;
}
带有适当信息的自定义错误将显著的提高代码可维护性:
function process(values) {
if (!(values instanceof Array)) {
throw new Error("process(): Argument must be an array.");
}
values.sort();
for (let value of values) {
if (value > 100) {
return value;
}
}
return -1;
在这个函数的重写版本中,如果values参数不是数组,则会引发错误。错误消息提供了函数的名称和关于错误发生原因的清晰描述。如果此错误发生在复杂的Web应用程序中,可更清楚的知道问题在哪里。
抛出错误与try-catch对比
出现的一个常见问题是何时抛出错误以及何时使用try-catch来捕获它们。一般来说,错误是在应用程序架构的底层抛出的,在这个层对正在进行的过程知之甚少,因此无法真正处理错误。如果编写可能在许多不同应用程序中使用的JavaScript库,或在单个应用程序中的不同地方使用的实用程序函数,则应该考虑抛出带有详细信息的错误,然后由应用程序来捕获错误并适当地处理它们。
考虑抛出错误和捕获错误之间的区别的最好方法是:只有确切地知道下一步要做什么时,才应该捕获错误。捕获错误的目的是防止浏览器以默认方式响应;抛出错误的目的是提供有关错误发生原因的信息。
error事件
任何 未被try-catch处理的错误 都会导致error事件在window对象上触发。此事件是Web浏览器最早支持的事件之一,其格式保持不变,以便在所有主要浏览器中向后兼容。onerror事件处理程序不会在任何浏览器中创建event对象。它接收三个参数:错误消息、发生错误的URL和行号。在大多数情况下,只有错误消息是相关的,因为URL与文档的位置相同,并且行号可能用于内联JavaScript或外部文件中的代码。此处需要使用DOM0技术分配onerror事件处理程序,因为它不遵循DOM2事件标准格式:
window.onerror = (message, url, line) => {
console.log(message);
};
当发生错误时,无论是否由浏览器生成,都会触发错误事件,并执行此事件处理程序。然后,默认浏览器行为会接管,像往常一样显示错误消息。可以通过返回false来阻止浏览器默认的错误报告行为:
window.onerror = (message, url, line) => {
console.log(message);
return false;
};
通过返回false,该函数有效地成为整个文档的try-catch语句,捕获所有未处理的运行时错误。此事件处理程序是防止错误被浏览器报告的最后一道防线。正确使用try-catch语句意味着没有错误到达浏览器层次,因此不应触发error事件。
图像也支持错误事件。任何时候图像的src特性中的URL没有返回可识别的图像格式,都会触发error事件。此事件遵循DOM格式,返回一个以图像为目标的event对象:
const image = new Image();
image.addEventListener("load", (event) => {
console.log("Image loaded!");
});
image.addEventListener("error", (event) => {
console.log("Image not loaded!");
});
image.src = "doesnotexist.gif"; // Image not loaded!
错误处理策略
传统的错误处理策略仅限于Web应用程序的服务器。通常有很多关于错误和错误处理的想法,包括日志记录和监控系统。
识别可能发生错误的位置
错误处理最重要的部分是先确定代码中可能出现错误的位置。由于JavaScript是松散类型的并且函数参数没有经过验证,因此通常只有在执行代码时才会出现明显的错误。一般来说,需要注意三个错误类别:
类型强制错误
数据类型错误
通讯错误
静态代码分析器
最常用的静态分析器是 JSHint、JSLint、Google Closure 和 TypeScript。
类型强制错误
Type coercion errors的发生是由于使用运算符或其他语言结构改变了值的数据类型。两种最常见的类型强制错误是使用等于(==)或不等于(!=)运算符以及在流控制语句中使用非布尔值(例如 if、for 和 while)而导致的。
在大多数情况下,最好使用完全相等 (===) 和不完全相等 (!==) 运算符来避免类型强制错误:
console.log(5 == "5"); // true
console.log(5 === "5"); // false
console.log(1 == true); // true
console.log(1 === true); // false
在此代码中,使用相等运算符和完全相等运算符比较数字 5 和字符串“5”。相等运算符首先将字符串“5”转换为数字 5 ,然后将其与另一个数字 5 进行比较,结果为true。完全相等运算符指出这两种数据类型不同,只是返回false。值 1 和true也会发生同样的情况:相等运算符认为它们相等,但使用完全相等运算符认为它们不相等。使用完全相等和不完全相等运算符可以防止在比较过程中发生类型强制错误,强烈建议不要使用相等和不相等运算符。
流控制语句中也会出现类型强制错误。在确定下一步之前,诸如if之类的语句会自动将任何值转换为布尔值。具体而言,if语句通常以容易出错的方式使用:
function concat(str1, str2, str3) {
let result = str1 + str2;
if (str3) { // 避免这样做
result += str3;
}
return result;
}
此函数的预期目的是连接两个或三个字符串并返回结果。第三个字符串是可选参数,因此必须检查。未使用的命名变量会自动赋值为undefined。值undefined转换为布尔值false,因此此函数中if语句的目的是仅在提供时连接第三个参数。问题是undefined不是唯一转换为false的值,字符串也不是唯一转换为true的值。例如,如果第三个参数是数字 0 ,则if条件失败,而值为 1 会导致条件通过。
在流控制语句中使用非布尔值作为条件是导致错误的一个非常常见的原因。为避免此类错误,请始终确保将布尔值作为条件传递。最常见的方式是通过进行某种比较来完成。例如,前面的函数可以重写为如下:
function concat(str1, str2, str3) {
let result = str1 + str2;
if (typeof str3 === "string") {
result += str3;
}
return result;
}
数据类型错误
由于JavaScript是弱类型语言,因此不会比较变量和函数参数以确保使用正确类型的数据。作为开发人员,需要进行适当数量的数据类型检查以确保不会发生错误。数据类型错误通常是由于将意外值传递给函数而导致的。
在前面的示例中,检查了第三个参数的数据类型以确保它是一个字符串,但根本不检查其他两个参数。如果函数必须返回一个字符串,则传入两个数字并省略第三个参数时很容易破坏它。类似的情况存在于以下函数中:
function getQueryString(url) {
const pos = url.indexOf("?");
if (pos > -1) {
return url.substring(pos + 1);
}
return "";
}
此函数的目的是返回给定URL的查询字符串。为此,它首先使用indexOf()在字符串中查找问号,如果找到,则使用substring()方法返回问号之后的所有内容。本示例中使用的两个方法是字符串固有的,因此传入的任何其他数据类型都会导致错误。以下简单的类型检查使此函数不易出错:
function getQueryString(url) {
if (typeof url === "string") {
let pos = url.indexOf("?");
if (pos > -1) {
return url.substring(pos + 1);
}
}
return "";
}
修正:
function reverseSort(values) {
if (values instanceof Array) {
values.sort();
values.reverse();
}
}
一般来说,值为原生类型的话应该使用typeof检查,值为对象的话应该使用instanceof来检查。
通讯错误
一种通信错误涉及格式错误的URL或发布数据。当数据在发送到服务器之前没有使用encodeURIComponent()进行编码时,通常会发生这种情况。例如,以下URL的格式不正确:
http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d
这个URL可以通过在“redir=”之后的所有内容上使用encodeURIComponent() 来修复,这会产生以下结果:
http://www.example.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd
encodeURIComponent() 方法应始终用于查询字符串参数。有时定义一个处理查询字符串构建的函数会很有帮助:
function addQueryStringArg(url, name, value) {
if (url.indexOf("?") == -1) {
url += "?";
} else {
url += "&";
}
url += '${encodeURIComponent(name)=${encodeURIComponent(value)}';
return url;
}
此函数接受三个参数:将查询字符串参数附加到的URL、参数名称和参数值。如果传入的URL不包含问号,则添加一个;否则,会添加一个&符号,因为这意味着还有其他查询字符串参数。然后对查询字符串名称和值进行编码并添加到URL。该函数可以在以下示例中使用:
const url = "http://www.somedomain.com";
const newUrl = addQueryStringArg(url, "redir", "http://www.someotherdomain.com?
a=b&c=d");
console.log(newUrl);
当服务器响应不符合预期时,也会发生通信错误。使用动态脚本加载或动态样式加载时,有可能请求的资源不可用。当资源没有返回时,一些浏览器会默默地失败,而另一些浏览器会出错。不幸的是,在使用这些技术来确定发生了错误时,几乎无能为力。在某些情况下,使用Ajax通信可以提供有关错误情况的额外信息。
区分致命错误和非致命错误
任何错误处理策略中最重要的部分之一是确定错误是否致命。以下一项或多项被认为是非致命错误:
它不会干扰用户的主要任务。
它只影响页面的一部分。
恢复是可能的。
重复该操作可能会成功。
本质上,非致命错误无需担心。例如,Gmail (https://mail.google .com) 具有允许用户从界面发送环聊消息(Hangouts messages)的功能。如果由于某种原因,环聊不工作,这是一个非致命错误,因为这不是应用程序的主要功能。Gmail的主要用法是阅读和编写电子邮件,只要用户可以这样做,就没有理由中断用户体验。非致命错误不需要向用户发送明确的消息。可以将受影响的页面区域替换为指示该功能不可用的消息,但没有必要打断用户。
另一方面,致命错误由以下一个或多个确定:
应用程序绝对不能继续。
该错误严重干扰了用户的主要目标。
结果会出现其他错误。
将错误记录到服务器
Web 应用程序中的一个常见做法是拥有一个集中的错误日志,其中重要的错误被写入以进行跟踪。数据库和服务器错误会定期写入日志并通过一些通用API进行分类。对于复杂的Web应用程序,建议将JavaScript错误记录到服务器。这个想法是将错误记录到用于服务器端错误的同一个系统中,并将它们归类为来自前端。无论错误的来源如何,使用相同的系统都可以对数据执行相同的分析。
要设置JavaScript错误日志系统,首先需要在服务器上有一个可以处理错误数据的页面或入口点。该页面只需从查询字符串中获取数据并将其保存到错误日志中即可。然后可以将此页面与如下代码一起使用:
function logError(sev, msg) {
let img = new Image(),
encodedSev = encodeURIComponent(sev),
encodedMsg = encodeURIComponent(msg);
img.src = 'log.php?sev=${encodedSev}&msg=${encodedMsg}';
}
logError() 函数接受两个参数:severity和error message。严重性可能是数字或字符串,具体取决于使用的系统。使用Image对象发送请求非常灵活,如下所述:
Image对象在所有浏览器中都可用,即使是那些不支持XMLHttpRequest对象的浏览器。
通常有一个服务器负责处理来自多个服务器的错误日志记录,而XMLHttpRequest在这种情况下将不起作用。
在记录错误的过程中发生错误的可能性较小。大多数Ajax通信是通过JavaScript库提供的功能包装器(functionality wrappers)处理的。如果该库的代码失败,且尝试使用它来记录错误,则可能永远不会记录该消息。
每当使用try-catch语句时,很可能应该记录错误:
for (let mod of mods) {
try {
mod.init();
} catch (ex) {
logError("nonfatal", 'Module init failed: ${ex.message}');
}
}
在此代码中,当模块初始化失败时调用logError()。第一个参数是“nonfatal”,表示错误的严重性,消息提供坏境信息以及真正的JavaScript错误消息。记录到服务器的错误消息应提供尽可能多的坏境信息,以帮助确定错误的确切原因。
调试技术
在JavaScript调试器可用之前,开发人员必须使用创造性的方法来调试他们的代码。这导致放置专门设计用于以一种或多种方式输出调试信息的代码。最常见的调试技术是在有问题的代码中插入警报,这既枯燥又乏味,因为它需要在调试代码后进行清理,而且如果警报被错误地留在生产环境中使用的代码中,则很烦人。不再建议将警报用于调试目的,因为还有其他几种更优雅的解决方案可用。
将消息记录到控制台
所有主要浏览器都有可用于查看JavaScript错误的JavaScript控制台。这三者还允许通过console对象直接写入JavaScript控制台,该对象具有以下方法:
error(message) 将错误消息记录到控制台
info(message) 将信息类消息记录到控制台
log(message) 将一般消息记录到控制台
warn(message) - 将警告消息记录到控制台
错误控制台上的消息显示因用于记录消息的方法而异。错误消息包含一个红色图标,而警告包含一个黄色图标。可以使用控制台消息,如在以下函数中:
function sum(num1, num2) {
console.log(`Entering sum(), arguments are ${num1},${num2}`);
console.log("Before calculation");
const result = num1 + num2;
console.log("After calculation");
console.log("Exiting sum()");
return result;
}
sum(2,33);
了解控制台运行时
浏览器控制台是一个REPL(read-eval-print loop),它与页面的JavaScript运行时同时发生。它的行为方式与浏览器计算在DOM中新发现的<script>一样。从控制台内部执行的命令可以以与页面级JavaScript相同的方式访问全局变量和各种API。可以从控制台计算任意数量的代码;与它阻塞的任何其他页面级代码的情况一样。
JavaScript运行时将限制不同窗口可以访问的内容,因此在所有主要浏览器中,都可以选择JavaScript控制台输入应在哪个窗口中执行。执行的代码不会以提升的权限执行——它仍然受到跨域限制和浏览器强制执行的任何其他控制的影响。
控制台运行时也会集成开发者工具,提供常规 JavaScript 开发中所没有的坏境调试工具。其中一个非常有用的工具是最后点击选择器,所有主流浏览器都会提供。在开发者工具的 Element(元素)标签页内,单击 DOM 树中一个节点,就可以在 Console (控制台)标签页中使用$0 引用该节点的 JavaScript实例。它就跟普通的 JavaScript 实例一样,因此可以读取属性(如$0.scrollWidth ),或者调用成员方法(如$0.remove())。
使用JavaScript调试器
可以在所有主要浏览器中使用JavaScript调试器。作为ECMAScript 5.1规范的一部分,debugger关键字将尝试调用任何可用的调试功能。如果没有关联的行为,则此语句将作为无操作静默跳过:
function pauseExecution() {
console.log("Will print before breakpoint");
debugger;
console.log("Will not print until breakpoint continues");
}
当运行时遇到关键字时,它会在所有主要浏览器中打开开发人员工具面板,并在该点设置断点。然后,能够使用单独的浏览器控制台在当前停止断点的特定词法范围内执行代码。此外,还能够执行标准代码调试器操作(step into:单步执行,进入子函数, step over:不进入子函数、继续等)。
浏览器通常还允许通过检查开发人员工具中实际加载的JavaScript代码并选择要设置断点的行来手动设置断点(不使用debugger关键字语句)。这个设置的断点将以相同的方式运行,但它不会在浏览器会话中持续存在。
输出消息到页面
记录调试消息的另一种常用方法是指定写入消息的页面区域。这可能是一直包含但在页面中但仅用于调试目的的元素,或者仅在必要时创建的元素。例如,可以将log()函数更改为以下内容:
function log(message) {
let console1 = document.getElementById("debuginfo");
if (console1 === null) {
console1 = document.createElement("div");
console1.id = "debuginfo";
console1.style.background = "#ff55ff";
console1.style.border = "1px solid silver";
console1.style.padding = "5px";
console1.style.width = "400px";
console1.style.position = "absolute";
console1.style.right = "0px";
console1.style.top = "0px";
document.body.appendChild(console1);
}
console1.innerHTML += `<p> ${message}</p>`;
}
log("666");
补充控制台方法
console是一个具有可写成员方法的全局对象,完全可以用自定义行为覆盖其成员方法,并愉快的在整个代码库中使用自定义的任何内容:
console.log = function() {
alert("666");
}
console.log("Ciri");
加强版:
console.log = (function(oriLogFunc) {
return function(str) {
oriLogFunc.call(console, "hello:" + str);
}
})(console.log);
console.log("Ciri"); //hello:Ciri
抛出错误
如前所述,抛出错误是调试代码的绝佳方式。如果错误消息足够具体,仅查看报告的错误就足以确定错误的来源。好的错误消息的关键是让他们提供有关错误原因的详细信息,以便最大限度地减少额外的调试:
function divide(num1, num2) {
return num1 / num2;
}
这个简单的函数将两个数字相除,但如果两个参数中的任何一个不是数字,则将返回NaN。简单的计算通常会在Web应用程序意外返回NaN时导致问题。在这种情况下,可以在尝试计算之前检查每个参数的类型是否为数字:
function divide(num1, num2) {
if (typeof num1 != "number" || typeof num2 != "number") {
throw new Error("divide(): Both arguments must be numbers.");
}
return num1 / num2;
}
在这里,如果两个参数中的任何一个不是数字,则会引发错误。错误消息提供了函数的名称和错误的确切原因。当浏览器报告此错误消息时,它会立即提供一个开始查找问题的位置和问题的基本摘要。这比处理非特定浏览器错误消息要容易得多。
在大型应用程序中,通常使用assert()函数抛出自定义错误。这样的函数接受一个应该为真的条件,如果条件为假则抛出一个错误。下面是一个非常基本的 assert() 函数:
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
assert() 函数可用于代替函数中的多个if语句,并且可以作为错误记录的好位置。该函数可以这样使用:
function divide(num1, num2) {
assert(typeof num1 == "number" && typeof num2 == "number" && num2 != 0,
"divide(): Both arguments must be numbers.");
return num1 / num2;
}
与前面的示例相比,使用assert()函数减少了抛出自定义错误所需的代码量,并使代码更具可读性。