이번에는 JS에서 자주 발생하는 BUG와 오류가 무엇인지, JS에서 흔히 발생하는 BUG와 오류를 해결하기 위한 주의사항은 무엇인지 알려드리겠습니다. 다음은 실제 사례입니다.
컴퓨터 프로그램의 결함을 흔히 버그라고 부릅니다. 프로그래머가 그것들을 우리 창작물에 우연히 발견된 작은 일로 생각하는 것은 좋은 일입니다. 사실, 물론 우리가 직접 그 곳에 두었습니다.
프로그램이 아이디어의 결정체라면 오류는 아이디어의 혼동으로 인한 오류와 아이디어를 코드로 변환할 때 발생하는 오류로 크게 나눌 수 있습니다. 전자는 후자보다 진단하고 수리하기가 더 어려운 경우가 많습니다.
Language
컴퓨터는 우리가 하려는 작업을 충분히 이해하면 자동으로 많은 오류를 지적할 수 있습니다. 그러나 JavaScript의 느슨함은 여기서 방해가 됩니다. 바인딩과 속성의 개념이 모호하며 실제로 프로그램을 실행할 때까지 오타가 거의 발견되지 않습니다. 그럼에도 불구하고 실제 *'원숭이'를 계산하는 것과 같이 오류를 발생시키지 않는 무의미한 작업을 수행할 수 있습니다.
JavaScript에는 오류를 보고하는 데 몇 가지 문제가 있습니다. 언어 구문을 따르지 않는 프로그램을 작성하면 컴퓨터가 즉시 오류를 보고하게 됩니다. 함수가 아닌 것을 호출하거나 정의되지 않은 값에서 속성을 찾는 등의 다른 작업으로 인해 프로그램이 작업을 수행하려고 할 때 오류가 보고됩니다.
그러나 JavaScript가 의미 없는 계산을 처리하는 경우 NaN(숫자가 아님을 나타냄) 또는 정의되지 않음과 같은 결과만 반환합니다. 프로그램은 자신이 실행하는 코드에 문제가 없다고 생각하고 원활하게 실행됩니다. 많은 함수가 이미 이 의미 없는 값을 사용한 경우 후속 실행까지는 문제가 발생하지 않습니다. 프로그램 실행 중에 오류가 발생하지 않고 잘못된 프로그램 출력만 생성되는 것도 가능합니다. 이러한 오류의 원인을 찾는 것은 매우 어렵습니다.
프로그램 디버깅에서 오류나 버그를 찾는 과정을 우리는 호출합니다.
엄격 모드
엄격 모드가 활성화되면 코드를 실행할 때 JavaScript가 더욱 엄격해집니다. 파일이나 함수 본문의 상단에 "use strict" 문자열을 배치하면 엄격 모드를 활성화할 수 있습니다. 샘플 코드는 다음과 같습니다.
function canYouSpotTheProblem() { "use strict"; for (counter = 0; counter < 10; counter++) { console.log("Happy happy"); } } canYouSpotTheProblem(); // → ReferenceError: counter is not defined
일반적으로 예제의 counter와 같이 바인딩 앞에 let을 두는 것을 잊어버린 경우 JavaScript는 자동으로 전역 바인딩을 생성하여 사용합니다. 엄격 모드에서는 오류를 보고합니다. 이것은 매우 도움이 됩니다. 그러나 바인딩이 이미 전역 바인딩으로 존재하는 경우에는 작동하지 않습니다. 이 경우 루프는 여전히 바인딩된 값을 자동으로 덮어씁니다.
엄격 모드의 또 다른 변경 사항은 메서드로 호출되지 않는 함수에서 이 바인딩이 정의되지 않은 값을 보유한다는 것입니다. 이러한 호출이 엄격 모드 외부에서 이루어지면 속성이 전역적으로 바인딩된 전역 범위 개체를 참조합니다. 따라서 엄격 모드에서 실수로 메서드나 생성자를 잘못 호출한 경우 JavaScript는 전역 범위에 쓰는 대신 여기에서 무언가를 읽으려고 할 때 오류를 생성합니다.
예를 들어 this가 새로 생성된 객체를 참조하지 않도록 new 키워드 없이 생성자를 호출하는 다음 코드를 생각해 보세요.
function Person(name) { this.name = name; } let ferdinand = Person("Ferdinand"); // oops console.log(name); // → Ferdinand
실수로 Person을 호출하더라도 코드도 성공적으로 실행되지만 정의되지 않은 객체를 반환합니다. 값을 지정하고 name이라는 전역 바인딩을 만듭니다. 엄격 모드에서는 결과가 다릅니다.
"use strict"; function Person(name) { this.name = name; } let ferdinand = Person("Ferdinand"); // → TypeError: Cannot set property 'name' of undefined
JavaScript는 코드에 오류가 있음을 즉시 알려줍니다. 이 기능은 매우 유용합니다.
다행히 new를 사용하지 않고 클래스 심볼을 사용하여 생성한 생성자를 호출하면 항상 오류가 보고되며, non-strict 모드에서도 문제가 없습니다.
엄격 모드는 더 많은 일을 합니다. 동일한 이름을 사용하는 함수에 대한 여러 인수를 허용하지 않으며 특정 문제가 있는 언어 기능(예: 잘못되었으므로 이 책에서 더 이상 논의하지 않는 with 문)을 완전히 제거합니다.
간단히 말해서, 프로그램 상단에 "use strict"를 넣는 것은 거의 문제가 되지 않으며 문제를 발견하는 데 도움이 될 수 있습니다.
Types
일부 언어에서는 프로그램을 실행하기 전에 모든 바인딩과 표현식의 유형을 알고 싶어하기도 합니다. 유형이 일관되지 않은 방식으로 사용되면 즉시 알려줄 것입니다. JavaScript는 실제로 프로그램을 실행할 때만 유형을 고려합니다. 비록 암시적으로 값을 기대하는 유형으로 변환하려고 시도하는 경우가 많기 때문에 별로 도움이 되지 않습니다.
그럼에도 불구하고 유형은 프로그램을 논의하는 데 유용한 프레임워크를 제공합니다. 많은 오류는 함수에 들어가거나 나오는 값의 유형에 대한 혼란에서 비롯됩니다. 이 정보를 적어두면 혼란스러울 가능성이 줄어듭니다.
你可以在上一章的goalOrientedRobot函数上面,添加一个像这样的注释来描述它的类型。
// (WorldState, Array) → {direction: string, memory: Array} function goalOrientedRobot(state, memory) { // ... }
有许多不同的约定,用于标注 JavaScript 程序的类型。
关于类型的一点是,他们需要引入自己的复杂性,以便能够描述足够有用的代码。 你认为从数组中返回一个随机元素的randomPick函数的类型是什么? 你需要引入一个绑定类型T,它可以代表任何类型,这样你就可以给予randomPick一个像([T])->T的类型(从T到T的数组的函数)。
当程序的类型已知时,计算机可以为你检查它们,在程序运行之前指出错误。 有几种 JavaScript 语言为语言添加类型并检查它们。 最流行的称为 TypeScript。 如果你有兴趣为你的程序添加更多的严谨性,我建议你尝试一下。
在本书中,我们将继续使用原始的,危险的,非类型化的 JavaScript 代码。
测试
如果语言不会帮助我们发现错误,我们将不得不努力找到它们:通过运行程序并查看它是否正确执行。
一次又一次地手动操作,是一个非常糟糕的主意。 这不仅令人讨厌,而且也往往是无效的,因为每次改变时都需要花费太多时间来详尽地测试所有内容。
计算机擅长重复性任务,测试是理想的重复性任务。 自动化测试是编写测试另一个程序的程序的过程。 编写测试比手工测试有更多的工作,但是一旦你完成了它,你就会获得一种超能力:它只需要几秒钟就可以验证,你的程序在你编写为其测试的所有情况下都能正常运行。 当你破坏某些东西时,你会立即注意到,而不是在稍后的时间里随机地碰到它。
测试通常采用小标签程序的形式来验证代码的某些方面。 例如,一组(标准的,可能已经由其他人测试过)toUpperCase方法的测试可能如下:
function test(label, body) { if (!body()) console.log(`Failed: ${label}`); } test("convert Latin text to uppercase", () => { return "hello".toUpperCase() == "HELLO"; }); test("convert Greek text to uppercase", () => { return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ"; }); test("don't convert case-less characters", () => { return "مرحبا".toUpperCase() == "مرحبا"; });
像这样写测试往往会产生很多重复,笨拙的代码。 幸运的是,有些软件通过提供适合于表达测试的语言(以函数和方法的形式),并在测试失败时输出丰富的信息来帮助你构建和运行测试集合(测试套件,test suite)。 这些通常被称为测试运行器(test runner)。
一些代码比其他代码更容易测试。 通常,代码与外部交互的对象越多,建立用于测试它的上下文就越困难。 上一章中显示的编程风格,使用自包含的持久值而不是更改对象,通常很容易测试。
调试
当程序的运行结果不符合预期或在运行过程中产生错误时,你就会注意到程序出现问题了,下一步就是要推断问题出在什么地方。
有时错误很明显。错误消息会指出错误出现在程序的哪一行,只要稍加阅读错误描述及出错的那行代码,你一般就知道如何修正错误了。
但不总是这样。 有时触发问题的行,只是第一个地方,它以无效方式使用其他地方产生的奇怪的值。 如果你在前几章中已经解决了练习,你可能已经遇到过这种情况。
下面的示例代码尝试将一个整数转换成给定进制表示的字符串(十进制、二进制等),其原理是:不断循环取出最后一位数字,并将其除以基数(将最后一位数从数字中除去)。但该程序目前的输出表明程序中是存在bug的。
function numberToString(n, base = 10) { let result = "", sign = ""; if (n < 0) { sign = "-"; n = -n; } do { result = String(n % base) + result; n /= base; } while (n > 0); return sign + result; } console.log(numberToString(13, 10)); // → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3…
你可能已经发现程序运行结果不对了,不过先暂时装作不知道。我们知道程序运行出了问题,试图找出其原因。
这是一个地方,你必须抵制随机更改代码来查看它是否变得更好的冲动。 相反,要思考。 分析正在发生的事情,并提出为什么可能发生的理论。 然后,再做一些观察来检验这个理论 - 或者,如果你还没有理论,可以进一步观察来帮助你想出一个理论。
有目的地在程序中使用console.log来查看程序当前的运行状态,是一种不错的获取额外信息的方法。在本例中,我们希望n的值依次变为 13,1,然后是 0。让我们先在循环起始处输出n的值。
13 1.3 0.13 0.013 … 1.5e-323
没错。13 除以 10 并不会产生整数。我们不应该使用n/=base,而应该使用n=Math.floor(n/base),使数字“右移”,这才是我们实际想要的结果。
使用console.log来查看程序行为的替代方法,是使用浏览器的调试器(debugger)功能。 浏览器可以在代码的特定行上设置断点(breakpoint)。 当程序执行到带有断点的行时,它会暂停,并且你可以检查该点的绑定值。 我不会详细讨论,因为调试器在不同浏览器上有所不同,但请查看浏览器的开发人员工具或在 Web 上搜索来获取更多信息。
设置断点的另一种方法,是在程序中包含一个debugger语句(仅由该关键字组成)。 如果你的浏览器的开发人员工具是激活的,则只要程序达到这个语句,程序就会暂停。
错误传播
不幸的是,程序员不可能避免所有问题。 如果你的程序以任何方式与外部世界进行通信,则可能会导致输入格式错误,工作负荷过重或网络故障。
如果你只为自己编程,那么你就可以忽略这些问题直到它们发生。 但是如果你创建了一些将被其他人使用的东西,你通常希望程序比只是崩溃做得更好。 有时候,正确的做法是不择手段地继续运行。 在其他情况下,最好向用户报告出了什么问题然后放弃。 但无论在哪种情况下,该程序都必须积极采取措施来回应问题。
假设你有一个函数promptInteger,要求用户输入一个整数并返回它。 如果用户输入"orange",它应该返回什么?
一种办法是返回一个特殊值,通常会使用null,undefined或 -1。
function promptNumber(question) { let result = Number(prompt(question, "")); if (Number.isNaN(result)) return null; else return result; } console.log(promptNumber("How many trees do you see?"));
现在,调用promptNumber的任何代码都必须检查是否实际读取了数字,否则必须以某种方式恢复 - 也许再次询问或填充默认值。 或者它可能会再次向它的调用者返回一个特殊值,表示它未能完成所要求的操作。
在很多情况下,当错误很常见并且调用者应该明确地考虑它们时,返回特殊值是表示错误的好方法。 但它确实有其不利之处。 首先,如果函数已经可能返回每一种可能的值呢? 在这样的函数中,你必须做一些事情,比如将结果包装在一个对象中,以便能够区分成功与失败。
function lastElement(array) { if (array.length == 0) { return {failed: true}; } else { return {element: array[array.length - 1]}; } }
返回特殊值的第二个问题是它可能产生非常笨拙的代码。 如果一段代码调用promptNumber 10 次,则必须检查是否返回null 10 次。 如果它对null的回应是简单地返回null本身,函数的调用者将不得不去检查它,以此类推。
异常
当函数无法正常工作时,我们只希望停止当前任务,并立即跳转到负责处理问题的位置。这就是异常处理的功能。
异常是一种当代码执行中遇到问题时,可以触发(或抛出)异常的机制,异常只是一个普通的值。触发异常类似于从函数中强制返回:异常不只跳出到当前函数中,还会跳出函数调用方,直到当前执行流初次调用函数的位置。这种方式被称为“堆栈展开(Unwinding the Stack)”。你可能还记得我们在第3章中介绍的函数调用栈,异常会减小堆栈的尺寸,并丢弃所有在缩减程序栈尺寸过程中遇到的函数调用上下文。
如果异常总是会将堆栈尺寸缩减到栈底,那么异常也就毫无用处了。它只不过是换了一种方式来彻底破坏你的程序罢了。异常真正强大的地方在于你可以在堆栈上设置一个“障碍物”,当异常缩减堆栈到达这个位置时会被捕获。一旦发现异常,你可以使用它来解决问题,然后继续运行该程序。
function promptDirection(question) { let result = prompt(question, ""); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new Error("Invalid direction: " + result); } function look() { if (promptDirection("Which way?") == "L") { return "a house"; } else { return "two angry bears"; } } try { console.log("You see", look()); } catch (error) { console.log("Something went wrong: " + error); }
throw关键字用于引发异常。 异常的捕获通过将一段代码包装在一个try块中,后跟关键字catch来完成。 当try块中的代码引发异常时,将求值catch块,并将括号中的名称绑定到异常值。 在catch块结束之后,或者try块结束并且没有问题时,程序在整个try / catch语句的下面继续执行。
在本例中,我们使用Error构造器来创建异常值。这是一个标准的 JavaScript 构造器,用于创建一个对象,包含message属性。在多数 JavaScript 环境中,构造器实例也会收集异常创建时的调用栈信息,即堆栈跟踪信息(Stack Trace)。该信息存储在stack属性中,对于调用问题有很大的帮助,我们可以从堆栈跟踪信息中得知问题发生的精确位置,即问题具体出现在哪个函数中,以及执行失败为止调用的其他函数链。
需要注意的是现在look函数可以完全忽略promptDirection出错的可能性。这就是使用异常的优势:只有在错误触发且必须处理的位置才需要错误处理代码。其间的函数可以忽略异常处理。
嗯,我们要讲解的理论知识差不多就这些了。
异常后清理
异常的效果是另一种控制流。 每个可能导致异常的操作(几乎每个函数调用和属性访问)都可能导致控制流突然离开你的代码。
这意味着当代码有几个副作用时,即使它的“常规”控制流看起来像它们总是会发生,但异常可能会阻止其中一些发生。
这是一些非常糟糕的银行代码。
const accounts = { a: 100, b: 0, c: 20 }; function getAccount() { let accountName = prompt("Enter an account name"); if (!accounts.hasOwnProperty(accountName)) { throw new Error(`No such account: ${accountName}`); } return accountName; } function transfer(from, amount) { if (accounts[from] < amount) return; accounts[from] -= amount; accounts[getAccount()] += amount; }
transfer函数将一笔钱从一个给定的账户转移到另一个账户,在此过程中询问另一个账户的名称。 如果给定一个无效的帐户名称,getAccount将引发异常。
但是transfer首先从帐户中删除资金,之后调用getAccount,之后将其添加到另一个帐户。 如果它在那个时候由异常中断,它就会让钱消失。
这段代码本来可以更智能一些,例如在开始转移资金之前调用getAccount。 但这样的问题往往以更微妙的方式出现。 即使是那些看起来不像是会抛出异常的函数,在特殊情况下,或者当他们包含程序员的错误时,也可能会这样。
解决这个问题的一个方法是使用更少的副作用。 同样,计算新值而不是改变现有数据的编程风格有所帮助。 如果一段代码在创建新值时停止运行,没有人会看到这个完成一半的值,并且没有问题。
但这并不总是实际的。 所以try语句具有另一个特性。 他们可能会跟着一个finally块,而不是catch块,也不是在它后面。 finally块会说“不管发生什么事,在尝试运行try块中的代码后,一定会运行这个代码。”
function transfer(from, amount) { if (accounts[from] < amount) return; let progress = 0; try { accounts[from] -= amount; progress = 1; accounts[getAccount()] += amount; progress = 2; } finally { if (progress == 1) { accounts[from] += amount; } } }
这个版本的函数跟踪其进度,如果它在离开时注意到,它中止在创建不一致的程序状态的位置,则修复它造成的损害。
请注意,即使finally代码在异常退出try块时运行,它也不会影响异常。finally块运行后,堆栈继续展开。
即使异常出现在意外的地方,编写可靠运行的程序也非常困难。 很多人根本就不关心,而且由于异常通常针对异常情况而保留,因此问题可能很少发生,甚至从未被发现。 这是一件好事还是一件糟糕的事情,取决于软件执行失败时会造成多大的损害。
选择性捕获
当程序出现异常且异常未被捕获时,异常就会直接回退到栈顶,并由 JavaScript 环境来处理。其处理方式会根据环境的不同而不同。在浏览器中,错误描述通常会写入 JavaScript 控制台中(可以使用浏览器工具或开发者菜单来访问控制台)。我们将在第 20 章中讨论的,无浏览器的 JavaScript 环境 Node.js 对数据损坏更加谨慎。 当发生未处理的异常时,它会中止整个过程。
对于程序员的错误,让错误通行通常是最好的。 未处理的异常是表示糟糕的程序的合理方式,而在现代浏览器上,JavaScript 控制台为你提供了一些信息,有关在发生问题时堆栈上调用了哪些函数的。
对于在日常使用中发生的预期问题,因未处理的异常而崩溃是一种糟糕的策略。
语言的非法使用方式,比如引用一个不存在的绑定,在null中查询属性,或调用的对象不是函数最终都会引发异常。你可以像自己的异常一样捕获这些异常。
进入catch语句块时,我们只知道try体中引发了异常,但不知道引发了哪一类或哪一个异常。
JavaScript(很明显的疏漏)并未对选择性捕获异常提供良好的支持,要不捕获所有异常,要不什么都不捕获。这让你很容易假设,你得到的异常就是你在写catch时所考虑的异常。
但它也可能不是。 可能会违反其他假设,或者你可能引入了导致异常的 bug。 这是一个例子,它尝试持续调用promptDirection,直到它得到一个有效的答案:
for (;;) { try { let dir = promtDirection("Where?"); // ← typo! console.log("You chose ", dir); break; } catch (e) { console.log("Not a valid direction. Try again."); } }
我们可以使用for (;;)循环体来创建一个无限循环,其自身永远不会停止运行。我们在用户给出有效的方向之后会跳出循环。但我们拼写错了promptDirection,因此会引发一个“未定义值”错误。由于catch块完全忽略了异常值,假定其知道问题所在,错将绑定错误信息当成错误输入。这样不仅会引发无限循环,而且会掩盖掉真正的错误消息——绑定名拼写错误。
一般而言,只有将抛出的异常重定位到其他地方进行处理时,我们才会捕获所有异常。比如说通过网络传输通知其他系统当前应用程序的崩溃信息。即便如此,我们也要注意编写的代码是否会将错误信息掩盖起来。
因此,我们转而会去捕获那些特殊类型的异常。我们可以在catch代码块中判断捕获到的异常是否就是我们期望处理的异常,如果不是则将其重新抛出。那么我们该如何辨别抛出异常的类型呢?
我们可以将它的message属性与我们所期望的错误信息进行比较。 但是,这是一种不稳定的编写代码的方式 - 我们将使用供人类使用的信息来做出程序化决策。 只要有人更改(或翻译)该消息,代码就会停止工作。
我们不如定义一个新的错误类型,并使用instanceof来识别异常。
class InputError extends Error {} function promptDirection(question) { let result = prompt(question); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new InputError("Invalid direction: " + result); }
新的错误类扩展了Error。 它没有定义它自己的构造器,这意味着它继承了Error构造器,它需要一个字符串消息作为参数。 事实上,它根本没有定义任何东西 - 这个类是空的。 InputError对象的行为与Error对象相似,只是它们的类不同,我们可以通过类来识别它们。
现在循环可以更仔细地捕捉它们。
for (;;) { try { let dir = promptDirection("Where?"); console.log("You chose ", dir); break; } catch (e) { if (e instanceof InputError) { console.log("Not a valid direction. Try again."); } else { throw e; } } }
这里的catch代码只会捕获InputError类型的异常,而其他类型的异常则不会在这里进行处理。如果又输入了不正确的值,那么系统会向用户准确报告错误——“绑定未定义”。
断言
断言(assertions)是程序内部的检查,用于验证某个东西是它应该是的方式。 它们并不是用于处理正常操作中可能出现的情况,而是发现程序员的错误。
例如,如果firstElement被描述为一个函数,永远不会在空数组上调用,我们可以这样写:
function firstElement(array) { if (array.length == 0) { throw new Error("firstElement called with []"); } return array[0]; }
现在,它不会默默地返回未定义值(当你读取一个不存在的数组属性的时候),而是在你滥用它时立即干掉你的程序。 这使得这种错误不太可能被忽视,并且当它们发生时更容易找到它们的原因。
我不建议尝试为每种可能的不良输入编写断言。 这将是很多工作,并会产生非常杂乱的代码。 你会希望为很容易犯(或者你发现自己做过)的错误保留他们。
本章小结
错误和无效的输入十分常见。编程的一个重要部分是发现,诊断和修复错误。 如果你拥有自动化测试套件或向程序添加断言,则问题会变得更容易被注意。
我们常常需要使用优雅的方式来处理程序可控范围外的问题。如果问题可以就地解决,那么返回一个特殊的值来跟踪错误就是一个不错的解决方案。或者,异常也可能是可行的。
抛出异常会引发堆栈展开,直到遇到下一个封闭的try/catch块,或堆栈底部为止。catch块捕获异常后,会将异常值赋予catch块,catch块中应该验证异常是否是实际希望处理的异常,然后进行处理。为了有助于解决由于异常引起的不可预测的执行流,可以使用finally块来确保执行try块之后的代码。
习题
重试
假设有一个函数primitiveMultiply,在 20% 的情况下将两个数相乘,在另外 80% 的情况下会触发MultiplicatorUnitFailure类型的异常。编写一个函数,调用这个容易出错的函数,不断尝试直到调用成功并返回结果为止。
确保只处理你期望的异常。
class MultiplicatorUnitFailure extends Error {} function primitiveMultiply(a, b) { if (Math.random() < 0.2) { return a * b; } else { throw new MultiplicatorUnitFailure(); } } function reliableMultiply(a, b) { // Your code here. } console.log(reliableMultiply(8, 8)); // → 64
上锁的箱子
考虑以下这个编写好的对象:
const box = { locked: true, unlock() { this.locked = false; }, lock() { this.locked = true; }, _content: [], get content() { if (this.locked) throw new Error("Locked!"); return this._content; } };
这是一个带锁的箱子。其中有一个数组,但只有在箱子被解锁时,才可以访问数组。不允许直接访问_content属性。
编写一个名为withBoxUnlocked的函数,接受一个函数类型的参数,其作用是解锁箱子,执行该函数,无论是正常返回还是抛出异常,在withBoxUnlocked函数返回前都必须锁上箱子。
const box = { locked: true, unlock() { this.locked = false; }, lock() { this.locked = true; }, _content: [], get content() { if (this.locked) throw new Error("Locked!"); return this._content; } }; function withBoxUnlocked(body) { // Your code here. } withBoxUnlocked(function() { box.content.push("gold piece"); }); try { withBoxUnlocked(function() { throw new Error("Pirates on the horizon! Abort!"); }); } catch (e) { console.log("Error raised:", e); } console.log(box.locked); // → true
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
위 내용은 JS에서 자주 발생하는 버그와 오류는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!