详解JavaScript的BUG和错误


Posted in Javascript onMay 07, 2018

计算机程序中的缺陷通常称为 bug。 它让程序员觉得很好,将它们想象成小事,只是碰巧进入我们的作品。 实际上,当然,我们自己把它们放在了那里。

如果一个程序是思想的结晶,你可以粗略地将错误分为因为思想混乱引起的错误,以及思想转换为代码时引入的错误。 前者通常比后者更难诊断和修复。

语言

计算机能够自动地向我们指出许多错误,如果它足够了解我们正在尝试做什么。 但是这里 JavaScript 的宽松是一个障碍。 它的绑定和属性概念很模糊,在实际运行程序之前很少会发现拼写错误。 即使这样,它也允许你做一些不会报错的无意义的事情,比如计算true *'monkey'。

JavaScript 有一些报错的事情。 编写不符合语言语法的程序会立即使计算机报错。 其他的东西,比如调用不是函数的东西,或者在未定义的值上查找属性,会导致在程序尝试执行操作时报告错误。

不过,JavaScript 在处理无意义的计算时,会仅仅返回NaN(表示不是数字)或undefined这样的结果。程序会认为其执行的代码毫无问题并顺利运行下去,要等到随后的运行过程中才会出现问题,而此时已经有许多函数使用了这个无意义的值。程序执行中也可能不会遇到任何错误,只会产生错误的程序输出。找出这类错误的源头是非常困难的。

我们将查找程序中的错误或者 bug 的过程称为调试(debug)。

严格模式

当启用了严格模式(strict mode)后,JavaScript 就会在执行代码时变得更为严格。我们只需在文件或函数体顶部放置字符串"use strict"就可以启用严格模式了。下面是示例代码:

function canYouSpotTheProblem() {
 "use strict";
 for (counter = 0; counter < 10; counter++) {
 console.log("Happy happy");
 }
}

canYouSpotTheProblem();
// → ReferenceError: counter is not defined

通常,当你忘记在绑定前面放置let时,就像在示例中的counter一样,JavaScript 静静地创建一个全局绑定并使用它。 在严格模式下,它会报告错误。 这非常有帮助。 但是,应该指出的是,当绑定已经作为全局绑定存在时,这是行不通的。 在这种情况下,循环仍然会悄悄地覆盖绑定的值。

严格模式中的另一个变化是,在未被作为方法而调用的函数中,this绑定持有值undefined。 当在严格模式之外进行这样的调用时,this引用全局作用域对象,该对象的属性是全局绑定。 因此,如果你在严格模式下不小心错误地调用方法或构造器,JavaScript 会在尝试从this读取某些内容时产生错误,而不是愉快地写入全局作用域。

例如,考虑下面的代码,该代码不带new关键字调用构造器,以便其this不会引用新构造的对象:

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 会立即告知我们代码中包含错误。这种特性十分有用。

幸运的是,使用class符号创建的构造器,如果在不使用new来调用,则始终会报错,即使在非严格模式下也不会产生问题。

严格模式做了更多的事情。 它不允许使用同一名称给函数赋多个参数,并且完全删除某些有问题的语言特性(例如with语句,这是错误的,本书不会进一步讨论)。

简而言之,在程序顶部放置"use strict"很少会有问题,并且可能会帮助你发现问题。

类型

有些语言甚至在运行程序之前想要知道,所有绑定和表达式的类型。 当类型以不一致的方式使用时,他们会马上告诉你。 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
Javascript 相关文章推荐
下拉列表选择项的选中在不同浏览器中的兼容性问题探讨
Sep 18 Javascript
JS实现的一个简单的Autocomplete自动完成例子
Apr 16 Javascript
JavaScript的原型继承详解
Feb 15 Javascript
js实现网页右上角滑出会自动消失大幅广告的方法
Feb 27 Javascript
JS实现左右无缝轮播图代码
May 01 Javascript
ionic实现滑动的三种方式
Aug 27 Javascript
Bootstrap table的使用方法
Nov 02 Javascript
详解Angular结合zTree异步加载节点数据
Jan 20 Javascript
Vue单页面应用保证F5强刷不清空数据的解决方案
Jan 31 Javascript
Angular PWA使用的Demo示例
Jan 31 Javascript
Vue项目history模式下微信分享爬坑总结
Mar 29 Javascript
vue+elementUI实现简单日历功能
Sep 24 Javascript
vue实现个人信息查看和密码修改功能
May 06 #Javascript
基于vue-element组件实现音乐播放器功能
May 06 #Javascript
VueJs组件之父子通讯的方式
May 06 #Javascript
vue自动化表单实例分析
May 06 #Javascript
node+koa2+mysql+bootstrap搭建一个前端论坛
May 06 #Javascript
JS中this的指向以及call、apply的作用
May 06 #Javascript
如何利用@angular/cli V6.0直接开发PWA应用详解
May 06 #Javascript
You might like
PHP的栏目导航程序
2006/10/09 PHP
php实现向javascript传递数组的方法
2015/07/27 PHP
PHP编程实现多维数组按照某个键值排序的方法小结【2种方法】
2017/04/27 PHP
js form 验证函数 当前比较流行的错误提示
2009/06/23 Javascript
几个有趣的Javascript Hack
2010/07/24 Javascript
打豆豆小游戏 用javascript编写的[打豆豆]小游戏
2013/01/08 Javascript
深入理解javascript中的立即执行函数(function(){…})()
2014/06/12 Javascript
Javascript核心读书有感之词法结构
2015/02/01 Javascript
jQuery中animate动画第二次点击事件没反应
2015/05/07 Javascript
ajax如何实现页面局部跳转与结果返回
2015/08/24 Javascript
JS开发中基本数据类型具体有哪几种
2017/10/19 Javascript
vue使用keep-alive实现数据缓存不刷新
2017/10/21 Javascript
vue 实现的树形菜的实例代码
2018/03/19 Javascript
jQuery发请求传输中文参数乱码问题的解决方案
2018/05/22 jQuery
Vue引入Stylus知识点总结
2020/01/16 Javascript
pydev使用wxpython找不到路径的解决方法
2013/02/10 Python
Python的__builtin__模块中的一些要点知识
2015/05/02 Python
Python计时相关操作详解【time,datetime】
2017/05/26 Python
使用python采集脚本之家电子书资源并自动下载到本地的实例脚本
2018/10/23 Python
浅析Python 实现一个自动化翻译和替换的工具
2019/04/14 Python
用python给自己做一款小说阅读器过程详解
2019/07/11 Python
在主流系统之上安装Pygame的方法
2020/05/20 Python
Python几种常见算法汇总
2020/06/02 Python
python如何快速生成时间戳
2020/07/21 Python
python 贪心算法的实现
2020/09/18 Python
Shoes For Crews法国官网:美国领先的防滑鞋设计和制造商
2018/01/01 全球购物
Godiva巧克力英国官网:比利时歌帝梵巧克力
2018/08/28 全球购物
Clarks西班牙官方在线商店:clarks鞋
2019/05/03 全球购物
Vans(范斯)新西兰官方网站:美国原创极限运动品牌
2020/09/19 全球购物
解释一下ruby中的特殊方法与特殊类
2013/02/26 面试题
人事部专员岗位职责
2014/03/04 职场文书
爱心活动计划书
2014/04/26 职场文书
大学团日活动总结书
2015/05/11 职场文书
网络舆情信息简报
2015/07/21 职场文书
2015年市场营销工作总结
2015/07/23 职场文书
关于antd tree 和父子组件之间的传值问题(react 总结)
2021/06/02 Javascript