实践

最后更新于:2022-04-01 21:00:59

';

高级

最后更新于:2022-04-01 21:00:57

';

中级

最后更新于:2022-04-01 21:00:55

';

单体内置对象

最后更新于:2022-04-01 21:00:52

# 单体内置对象 ECMA-262 对内置对象的定义是「由 JavaScript 实现提供的、不依赖于宿主环境的对象,这些对象在 JavaScript 程序执行之前就已经存在了」。意思就是说,开发人员不必显式地实例化内置对象,因为它们已经实例化了。前面我们已经介绍了大多数内置对象,例如 `Object`、`Array` 和 `String`。ECMA-262 还定义了两个单体内置对象:`Global` 和 `Math`。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#global-对象)`Global` 对象 `Global` 对象可以说是 JavaScript 中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。`Global` 对象在某种意义上是作为一个终极的「兜底儿对象」来定义的。换句话说,不属于任何其他对象的属性和方法,最终都是它的属性和方法。所有在全局作用域中定义的属性和函数,都是 `Global` 对象的属性。本书前面介绍过的那些函数,诸如`isNaN()`、`isFinite()`、`parseInt()` 以及 `parseFloat()`,实际上全都是 `Global` 对象的方法。除此之外,`Global` 对象还包含其他一些方法。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#uri-编码方法)URI 编码方法 `Global` 对象的 `encodeURI()` 和 `encodeURIComponent()` 方法可以对 URI(Uniform Resource Identifiers,通用资源标识符)进行编码,以便发送给浏览器。有效的 URI 中不能包含某些字符,例如空格。而这两个 URI 编码方法就可以对 URI 进行编码,它们用特殊的 UTF-8 编码替换所有无效的字符,从而让浏览器能够接受和理解。 其中,`encodeURI()` 主要用于整个 URI,而 `encodeURIComponent()` 主要用于对 URI 中的某一段进行编码。它们的主要区别在于,`encodeURI()` 不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而 `encodeURIComponent()`则会对它发现的任何非标准字符进行编码。来看下面的例子。 ~~~ var uri = "http://shijiajie.com/illegal value.htm#start"; console.log(encodeURI(uri)); // "http://shijiajie.com/illegal%20value.htm#start" console.log(encodeURIComponent(uri)); // "http%3A%2F%2Fshijiajie.com%2Fillegal%20value.htm%23start" ~~~ 使用 `encodeURI()` 编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了 `%20`。而`encodeURIComponent()` 方法则会使用对应的编码替换所有非字母数字字符。这也正是可以对整个 URI 使用 `encodeURI()`,而只能对附加在现有 URI 后面的字符串使用 `encodeURIComponent()` 的原因所在。 一般来说,我们使用 `encodeURIComponent()` 方法的时候要比使用 `encodeURI()` 更多,因为在实践中更常见的是对查询字符串参数而不是对基础 URI 进行编码。 与 `encodeURI()` 和 `encodeURIComponent()` 方法对应的两个方法分别是 `decodeURI()` 和 `decodeURIComponent()`。其中,`decodeURI()` 只能对使用 `encodeURI()` 替换的字符进行解码。例如,它可将 `%20` 替换成一个空格,但不会对 `%23` 作任何处理,因为 `%23` 表示井字号 `#`,而井字号不是使用 `encodeURI()` 替换的。同样地,`decodeURIComponent()` 能够解码使用 `encodeURIComponent()` 编码的所有字符,即它可以解码任何特殊字符的编码。来看下面的例子: ~~~ var uri = "http%3A%2F%2Fshijiajie.com%2Fillegal%20value.htm%23start"; console.log(decodeURI(uri)); // http%3A%2F%2Fshijiajie.com%2Fillegal value.htm%23start console.log(decodeURIComponent(uri)); // http://shijiajie.com/illegal value.htm#start ~~~ 这里,变量 `uri` 包含着一个由 `encodeURIComponent()` 编码的字符串。在第一次调用 `decodeURI()` 输出的结果中,只有`%20` 被替换成了空格。而在第二次调用 `decodeURIComponent()` 输出的结果中,所有特殊字符的编码都被替换成了原来的字符,得到了一个未经转义的字符串(但这个字符串并不是一个有效的 URI)。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#eval-方法)`eval()` 方法 `eval()` 方法就像是一个完整的 JavaScript 解析器,它只接受一个参数,即要执行的 JavaScript 字符串。看下面的例子: ~~~ eval("console.log('hi')"); ~~~ 这行代码的作用等价于下面这行代码: ~~~ console.log("hi"); ~~~ 当解析器发现代码中调用 `eval()` 方法时,它会将传入的参数当作实际的 JavaScript 语句来解析,然后把执行结果插入到原位置。通过 `eval()` 执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过 `eval()` 执行的代码可以引用在包含环境中定义的变量,举个例子: ~~~ var msg = "hello world"; eval("console.log(msg)"); // "hello world" ~~~ 可见,变量 `msg` 是在 `eval()` 调用的环境之外定义的,但其中调用的 `console.log()` 仍然能够显示 `"hello world"`。这是因为上面第二行代码最终被替换成了一行真正的代码。同样地,我们也可以在 `eval()` 调用中定义一个函数,然后再在该调用的外部代码中引用这个函数: ~~~ eval("function sayHi() { console.log('hi'); }"); sayHi(); // "hi" ~~~ 显然,函数 `sayHi()` 是在 `eval()` 内部定义的。但由于对 `eval()` 的调用最终会被替换成定义函数的实际代码,因此可以在下一行调用 `sayHi()` 。对于变量也一样: ~~~ eval("var msg = 'hello world';"); console.log(msg); // "hello world" ~~~ 在 `eval()` 中创建的任何变量或函数都不会被提升,因为在解析代码的时候,它们被包含在一个字符串中;它们只在 `eval()`执行的时候创建。 严格模式下,在外部访问不到 `eval()` 中创建的任何变量或函数,因此前面两个例子都会导致错误。同样,在严格模式下,为`eval` 赋值也会导致错误: ~~~ "use strict"; eval = "hi"; // causes error ~~~ 能够解释代码字符串的能力非常强大,但也非常危险。因此在使用 `eval()` 时必须极为谨慎,特别是在用它执行用户输入数据的情况下。否则,可能会有恶意用户输入威胁你的站点或应用程序安全的代码(即所谓的代码注入)。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#global-对象的属性)`Global` 对象的属性 `Global` 对象还包含一些属性,其中一部分属性已经在本书前面介绍过了。例如,特殊的值 `undefined`、`NaN` 以及`Infinity` 都是 `Global` 对象的属性。此外,所有原生引用类型的构造函数,像 `Object` 和 `Function`,也都是 `Global` 对象的属性。下表列出了 `Global` 对象的所有属性。 | 属性 | 说明 | 属性 | 说明 | | --- | --- | --- | --- | | undefined | 特殊值undefined | Date | 构造函数Date | | NaN | 特殊值NaN | RegExp | 构造函数RegExp | | Infinity | 特殊值Infinity | Error | 构造函数Error | | Object | 构造函数Object | EvalError | 构造函数EvalError | | Array | 构造函数Array | RangeError | 构造函数RangeError | | Function | 构造函数Function | ReferenceError | 构造函数ReferenceError | | Boolean | 构造函数Boolean | SyntaxError | 构造函数SyntaxError | | String | 构造函数String | TypeError | 构造函数TypeError | | Number | 构造函数Number | URIError | 构造函数URIError | ECMAScript 5明确禁止给 `undefined`、`NaN` 和 `Infinity` 赋值,这样做即使在非严格模式下也会导致错误。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#window-对象)`window` 对象 JavaScript 虽然没有指出如何直接访问 `Global` 对象,但 Web 浏览器都是将这个全局对象作为 `window` 对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了 `window` 对象的属性。来看下面的例子。 ~~~ var color = "red"; function sayColor(){ console.log(window.color); } window.sayColor(); // "red" ~~~ JavaScript 中的 `window` 对象除了扮演规定的 `Global` 对象的角色外,还承担了很多别的任务。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#math-对象) `Math` 对象 JavaScript 还为保存数学公式和信息提供了一个公共位置,即 `Math` 对象。与我们在 `JavaScript` 直接编写的计算功能相比,`Math` 对象提供的计算功能执行起来要快得多。`Math` 对象中还提供了辅助完成这些计算的属性和方法。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#math-对象的属性)`Math` 对象的属性 `Math` 对象包含的属性大都是数学计算中可能会用到的一些特殊值。下表列出了这些属性。 | 属性 | 说明 | | --- | --- | | Math.E | 自然对数的底数,即常量e的值 | | Math.LN10 | 10的自然对数 | | Math.LN2 | 2的自然对数 | | Math.LOG2E | 以2为底e的对数 | | Math.LOG10E | 以10为底e的对数 | | Math.PI | π的值 | | Math.SQRT1_2 | 1/2的平方根(即2的平方根的倒数) | | Math.SQRT2 | 2的平方根 | ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#min-和-max-方法)`min()` 和 `max()` 方法 `Math` 对象还包含许多方法,用于辅助完成简单和复杂的数学计算。其中,`min()` 和 `max()` 方法用于确定一组数值中的最小值和最大值。这两个方法都可以接收任意多个数值参数,如下面的例子所示。 ~~~ var max = Math.max(3, 54, 32, 16); console.log(max); // 54 var min = Math.min(3, 54, 32, 16); console.log(min); // 3 ~~~ 要找到数组中的最大或最小值,可以像下面这样使用 `apply()` 方法。 ~~~ var values = [1, 2, 3, 4, 5, 6, 7, 8]; var max = Math.max.apply(Math, values); console.log(max); // 8 ~~~ 这个技巧的关键是把 `Math` 对象作为 `apply()` 的第一个参数,从而正确地设置 `this` 值。然后,可以将任何数组作为第二个参数。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#舍入方法)舍入方法 下面来介绍将小数值舍入为整数的几个方法:`Math.ceil()`、`Math.floor()` 和 `Math.round()`。这三个方法分别遵循下列舍入规则: * `Math.ceil()` 执行向上舍入,即它总是将数值向上舍入为最接近的整数; * `Math.floor()` 执行向下舍入,即它总是将数值向下舍入为最接近的整数; * `Math.round()` 执行标准舍入,即它总是将数值四舍五入为最接近的整数。 下面是使用这些方法的示例: ~~~ console.log(Math.ceil(25.9)); // 26 console.log(Math.ceil(25.5)); // 26 console.log(Math.ceil(25.1)); // 26 console.log(Math.round(25.9)); // 26 console.log(Math.round(25.5)); // 26 console.log(Math.round(25.1)); // 25 console.log(Math.floor(25.9)); // 25 console.log(Math.floor(25.5)); // 25 console.log(Math.floor(25.1)); // 25 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#random-方法)`random()` 方法 `Math.random()` 方法返回介于0和1之间一个随机数,不包括0和1。对于某些站点来说,这个方法非常实用,因为可以利用它来随机显示一些名人名言和新闻事件。套用下面的公式,就可以利用 `Math.random()` 从某个整数范围内随机选择一个值。 ~~~ 值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值) ~~~ 公式中用到了 `Math.floor()` 方法,这是因为 `Math.random()` 总返回一个小数值。而用这个小数值乘以一个整数,然后再加上一个整数,最终结果仍然还是一个小数。举例来说,如果你想选择一个1到10之间的数值,可以像下面这样编写代码: ~~~ var num = Math.floor(Math.random() * 10 + 1); ~~~ 总共有10个可能的值(1到10),而第一个可能的值是1。而如果想要选择一个介于2到10之间的值,就应该将上面的代码改成这样: ~~~ var num = Math.floor(Math.random() * 9 + 2); ~~~ 从2数到10要数9个数,因此可能值的总数就是9,而第一个可能的值就是2。多数情况下,其实都可以通过一个函数来计算可能值的总数和第一个可能的值,例如: ~~~ function selectFrom(lowerValue, upperValue) { var choices = upperValue - lowerValue + 1; return Math.floor(Math.random() * choices + lowerValue); } var num = selectFrom(2, 10); console.log(num); // 介于2和10之间(包括2和10)的一个数值 ~~~ 函数 `selectFrom()` 接受两个参数:应该返回的最小值和最大值。而用最大值减最小值再加1得到了可能值的总数,然后它又把这些数值套用到了前面的公式中。这样,通过调用 `selectFrom(2,10)` 就可以得到一个介于2和10之间(包括2和10)的数值了。利用这个函数,可以方便地从数组中随机取出一项,例如: ~~~ var colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"]; var color = colors[selectFrom(0, colors.length-1)]; console.log(color); // 可能是数组中包含的任何一个字符串 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#其他方法)其他方法 `Math` 对象中还包含其他一些与完成各种简单或复杂计算有关的方法,但详细讨论其中每一个方法的细节及适用情形超出了本书的范围。下面我们就给出一个表格,其中列出了这些没有介绍到的 `Math` 对象的方法。 | 方法 | 说明 | | --- | --- | | Math.abs(num) | 返回num的绝对值 | | Math.asin(x) | 返回x的反正弦值 | | Math.exp(num) | 返回Math.E的num次幂 | | Math.atan(x) | 返回x的反正切值 | | Math.log(num) | 返回num的自然对数 | | Math.atan2(y,x) | 返回y/x的反正切值 | | Math.pow(num,power) | 返回num的power次幂 | | Math.cos(x) | 返回x的余弦值 | | Math.sqrt(num) | 返回num的平方根 | | Math.sin(x) | 返回x的正弦值 | | Math.acos(x) | 返回x的反余弦值 | | Math.tan(x) | 返回x的正切值 | 虽然 ECMA-262 规定了这些方法,但不同实现可能会对这些方法采用不同的算法。毕竟,计算某个值的正弦、余弦和正切的方式多种多样。也正因为如此,这些方法在不同的实现中可能会有不同的精度。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.12-SingletonBuiltInObjects#关卡)关卡 ~~~ // 如何高效产生m个n范围内的不重复随机数(m<=n) var getRandomNumber = function(n, m){ // 待实现方法体 } console.log(getRandomNumber(20, 3)); // 8,4,19 ~~~
';

基本包装类型

最后更新于:2022-04-01 21:00:50

# 基本包装类型 为了便于操作基本类型值,JavaScript 还提供了3个特殊的引用类型:`Boolean`、`Number` 和 `String`。实际上,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。来看下面的例子。 ~~~ var s1 = "some text"; var s2 = s1.substring(2); ~~~ 这个例子中的变量 `s1` 包含一个字符串,字符串当然是基本类型值。而下一行调用了 `s1` 的 `substring()` 方法,并将返回的结果保存在了 `s2` 中。我们知道,基本类型值不是对象,因而从逻辑上讲它们不应该有方法(尽管如我们所愿,它们确实有方法)。其实,为了让我们实现这种直观的操作,后台已经自动完成了一系列的处理。当第二行代码访问 `s1` 时,访问过程处于一种读取模式,也就是要从内存中读取这个字符串的值。而在读取模式中访问字符串时,后台都会自动完成下列处理。 1. 创建 `String` 类型的一个实例; 2. 在实例上调用指定的方法; 3. 销毁这个实例。 可以将以上三个步骤想象成是执行了下列 JavaScript 代码。 ~~~ var s1 = new String("some text"); var s2 = s1.substring(2); s1 = null; ~~~ 经过此番处理,基本的字符串值就变得跟对象一样了。而且,上面这三个步骤也分别适用于 `Boolean` 和 `Number` 类型对应的布尔值和数字值。 引用类型与基本包装类型的主要区别就是对象的生存期。使用 `new` 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。来看下面的例子: ~~~ var s1 = "some text"; s1.color = "red"; console.log(s1.color); // undefined ~~~ 当然,可以显式地调用 `Boolean`、`Number` 和 `String` 来创建基本包装类型的对象。不过,应该在绝对必要的情况下再这样做,因为这种做法很容易让人分不清自己是在处理「基本类型」还是「引用类型」的值。对基本包装类型的实例调用 `typeof`会返回 `"object"`,而且所有基本包装类型的对象都会被转换为布尔值 `true`。 `Object` 构造函数也会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。例如: ~~~ var obj = new Object("some text"); console.log(obj instanceof String); // true ~~~ 把字符串传给 `Object` 构造函数,就会创建 `String` 的实例;而传入数值参数会得到 `Number` 的实例,传入布尔值参数就会得到 `Boolean` 的实例。 要注意的是,使用 `new` 调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的。 例如: ~~~ var value = "25"; var number = Number(value); // 转型函数 console.log(typeof number); // "number" var obj = new Number(value); // 构造函数 console.log(typeof obj); // "object" ~~~ 尽管我们不建议显式地创建基本包装类型的对象,但它们操作基本类型值的能力还是相当重要的。而每个基本包装类型都提供了操作相应值的便捷方法。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#boolean-类型) `Boolean` 类型 `Boolean` 类型是与布尔值对应的引用类型。要创建 `Boolean` 对象,可以像下面这样调用 `Boolean` 构造函数并传入 `true` 或`false` 值。 ~~~ var booleanObject = new Boolean(true); ~~~ `Boolean` 类型的实例重写了 `valueOf()` 方法,返回基本类型值 `true` 或 `false`;重写了 `toString()` 方法,返回字符串`"true"` 和 `"false"`。可是,`Boolean` 对象在 JavaScript 中的用处不大,因为它经常会造成人们的误解。其中最常见的问题就是在布尔表达式中使用 `Boolean` 对象,例如: ~~~ var falseObject = new Boolean(false); var result = falseObject && true; console.log(result); // true var falseValue = false; result = falseValue && true; console.log(result); // false ~~~ 在这个例子中,我们使用 `false` 值创建了一个 `Boolean` 对象。然后,将这个对象与基本类型值 `true` 构成了逻辑与表达式。在布尔运算中,`false && true` 等于 `false`。可是,示例中的这行代码是对 `falseObject` 而不是对它的值 `false` 进行求值。布尔表达式中的所有对象都会被转换为 `true`,因此 `falseObject` 对象在布尔表达式中代表的是 `true`。结果,`true && true` 当然就等于 `true` 了。 基本类型与引用类型的布尔值还有两个区别。首先,`typeof` 操作符对基本类型返回 `"boolean"`,而对引用类型返回`"object"`。其次,由于 `Boolean` 对象是 `Boolean` 类型的实例,所以使用 `instanceof` 操作符测试 `Boolean` 对象会返回`true`,而测试基本类型的布尔值则返回 `false`。例如: ~~~ console.log(typeof falseObject); // object console.log(typeof falseValue); // boolean console.log(falseObject instanceof Boolean); // true console.log(falseValue instanceof Boolean); // false ~~~ 理解基本类型的布尔值与 `Boolean` 对象之间的区别非常重要,我们的建议是永远不要使用 `Boolean` 对象。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#number-类型) `Number` 类型 `Number` 是与数字值对应的引用类型。要创建 `Number` 对象,可以在调用 `Number` 构造函数时向其中传递相应的数值。下面是一个例子。 ~~~ var numberObject = new Number(10); ~~~ 与 `Boolean` 类型一样,`Number` 类型也重写了 `valueOf()`、`toLocaleString()` 和 `toString()` 方法。重写后的 `valueOf()`方法返回对象表示的基本类型的数值,另外两个方法则返回字符串形式的数值。可以为 `toString()` 方法传递一个表示基数的参数,告诉它返回几进制数值的字符串形式,如下面的例子所示。 ~~~ var num = 10; console.log(num.toString()); // "10" console.log(num.toString(2)); // "1010" console.log(num.toString(8)); // "12" console.log(num.toString(10)); // "10" console.log(num.toString(16)); // "a" ~~~ 除了继承的方法之外,`Number` 类型还提供了一些用于将数值格式化为字符串的方法。其中,`toFixed()` 方法会按照指定的小数位返回数值的字符串表示,例如: ~~~ var num = 10; console.log(num.toFixed(2)); // "10.00" ~~~ 这里给 `toFixed()` 方法传入了数值 `2`,意思是显示几位小数。于是,这个方法返回了 `"10.00"`,即以 `0` 填补了必要的小数位。如果数值本身包含的小数位比指定的还多,那么接近指定的最大小数位的值就会舍入,如下面的例子所示。 ~~~ var num = 10.005; console.log(num.toFixed(2)); // "10.01" ~~~ 能够自动舍入的特性,使得 `toFixed()` 方法很适合处理货币值。 > 但需要注意的是,不同浏览器给这个方法设定的舍入规则可能会有所不同。 > > 在给 `toFixed()` 传入0的情况下,IE8 及之前版本不能正确舍入范围在{(-0.94,-0.5],[0.5,0.94)}之间的值。对于这个范围内的值,IE8 会返回0,而不是-1或1;其他浏览器都能返回正确的值。IE9 修复了这个问题。 > > `toFixed()` 方法可以表示带有0到20个小数位的数值。但这只是标准实现的范围,有些浏览器也可能支持更多位数。 另外可用于格式化数值的方法是 `toExponential()`,该方法返回以指数表示法(也称 e 表示法)表示的数值的字符串形式。与`toFixed()` 一样,`toExponential()` 也接收一个参数,而且该参数同样也是指定输出结果中的小数位数。看下面的例子。 ~~~ var num = 10; console.log(num.toExponential(1)); // "1.0e+1" ~~~ 以上代码输出了 `"1.0e+1"`;不过,这么小的数值一般不必使用 e 表示法。如果你想得到表示某个数值的最合适的格式,就应该使用 `toPrecision()` 方法。 对于一个数值来说,`toPrecision()` 方法可能会返回固定大小(fixed)格式,也可能返回指数(exponential)格式;具体规则是看哪种格式最合适。这个方法接收一个参数,即表示数值的所有数字的位数(不包括指数部分)。请看下面的例子。 ~~~ var num = 99; console.log(num.toPrecision(1)); // "1e+2" console.log(num.toPrecision(2)); // "99" console.log(num.toPrecision(3)); // "99.0" ~~~ 以上代码首先完成的任务是以一位数来表示 `99`,结果是 `"1e+2"`,即 `100`。因为一位数无法准确地表示 `99`,因此`toPrecision()` 就将它向上舍入为 `100`,这样就可以使用一位数来表示它了。而接下来的用两位数表示 `99`,当然还是`"99"`。最后,在想以三位数表示 `99` 时,`toPrecision()` 方法返回了 `"99.0"`。实际上,`toPrecision()` 会根据要处理的数值决定到底是调用 `toFixed()` 还是调用 `toExponential()`。而这三个方法都可以通过向上或向下舍入,做到以最准确的形式来表示带有正确小数位的值。 > `toPrecision()` 方法可以表现1到21位小数。但这只是标准实现的范围,有些浏览器也可能支持更多位数。 与 `Boolean` 对象类似,`Number` 对象也以后台方式为数值提供了重要的功能。但与此同时,我们仍然不建议直接实例化`Number` 类型,而原因与显式创建 `Boolean` 对象一样。具体来讲,就是在使用 `typeof` 和 `instanceof` 操作符测试基本类型数值与引用类型数值时,得到的结果完全不同,如下面的例子所示。 ~~~ var numberObject = new Number(10); var numberValue = 10; console.log(typeof numberObject); // "object" console.log(typeof numberValue); // "number" console.log(numberObject instanceof Number); // true console.log(numberValue instanceof Number); // false ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#string-类型)`String` 类型 `String` 类型是字符串的对象包装类型,可以像下面这样使用 `String` 构造函数来创建。 ~~~ var stringObject = new String("hello world"); ~~~ `String` 对象的方法也可以在所有基本的字符串值中访问到。其中,继承的 `valueOf()`、`toLocaleString()` 和 `toString()`方法,都返回对象所表示的基本字符串值。 `String` 类型的每个实例都有一个 `length` 属性,表示字符串中包含多个字符。来看下面的例子。 ~~~ var stringValue = "hello world"; console.log(stringValue.length); // 11 ~~~ 应该注意的是,即使字符串中包含双字节字符(不是占一个字节的 ASCII 字符),每个字符也仍然算一个字符。例如: ~~~ var stringValue = "大家好"; console.log(stringValue.length); // 3 ~~~ `String` 类型提供了很多方法,用于辅助完成对 JavaScript 中字符串的解析和操作。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#字符方法)字符方法 两个用于访问字符串中特定字符的方法是:`charAt()` 和 `charCodeAt()`。这两个方法都接收一个参数,即基于0的字符位置。其中,`charAt()` 方法以单字符字符串的形式返回给定位置的那个字符(JavaScript 中没有字符类型)。例如: ~~~ var stringValue = "hello world"; console.log(stringValue.charAt(1)); // "e" ~~~ 如果你想得到的不是字符而是字符编码,那么就要像下面这样使用 `charCodeAt()` 了。例如: ~~~ var stringValue = "hello world"; console.log(stringValue.charCodeAt(1)); // 101,101是小写字母"e"的字符编码 ~~~ ECMAScript 5还定义了另一个访问个别字符的方法。在支持浏览器中,可以使用方括号加数字索引来访问字符串中的特定字符,如下面的例子所示。 ~~~ var stringValue = "hello world"; console.log(stringValue[1]); // "e" ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#字符串操作方法)字符串操作方法 下面介绍与操作字符串有关的几个方法。第一个就是 `concat()`,用于将一或多个字符串拼接起来,返回拼接得到的新字符串。先来看一个例子。 ~~~ var stringValue = "hello "; var result = stringValue.concat("world"); console.log(result); // "hello world" console.log(stringValue); // "hello" ~~~ 实际上,`concat()` 方法可以接受任意多个参数,也就是说可以通过它拼接任意多个字符串。再看一个例子: ~~~ var stringValue = "hello "; var result = stringValue.concat("world", "!"); console.log(result); // "hello world!" console.log(stringValue); // "hello" ~~~ 虽然 `concat()` 是专门用来拼接字符串的方法,但实践中使用更多的还是加号操作符 `+` 。而且,使用加号操作符 `+` 在大多数情况下都比使用 `concat()`方法要简便易行(特别是在拼接多个字符串的情况下)。 JavaScript 还提供了三个基于子字符串创建新字符串的方法:`slice()`、`substr()` 和 `substring()`。这三个方法都会返回被操作字符串的一个子字符串,而且也都接受一或两个参数。第一个参数指定子字符串的开始位置,第二个参数(在指定的情况下)表示子字符串到哪里结束。具体来说,`slice()` 和 `substring()` 的第二个参数指定的是子字符串最后一个字符后面的位置。而 `substr()` 的第二个参数指定的则是返回的字符个数。如果没有给这些方法传递第二个参数,则将字符串的长度作为结束位置。与 `concat()` 方法一样,`slice()`、`substr()` 和 `substring()`也不会修改字符串本身的值,它们只是返回一个基本类型的字符串值,对原始字符串没有任何影响。请看下面的例子。 ~~~ var stringValue = "hello world"; console.log(stringValue.slice(3)); // "lo world" console.log(stringValue.substring(3)); // "lo world" console.log(stringValue.substr(3)); // "lo world" console.log(stringValue.slice(3, 7)); // "lo w" console.log(stringValue.substring(3,7)); // "lo w" console.log(stringValue.substr(3, 7)); // "lo worl" ~~~ 在传递给这些方法的参数是负值的情况下,它们的行为就不尽相同了。其中,`slice()` 方法会将传入的负值与字符串的长度相加,`substr()` 方法将负的第一个参数加上字符串的长度,而将负的第二个参数转换为0。最后,`substring()` 方法会把所有负值参数都转换为0。下面来看例子。 ~~~ var stringValue = "hello world"; console.log(stringValue.slice(-3)); // "rld" console.log(stringValue.substring(-3)); // "hello world" console.log(stringValue.substr(-3)); // "rld" console.log(stringValue.slice(3, -4)); // "lo w" console.log(stringValue.substring(3, -4)); // "hel" console.log(stringValue.substr(3, -4)); //""(空字符串) ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#字符串位置方法)字符串位置方法 有两个可以从字符串中查找子字符串的方法:`indexOf()` 和 `lastIndexOf()`。这两个方法都是从一个字符串中搜索给定的子字符串,然后返子字符串的位置(如果没有找到该子字符串,则返回-1)。这两个方法的区别在于:`indexOf()` 方法从字符串的开头向后搜索子字符串,而 `lastIndexOf()` 方法是从字符串的末尾向前搜索子字符串。还是来看一个例子吧。 ~~~ var stringValue = "hello world"; console.log(stringValue.indexOf("o")); // 4 console.log(stringValue.lastIndexOf("o")); // 7 ~~~ 这两个方法都可以接收可选的第二个参数,表示从字符串中的哪个位置开始搜索。换句话说,indexOf()会从该参数指定的位置向后搜索,忽略该位置之前的所有字符;而lastIndexOf()则会从指定的位置向前搜索,忽略该位置之后的所有字符。看下面的例子。 ~~~ var stringValue = "hello world"; console.log(stringValue.indexOf("o", 6)); // 7 console.log(stringValue.lastIndexOf("o", 6)); // 4 ~~~ 在使用第二个参数的情况下,可以通过循环调用 `indexOf()` 或 `lastIndexOf()` 来找到所有匹配的子字符串,如下面的例子所示: ~~~ var stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit"; var positions = new Array(); var pos = stringValue.indexOf("e"); while(pos > -1){ positions.push(pos); pos = stringValue.indexOf("e", pos + 1); } console.log(positions); // "3,24,32,35,52" ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#trim-方法)`trim()` 方法 ECMAScript 5为所有字符串定义了 `trim()` 方法。这个方法会创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果。例如: ~~~ var stringValue = " hello world "; var trimmedStringValue = stringValue.trim(); console.log(stringValue); // " hello world " console.log(trimmedStringValue); // "hello world" ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#字符串大小写转换方法)字符串大小写转换方法 JavaScript 中涉及字符串大小写转换的方法有4个:`toLowerCase()`、`toLocaleLowerCase()`、`toUpperCase()` 和`toLocaleUpperCase()`。其中,`toLowerCase()` 和 `toUpperCase()` 是两个经典的方法,借鉴自 `java.lang.String` 中的同名方法。而 `toLocaleLowerCase()` 和 `toLocaleUpperCase()` 方法则是针对特定地区的实现。对有些地区来说,针对地区的方法与其通用方法得到的结果相同,但少数语言(如土耳其语)会为 Unicode 大小写转换应用特殊的规则,这时候就必须使用针对地区的方法来保证实现正确的转换。以下是几个例子。 ~~~ var stringValue = "hello world"; console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD" console.log(stringValue.toUpperCase()); // "HELLO WORLD" console.log(stringValue.toLocaleLowerCase()); // "hello world" console.log(stringValue.toLowerCase()); // "hello world" ~~~ 一般来说,在不知道自己的代码将在哪种语言环境中运行的情况下,还是使用针对地区的方法更稳妥一些。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#字符串的模式匹配方法)字符串的模式匹配方法 `String` 类型定义了几个用于在字符串中匹配模式的方法。第一个方法就是 `match()`,在字符串上调用这个方法,本质上与调用 `RegExp` 的 `exec()` 方法相同。`match()` 方法只接受一个参数,要么是一个正则表达式,要么是一个 `RegExp` 对象。来看下面的例子。 ~~~ var text = "cat, bat, sat, fat"; var pattern = /.at/; // 与pattern.exec(text)相同 var matches = text.match(pattern); console.log(matches.index); // 0 console.log(matches[0]); // "cat" console.log(pattern.lastIndex); // 0 ~~~ 另一个用于查找模式的方法是 `search()`。这个方法的唯一参数与 `match()` 方法的参数相同:由字符串或 `RegExp` 对象指定的一个正则表达式。`search()` 方法返回字符串中第一个匹配项的索引;如果没有找到匹配项,则返回-1。而且,`search()`方法始终是从字符串开头向后查找模式。看下面的例子。 ~~~ var text = "cat, bat, sat, fat"; var pos = text.search(/at/); console.log(pos); // 1,即"at"第一次出现的位置 ~~~ 为了简化替换子字符串的操作,JavaScript 提供了 `replace()` 方法。这个方法接受两个参数:第一个参数可以是一个 `RegExp`对象或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数可以是一个字符串或者一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,唯一的办法就是提供一个正则表达式,而且要指定全局 `g`标志,如下所示。 ~~~ var text = "cat, bat, sat, fat"; var result = text.replace("at", "ond"); console.log(result); // "cond, bat, sat, fat" result = text.replace(/at/g, "ond"); console.log(result); // "cond, bond, sond, fond" ~~~ 最后一个与模式匹配有关的方法是 `split()`,这个方法可以基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中。分隔符可以是字符串,也可以是一个 `RegExp` 对象(这个方法不会将字符串看成正则表达式)。`split()` 方法可以接受可选的第二个参数,用于指定数组的大小,以便确保返回的数组不会超过既定大小。请看下面的例子。 ~~~ var colorText = "red,blue,green,yellow"; var colors1 = colorText.split(","); // ["red", "blue", "green", "yellow"] var colors2 = colorText.split(",", 2); // ["red", "blue"] ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#localecompare-方法)`localeCompare()` 方法 这个方法比较两个字符串,并返回下列值中的一个: * 如果字符串在字母表中应该排在字符串参数之前,则返回一个负数(大多数情况下是-1,具体的值要视实现而定); * 如果字符串等于字符串参数,则返回0; * 如果字符串在字母表中应该排在字符串参数之后,则返回一个正数(大多数情况下是1,具体的值同样要视实现而定)。 下面是几个例子。 ~~~ var stringValue = "yellow"; console.log(stringValue.localeCompare("brick")); // 1 console.log(stringValue.localeCompare("yellow")); // 0 console.log(stringValue.localeCompare("zoo")); // -1 ~~~ 这个例子比较了字符串 `"yellow"` 和另外几个值:`"brick"`、`"yellow"` 和 `"zoo"`。因为 `"brick"` 在字母表中排在`"yellow"` 之前,所以 `localeCompare()` 返回了1;而 `"yellow"` 等于 `"yellow"`,所以 `localeCompare()` 返回了0;最后,`"zoo"` 在字母表中排在 `"yellow"` 后面,所以 `localeCompare()` 返回了-1。再强调一次,因为 `localeCompare()` 返回的数值取决于实现,所以最好是像下面例子所示的这样使用这个方法。 ~~~ function determineOrder(value) { var result = stringValue.localeCompare(value); if (result < 0){ console.log("The string 'yellow' comes before the string '" + value + "'."); } else if (result > 0) { console.log("The string 'yellow' comes after the string '" + value + "'."); } else { console.log("The string 'yellow' is equal to the string '" + value + "'."); } } determineOrder("brick"); determineOrder("yellow"); determineOrder("zoo"); ~~~ 使用这种结构,就可以确保自己的代码在任何实现中都可以正确地运行了。 `localeCompare()` 方法比较与众不同的地方,就是实现所支持的地区(国家和语言)决定了这个方法的行为。比如,美国以英语作为 JavaScript 实现的标准语言,因此 `localeCompare()` 就是区分大小写的,于是大写字母在字母表中排在小写字母前头就成为了一项决定性的比较规则。不过,在其他地区恐怕就不是这种情况了。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.11-PrimitiveWrapperObjects#fromcharcode-方法)`fromCharCode()` 方法 另外,`String` 构造函数本身还有一个静态方法:`fromCharCode()`。这个方法的任务是接收一或多个字符编码,然后将它们转换成一个字符串。从本质上来看,这个方法与实例方法 `charCodeAt()` 执行的是相反的操作。来看一个例子: ~~~ console.log(String.fromCharCode(104, 101, 108, 108, 111)); // "hello" var s = 'hello'; for(let i=0;i ';

正则表达式

最后更新于:2022-04-01 21:00:48

# 正则表达式 > 由于本课程的核心是 JavaScript,所以本文着重讲解了「正则表达式」在 JavaScript 中的用法,并未深入「正则表达式」的具体细节。如果您尚不了解「正则表达式」,强烈推荐您先学习「正则表达式30分钟入门教程[http://deerchao.net/tutorials/regex/regex.htm」之后,再进行本课程的学习。](http://deerchao.net/tutorials/regex/regex.htm%E3%80%8D%E4%B9%8B%E5%90%8E%EF%BC%8C%E5%86%8D%E8%BF%9B%E8%A1%8C%E6%9C%AC%E8%AF%BE%E7%A8%8B%E7%9A%84%E5%AD%A6%E4%B9%A0%E3%80%82) 正则表达式(regular expression)是一个描述字符模式的对象,使用正则表达式可以进行强大的模式匹配和文本检索与替换功能。JavaScript 的正则表达式语法是 Perl5 的正则表达式语法的大型子集,所以对于有 Perl 编程经验的程序员来说,学习 JavaScript 中的正则表达式是小菜一碟。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.10-RegExpObjects#正则表达式的定义)正则表达式的定义 JavaScript 中的正则表达式用 `RegExp` 对象表示,可以使用 `RegExp()` 构造函数来创建 `RegExp` 对象,不过 `RegExp` 对象更多是通过字面量的语法来创建,使用下面类似 Perl 的语法,就可以创建一个正则表达式。例如: ~~~ // 推荐写法 var expression = / pattern / flags ; // 不推荐写法 var expression = new RegExp(pattern, flags); ~~~ 其中的模式(pattern)部分,是包含在一对斜杠 `/` 之间的字符,可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标志(flags),用以标明正则表达式的行为。正则表达式的匹配模式支持下列3个标志。 * `g`:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止; * `i`:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写; * `m`:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。 因此,一个正则表达式就是一个模式与上述3个标志的组合体。不同组合产生不同结果,如下面的例子所示。 ~~~ // 匹配字符串中所有"at"的实例 var pattern1 = /at/g; // 匹配第一个"bat"或"cat",不区分大小写 var pattern2 = /[bc]at/i; // 匹配所有以"at"结尾的3个字符的组合,不区分大小写 var pattern3 = /.at/gi; ~~~ 与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括: ~~~ ( [ { \ ^ $ | ) ? * + . ] } ~~~ 这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义。下面给出几个例子。 ~~~ // 匹配第一个"bat"或"cat",不区分大小写 var pattern1 = /[bc]at/i; // 匹配第一个" [bc]at",不区分大小写 var pattern2 = /\[bc\]at/i; // 匹配所有以"at"结尾的3个字符的组合,不区分大小写 var pattern3 = /.at/gi; // 匹配所有".at",不区分大小写 var pattern4 = /\.at/gi; ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.10-RegExpObjects#regexp-实例属性)`RegExp` 实例属性 `RegExp` 的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息。 * `global`:布尔值,表示是否设置了 `g` 标志。 * `ignoreCase`:布尔值,表示是否设置了 `i` 标志。 * `lastIndex`:整数,表示开始搜索下一个匹配项的字符位置,从0算起。 * `multiline`:布尔值,表示是否设置了 `m` 标志。 * `source`:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。 通过这些属性可以获知一个正则表达式的各方面信息,但却没有多大用处,因为这些信息全都包含在模式声明中。例如: ~~~ var pattern1 = /\[bc\]at/i; console.log(pattern1.global); // false console.log(pattern1.ignoreCase); // true console.log(pattern1.multiline); // false console.log(pattern1.lastIndex); // 0 console.log(pattern1.source); // "\[bc\]at" var pattern2 = new RegExp("\\[bc\\]at", "i"); console.log(pattern2.global); // false console.log(pattern2.ignoreCase); // true console.log(pattern2.multiline); // false console.log(pattern2.lastIndex); // 0 console.log(pattern2.source); // "\[bc\]at" ~~~ 我们注意到,尽管第一个模式使用的是字面量,第二个模式使用了 `RegExp` 构造函数,但它们的 `source` 属性是相同的。可见,`source` 属性保存的是规范形式的字符串,即字面量形式所用的字符串。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.10-RegExpObjects#regexp-实例方法)`RegExp` 实例方法 `RegExp` 对象的主要方法是 `exec()`,该方法是专门为捕获组而设计的。`exec()` 接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回 `null`。返回的数组虽然是 `Array` 的实例,但包含两个额外的属性:`index` 和 `input`。其中,`index` 表示匹配项在字符串中的位置,而 `input` 表示应用正则表达式的字符串。在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。请看下面的例子。 ~~~ var text = "mom and dad and baby"; var pattern = /mom( and dad( and baby)?)?/gi; var matches = pattern.exec(text); console.log(matches.index); // 0 console.log(matches.input); // "mom and dad and baby" console.log(matches[0]); // "mom and dad and baby" console.log(matches[1]); // " and dad and baby" console.log(matches[2]); // " and baby" ~~~ 这个例子中的模式包含两个捕获组。最内部的捕获组匹配 `"and baby"`,而包含它的捕获组匹配 `"and dad"` 或者 `"and dad and baby"`。当把字符串传入 `exec()` 方法中之后,发现了一个匹配项。因为整个字符串本身与模式匹配,所以返回的数组`matchs` 的 `index` 属性值为 `0`。数组中的第一项是匹配的整个字符串,第二项包含与第一个捕获组匹配的内容,第三项包含与第二个捕获组匹配的内容。 对于 `exec()` 方法而言,即使在模式中设置了全局标志 `g`,它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用 `exec()` 将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用 `exec()` 则都会在字符串中继续查找新匹配项,如下面的例子所示。 ~~~ var text = "cat, bat, sat, fat"; var pattern1 = /.at/; // 非全局模式,第一次匹配 var matches = pattern1.exec(text); console.log(matches.index); // 0 console.log(matches[0]); // cat console.log(pattern1.lastIndex); // 0 // 非全局模式,第二次匹配 matches = pattern1.exec(text); console.log(matches.index); // 0 console.log(matches[0]); // cat console.log(pattern1.lastIndex); // 0 var pattern2 = /.at/g; // 全局模式,第一次匹配 var matches = pattern2.exec(text); console.log(matches.index); // 0 console.log(matches[0]); // cat console.log(pattern2.lastIndex); // 0 // 全局模式,第二次匹配 matches = pattern2.exec(text); console.log(matches.index); // 5 console.log(matches[0]); // bat console.log(pattern2.lastIndex); // 8 ~~~ 这个例子中的第一个模式 `pattern1` 不是全局模式,因此每次调用 `exec()` 返回的都是第一个匹配项 `"cat"`。而第二个模式`pattern2` 是全局模式,因此每次调用 `exec()` 都会返回字符串中的下一个匹配项,直至搜索到字符串末尾为止。此外,还应该注意模式的 `lastIndex` 属性的变化情况。在全局匹配模式下,`lastIndex` 的值在每次调用 `exec()` 后都会增加,而在非全局模式下则始终保持不变。 > IE 的 JavaScript 实现在 `lastIndex` 属性上存在偏差,即使在非全局模式下,`lastIndex` 属性每次也会变化。 正则表达式的第二个方法是 `test()`,它接受一个字符串参数。在模式与该参数匹配的情况下返回 `true`;否则,返回`false`。在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的情况下,使用这个方法非常方便。因此,`test()` 方法经常被用在 `if` 语句中,如下面的例子所示。 ~~~ var text = "000-00-0000"; var pattern = /\d{3}-\d{2}-\d{4}/; if (pattern.test(text)){ console.log("The pattern was matched."); } ~~~ 在这个例子中,我们使用正则表达式来测试了一个数字序列。如果输入的文本与模式匹配,则显示一条消息。这种用法经常出现在验证用户输入的情况下,因为我们只想知道输入是不是有效,至于它为什么无效就无关紧要了。 `RegExp` 实例继承的 `toLocaleString()` 和 `toString()` 方法都会返回正则表达式的字面量,与创建正则表达式的方式无关。例如: ~~~ var pattern = new RegExp("\\[bc\\]at", "gi"); console.log(pattern.toString()); // /\[bc\]at/gi console.log(pattern.toLocaleString()); // /\[bc\]at/gi ~~~ 即使上例中的模式是通过调用 `RegExp` 构造函数创建的,但 `toLocaleString()` 和 `toString()` 方法仍然会像它是以字面量形式创建的一样显示其字符串表示。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.10-RegExpObjects#regexp-构造函数属性)`RegExp` 构造函数属性 `RegExp` 构造函数包含一些属性(这些属性在其他语言中被看成是静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名(Opera是例外,它不支持短属性名)。下表列出了RegExp构造函数的属性。 | 长属性名 | 短属性名 | 说明 | | --- | --- | --- | | input | $_ | 最近一次要匹配的字符串。Opera未实现此属性。 | | lastMatch | $& | 最近一次的匹配项。Opera未实现此属性。 | | lastParen | $+ | 最近一次匹配的捕获组。Opera未实现此属性。 | | leftContext | $` | input字符串中lastMatch之前的文本。 | | multiline | $* | 布尔值,表示是否所有表达式都使用多行模式。IE和Opera未实现此属性。 | | rightContext | $' | Input字符串中lastMatch之后的文本。 | 使用这些属性可以从 `exec()` 或 `test()` 执行的操作中提取出更具体的信息。请看下面的例子。 ~~~ var text = "this has been a short summer"; var pattern = /(.)hort/g; /* * 注意:Internet Explorer 不支持 multiline 属性 * Opera 不支持 input、lastMatch、lastParen 和 multiline 属性 */ if (pattern.test(text)){ console.log(RegExp.input); // this has been a short summer console.log(RegExp.leftContext); // this has been a console.log(RegExp.rightContext); // summer console.log(RegExp.lastMatch); // short console.log(RegExp.lastParen); // s console.log(RegExp.multiline); // false } ~~~ 如前所述,例子使用的长属性名都可以用相应的短属性名来代替。只不过,由于这些短属性名大都不是有效的 JavaScript 标识符,因此必须通过方括号语法来访问它们,如下所示。 ~~~ var text = "this has been a short summer"; var pattern = /(.)hort/g; /* * 注意:Internet Explorer 不支持 multiline 属性 * Opera 不支持 input、lastMatch、lastParen 和 multiline 属性 */ if (pattern.test(text)){ console.log(RegExp.$_); // this has been a short summer console.log(RegExp["$`"]); // this has been a console.log(RegExp["$'"]); // summer console.log(RegExp["$&"]); // short console.log(RegExp["$+"]); // s console.log(RegExp["$*"]); // false } ~~~ 除了上面介绍的几个属性之外,还有多达9个用于存储捕获组的构造函数属性。访问这些属性的语法是`RegExp.$1`、`RegExp.$2`...`RegExp.$9`,分别用于存储第一、第二...第九个匹配的捕获组。在调用 `exec()` 或 `test()` 方法时,这些属性会被自动填充。然后,我们就可以像下面这样来使用它们。 ~~~ var text = "this has been a short summer"; var pattern = /(..)or(.)/g; if (pattern.test(text)){ console.log(RegExp.$1); // sh console.log(RegExp.$2); // t } ~~~ 这里创建了一个包含两个捕获组的模式,并用该模式测试了一个字符串。即使 `test()` 方法只返回一个布尔值,但 `RegExp` 构造函数的属性 `$1` 和 `$2` 也会被匹配相应捕获组的字符串自动填充。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.10-RegExpObjects#模式的局限性)模式的局限性 尽管 JavaScript 中的正则表达式功能还是比较完备的,但仍然缺少某些语言(特别是 Perl)所支持的高级正则表达式特性。下面列出了 JavaScript 正则表达式所不支持的特性。 * 匹配字符串开始和结尾的\A和\Z锚 * 向后查找(lookbehind) * 并集和交集类 * 原子组(atomic grouping) * Unicode支持(单个字符除外,如\uFFFF) * 命名的捕获组 * s(single,单行)和x(free-spacing,无间隔)匹配模式 * 条件匹配 * 正则表达式注释 即使存在这些限制,JavaScript 正则表达式仍然是非常强大的,能够帮我们完成绝大多数模式匹配任务。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.10-RegExpObjects#关卡)关卡 按要求完成下列常用的正则表达式。 ~~~ // 挑战一:数字 var pattern1 = null; // 补全该正则表达式 console.log(pattern1.test('123')); // true console.log(pattern1.test('abc')); // false ~~~ ~~~ // 挑战二:3位的数字 var pattern2 = null; // 补全该正则表达式 console.log(pattern2.test('123')); // true console.log(pattern2.test('1234')); // false ~~~ ~~~ // 挑战三:至少3位的数字 var pattern3 = null; // 补全该正则表达式 console.log(pattern3.test('1234')); // true console.log(pattern3.test('12')); // false ~~~ ~~~ // 挑战四:3-5位的数字 var pattern4 = null; // 补全该正则表达式 console.log(pattern4.test('1234')); // true console.log(pattern4.test('1')); // false ~~~ ~~~ // 挑战五:由26个英文字母组成的字符串 var pattern5 = null; // 补全该正则表达式 console.log(pattern5.test('abc')); // true console.log(pattern5.test('1abc')); // false ~~~ ~~~ // 挑战六:由数字和26个英文字母组成的字符串 var pattern6 = null; // 补全该正则表达式 console.log(pattern6.test('1abc')); // true console.log(pattern6.test('_abc')); // false ~~~ ~~~ // 挑战七:日期格式:年-月-日 var pattern7 = null; // 补全该正则表达式 console.log(pattern7.test('2016-08-20')); // true console.log(pattern7.test('2016/08/20')); // false ~~~ ~~~ // 挑战八:时间格式:小时:分钟, 24小时制 var pattern8 = null; // 补全该正则表达式 console.log(pattern8.test('13:45')); // true console.log(pattern8.test('13点45')); // false ~~~ ~~~ // 挑战九:中国大陆身份证号,15位或18位 var pattern9 = null; // 补全该正则表达式 console.log(pattern9.test('4223222199901090033')); // true console.log(pattern9.test('asdfasdfasfasdf1234')); // false ~~~
';

函数

最后更新于:2022-04-01 21:00:45

# 函数 函数是一段代码,它只定义一次,但可以被执行或调用任意次。在 JavaScript 里,函数即对象,程序可以随意操控它们。比如,可以把函数赋值给变量,或者作为参数传递给其他函数,也可以给它们设置属性,甚至调用它们的方法。如果函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。如果函数嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#函数定义)函数定义 在 JavaScript 中,函数实际上是对象,每个函数都是 `Function` 构造函数的实例,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。函数通常有以下3中定义方式。例如: ~~~ // 写法一:函数声明(推荐写法) function sum (num1, num2) { return num1 + num2; } // 写法二:函数表达式(推荐写法) var sum = function(num1, num2){ return num1 + num2; }; // 写法三:Function 构造函数(不推荐写法) var sum = new Function("num1", "num2", "return num1 + num2"); ~~~ 由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字。例如: ~~~ function sum(num1, num2){ return num1 + num2; } console.log(sum(10,10)); // 20 var anotherSum = sum; console.log(anotherSum(10,10)); // 20 sum = null; console.log(anotherSum(10,10)); // 20 ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#没有重载)没有重载 将函数名想象为指针,也有助于理解为什么 JavaScript 中没有函数重载的概念。 ~~~ function addSomeNumber(num){ return num + 100; } function addSomeNumber(num) { return num + 200; } var result = addSomeNumber(100); // 300 ~~~ 显然,这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。以上代码实际上与下面的代码没有什么区别。 ~~~ var addSomeNumber = function (num){ return num + 100; }; addSomeNumber = function (num) { return num + 200; }; var result = addSomeNumber(100); // 300 ~~~ 通过重写代码之后可以很容易明白,在创建第二个函数时,实际上覆盖了引用第一个函数的变量 `addSomeNumber`。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#函数声明与函数表达式)函数声明与函数表达式 解析器在向执行环境中加载数据时,对「函数声明」和「函数表达式」并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。例如: ~~~ console.log(sum(10,10)); // 20 function sum(num1, num2){ return num1 + num2; } ~~~ 以上代码完全可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript 引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,JavaScript 引擎也能把函数声明提升到顶部。把上面的「函数声明」改为等价的「函数表达式」,就会在执行期间导致错误。例如: ~~~ console.log(sum(10,10)); // Uncaught TypeError: sum is not a function var sum = function(num1, num2){ return num1 + num2; }; ~~~ 除了上述区别之外,「函数声明」与「函数表达式」的语法是等价的。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#作为值的函数)作为值的函数 因为 JavaScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。来看一看下面的函数。 ~~~ function callSomeFunction(someFunction, someArgument){ return someFunction(someArgument); } ~~~ 这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是要传递给该函数的一个值。然后,就可以像下面的例子一样传递函数了。 ~~~ function add10(num){ return num + 10; } var result1 = callSomeFunction(add10, 10); console.log(result1); // 20 function getGreeting(name){ return "Hello, " + name; } var result2 = callSomeFunction(getGreeting, "Nicholas"); console.log(result2); // "Hello, Nicholas" ~~~ 这里的 `callSomeFunction()` 函数是通用的,即无论第一个参数中传递进来的是什么函数,它都会返回执行第一个参数后的结果。要访问函数的指针而不执行函数的话,必须去掉函数名后面的那对圆括号。因此上面例子中传递给 `callSomeFunction()`的是 `add10` 和 `getGreeting`,而不是执行它们之后的结果。 当然,还可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。例如,假设有一个对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组 `sort()` 方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数,下面就是这个函数的定义。 ~~~ function createComparisonFunction(propertyName) { return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } }; } ~~~ 这个函数定义看起来有点复杂,但实际上无非就是在一个函数中嵌套了另一个函数,而且内部函数前面加了一个 `return` 操作符。在内部函数接收到 `propertyName` 参数后,它会使用方括号表示法来取得给定属性的值。取得了想要的属性值之后,定义比较函数就非常简单了。上面这个函数可以像在下面例子中这样使用。 ~~~ var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}]; data.sort(createComparisonFunction("name")); console.log(data[0].name); // Nicholas data.sort(createComparisonFunction("age")); console.log(data[0].name); // Zachary ~~~ 这里,我们创建了一个包含两个对象的数组 `data`。其中,每个对象都包含一个 `name` 属性和一个 `age` 属性。在默认情况下,`sort()` 方法会调用每个对象的 `toString()` 方法以确定它们的次序;但得到的结果往往并不符合人类的思维习惯。因此,我们调用 `createComparisonFunction("name")` 方法创建了一个比较函数,以便按照每个对象的 `name` 属性值进行排序。而结果排在前面的第一项是 `name` 为 `"Nicholas"`,`age` 是 `29` 的对象。然后,我们又使用了`createComparisonFunction("age")` 返回的比较函数,这次是按照对象的age属性排序。得到的结果是 `name` 值为`"Zachary"`,`age` 值是 `28` 的对象排在了第一位。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#函数的形参和实参)函数的形参和实参 在函数内部,有两个特殊的对象:`arguments` 和 `this`。其中,`arguments` 是一个类数组对象,包含着传入函数中的所有参数。虽然 `arguments` 的主要用途是保存函数参数,但这个对象还有一个名叫 `callee` 的属性,该属性是一个指针,指向拥有这个 `arguments` 对象的函数。请看下面这个非常经典的阶乘函数。 ~~~ function factorial(num){ if (num <= 1) { return 1; } else { return num * factorial(num-1) } } ~~~ 定义阶乘函数一般都要用到递归算法,如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 `factorial` 紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用`arguments.callee`。 ~~~ function factorial(num){ if (num <=1) { return 1; } else { return num * arguments.callee(num-1) } } ~~~ 在这个重写后的 `factorial()` 函数的函数体内,没有再引用函数名 `factorial`。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。例如: ~~~ var trueFactorial = factorial; factorial = function(){ return 0; }; console.log(trueFactorial(5)); // 120 console.log(factorial(5)); // 0 ~~~ 在此,变量 `trueFactorial` 获得了 `factorial` 的值,实际上是在另一个位置上保存了一个函数的指针。然后,我们又将一个简单地返回 `0` 的函数赋值给 `factorial` 变量。如果像原来的 `factorial()` 那样不使用 `arguments.callee`,调用`trueFactorial()` 就会返回 `0`。可是,在解除了函数体内的代码与函数名的耦合状态之后,`trueFactorial()` 仍然能够正常地计算阶乘;至于 `factorial()`,它现在只是一个返回 `0` 的函数。 函数内部的另一个特殊对象是 `this`,其行为与 Java 和 C# 中的 `this` 大致类似。换句话说,`this` 引用的是函数据以执行的环境对象(当在网页的全局作用域中调用函数时,`this` 对象引用的就是 `window`)。来看下面的例子。 ~~~ window.color = "red"; var o = { color: "blue" }; function sayColor(){ console.log(this.color); } sayColor(); // "red" o.sayColor = sayColor; o.sayColor(); // "blue" ~~~ 上面这个函数 `sayColor()` 是在全局作用域中定义的,它引用了 `this` 对象。由于在调用函数之前,`this` 的值并不确定,因此 `this` 可能会在代码执行过程中引用不同的对象。当在全局作用域中调用 `sayColor()` 时,`this` 引用的是全局对象`window`;换句话说,对 `this.color` 求值会转换成对 `window.color` 求值,于是结果就返回了 `"red"`。而当把这个函数赋给对象 `o` 并调用 `o.sayColor()` 时,`this` 引用的是对象 `o`,因此对 `this.color` 求值会转换成对 `o.color` 求值,结果就返回了 `"blue"`。 请大家一定要牢记,函数的名字仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的 `sayColor()` 函数与 `o.sayColor()` 指向的仍然是同一个函数。 ECMAScript 5也规范化了另一个函数对象的属性 `caller`。这个属性中保存着「调用当前函数的函数的引用」,如果是在全局作用域中调用当前函数,它的值为 `null`。例如: ~~~ function outer(){ inner(); } function inner(){ console.log(arguments.callee.caller); } outer(); ~~~ 以上代码会导致警告框中显示 `outer()` 函数的源代码。因为 `outer()` 调用了 `inter()`,所以 `arguments.callee.caller` 就指向 `outer()`。 在严格模式下,访问 `arguments.callee`属性,或为函数的 `caller` 属性赋值,都会导致错误。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#函数的属性和方法)函数的属性和方法 JavaScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:`length` 和 `prototype`。其中,`length`属性表示函数希望接收的命名参数的个数,如下面的例子所示。 ~~~ function sayName(name){ console.log(name); } function sum(num1, num2){ return num1 + num2; } function sayHi(){ console.log("hi"); } console.log(sayName.length); // 1 console.log(sum.length); // 2 console.log(sayHi.length); // 0 ~~~ 对于 JavaScript 中的引用类型而言,`prototype` 是保存它们所有实例方法的真正所在。换句话说,诸如 `toString()` 和`valueOf()` 等方法实际上都保存在 `prototype` 名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,`prototype` 属性的作用是极为重要的。在 ECMAScript 5中,`prototype` 属性是不可枚举的,因此使用 `for/in` 无法发现。 每个函数都包含两个非继承而来的方法:`apply()` 和 `call()`。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 `this` 对象的值。首先,`apply()` 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 `Array` 的实例,也可以是 `arguments` 对象。例如: ~~~ function sum(num1, num2){ return num1 + num2; } function callSum1(num1, num2){ return sum.apply(this, arguments); // 传入 arguments 对象 } function callSum2(num1, num2){ return sum.apply(this, [num1, num2]); // 传入数组 } console.log(callSum1(10,10)); // 20 console.log(callSum2(10,10)); // 20 ~~~ 在上面这个例子中,`callSum1()` 在执行 `sum()` 函数时传入了 `this`(因为是在全局作用域中调用的,所以传入的就是`window` 对象)和 `arguments` 对象。而 `callSum2` 同样也调用了 `sum()` 函数,但它传入的则是 `this` 和一个参数数组。这两个函数都会正常执行并返回正确的结果。 `call()` 方法与 `apply()` 方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 `call()` 方法而言,第一个参数是`this` 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用 `call()` 方法时,传递给函数的参数必须逐个列举出来,如下面的例子所示。 ~~~ function sum(num1, num2){ return num1 + num2; } function callSum(num1, num2){ return sum.call(this, num1, num2); } console.log(callSum(10,10)); // 20 ~~~ 在使用 `call()` 方法的情况下,`callSum()` 必须明确地传入每一个参数。结果与使用 `apply()` 没有什么不同。至于是使用`apply()` 还是 `call()`,完全取决于你采取哪种给函数传递参数的方式最方便。如果你打算直接传入 `arguments` 对象,或者包含函数中先接收到的也是一个数组,那么使用 `apply()` 肯定更方便;否则,选择 `call()` 可能更合适。(在不给函数传递参数的情况下,使用哪个方法都无所谓。) 事实上,传递参数并非 `apply()` 和 `call()` 真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。下面来看一个例子。 ~~~ window.color = "red"; var o = { color: "blue" }; function sayColor(){ console.log(this.color); } sayColor(); // red sayColor.call(this); // red sayColor.call(window); // red sayColor.call(o); // blue ~~~ 这个例子是在前面说明 `this` 对象的示例基础上修改而成的。这一次,`sayColor()` 也是作为全局函数定义的,而且当在全局作用域中调用它时,它确实会显示 `"red"`,因为对 `this.color` 的求值会转换成对 `window.color` 的求值。而`sayColor.call(this)` 和 `sayColor.call(window)`,则是两种显式地在全局作用域中调用函数的方式,结果当然都会显示`"red"`。但是,当运行 `sayColor.call(o)` 时,函数的执行环境就不一样了,因为此时函数体内的 `this` 对象指向了 `o`,于是结果显示的是 `"blue"`。 使用 `call()` 或 `apply()` 来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。在前面例子的第一个版本中,我们是先将 `sayColor()` 函数放到了对象 `o` 中,然后再通过 `o` 来调用它的;而在这里重写的例子中,就不需要先前那个多余的步骤了。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.9-FunctionObjects#关卡)关卡 ~~~ // 挑战一,合并任意个数的字符串 var concat = function(){ // 待实现方法体 } console.log(concat('st','on','e')); // stone ~~~ ~~~ // 挑战二,输出指定位置的斐波那契数列 var fioacciSequece = function(count){ // 待实现方法体 } console.log(fioacciSequece(12)); // 0、1、1、2、3、5、8、13、21、34、55、[89] ~~~ ~~~ // 挑战三,三维数组或 n 维数组去重,使用 arguments 重写 var arr = [2,3,4,[2,3,[2,3,4,2],5],3,5,[2,3,[2,3,4,2],2],4,3,6,2]; var unique = function(arr){ // 待实现方法体 } console.log(unique(arr)); // [2,3,4,5,6] ~~~
';

数组

最后更新于:2022-04-01 21:00:43

# 数组 数组是值的有序集合。每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。 JavaScript 数组是无类型的,数组元素可以是任意类型,并且同一个数组中的不同元素也可能有不同的类型。数组的元素甚至也可能是对象或其他数组。 JavaScript数组是动态的,根据需要它们会增长或缩减,并且在创建数组时无须声明一个固定的大小或者在数组大小变化时无须重新分配空间。 JavaScript 数组可能是稀疏的,数组元素的索引不一定要连续的,它们之间可以有空缺。每个JavaScript数组都有一个length属性。针对非稀疏数组,该属性就是数组元素的个数。针对稀疏数组,length比所有元素的索引要大。 JavaScript 数组是 JavaScript 对象的特殊形式,数组索引实际上和碰巧是整数的属性名差不多。通常,数组的实现是经过优化的,用数字索引来访问数组元素一般来说比访问常规的对象属性要快很多。 数组继承自 `Array.prototype` 中的属性,它定义了一套丰富的数组操作方法。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#创建数组)创建数组 可以使用数组字面量和 `new` 关键字来创建数组。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#使用数组字面量创建数组推荐)使用数组字面量创建数组(推荐) ~~~ var empty = []; // 没有元素的数组 var primes = [2, 3, 5, 7, 11]; // 有5个数值的数组 var misc = [1.1, true, "a"]; // 3个不同类型的元素 // 数组直接量中的值不一定要是常量,可以是任意的表达式 var base = 1024; var table = [base, base+1, base+2, base+3]; // 也可以包含对象直接量或其他数组直接量 var b = [[1, {x:1, y:2}], [2, {x:3, y:4}]]; ~~~ 注意,不要忽略数组字面量的最后一个元素,仅以逗号结尾。下面几个案例,在不同的浏览器下,可能会被识别成2个元素,也有可能识别成3个元素,而造成程序bug。例如: ~~~ var nums = [,,,]; // 不好的写法 var names = ["stone",,]; // 不好的写法 var colors = ["red","green",]; // 不好的写法 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#使用-new-关键字创建数组)使用 `new` 关键字创建数组 使用 `new` 关键字调用构造函数 `Array()` 是创建数组的另一种方法,可以用三种方式调用构造函数。例如: ~~~ // 调用时没有参数 var a = new Array(); // 调用时有一个数值参数,它指定长度 var a = new Array(10); // 显式指定多个数组元素或者数组的一个非数值元素 var a = new Array(5, 4, 3, 2, 1, "testing"); ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#数组元素的读和写)数组元素的读和写 使用 `[]` 操作符来访问数组中的一个元素。数组的引用位于方括号的左边。方括号中是一个返回非负整数值的任意表达式。使用该语法既可以读又可以写数组的一个元素。例如: ~~~ var a = ["world"]; // 从一个元素的数组开始 var value = a[0]; // 读第0个元素 a[1] = 3.14; // 写第1个元素 var i = 2; a[i] = 3; // 写第2个元素 a[i + 1] = "hello"; // 写第3个元素 a[a[i]] = a[0]; // 读第0个和第2个元素,写第3个元素 ~~~ 请记住,数组是对象的特殊形式,可以为其创建任意名字的属性。但如果使用的属性是数组的索引,数组的特殊行为就是将根据需要更新它们的length属性值。 注意,可以使用负数或非整数来索引数组。这种情况下,数值转换为字符串,字符串作为属性名来用。既然名字不是非负整数,它就只能当做常规的对象属性,而非数组的索引。同样,如果凑巧使用了是非负整数的字符串,它就当做数组索引,而非对象属性。当使用的一个浮点数和一个整数相等时情况也是一样的。例如: ~~~ a[-1.23] = true; // 这将创建一个名为"-1.23"的属性 a["1000"] = 0; // 这是数组的第1001个元素 a[1.000] // 和 a[1] 相等 ~~~ 事实上数组索引仅仅是对象属性名的一种特殊类型,这意味着 JavaScript 数组没有「越界」错误的概念。当试图查询任何对象中不存在的属性时,不会报错,只会得到 `undefined` 值。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#稀疏数组)稀疏数组 稀疏数组就是包含从0开始的不连续索引的数组。通常,数组的 `length` 属性值代表数组中元素的个数。如果数组是稀疏的,`length` 属性值大于元素的个数。可以用 `Array()` 构造函数或简单地指定数组的索引值大于当前的数组长度来创建稀疏数组。 ~~~ a = new Array(5); // 数组没有元素,但是 a.length = 5 a = []; // 创建一个空数组,a.length = 0 a[1000] = 0; // 添加一个元素,a.length 被自动更新为1001 ~~~ 足够稀疏的数组通常在实现上比稠密的数组更慢、内存利用率更高,在这样的数组中查找元素的时间与常规对象属性的查找时间一样长。 需要注意的是,当省略数组直接量中的值时(使用连续的逗号,比如 `[1,,3]` ),这时所得到的数组也是稀疏数组,省略掉的值是不存在的: ~~~ var a1 = [,'1','2']; // 此数组长度是3 var a2 = [undefined]; // 此数组包含一个值为 undefined 的元素 console.log(0 in a1); // false,a1 在索引0处没有元素 console.log(0 in a2); // true,a2 在索引0处有一个值为 undefined 的元素 ~~~ 了解稀疏数组是了解 JavaScript 数组的真实本质的一部分。尽管如此,实际上你所碰到的绝大多数 JavaScript 数组不是稀疏数组。并且,如果你确实碰到了稀疏数组,你的代码很可能像对待非稀疏数组一样来对待它们,只不过它们包含一些 `undefined`值。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#数组长度)数组长度 每个数组有一个 `length` 属性,就是这个属性使其区别于常规的 JavaScript 对象。针对稠密(也就是非稀疏)数组,`length`属性值代表数组中元素的个数。其值比数组中最大的索引大1。例如: ~~~ [].length // 0,数组没有元素 ['a','b','c'].length // 3,最大的索引为2,length 为3 ~~~ 当数组是稀疏的时,`length` 属性值大于元素的个数。而且关于此我们可以说的一切也就是数组长度保证大于它每个元素的索引值。或者,换一种说法,在数组中(无论稀疏与否)肯定找不到一个元素的索引值大于或等于它的长度。为了维持此规则不变化,数组有两个特殊的行为。 * 第一个如同上面的描述:如果为一个数组元素赋值,它的索引 `i` 大于或等于现有数组的长度时,`length` 属性的值将设置为 `i+1`。 * 第二个特殊的行为就是设置 `length` 属性为一个小于当前长度的非负整数 `n` 时,当前数组中那些索引值大于或等于 `n` 的元素将从中删除。例如: ~~~ a = [1,2,3,4,5]; // 从5个元素的数组开始 a.length = 3; // 现在 a 为[1,2,3] a.length = 0; // 删除所有的元素。a 为[ ] a.length = 5; // 长度为5,但是没有元素,就像 new Array(5) ~~~ 还可以将数组的 `length` 属性值设置为大于其当前的长度。实际上这不会向数组中添加新的元素,它只是在数组尾部创建一个空的区域。 在 ECMAScript 5中,可以用 `Object.defineProperty()` 让数组的 `length` 属性变成只读的。例如: ~~~ a = [1,2,3]; // 从3个元素的数组开始 Object.defineProperty(a, "length", {writable: false}); // 让 length 属性只读 a.length = 0; // a 不会改变 ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#数组元素的添加和删除)数组元素的添加和删除 我们已经见过添加数组元素最简单的方法,为新索引赋值。例如: ~~~ a = [] // 开始是一个空数组 a[0] = "zero"; // 然后向其中添加元素 a[1] = "one"; ~~~ 也可以使用 `push()` 方法在数组末尾增加一个或多个元素。例如: ~~~ a = []; // 开始是一个空数组 a.push("zero"); // 在末尾添加一个元素。a = ["zero"] a.push("one", "two"); // 再添加两个元素。a = ["zero", "one", "two"] ~~~ 可以像删除对象属性一样使用 `delete` 运算符来删除数组元素。例如: ~~~ a = [1,2,3]; delete a[1]; // a在索引1的位置不再有元素 1 in a // => false: 数组索引1并未在数组中定义 a.length // => 3: delete操作并不影响数组长度 ~~~ 注意,对一个数组元素使用 `delete` 不会修改数组的 `length` 属性,也不会将元素从高索引处移下来填充已删除属性留下的空白。如果从数组中删除一个元素,它就变成稀疏数组。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#数组遍历)数组遍历 使用 `for` 循环是遍历数组元素最常见的方法。例如: ~~~ var keys = Object.keys(o); // 获得 o 对象属性名组成的数组 var values = [] // 在数组中存储匹配属性的值 for(var i = 0; i < keys.length; i++) { // 对于数组中每个索引 var key = keys[i]; // 获得索引处的键值 values[i] = o[key]; // 在 values 数组中保存属性值 } ~~~ 在嵌套循环或其他性能非常重要的上下文中,可以看到这种基本的数组遍历需要优化,数组的长度应该只查询一次而非每次循环都要查询。例如: ~~~ for(var i = 0, len = keys.length; i < len; i++) { // 循环体仍然不变 } ~~~ 这些例子假设数组是稠密的,并且所有的元素都是合法数据。否则,使用数组元素之前应该先检测它们。例如: ~~~ for(var i = 0; i < a.length; i++) { if (!a[i]) continue; // 跳过 null、undefined 和不存在的元素 if (!(i in a)) continue ; // 跳过不存在的元素 if (a[i] === undefined) continue; // 跳过 undefined 和不存在的元素 // 循环体 } ~~~ 还可以使用 `for/in` 循环处理稀疏数组。循环每次将一个可枚举的属性名(包括数组索引)赋值给循环变量,不存在的索引将不会遍历到。例如: ~~~ for(var index in sparseArray) { var value = sparseArray[index]; // 此处可以使用索引和值做一些事情 } ~~~ 但由于 `for/in` 循环能够枚举继承的属性名,如添加到 `Array.prototype` 中的方法。基于这个原因,在数组上不应该使用`for/in` 循环,除非使用额外的检测方法来过滤不想要的属性。例如: ~~~ for(var i in a) { // 跳过继承的属性 if (!a.hasOwnProperty(i)) continue; // 跳过不是非负整数的 i if (String(Math.floor(Math.abs(Number(i)))) !== i) continue; } ~~~ `JavaScript` 规范允许 `for/in` 循环以不同的顺序遍历对象的属性。通常数组元素的遍历实现是升序的,但不能保证一定是这样的。如果数组同时拥有对象属性和数组元素,返回的属性名很可能是按照创建的顺序而非数值的大小顺序。如何处理这个问题的实现,各个浏览器都不相同,如果算法依赖于遍历的顺序,那么最好不要使用 `for/in` 而用常规的 `for` 循环。 ECMAScript 5定义了一些遍历数组元素的新方法,按照索引的顺序按个传递给定义的一个函数。这些方法中最常用的就是`forEach()` 方法。例如: ~~~ var data = [1,2,3,4,5]; // 这是需要遍历的数组 var sumOfSquares = 0; // 要得到数据的平方和 data.forEach(function(x) { // 把每个元素传递给此函数 sumOfSquares += x*x; // 平方相加 }); console.log(sumOfSquares); // 55,1 + 4 + 9 + 16 + 25 ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#数组检测)数组检测 给定一个未知的对象,判定它是否为数组通常非常有用。在 ECMAScript 5中,可以使用 `Array.isArray()` 函数来做这件事情。例如: ~~~ Array.isArray([]) // true Array.isArray({}) // false ~~~ 但是,在 ECMAScript 5以前,要区分数组和非数组对象很困难。`typeof` 运算符对数组返回 `"object"`(并且对于除了函数以外的所有对象都是如此)。`instanceof` 操作符也只能用于简单的情形。例如: ~~~ [] instanceof Array // true ({}) instanceof Array // false ~~~ 使用 `instanceof` 的问题是在 Web 浏览器中有可能有多个窗体存在。每个窗体都有自己的 JavaScript 环境,有自己的全局对象。并且,每个全局对象有自己的一组构造函数。因此一个窗体中的对象将不可能是另外窗体中的构造函数的实例。窗体之间的混淆不常发生,但这个问题足已证明 `instanceof` 操作符不能视为一个可靠的数组检测方法。 解决方案是检查对象的类属性,对数组而言该属性的值总是 `"Array"`,因此在 ECMAScript 3中 `isArray()` 函数的代码可以这样书写。例如: ~~~ var isArray = Array.isArray || function(o) { return typeof o === "object" && Object.prototype.toString.call(o) === "[object Array]"; }; ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#数组方法)数组方法 ECMAScript 3和 ECMAScript 5在 `Array.prototype` 中定义了一些很有用的操作数组的方法。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#转换方法)转换方法 所有对象都具有 `toLocaleString()`、`toString()` 和 `valueOf()` 方法。其中,调用数组的 `toString()` 和 `valueOf()` 方法会返回相同的值,即由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。实际上,为了创建这个字符串会调用数组每一项的 `toString()` 方法。例如: ~~~ var colors = ["red", "blue", "green"]; // 创建一个包含3个字符串的数组 alert(colors.toString()); // red,blue,green alert(colors.valueOf()); // red,blue,green alert(colors); // red,blue,green ~~~ 在这里,我们首先显式地调用了 `toString()` 和 `valueOf()` 方法,以便返回数组的字符串表示,每个值的字符串表示拼接成了一个字符串,中间以逗号分隔。最后一行代码直接将数组传递给了 `alert()`。由于 `alert()`要接收字符串参数,所以它会在后台调用 `toString()` 方法,由此会得到与直接调用 `toString()` 方法相同的结果。 另外,`toLocaleString()` 方法经常也会返回与 `toString()` 和 `valueOf()` 方法相同的值,但也不总是如此。当调用数组的`toLocaleString()` 方法时,它也会创建一个数组值的以逗号分隔的字符串。而与前两个方法唯一的不同之处在于,这一次为了取得每一项的值,调用的是每一项的 `toLocaleString()` 方法,而不是 `toString()` 方法。例如: ~~~ var person1 = { toLocaleString : function () { return "Nikolaos"; }, toString : function() { return "Nicholas"; } }; var person2 = { toLocaleString : function () { return "Grigorios"; }, toString : function() { return "Greg"; } }; var people = [person1, person2]; alert(people); // Nicholas,Greg alert(people.toString()); // Nicholas,Greg alert(people.toLocaleString()); // Nikolaos,Grigorios ~~~ 数组继承的 `toLocaleString()`、`toString()` 和 `valueOf()`方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而如果使用 `join()` 方法,则可以使用不同的分隔符来构建这个字符串。`join()` 方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。例如: ~~~ var colors = ["red", "green", "blue"]; console.log(colors.join(",")); // red,green,blue console.log(colors.join("||")); // red||green||blue ~~~ 如果数组中的某一项的值是 `null` 或者 `undefined`,那么该值在 `join()`、`toLocaleString()`、`toString()` 和 `valueOf()`方法返回的结果中以空字符串表示。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#栈方法)栈方法 栈是一种 LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。`push()` 方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而 `pop()` 方法则从数组末尾移除最后一项,减少数组的`length` 值,然后返回移除的项。结合 `push()` 和 `pop()` 方法,就可以像栈一样使用数组。例如: ~~~ var colors = []; // 创建一个数组 var count = colors.push("red", "green"); // 推入两项 console.log(count); // 2,数组的长度 count = colors.push("black"); // 推入另一项 console.log(count); // 3,数组的长度 var item = colors.pop(); // 取得最后一项 console.log(item); // "black" console.log(colors.length); // 2,数组的长度 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#队列方法)队列方法 队列是一种 FIFO(First-In-First-Out,先进先出)的数据结构,队列在列表的末端添加项,从列表的前端移除项。`shift()` 方法则从数组前端移除第一项,减少数组的 `length` 值,然后返回移除的项。结合 `push()` 和 `shift()` 方法,就可以像队列一样使用数组。例如: ~~~ var colors = []; // 创建一个数组 var count = colors.push("red", "green"); // 推入两项 console.log(count); // 2,数组的长度 count = colors.push("black"); // 推入另一项 console.log(count); // 3,数组的长度 var item = colors.shift(); // 取得第一项 console.log(item); // "red" console.log(colors.length); // 2,数组的长度 ~~~ JavaScipt 还为数组提供了一个 `unshift()` 方法。顾名思义,`unshift()` 与 `shift()` 的用途相反,它能在数组前端添加任意个项并返回新数组的长度。因此,同时使用 `unshift()` 和 `pop()` 方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#重排序方法)重排序方法 数组中有两个重排序的方法:`reverse()` 和 `sort()`。`reverse()` 方法可以反转数组元素的顺序。例如: ~~~ var values = [1, 2, 3, 4, 5]; values.reverse(); console.log(values); //5,4,3,2,1 ~~~ `sort()` 方法可以按升序排列数组元素(即最小的值位于最前面,最大的值排在最后面)。`sort()` 方法在排序的过程中会调用每个数组元素的 `toString()`,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,`sort()` 方法比较的也是字符串,例如: ~~~ var values = [0, 1, 5, 10, 15]; values.sort(); console.log(values); // 0,1,10,15,5 ~~~ 这种排序方式在很多情况下都不是最佳方案,因此 `sort()` 方法可以接收一个比较函数作为参数,以便我们指定哪个值位于哪个值的前面,以下就是一个简单的比较函数。例如: ~~~ function compare(value1, value2) { if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } ~~~ 这个比较函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回0,如果第一个参数应该位于第二个之后则返回一个正数。它可以适用于大多数情况,只要将其作为参数传递给 `sort()` 方法即可。例如: ~~~ var values = [10, 5, 1, 0, 15]; values.sort(compare); console.log(values); // 0,1,5,10,15 ~~~ 对于数值类型或者其 `valueOf()` 方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。这个函数只要用第二个值减第一个值即可。例如: ~~~ function compare(value1, value2){ return value2 - value1; } ~~~ 由于比较函数通过返回一个小于零、等于零或大于零的值来影响排序结果,因此减法操作就可以适当地处理所有这些情况。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#操作方法)操作方法 JavaScript 为操作已经包含在数组中的元素提供了很多方法。其中,`concat()` 方法可以基于当前数组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。在没有给 `concat()` 方法传递参数的情况下,它只是复制当前数组并返回副本。如果传递给 `concat()` 方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中。如果传递的值不是数组,这些值就会被简单地添加到结果数组的末尾。例如: ~~~ var colors = ["red", "green", "blue"]; var colors2 = colors.concat("yellow", ["black", "brown"]); console.log(colors); // red,green,blue console.log(colors2); // red,green,blue,yellow,black,brown ~~~ 下一个方法是 `slice()`,它能够基于当前数组中的一或多个项创建一个新数组。`slice()` 方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,`slice()` 方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项,但不包括结束位置的项。注意,`slice()` 方法不会影响原始数组。例如: ~~~ var colors = ["red", "green", "blue", "yellow", "purple"]; var colors2 = colors.slice(1); var colors3 = colors.slice(1,4); console.log(colors2); // green,blue,yellow,purple console.log(colors3); // green,blue,yellow ~~~ > 如果 `slice()` 方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。例如,在一个包含5项的数组上调用`slice(-2,-1)` 与调用 `slice(3,4)` 得到的结果相同。如果结束位置小于起始位置,则返回空数组。 下一个方法是 `splice()`,它的主要用途是向数组的中部插入元素,主要有以下3种使用方式。 * 删除:可以删除任意数量的项,只需指定2个参数:起始位置和要删除元素的数量。例如,`splice(0,2)` 会删除数组中的前两项。 * 插入:可以向指定位置插入任意数量的项,只需提供3个参数:起始位置、0(要删除元素的数量)和要插入的元素。如果要插入多个元素,可以再传入第四、第五,以至任意多个元素。例如,`splice(2,0,"red","green")` 会从当前数组的位置2开始插入字符串 `"red"` 和 `"green"`。 * 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除元素的数量和要插入的元素。插入的项数不必与删除的项数相等。例如,`splice (2,1,"red","green")`会删除当前数组位置2的项,然后再从位置2开始插入字符串 `"red"` 和 `"green"`。 `splice()` 方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。下面的代码展示了上述3种使用 `splice()` 方法的方式。例如: ~~~ var colors = ["red", "green", "blue"]; var removed = colors.splice(0,1); // 删除第一项 console.log(colors); // green,blue console.log(removed); // red,返回的数组中只包含一项 removed = colors.splice(1, 0, "yellow", "orange"); // 从位置1开始插入两项 console.log(colors); // green,yellow,orange,blue console.log(removed); // 返回的是一个空数组 removed = colors.splice(1, 1, "red", "purple"); // 插入两项,删除一项 console.log(colors); // green,red,purple,orange,blue console.log(removed); // yellow,返回的数组中只包含一项 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#位置方法)位置方法 ECMAScript 5为数组实例添加了两个位置方法:`indexOf()` 和 `lastIndexOf()`。这两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。其中,`indexOf()` 方法从数组的开头(位置0)开始向后查找,`lastIndexOf()` 方法则从数组的末尾开始向前查找。 这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回 `-1`。在比较第一个参数与数组中的每一项时,会使用全等操作符;也就是说,要求查找的项必须严格相等(就像使用 `===` 一样)。例如: ~~~ var numbers = [1,2,3,4,5,4,3,2,1]; console.log(numbers.indexOf(4)); // 3 console.log(numbers.lastIndexOf(4)); // 5 console.log(numbers.indexOf(4, 4)); // 5 console.log(numbers.lastIndexOf(4, 4)); // 3 var person = { name: "Nicholas" }; var people = [{ name: "Nicholas" }]; var morePeople = [person]; console.log(people.indexOf(person)); // -1 console.log(morePeople.indexOf(person)); // 0 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#迭代方法)迭代方法 ECMAScript 5为数组定义了5个迭代方法。每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象。传入这些方法中的函数会接收三个参数:数组项的值、该项在数组中的位置和数组对象本身。根据使用的方法不同,这个函数执行后的返回值可能会也可能不会影响访问的返回值。以下是这5个迭代方法的作用。 * `every()`,对数组中的每一项运行给定函数,如果该函数对每一项都返回 `true` ,则返回 `true`。 * `filter()`,对数组中的每一项运行给定函数,返回该函数会返回 `true` 的项组成的数组。 * `forEach()`,对数组中的每一项运行给定函数。这个方法没有返回值。 * `map()`,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。 * `some()`,对数组中的每一项运行给定函数,如果该函数对任一项返回 `true` ,则返回 `true`。 以上方法都不会修改数组中的包含的值。在这些方法中,最相似的是 `every()` 和 `some()`,它们都用于查询数组中的项是否满足某个条件。对 `every()` 来说,传入的函数必须对每一项都返回 `true`,这个方法才返回 `true`;否则,它就返回`false`。而 `some()`方法则是只要传入的函数对数组中的某一项返回 `true`,就会返回 `true`。例如: ~~~ var numbers = [1,2,3,4,5,4,3,2,1]; var everyResult = numbers.every(function(item, index, array){ return (item > 2); }); console.log(everyResult); // false var someResult = numbers.some(function(item, index, array){ return (item > 2); }); console.log(someResult); // true ~~~ 下面再看一看 `filter()` 函数,它利用指定的函数确定是否在返回的数组中包含的某一项。例如: ~~~ var numbers = [1,2,3,4,5,4,3,2,1]; var filterResult = numbers.filter(function(item, index, array){ return (item > 2); }); console.log(filterResult); // [3,4,5,4,3] ~~~ `map()` 也返回一个数组,而这个数组的每一项都是在原始数组中的对应项上运行传入函数的结果。例如,可以给数组中的每一项乘以2,然后返回这些乘积组成的数组。例如: ~~~ var numbers = [1,2,3,4,5,4,3,2,1]; var mapResult = numbers.map(function(item, index, array){ return item * 2; }); console.log(mapResult); // [2,4,6,8,10,8,6,4,2] ~~~ 最后一个方法是 `forEach()`,它只是对数组中的每一项运行传入的函数。这个方法没有返回值,本质上与使用 `for` 循环迭代数组一样。例如: ~~~ var numbers = [1,2,3,4,5,4,3,2,1]; numbers.forEach(function(item, index, array){ //执行某些操作 }); ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#缩小方法) 缩小方法 ECMAScript 5还新增了两个缩小数组的方法:`reduce()` 和 `reduceRight()`。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中,`reduce()` 方法从数组的第一项开始,逐个遍历到最后。而 `reduceRight()` 则从数组的最后一项开始,向前遍历到第一项。 这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为缩小基础的初始值。传给 `reduce()`和`reduceRight()` 的函数接收4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数就是数组的第二项。 使用 `reduce()` 方法可以执行求数组中所有值之和的操作。例如: ~~~ var values = [1,2,3,4,5]; var sum = values.reduce(function(prev, cur, index, array){ return prev + cur; }); console.log(sum); // 15 ~~~ 第一次执行回调函数,prev是1,cur是2。第二次,prev是3(1加2的结果),cur是3(数组的第三项)。这个过程会持续到把数组中的每一项都访问一遍,最后返回结果。 `reduceRight()` 的作用类似,只不过方向相反而已。例如: ~~~ var values = [1,2,3,4,5]; var sum = values.reduceRight(function(prev, cur, index, array){ return prev + cur; }); console.log(sum); // 15 ~~~ 使用 `reduce()` 还是 `reduceRight()`,主要取决于要从哪头开始遍历数组。除此之外,它们完全相同。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.8-ArrayObjects#关卡)关卡 完成下面3个数组去重方法。 ~~~ // 挑战一,一维数组 var arr = [2,3,4,2,3,5,6,4,3,2]; var unique = function(arr){ // 待实现方法体 } console.log(unique(arr)); // [2,3,4,5,6] ~~~ ~~~ // 挑战二,二维数组 var arr = [2,3,4,[2,3,4,5],3,5,[2,3,4,2],4,3,6,2]; var unique = function(arr){ // 待实现方法体 } console.log(unique(arr)); // [2,3,4,5,6] ~~~ ~~~ // 挑战三,三维数组或 n 维数组 var arr = [2,3,4,[2,3,[2,3,4,2],5],3,5,[2,3,[2,3,4,2],2],4,3,6,2]; var unique = function(arr){ // 待实现方法体 } console.log(unique(arr)); // [2,3,4,5,6] ~~~
';

对象

最后更新于:2022-04-01 21:00:41

# 对象 对象是 JavaScript 的数据类型。它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值,因此我们可以把它看成是从字符串到值的映射。对象是动态的,可以随时新增和删除自有属性。对象除了可以保持自有的属性,还可以从一个称为原型的对象继承属性,这种「原型式继承(prototypal inheritance)」是 JavaScript 的核心特征。 对象最常见的用法是创建(create)、设置(set)、查找(query)、删除(delete)、检测(test)和枚举(enumerate)它的属性。 属性包括名字和值。属性名可以是包含空字符串在内的任意字符串,但对象中不能存在两个同名的属性。值可以是任意 JavaScript 值,或者在 ECMAScript 5中可以是 `getter` 或 `setter` 函数。 除了名字和值之外,每个属性还有一些与之相关的值,称为「属性特性(property attribute)」: * 可写(writable attribute),表明是否可以设置该属性的值。 * 可枚举(enumerable attribute),表明是否可以通过 `for/in` 循环返回该属性。 * 可配置(configurable attribute),表明是否可以删除或修改该属性。 在 ECMAScript 5之前,通过代码给对象创建的所有属性都是可写的、可枚举的和可配置的。在 ECMAScript 5中则可以对这些特性加以配置。 除了包含属性特性之外,每个对象还拥有三个相关的「对象特性(object attribute)」: * 对象的类(class),是一个标识对象类型的字符串。 * 对象的原型(prototype),指向另外一个对象,本对象的属性继承自它的原型对象。 * 对象的扩展标记(extensible flag),指明了在 ECMAScript 5中是否可以向该对象添加新属性。 最后,用下面术语来对 JavaScript 的「三类对象」和「两类属性」进行区分: * 内置对象(native object),是由 JavaScript 规范定义的对象或类。例如,数组、函数、日期和正则表达式都是内置对象。 * 宿主对象(host object),是由 JavaScript 解释器所嵌入的宿主环境(比如 Web 浏览器)定义的。客户端 JavaScript 中表示网页结构的 HTMLElement 对象均是宿主对象。 * 自定义对象(user-defined object),是由运行中的 JavaScript 代码创建的对象。 * 自有属性(own property),是直接在对象中定义的属性。 * 继承属性(inherited property),是在对象的原型对象中定义的属性。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#创建对象)创建对象 可以使用对象字面量、`new` 关键字和 ECMAScript 5中的 `Object.create()` 函数来创建对象。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#使用对象字面量创建对象推荐)使用对象字面量创建对象(推荐) 创建对象最简单的方式就是在 JavaScript 代码中使用对象字面量。对象字面量是由若干名值对组成的映射表,名值对中间用冒号分隔,名值对之间用逗号分隔,整个映射表用花括号括起来。属性名可以是 JavaScript 标识符也可以是字符串直接量(包括空字符串)。属性的值可以是任意类型的 JavaScript 表达式,表达式的值(可以是原始值也可以是对象值)就是这个属性的值。例如: ~~~ // 推荐写法 var person = { name : "stone", age : 28 }; // 也可以写成 var person = {}; person.name = "stone"; person.age = 28; ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#使用-new-关键字创建对象)使用 `new` 关键字创建对象 new 关键字创建并初始化一个新对象。关键字 new 后跟随一个函数调用。这里的函数称做构造函数(constructor),构造函数用以初始化一个新创建的对象。JavaScript 语言核心中的原始类型都包含内置构造函数。例如: ~~~ var person = new Object(); person.name = "stone"; person.age = 28; ~~~ 其中 `var person = new Object();` 等价于 `var person = {};` 。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#使用-objectcreate-函数创建对象)使用 `Object.create()` 函数创建对象 ECMAScript 5定义了一个名为 `Object.create()` 的方法,它创建一个新对象,其中第一个参数是这个对象的原型。`Object.create()` 提供第二个可选参数,用以对对象的属性进行进一步描述。`Object.create()` 是一个静态函数,而不是提供给某个对象调用的方法。使用它的方法很简单,只须传入所需的原型对象即可。例如: ~~~ var person = Object.create(Object.prototype); person.name = "stone"; person.age = 28; ~~~ 其中 `var person = Object.create(Object.prototype);` 也等价于 `var person = {};` 。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#原型prototype)原型(prototype) 所有通过对象字面量创建的对象都具有同一个原型对象,并可以通过 JavaScript 代码 `Object.prototype` 获得对原型对象的引用。通过关键字 `new` 和构造函数调用创建的对象的原型就是构造函数的 `prototype` 属性的值。因此,同使用 `{}` 创建对象一样,通过 `new Object()` 创建的对象也继承自 `Object.prototype`。同样,通过 `new Array()` 创建的对象的原型就是`Array.prototype`,通过 `new Date()` 创建的对象的原型就是 `Date.prototype`。 没有原型的对象为数不多,`Object.prototype` 就是其中之一。它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都具有一个继承自 `Object.prototype` 的原型。例如,`Date.prototype` 的属性继承自 `Object.prototype`,因此由 `new Date()` 创建的 `Date` 对象的属性同时继承自`Date.prototype` 和 `Object.prototype`。 这一系列链接的原型对象就是所谓的「原型链(prototype chain)」。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#属性的查询和设置)属性的查询和设置 前面有提到过,可以通过点 `.` 或方括号 `[]` 运算符来获取属性的值。对于点 `.` 来说,左侧应当是一个对象,右侧必须是一个以属性名称命名的简单标识符。对于方括号来说 `[]` ,方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名称。例如: ~~~ // 推荐写法 console.log(person.name); // "stone" console.log(person.age); // "28" // 也可以写成 console.log(person["name"]); // stone console.log(person["age"]); // 28 ~~~ 和获取属性的值写法一样,通过点和方括号也可以创建属性或给属性赋值,但需要将它们放在赋值表达式的左侧。例如: ~~~ // 推荐写法 person.name = "sophie"; // 赋值 person.age = 30; // 赋值 person.weight = 38; // 创建 // 也可以写成 person["name"] = "sophie"; // 赋值 person["age"] = 30; // 赋值 person["weight"] = 38; // 创建 ~~~ 当使用方括号时,方括号内的表达式必须返回字符串。更严格地讲,表达式必须返回字符串或返回一个可以转换为字符串的值。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#属性的访问错误)属性的访问错误 查询一个不存在的属性并不会报错,如果在对象 `o` 自身的属性或继承的属性中均未找到属性 `x`,属性访问表达式 `o.x` 返回`undefined`。例如: ~~~ var person = {}; person.wife; // undefined ~~~ 但是,如果对象不存在,那么试图查询这个不存在的对象的属性就会报错。`null` 和 `undefined` 值都没有属性,因此查询这些值的属性会报错。例如: ~~~ var person = {}; person.wife.name; // Uncaught TypeError: Cannot read property 'name' of undefined. ~~~ 除非确定 `person` 和 `person.wife` 都是对象,否则不能这样写表达式 `person.wife.name`,因为会报「未捕获的错误类型」,下面提供了两种避免出错的方法: ~~~ // 冗余但易懂的写法 var name; if (person) { if (person.wife) name = person.wife.name; } // 简练又常用的写法(推荐写法) var name = person && person.wife && person.wife.name; ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#删除属性)删除属性 `delete` 运算符用来删除对象属性,事实上 `delete` 只是断开属性和宿主对象的联系,并没有真正的删除它。`delete` 运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,而且这会影响到所有继承自这个原型的对象)。 > 代码范例,请参见[「变量和数据类型」-「数据类型」-「delete 运算符」](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression%26Operators#delete-%E8%BF%90%E7%AE%97%E7%AC%A6)。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#检测属性)检测属性 JavaScript 对象可以看做属性的集合,我们经常会检测集合中成员的所属关系(判断某个属性是否存在于某个对象中)。可以通过 `in` 运算符、`hasOwnPreperty()` 和 `propertyIsEnumerable()` 来完成这个工作,甚至仅通过属性查询也可以做到这一点。 `in` 运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回 `true`。例如: ~~~ var o = { x: 1 } console.log("x" in o); // true,x是o的属性 console.log("y" in o); // false,y不是o的属性 console.log("toString" in o); // true,toString是继承属性 ~~~ 对象的 `hasOwnProperty()` 方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回 `false`。例如: ~~~ var o = { x: 1 } console.log(o.hasOwnProperty("x")); // true,x是o的自有属性 console.log(o.hasOwnProperty("y")); // false,y不是o的属性 console.log(o.hasOwnProperty("toString")); // false,toString是继承属性 ~~~ `propertyIsEnumerable()` 是 `hasOwnProperty()` 的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为 `true` 时它才返回 `true`。某些内置属性是不可枚举的。通常由 JavaScript 代码创建的属性都是可枚举的,除非在 ECMAScript 5中使用一个特殊的方法来改变属性的可枚举性。例如: ~~~ var o = inherit({ y: 2 }); o.x = 1; o.propertyIsEnumerable("x"); // true:,x是o的自有属性,可枚举 o.propertyIsEnumerable("y"); // false,y是继承属性 Object.prototype.propertyIsEnumerable("toString"); // false,不可枚举 ~~~ 除了使用 `in` 运算符之外,另一种更简便的方法是使用 `!==` 判断一个属性是否是 `undefined`。例如: ~~~ var o = { x: 1 } console.log(o.x !== undefined); // true,x是o的属性 console.log(o.y !== undefined); // false,y不是o的属性 console.log(o.toString !== undefined); // true,toString是继承属性 ~~~ 然而有一种场景只能使用 `in` 运算符而不能使用上述属性访问的方式。`in` 可以区分不存在的属性和存在但值为 `undefined`的属性。例如: ~~~ var o = { x: undefined } // 属性被显式赋值为undefined console.log(o.x !== undefined); // false,属性存在,但值为undefined console.log(o.y !== undefined); // false,属性不存在 console.log("x" in o); // true,属性存在 console.log("y" in o); // false,属性不存在 console.log(delete o.x); // true,删除了属性x console.log("x" in o); // false,属性不再存在 ~~~ > 扩展阅读「JavaScript 检测原始值、引用值、属性」 > [http://shijiajie.com/2016/06/20/javascript-maintainable-javascript-validate1/](http://shijiajie.com/2016/06/20/javascript-maintainable-javascript-validate1/) > > 扩展阅读「JavaScript 检测之 basevalidate.js」 > [http://shijiajie.com/2016/06/25/javascript-maintainable-javascript-basevalidatejs/](http://shijiajie.com/2016/06/25/javascript-maintainable-javascript-basevalidatejs/) ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#枚举属性)枚举属性 除了检测对象的属性是否存在,我们还会经常遍历对象的属性。通常使用 `for/in` 循环遍历,ECMAScript 5提供了两个更好用的替代方案。 `for/in` 循环可以在循环体中遍历对象中所有可枚举的属性(包括自有属性和继承的属性),把属性名称赋值给循环变量。对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的。例如: ~~~ var o = {x:1, y:2, z:3}; // 三个可枚举的自有属性 o.propertyIsEnumerable("toString"); // false,不可枚举 for (p in o) { // 遍历属性 console.log(p); // 输出x、y和z,不会输出toString } ~~~ 有许多实用工具库给 `Object.prototype` 添加了新的方法或属性,这些方法和属性可以被所有对象继承并使用。然而在ECMAScript 5标准之前,这些新添加的方法是不能定义为不可枚举的,因此它们都可以在 `for/in` 循环中枚举出来。为了避免这种情况,需要过滤 `for/in` 循环返回的属性,下面两种方式是最常见的: ~~~ for(p in o) { if (!o.hasOwnProperty(p)) continue; // 跳过继承的属性 if (typeof o[p] === "function") continue; // 跳过方法 } ~~~ 除了 `for/in` 循环之外,ECMAScript 5定义了两个用以枚举属性名称的函数。第一个是 `Object.keys()`,它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。第二个是 `Object.getOwnPropertyNames()`,它和 `Ojbect.keys()` 类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。在ECMAScript 3中是无法实现的类似的函数的,因为ECMAScript 3中没有提供任何方法来获取对象不可枚举的属性。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#属性的-getter-和-setter)属性的 `getter` 和 `setter` 我们知道,对象属性是由名字、值和一组特性(attribute)构成的。在ECMAScript 5中,属性值可以用一个或两个方法替代,这两个方法就是 `getter` 和 `setter`。由 `getter` 和 `setter` 定义的属性称做「存取器属性(accessor property)」,它不同于「数据属性(data property)」,数据属性只有一个简单的值。 当程序查询存取器属性的值时,JavaScript 调用 `getter` 方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 `setter` 方法,将赋值表达式右侧的值当做参数传入 `setter`。从某种意义上讲,这个方法负责「设置」属性值。可以忽略 `setter` 方法的返回值。 和数据属性不同,存取器属性不具有可写性(writable attribute)。如果属性同时具有 `getter` 和 `setter` 方法,那么它是一个读/写属性。如果它只有 `getter` 方法,那么它是一个只读属性。如果它只有 `setter` 方法,那么它是一个只写属性,读取只写属性总是返回 `undefined`。定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法。例如: ~~~ var o = { // 普通的数据属性 data_prop: value, // 存取器属性都是成对定义的函数 get accessor_prop() { /*这里是函数体 */ }, set accessor_prop(value) { /* 这里是函数体*/ } }; ~~~ 存取器属性定义为一个或两个和属性同名的函数,这个函数定义没有使用 `function` 关键字,而是使用 `get` 或 `set`。注意,这里没有使用冒号将属性名和函数体分隔开,但在函数体的结束和下一个方法或数据属性之间有逗号分隔。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#序列化对象json)序列化对象(JSON) 对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5提供了内置函数`JSON.stringify()` 和 `JSON.parse()` 用来序列化和还原 JavaScript 对象。这些方法都使用 JSON 作为数据交换格式,JSON 的全称是「JavaScript 对象表示法(JavaScript Object Notation)」,它的语法和 JavaScript 对象与数组直接量的语法非常相近。例如: ~~~ o = {x:1, y:{z:[false,null,""]}}; // 定义一个对象 s = JSON.stringify(o); // s是 '{"x":1,"y":{"z":[false,null,""]}}' p = JSON.parse(s); // p是o的深拷贝 ~~~ ECMAScript 5中的这些函数的本地实现和 [https://github.com/douglascrockford/JSON-js](https://github.com/douglascrockford/JSON-js) 中的公共域ECMAScript 3版本的实现非常类似,或者说完全一样,因此可以通过引入 `json2.js` 模块在ECMAScript 3的环境中使用ECMAScript 5中的这些函数。 JSON 的语法是 JavaScript 语法的子集,它并不能表示 JavaScript 里的所有值。它支持对象、数组、字符串、无穷大数字、`true`、`false` 和 `null`,可以序列化和还原它们。`NaN`、`Infinity` 和 `-Infinity` 序列化的结果是 `null`,日期对象序列化的结果是 ISO 格式的日期字符串(参照 `Date.toJSON()` 函数),但 `JSON.parse()` 依然保留它们的字符串形态,而不会将它们还原为原始日期对象。函数、`RegExp`、`Error` 对象和 `undefined` 值不能序列化和还原。`JSON.stringify()` 只能序列化对象可枚举的自有属性。对于一个不能序列化的属性来说,在序列化后的输出字符串中会将这个属性省略掉。`JSON.stringify()` 和 `JSON.parse()` 都可以接收第二个可选参数,通过传入需要序列化或还原的属性列表来定制自定义的序列化或还原操作。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.7-ObjectObjects#关卡)关卡 请实现下面用来枚举属性的对象工具函数: ~~~ /* * 把 p 中的可枚举属性复制到 o 中,并返回 o * 如果 o 和 p 中含有同名属性,则覆盖 o 中的属性 */ function extend(o, p) { // 请实现函数体 } ~~~ ~~~ /* * 将 p 中的可枚举属性复制至 o 中,并返回 o * 如果 o 和 p 中有同名的属性,o 中的属性将不受影响 */ function merge(o, p) { // 请实现函数体 } ~~~ ~~~ /* * 如果 o 中的属性在 p 中没有同名属性,则从 o 中删除这个属性 * 返回 o */ function restrict(o, p) { // 请实现函数体 } ~~~ ~~~ /* * 如果 o 中的属性在 p 中存在同名属性,则从 o 中删除这个属性 * 返回 o */ function subtract(o, p) { // 请实现函数体 } ~~~ ~~~ /* * 返回一个新对象,这个对象同时拥有 o 的属性和 p 的属性 * 如果 o 和 p 中有重名属性,使用 p 中的属性值 */ function union(o, p) { // 请实现函数体 } ~~~ ~~~ /* * 返回一个新对象,这个对象拥有同时在 o 和 p 中出现的属性 * 很像求 o 和 p 的交集,但 p 中属性的值被忽略 */ function intersection(o, p) { // 请实现函数体 } ~~~ ~~~ /* * 返回一个数组,这个数组包含的是 o 中可枚举的自有属性的名字 */ function keys(o) { // 请实现函数体 } ~~~
';

语句

最后更新于:2022-04-01 21:00:38

# 语句 表达式在 JavaScript 中是短语,那么语句就是整句命令。表达式用来计算出一个值,语句用来执行以使某件事发生。从本质上看,语句定义了 JavaScript 中的主要语法,语句通常使用一或多个关键字来完成给定任务。语句可以很简单,例如通知函数退出;也可以比较复杂,例如指定重复执行某个命令的次数。下表列出了 JavaScript 大部分语句的语法和用途: | 语句 | 语法 | 用途 | | --- | --- | --- | | `break` | `break [label];` | 退出最内层循环或者退出 `switch` 语句,又或者退出 `label` 指定的语句 | | `case` | `case expression:` | 在 `switch` 语句中标记一条语句 | | `continue` | `continue [label];` | 重新开始最内层的循环或重新开始 `label` 指定的循环 | | `debugger` | `debugger;` | 断点器调试 | | `default` | `default;` | 在 `switch` 中标记默认的语句 | | `do-while` | `do statement while(expression);` | `while` 循环的一种替代形式 | | `empty` | `;` | 什么都不做 | | `for` | `for(init;expr;incr) statement` | 简写的循环结构 | | `for-in` | `for(var in object) statement` | 遍历一个对象的属性 | | `function` | `function name([param[],...])``{statement}` | 声明一个函数 | | `if-else` | `if (expression) statement1``[else statement2]` |执行 `statement1` 或者 `statement2` | | `label` | `label:statement` | 给 `statement` 指定一个名字 `label` | | `return` | `return [expression];` | 从函数返回一个值 | | `switch` | `switch(expression){statement}` | 用 `case` 或者 `default` 语句标记的多分支语句 | | `throw` | `throw expression;` | 抛出异常 | | `try` | `try {statement}``[catch {handler statement}]``[finally {cleaup statement}]` | 捕获异常 | | `use strict` | `"use strict"` | 对脚本和函数应用严格模式 | | `var` | `var name=[=expr][,...];` | 声明并初始化一个或多个变量 | | `while` | `while(expression) statement` | 基本的循环结构 | | `with` | `with(object) statement` | 扩展作用域链 | ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#条件语句)条件语句 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#if-else-语句)`if-else` 语句 大多数编程语言中最为常用的一个语句就是 `if-else` 语句。以下是 `if-else` 语句的语法: ~~~ if (condition) statement1 [else statement2] ~~~ 其中的 `condition` 可以是任意表达式;而且对这个表达式求值的结果不一定是布尔值。JavaScript 会自动调用 `Boolean()` 转换函数将这个表达式的结果转换为一个布尔值。如果对 `condition` 求值的结果是 `true`,则执行 `statement1`,如果对`condition` 求值的结果是 `false`,则执行 `statement2`。而且这两个语句既可以是一行代码,也可以是一个代码块(以一对花括号括起来的多行代码)。请看下面的例子: ~~~ if (i > 25) console.log("Greater than 25."); // 单行语句 else { console.log("Less than or equal to 25."); // 代码块中的语句 } ~~~ 业界普遍推崇的最佳实践是始终使用代码块,即使要执行的只有一行代码。因为这样可以消除人们的误解,否则可能让人分不清在不同条件下要执行哪些语句。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#switch-语句)`switch` 语句 `switch` 语句与 `if` 语句的关系最为密切,而且也是在其他语言中普遍使用的一种流控制语句。JavaScript 中 `switch` 语句的语法与其他基于 C 的语言非常接近,如下所示: ~~~ switch (expression) { case value: statement break; case value: statement break; case value: statement break; case value: statement break; default: statement } ~~~ `switch` 语句中的每一种情形的含义是:“如果表达式等于这个值(value),则执行后面的语句(statement)”。而 `break` 关键字会导致代码执行流跳出 `switch` 语句。如果省略 `break` 关键字,就会导致执行完当前 `case` 后,继续执行下一个`case`。最后的 `default` 关键字则用于在表达式不匹配前面任何一种情形的时候,也相当于一个 `else` 语句。从根本上讲,`switch` 语句就是为了让开发人员免于编写像下面这样的代码: ~~~ if (i === 25){ console.log("25"); } else if (i === 35) { console.log("35"); } else if (i === 45) { console.log("45"); } else { console.log("Other"); } ~~~ 而与此等价的switch语句如下所示: ~~~ switch (i) { case 25: console.log("25"); break; case 35: console.log("35"); break; case 45: console.log("45"); break; default: console.log("Other"); } ~~~ 通过为每个case后面都添加一个break语句,就可以避免同时执行多个case代码的情况。假如确实需要混合几种情形,不要忘了在代码中添加注释,说明你是有意省略了break关键字。 虽然 JavaScript 中的 `switch` 语句借鉴自其他语言,但这个语句也有自己的特色。首先,可以在 `switch` 语句中使用任何数据类型(在很多其他语言中只能使用数值),无论是字符串,还是对象都没有问题。其次,每个 `case` 的值不一定是常量,可以是变量,甚至是表达式。请看下面这两个例子: ~~~ switch ("hello world") { case "hello" + " world": console.log("Greeting was found."); break; case "goodbye": console.log("Closing was found."); break; default: console.log("Unexpected message was found."); } var num = 25; switch (true) { case num < 0: console.log("Less than 0."); break; case num >= 0 && num <= 10: console.log("Between 0 and 10."); break; case num > 10 && num <= 20: console.log("Between 10 and 20."); break; default: console.log("More than 20."); } ~~~ `switch` 语句首先计算 `switch` 关键字后的表达式,然后按照从上到下的顺序计算每个 `case` 后的表达式,直到执行到 `case`的表达式的值与 `switch` 的表达式的值相等时为止。由于对每个 `case` 的匹配操作实际上是 `===` 恒等运算符比较,而不是`==` 相等运算符比较,因此,表达式和 `case` 的匹配并不会做任何类型转换。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#循环)循环 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#while-语句)`while` 语句 `while` 语句属于前测试循环语句,也就是说,在循环体内的代码被执行之前,就会对出口条件求值。因引,循环体内的代码有可能永远不会被执行。以下是 `while` 语句的语法: ~~~ while(expression) statement ~~~ 下面是一个示例: ~~~ var i = 0; while (i < 10) { i += 2; } ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#do-while-语句)`do-while` 语句 `do-while` 语句是一种后测试循环语句,即只有在循环体中的代码执行之后,才会测试出口条件。换句话说,在对条件表达式求值之前,循环体内的代码至少会被执行一次。以下是 `do-while` 语句的语法: ~~~ do { statement } while (expression); ~~~ 下面是一个示例: ~~~ var i = 0; do { i += 2; } while (i < 10); ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#for-语句)`for` 语句 `for` 语句也是一种前测试循环语句,但它具有在执行循环之前初始化变量和定义循环后要执行的代码的能力。以下是 `for` 语句的语法: ~~~ for (initialization; expression; post-loop-expression) statement ~~~ 下面是一个示例: ~~~ var count = 10; for (var i = 0; i < count; i++){ console.log(i); } ~~~ 这个 `for` 循环语句与下面的 `while` 语句的功能相同: ~~~ var count = 10; var i = 0; while (i < count){ console.log(i); i++; } ~~~ 由于 JavaScript 中不存在块级作用域,因此在循环内部定义的变量也可以在外部访问到。例如: ~~~ var count = 10; for (var i = 0; i < count; i++){ console.log(i); } console.log(i); // 10 ~~~ 此外,`for` 语句中的初始化表达式、控制表达式和循环后表达式都是可选的。将这两个表达式全部省略,就会创建一个无限循环,例如: ~~~ // 无限循环 for (;;) { doSomething(); } ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#for-in-语句)`for-in` 语句 `for-in` 语句是一种精准的迭代语句,可以用来枚举对象的属性。以下是 `for-in` 语句的语法: ~~~ for (property in object) statement ~~~ 下面是一个示例: ~~~ for (var propName in window) { console.log(propName); } ~~~ 在这个例子中,我们使用 `for-in` 循环来显示了 BOM 中 `window` 对象的所有属性。每次执行循环时,都会将 `window` 对象中存在的一个属性名赋值给变量 `propName`。这个过程会一直持续到对象中的所有属性都被枚举一遍为止。与 `for` 语句类似,这里控制语句中的 `var` 操作符也不是必需的。但是,为了保证使用局部变量,我们推荐上面例子中的这种做法。 JavaScript 对象的属性没有顺序。因此,通过 `for-in` 循环输出的属性名的顺序是不可预测的。具体来讲,所有属性都会被返回一次,但返回的先后次序可能会因浏览器而异。 如果表示要迭代的对象的变量值为 `null` 或 `undefined`,`for-in` 语句会抛出错误。虽然 ECMAScript 5更正了这一行为;对这种情况不再抛出错误,而只是不执行循环体。为了保证最大限度的兼容性,建议在使用 `for-in` 循环之前,先检测确认该对象的值不是 `null` 或 `undefined`。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#跳转)跳转 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#label-语句)`label` 语句 使用 `label` 语句可以在代码中添加标签,以便将来使用。以下是 `label` 语句的语法: ~~~ label: statement ~~~ 下面是一个示例: ~~~ start: for (var i=0; i < count; i++) { console.log(i); } ~~~ 这个例子中定义的 `start` 标签可以在将来由 `break` 或 `continue` 语句引用。加标签的语句一般都要与 `for` 语句等循环语句配合使用。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#break-和-continue-语句)`break` 和 `continue` 语句 `break` 和 `continue` 语句用于在循环中精确地控制代码的执行。其中,`break` 语句会立即退出循环,强制继续执行循环后面的语句。而 `continue` 语句虽然也是立即退出循环,但退出循环后会从循环的顶部继续执行。请看下面的例子: ~~~ var num = 0; for (var i=1; i < 10; i++) { if (i % 5 == 0) { break; } num++; } console.log(num); // 4 ~~~ 这个例子中的 `for` 循环会将变量 `i` 由1递增至 `10`。在循环体内,有一个 `if` 语句检查 `i` 的值是否可以被 `5` 整除(使用求模运算符)。如果是,则执行 `break` 语句退出循环。另一方面,变量 `num` 从 `0` 开始,用于记录循环执行的次数。在执行`break` 语句之后,结果显示 `4`。也就是说,在变量 `i` 等于 `5` 时,循环总共执行了 `4` 次;而 `break` 语句的执行,导致了循环在 `num` 再次递增之前就退出了。如果在这里把 `break` 替换为 `continue` 的话,则可以看到另一种结果: ~~~ var num = 0; for (var i=1; i < 10; i++) { if (i % 5 == 0) { continue; } num++; } console.log(num); // 8 ~~~ 例子的结果显示 `8`,也就是循环总共执行了 `8` 次。当变量 `i` 等于 `5` 时,循环会在 `num` 再次递增之前退出,但接下来执行的是下一次循环,即i的值等于 `6` 的循环。于是,循环又继续执行,直到 `i` 等于 `10` 时自然结束。而 `num` 的最终值之所以是`8`,是因为 `continue` 语句导致它少递增了一次。 `break` 和 `continue` 语句都可以与 `label` 语句联合使用,从而返回代码中特定的位置。这种联合使用的情况多发生在循环嵌套的情况下,如下面的例子所示: ~~~ var num = 0; outermost: for (var i = 0; i < 10; i++) { for (var j = 0; j < 10; j++) { if (i == 5 && j == 5) { break outermost; } num++; } } console.log(num); // 55 ~~~ 在这个例子中,`outermost` 标签表示外部的 `for` 语句。如果每个循环正常执行 `10` 次,则 `num++` 语句就会正常执行 `100`次。换句话说,如果两个循环都自然结束,`num` 的值应该是 `100`。但内部循环中的 `break` 语句带了一个参数:要返回到的标签。添加这个标签的结果将导致 `break` 语句不仅会退出内部的 `for` 语句(即使用变量 `j` 的循环),而且也会退出外部的`for` 语句(即使用变量 `i` 的循环)。为此,当变量 `i` 和 `j` 都等于 `5` 时, `num`的值正好是 `55`。同样,`continue` 语句也可以像这样与 `label` 语句联用,如下面的例子所示: ~~~ var num = 0; outermost: for (var i = 0; i < 10; i++) { for (var j = 0; j < 10; j++) { if (i == 5 && j == 5) { continue outermost; } num++; } } console.log(num); // 95 ~~~ 在这种情况下,`continue` 语句会强制继续执行循环,退出内部循环,执行外部循环。当 `j` 是 `5` 时,`continue` 语句执行,而这也就意味着内部循环少执行了 `5` 次,因此 `num` 的结果是 `95`。 虽然联用 `break`、`continue` 和 `label` 语句能够执行复杂的操作,但如果使用过度,也会给调试带来麻烦。在此,我们建议如果使用 `label` 语句,一定要使用描述性的标签,同时不要嵌套过多的循环。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#return-语句)`return` 语句 `return` 语句的作用是指定函数调用后的返回值。`return` 语句的语法如下: ~~~ return [expression]; ~~~ 下面是一个示例: ~~~ function square(x) { return x*x; } // 一个包含 return 语句的函数 square(2); // 调用结果为 4 ~~~ `return` 语句只能在函数体内出现,如果不是的话会报语法错误。当执行到 `return` 语句的时候,函数终止执行,并返回`expression` 的值给调用程序。如果没有 `return` 语句,则函数调用仅依次执行函数体内的每一条语句直到函数结束,最后返回调用程序。这种情况下,调用表达式的结果是 `undefined`。`return` 语句经常作为函数内的最后一条语句出现,但并不是说要一定放在函数最后。`return` 语句可以单独使用而不必带有 `expression`,这样的话函数也会向调用程序返回 `undefined`。 由于 JavaScript 可以自动插入分号,因此在 `return` 关键字和它后面的表达式之间不能有换行。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#throw-语句)`throw` 语句 `throw` 语句的作用是把程序运行时产生的错误显式地抛出异常。`throw` 语句的语法如下: ~~~ throw expression; ~~~ `expression` 的值可以是任意类型的。可以抛出一个代表错误码的数字,或者包含可读的错误消息的字符串。当 JavaScript 解释器抛出异常的时候通常采用 `Error` 类型和其子类型。`Error` 对象有一个 `name` 属性表示错误类型,一个 `message` 属性用来存放传递给构造函数的字符串,在下面的例子中,当使用非法参数调用函数时就抛出一个 `Error` 对象: ~~~ function factorial(x) { // 如果输入参数是非法的,则抛出一个异常 if (x < 0) throw new Error("x不能是负数"); // 否则,计算出一个值,并正常地返回它 for(var f = 1; x > 1; f *= x, x--) /* empty */ ; return f; } ~~~ 当抛出异常时,JavaScript 解释器会立即停止当前正在执行的逻辑,并跳转至就近的异常处理程序。如果没有找到任何异常处理程序,异常就会沿着 JavaScript 方法的词法结构和调用栈向上传播。最终 JavaScript 将把异常当成程序错误来处理,并报告给用户。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#try-语句)`try` 语句 `try-catch-finally` 语句是 JavaScript 中异常处理机制,`try-catch-finally` 语句的语法如下: ~~~ try {statement} [catch {handler statement}] [finally {cleaup statement}] ~~~ `try` 从句定义了需要处理的异常所在的代码块。`catch` 从句跟随在 `try` 从句之后,当 `try` 块内某处发生了异常时,调用`catch` 内的代码逻辑。`catch` 从句后跟随 `finally` 块,后者中放置清理代码,不管 `try` 块中是否产生异常,`finally` 块内的逻辑总是会执行。尽管 `catch` 和 `finally` 都是可选的,但 `try` 从句需要至少二者之一与之组成完整的语句。`try`、`catch` 和 `finally` 语句块都需要使用花括号括起来,这里的花括号是必需的,即使从句中只有一条语句也不能省略花括号。 下面的代码详细的说明了 `try-catch-finally` 的使用目的: ~~~ try { // 通常来讲,这里的代码会从头执行到尾而不会产生任何问题, // 但有时会抛出一个异常,要么是由 throw 语句直接抛出异常, // 要么是通过调用一个方法间接抛出异常 } catch(e) { // 当且仅当 try 语句块抛出了异常,才会执行这里的代码 // 这里可以通过局部变量 e 来获得对 Error 对象或者抛出的其他值的引用 // 这里的代码块可以基于某种原因处理这个异常,也可以忽略这个异常, // 还可以通过 throw 语句重新抛出异常 } finally { // 不管 try 语句块是否抛出了异常,这里的逻辑总是会执行,终止 try 语句块的方式有: // 1)正常终止,执行完语句块的最后一条语句 // 2)通过 break、continue 或 return 语句终止 // 3)抛出一个异常,异常被 catch 从句捕获 // 4)抛出一个异常,异常未被捕获,继续向上传播 } ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#其他)其他 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#with-语句)`with` 语句 `with` 语句的作用是将代码的作用域设置到一个特定的对象中。`with` 语句的语法如下: ~~~ with (expression) statement; ~~~ 定义 `with` 语句的目的主要是为了简化多次编写同一个对象的工作,如下面的例子所示: ~~~ var qs = location.search.substring(1); var hostName = location.hostname; var url = location.href; ~~~ 上面几行代码都包含 `location` 对象。如果使用 `with` 语句,可以把上面的代码改写成如下所示: ~~~ with(location){ var qs = search.substring(1); var hostName = hostname; var url = href; } ~~~ 在这个重写后的例子中,使用 `with` 语句关联了 `location` 对象。这意味着在 `with` 语句的代码块内部,每个变量首先被认为是一个局部变量,而如果在局部环境中找不到该变量的定义,就会查询 `location` 对象中是否有同名的属性。如果发现了同名属性,则以 `location` 对象属性的值作为变量的值。 由于大量使用 `with` 语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用 `with` 语句。严格模式下不允许使用 `with` 语句,否则将视为语法错误。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#debugger-语句)`debugger` 语句 `debugger` 语句通常什么也不做。然而,当浏览器的调试工具可用并运行的时候,JavaScript 解释器将会以调式模式运行。实际上,这条语句用来产生一个断点(breakpoint),JavaScript 代码的执行会停止在断点的位置,这时可以使用调试器输出变量的值、检查调用栈等。例如: ~~~ function f(o) { if (o === undefined) { debugger; // 程序会停止在该位置 } // 函数的其他部分 } ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#use-strict-语句)`use strict` 语句 > 请参见[「语法」-「严格模式」](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#%E4%B8%A5%E6%A0%BC%E6%A8%A1%E5%BC%8F)。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.6-Statements#关卡)关卡 ~~~ // 挑战一 var k; for(i=0, j=0; i<10, j<6; i++, j++){ k = i + j; } console.log(k); // ??? ~~~ ~~~ // 挑战二 var nums = [12,32,54,56,78,89]; for(var n in nums){ console.log(n); // ??? } ~~~ ~~~ // 挑战三 function showCase(value) { switch (value) { case 'A': console.log('Case A'); break; case 'B': console.log('Case B'); break; case undefined: console.log('undefined'); break; default: console.log('Do not know!'); } } showCase(new String('A')); // ??? ~~~ ~~~ // 挑战四 function showCase(value) { switch (value) { case 'A': console.log('Case A'); case 'B': console.log('Case B'); break; case undefined: console.log('undefined'); break; default: console.log('Do not know!'); } } showCase(String('A')); // ??? ~~~ ~~~ // 挑战五 var i = 0; for (;;) { if (i = 2) { continue; } if (i = 20) { break; } i++; } console.log(i); // ??? ~~~
';

表达式和运算符

最后更新于:2022-04-01 21:00:36

# 表达式和运算符 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#表达式)表达式 表达式是由数字、运算符、数字分组符号(如括号)、自由变量和约束变量等以能求得数值的有意义排列方法所得的组合。JavaScript 表达式主要有以下几种形式: * 原始表达式:常量、变量、保留字。 * 对象、数组初始化表达式:`var obj={a:1,b:2};`,`var arr=[1,2,3];`。 * 函数定义表达式:`var fn=function(){}`。 * 属性访问表达式:`Math.abs`。 * 调用表达式:`alert('hello');`。 * 对象创建表达式:`new object();`。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#运算符)运算符 JavaScript 中的运算符用于算术表达式、比较表达式、逻辑表达式、赋值表达式等。需要注意的是,大多数运算符都是由标点符号表示的,比如 `+` 和 `=`。而另外一些运算符则是由关键字表示的,比如 `typeof` 和 `instanceof`,关键字运算符和标点符号都是正规的运算符。 下表列出了 JavaScript 中所有的运算符,并按照运算符的优先级排序的,前面的运算符优先级要高于后面的运算符优先级,被空行分隔开来的运算符具有不同的优先级。标题为 A 的列表示运算符的结合性(Associativity),L 表示从左至右、R 表示从右至左,标题为 N 的列表示操作数的个数(Number)。 | 运算符 | 操作 | A | N | | --- | --- | --- | --- | | `++` | 前/后增量 | R | 1 | | `--` | 前/后增量 | R | 1 | | `-` | 求反 | R | 1 | | `+` | 转换为数字 | R | 1 | | `~` | 按位求反 | R | 1 | | `!` | 逻辑非 | R | 1 | | `delete` | 删除属性 | R | 1 | | `typeof` | 检测类型 | R | 1 | | `void` | 返回`undefined` | R | 1 | | | | | | | `*` `/` `%` | 乘,除,求模 | L | 2 | | `+` `-` | 加,减 | L | 2 | | `+` | 字符串连接 | L | 2 | | `<<` | 左移位 | L | 2 | | `>>` | 有符号右移 | L | 2 | | `>>>` | 无符号右移 | L | 2 | | `<` `<=` `>` `>=` | 比较数字顺序 | L | 2 | | `<` `<=` `>` `>=` | 比较字母顺序 | L | 2 | | `instanceof` | 测试对象类 | L | 2 | | `in` | 测试属性是否存在 | L | 2 | | | | | | | `==` | 判断相等 | L | 2 | | `!=` | 判断不等 | L | 2 | | `===` | 判断恒等 | L | 2 | | `!==` | 判断恒不等 | L | 2 | | `&` | 按位与 | L | 2 | | | | | | | `^` | 按位异或 | L | 2 | | `┃` | 按位或 | L | 2 | | | | | | | `&&` | 逻辑与 | L | 2 | | | | | | | `┃┃` | 逻辑或 | L | 2 | | `?:` | 条件运算符 | R | 3 | | | | | | | `=` | 赋值 | R | 2 | | `*=` `/=` `%=`  `+=` `-=` `&=`  `<<=` `>>=`  `^=` `┃=` `>>>=` | 运算且赋值 | R | 2 | | | | | | | `,` | 忽略第一个操作数, 返回第二个操作数 | L | 2 | > 因为 `|` 是制表符,会导致格式混乱,所以表格中的 `|` 均以 `┃` 代替。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#一元运算符)一元运算符 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#delete-运算符)`delete` 运算符 `delete` 运算符用来删除对象属性或者数组元素,如果删除成功或所删除的目标不存在,`delete` 将返回 `true`。然而,并不是所有的属性都可删除,一些内置核心和客户端属性是不能删除的,通过 `var` 语句声明的变量不能删除,通过 `function` 语句定义的函数也是不能删除的。例如: ~~~ var o = { x: 1, y: 2}; // 定义一个对象 console.log(delete o.x); // true,删除一个属性 console.log(delete o.x); // true,什么都没做,x 在已上一步被删除 console.log("x" in o); // false,这个属性在对象中不再存在 console.log(delete o.toString); // true,什么也没做,toString是继承来的 console.log(delete 1); // true,无意义 var a = [1,2,3]; // 定义一个数组 console.log(delete a[2]); // true,删除最后一个数组元素 console.log(2 in a); // false,元素2在数组中不再存在 console.log(a.length); // 3,数组长度并不会因 delete 而改变 console.log(a[2]); // undefined,元素2所在的位置被空了出来 console.log(delete a); // false,通过 var 语句声明的变量不能删除 function f(args){} // 定义一个函数 console.log(delete f); // false,通过 function 语句声明的函数不能删除 ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#void-运算符)`void` 运算符 `void` 运算符可以应用于任何表类型的表达式,表达式会被执行,但计算结果会被忽略并返回 `undefined`。例如: ~~~ void 0; void "you are useless?"; void false; void []; void /(useless)/ig; void function(){ console.log("you are so useless?"); } // always return undefined ~~~ > 扩展阅读「谈谈 JavaScript 中的 void 运算符」 > [https://segmentfault.com/a/1190000000474941](https://segmentfault.com/a/1190000000474941) #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#typeof-运算符)`typeof` 运算符 > 请参见[「变量和数据类型」-「数据类型」-「`typeof` 运算符」](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable%26Types#typeof-%E8%BF%90%E7%AE%97%E7%AC%A6)。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#----运算符)`++` `--` 运算符 `++` `--` 递增递减运算符借鉴自 C 语言,它们分前置型和后置型,作用是改变一个变量的值。例如: ~~~ var a = 5; console.log(a++); // 5 console.log(++a); // 7 console.log(a--); // 7 console.log(--a); // 5 ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#---运算符)`+` `-` 运算符 当 `+` `-` 作为一元运算符时,应用于数值,表示数值的正负。应用于非数值,先按 `Number()` 转型函数对这个值执行转换,再表示该值的正负。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#--运算符)`~` `!` 运算符 > `~` 按位非运算符,请参见下面[「位运算符」](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6)。 > `!` 逻辑非运算符,请参见下面[「逻辑运算符」](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97%E7%AC%A6)。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#乘性运算符)乘性运算符 JavaScript 定义了3个乘性运算符:乘法、除法和求模。这些运算符与 C 语言的相应运算符用途类似,只不过在操作数为非数值的情况下会执行自动的类型转换。如果参与乘法计算的某个操作数不是数值,后台会先使用 `Number()` 转型函数将其转换为数值。也就是说,空字符串将被当作 `0`,布尔值 `true` 将被当作 `1`。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-乘法运算符)`*` 乘法运算符 用于计算两个数值的乘积,在处理特殊值的情况下,乘法运算符遵循下列特殊的规则: * 如果操作数都是数值,执行常规的乘法计算,即两个正数或两个负数相乘的结果还是正数,而如果只有一个操作数有符号,那么结果就是负数。如果乘积超过了 JavaScript 数值的表示范围,则返回 `Infinity` 或 `-Infinity`; * 如果有一个操作数是 `NaN`,则结果是 `NaN`; * 如果是 `Infinity` 与 `0` 相乘,则结果是 `NaN`; * 如果是 `Infinity` 与非 `0` 数值相乘,则结果是 `Infinity` 或 `-Infinity`,取决于有符号操作数的符号; * 如果是 `Infinity` 与 `Infinity` 相乘,则结果是 `Infinity`; 如果有一个操作数不是数值,则在后台调用 `Number()`将其转换为数值,然后再应用上面的规则。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-除法运算符)`/` 除法运算符 用于计算两个数值的商,与乘法运算符类似,除法运算符对特殊的值也有特殊的处理规则。这些规则如下: * 如果操作数都是数值,执行常规的除法计算,即两个正数或两个负数相除的结果还是正数,而如果只有一个操作数有符号,那么结果就是负数。如果商超过了 JavaScript 数值的表示范围,则返回 `Infinity` 或 `-Infinity`; * 如果有一个操作数是 `NaN`,则结果是 `NaN`; * 如果是 `Infinity` 被 `Infinity` 除,则结果是 `NaN`; * 如果是零被零除,则结果是 `NaN`; * 如果是非零的有限数被零除,则结果是 `Infinity` 或 `-Infinity`,取决于有符号操作数的符号; * 如果是 `Infinity` 被任何非零数值除,则结果是 `Infinity` 或 `-Infinity`,取决于有符号操作数的符号; * 如果有一个操作数不是数值,则在后台调用 `Number()` 将其转换为数值,然后再应用上面的规则。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-求模运算符)`%` 求模运算符 用于计算两个数值的余数,与另外两个乘性运算符类似,求模运算符会遵循下列特殊规则来处理特殊的值: * 如果操作数都是数值,执行常规的除法计算,返回除得的余数; * 如果被除数是无穷大值而除数是有限大的数值,则结果是 `NaN`; * 如果被除数是有限大的数值而除数是零,则结果是 `NaN`; * 如果是 `Infinity` 被 `Infinity` 除,则结果是 `NaN`; * 如果被除数是有限大的数值而除数是无穷大的数值,则结果是被除数; * 如果被除数是零,则结果是零; * 如果有一个操作数不是数值,则在后台调用 `Number()` 将其转换为数值,然后再应用上面的规则。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#加性运算符)加性运算符 加法和减法这两个加性运算符应该说是编程语言中最简单的算术运算符了。但是在 JavaScript 中,这两个运算符却都有一系列的特殊行为。与乘性运算符类似,加性运算符也会在后台转换不同的数据类型。然而,对于加性运算符而言,相应的转换规则还稍微有点复杂。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-加法运算符)`+` 加法运算符 如果两个运算符都是数值,执行常规的加法计算,然后根据下列规则返回结果: * 如果有一个操作数是 `NaN`,则结果是 `NaN`; * 如果是 `Infinity` 加 `Infinity`,则结果是 `Infinity`; * 如果是 `-Infinity` 加 `-Infinity`,则结果是 `-Infinity`; * 如果是 `Infinity` 加- `Infinity`,则结果是 `NaN`; * 如果是 `+0` 加 `+0`,则结果是 `+0`; * 如果是 `-0` 加 `-0`,则结果是 `-0`; * 如果是 `+0` 加 `-0`,则结果是 `+0`; 如果有一个操作数不是数值,那么就要应用如下规则: * 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来; * 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来。 * 如果有一个操作数是对象、数值或布尔值,则调用它们的 `toString()` 方法取得相应的字符串值,然后再应用前面关于字符串的规则。对于 `undefined` 和 `null`,则分别调用 `String()` 函数并取得字符串 `"undefined"` 和 `"null"`。 * 如果是 `null` 加 `null`,则结果是 `0`; * 如果是 `undefined` 加 `undefined`,则结果是 `NaN`; 下面来举几个例子: ~~~ var result1 = 5 + 5; // 两个数值相加 console.log(result1); // 10 var result2 = 5 + "5"; // 一个数值和一个字符串相加 console.log(result2); // "55" var num1 = 5; var num2 = 10; var message = "The sum of 5 and 10 is " + num1 + num2; console.log(message); // "The sum of 5 and 10 is 510",如何修改? ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#--减法运算符)`-` 减法运算符 如果两个运算符都是数值,执行常规的减法计算,然后根据下列规则返回结果: * 如果有一个操作数是 `NaN`,则结果是 `NaN`; * 如果是 `Infinity` 减 `Infinity`,则结果是 `NaN`; * 如果是 `-Infinity` 减 `-Infinity`,则结果是 `NaN`; * 如果是 `Infinity` 减 `-Infinity`,则结果是 `Infinity`; * 如果是 `-Infinity` 减 `Infinity`,则结果是 `-Infinity`; * 如果是 `+0` 减 `+0`,则结果是 `+0`; * 如果是 `+0` 减 `-0`,则结果是 `-0`; * 如果是 `-0` 减 `-0`,则结果是 `+0`; 如果有一个操作数不是数值,那么就要应用如下规则: * 如果有一个操作数是字符串、布尔值、`null` 或 `undefined`,则先在后台调用 `Number()` 函数将其转换为数值,然后再根据前面的规则执行减法计算。如果转换的结果是 `NaN`,则减法的结果就是 `NaN`; * 如果有一个操作数是对象,则调用对象的 `valueOf()` 方法以取得表示该对象的数值。如果得到的值是 `NaN`,则减法的结果就是 `NaN`。如果对象没有 `valueOf()` 方法,则调用其 `toString()`方法并将得到的字符串转换为数值。 * 如果是 `null` 减 `null`,则结果是 `0`; * 如果是 `undefined` 减 `undefined`,则结果是 `NaN`; 下面来举几个例子: ~~~ var result1 = 5 - true; // 4,因为true被转换成了1 var result2 = NaN - 1; // NaN var result3 = 5 - 3; // 2 var result4 = 5 - ""; // 5,因为"" 被转换成了0 var result5 = 5 - "2"; // 3,因为"2"被转换成了2 var result6 = 5 - null; // 5,因为null被转换成了0 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#等值运算符)等值运算符 确定两个变量是否相等是编程中的一个非常重要的操作。在比较简单数据类型之间的相等性时,问题还比较简单。但在涉及到对象之间的比较时,问题就变得复杂了。最早的 JavaScript 中的相等和不等运算符会在执行比较之前,先将对象转换成相似的类型。后来,有人提出了这种转换到底是否合理的质疑。最后,JavaScript 的解决方案就是提供两组运算符:相等和不相等(先转换再比较),恒等和不恒等(仅比较而不转换)。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#--运算符-1)`==` `!=` 运算符 `==` `!=` 这两个运算符都会先转换操作数(通常称为强制转型),然后再比较它们的相等性。在转换不同的数据类型时,相等和不相等运算符遵循下列基本规则: * 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值(`false` 转换为 `0`,而 `true` 转换为 `1`); * 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值; * 如果一个操作数是对象,另一个操作数不是,则调用对象的 `valueOf()` 方法,用得到的基本类型值按照前面的规则进行比较; * `null` 和 `undefined` 是相等的。 要比较相等性之前,不能将 `null` 和 `undefined` 转换成其他任何值。 * 如果有一个操作数是 `NaN`,则相等运算符返回 `false`,而不相等运算符返回 `true`。重要提示:即使两个操作数都是`NaN`,相等运算符也返回 `false`;因为按照规则,`NaN` 不等于 `NaN`。 * 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等运算符返回`true`;否则,返回 `false`。 列出了一些特殊情况及比较结果: ~~~ null == undefined // true "NaN" == NaN // false 5 == NaN // false NaN == NaN // false NaN != NaN // true false == 0 // true true == 1 // true true == 2 // false undefined == 0 // false null == 0 // false "5" == 5 // true ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#--运算符-2)`===` `!==` 运算符 除了在比较之前不转换操作数之外,恒等和不恒等运算符与相等和不相等运算符没有什么区别。它只在两个操作数未经转换就相等的情况下返回 `true`,如下面的例子所示: ~~~ var result1 = ("55" == 55); // true,因为转换后相等 var result2 = ("55" === 55); // false,因为不同的数据类型不相等 var result3 = (null == undefined) // true,因为它们是类似的值 var result4 = (null === undefined) // false,因为它们是不同类型的值 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#关系运算符)关系运算符 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#----运算符-1)`<` `>` `<=` `>=` 运算符 `<` 小于、`>` 大于、`<=` 小于等于、 `>=` 大于等于 这几个关系运算符用于对两个值进行比较返回一个布尔值。与 JavaScript 中的其他运算符一样,当关系运算符的操作数使用了非数值时,也要进行数据转换或完成某些奇怪的操作。以下就是相应的规则。 * 如果两个操作数都是数值,则执行数值比较。 * 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值(可以通过字符串的 `charCodeAt()` 函数获取字符编码值)。 * 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较。 * 如果一个操作数是对象,则调用这个对象的 `valueOf()` 方法,用得到的结果按照前面的规则执行比较。如果对象没有`valueOf()`方法,则调用 `toString()`方法,并用得到的结果根据前面的规则执行比较。 * 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较。 请思考下面几个例子的结果是如何得出的: ~~~ var result1 = "Brick" < "alphabet"; // true var result2 = "brick" < "alphabet"; // false var result3 = "23" < "3"; // true var result4 = "23" < 3; // false var result5 = "a" < 3; // false var result6 = NaN < 3; // false var result7 = NaN >= 3; // false ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#in-运算符)`in` 运算符 `in` 运算符希望它的左操作数是一个字符串或可以转换为字符串,希望它的右操作数是一个对象。如果右侧的对象拥有一个名为左操作数值的属性名,那么表达式返回 `true`,例如: ~~~ var point = { x:1, y:1 }; // 定义一个对象 "x" in point // true,对象有一个名为"x"的属性 "z" in point // false,对象中不存在名为"z"的属性 "toString" in point // true,对象继承了toString()方法 var data = [7,8,9]; // 拥有三个元素的数组 "0" in data // true,数组包含元素"0" 1 in data // true,数字转换为字符串 3 in data // false,没有索引为3的元素 ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#instanceof-运算符)`instanceof` 运算符 `instanceof` 运算符希望左操作数是一个对象,右操作数标识对象的类。如果左侧的对象是右侧类的实例,则表达式返回`true`;否则返回 `false`。后面会讲 JavaScript 中对象的类是通过初始化它们的构造函数来定义的。这样的话,`instanceof`的右操作数应当是一个函数。比如: ~~~ var d = new Date(); // 通过 Date() 构造函数来创建一个新对象 d instanceof Date; // true,d 是由 Date() 创建的 d instanceof Object; // true,所有的对象都是 Object 的实例 d instanceof Number; // false,d 不是一个 Number 对象 var a = [1, 2, 3]; // 通过数组字面量的写法创建一个数组 a instanceof Array; // true,a 是一个数组 a instanceof Object; // true,所有的数组都是对象 a instanceof RegExp; // false,数组不是正则表达式 ~~~ 需要注意的是,所有的对象都是 `Object` 的实例。当通过 `instanceof` 判断一个对象是否是一个类的实例的时候,这个判断也会包含对「父类」的检测。如果 `instanceof` 的左操作数不是对象的话,`instanceof` 返回 `false`。如果右操作数不是函数,则抛出一个类型错误异常。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#逻辑运算符)逻辑运算符 逻辑运算符是对操作数进行布尔算术运算,经常和关系运算符一起配合使用,逻辑运算符将多个关系表达式组合起来组成一个更复杂的表达式。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-逻辑与)`&&` 逻辑与 逻辑与操作可以应用于任何类型的操作数,而不仅仅是布尔值。在有一个操作数不是布尔值的情况下,逻辑与操作不一定返回布尔值;此时,它遵循下列规则: * 如果第一个操作数是对象,则返回第二个操作数; * 如果第二个操作数是对象,则只有在第一个操作数的求值结果为 `true` 的情况下才会返回该对象; * 如果两个操作数都是对象,则返回第二个操作数; * 如果有一个操作数是 `null`,则返回 `null`; * 如果有一个操作数是 `NaN`,则返回 `NaN`; * 如果有一个操作数是 `undefined`,则返回 `undefined`。 逻辑与操作属于短路操作,即如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值。对于逻辑与操作而言,如果第一个操作数是 `false`,无论第二个操作数是什么值,结果都不再可能是 `true` 了。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-逻辑或)`||` 逻辑或 与逻辑与操作相似,如果有一个操作数不是布尔值,逻辑或也不一定返回布尔值;此时,它遵循下列规则: * 如果第一个操作数是对象,则返回第一个操作数; * 如果第一个操作数的求值结果为 `false`,则返回第二个操作数; * 如果两个操作数都是对象,则返回第一个操作数; * 如果两个操作数都是 `null`,则返回 `null`; * 如果两个操作数都是 `NaN`,则返回 `NaN`; * 如果两个操作数都是 `undefined`,则返回 `undefined`。 与逻辑与运算符相似,逻辑或运算符也是短路运算符。也就是说,如果第一个操作数的求值结果为 `true`,就不会对第二个操作数求值了。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#--逻辑非)`!` 逻辑非 逻辑非操作可以应用于任何类型的操作数,无论这个值是什么数据类型,这个运算符都会返回一个布尔值。逻辑非运算符首先会将它的操作数转换为一个布尔值,然后再对其求反。逻辑非运算符遵循下列规则: * 如果操作数是一个对象,返回 `false`; * 如果操作数是一个空字符串,返回 `true`; * 如果操作数是一个非空字符串,返回 `false`; * 如果操作数是数值 `0`,返回 `true`; * 如果操作数是任意非 `0` 数值(包括 `Infinity`),返回 `false`; * 如果操作数是 `null`,返回 `true`; * 如果操作数是 `NaN`,返回 `true`; * 如果操作数是 `undefined`,返回 `true`。 下面几个例子展示了应用上述规则的结果: ~~~ console.log(!false); // true console.log(!"blue"); // false console.log(!0); // true console.log(!NaN); // true console.log(!""); // true console.log(!12345); // false ~~~ 逻辑非运算符也可以用于将一个值转换为与其对应的布尔值。而同时使用两个逻辑非运算符,实际上就会模拟 `Boolean()` 转型函数的行为。其中,第一个逻辑非操作会基于无论什么操作数返回一个布尔值,而第二个逻辑非操作则对该布尔值求反,于是就得到了这个值真正对应的布尔值。当然,最终结果与对这个值使用 `Boolean()` 函数相同,例如: ~~~ console.log(!!"blue"); //true console.log(!!0); //false console.log(!!NaN); //false console.log(!!""); //false console.log(!!12345); //true ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#位运算符)位运算符 在 JavaScript 中,当对数值应用位运算符时,后台会发生如下转换过程:64位的数值被转换成32位数值,然后执行位操作,最后再将32位的结果转换回64位数值。这个转换过程导致了一个严重的副效应,即在对特殊的 `NaN` 和 `Infinity` 值应用位操作时,这两个值都会被当成 `0` 来处理。如果对非数值应用位运算符,会先使用 `Number()` 函数将该值转换为一个数值,然后再应用位操作,得到的结果将是一个数值。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-按位非)`~` 按位非 简单的理解,对任一数值 `x` 进行按位非操作的结果为 `-(x+1)`。例如: ~~~ console.log(~null); // -1 console.log(~undefined); // -1 console.log(~0); // -1 console.log(~{}); // -1 console.log(~[]); // -1 console.log(~(1/0)); // -1 console.log(~false); // -1 console.log(~true); // -2 console.log(~1.2543); // -2 console.log(~4.9); // -5 console.log(~(-2.999)); // 1 ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-按位与)`&` 按位与 按位与操作就是将两个数值的每一位对齐,两个数值的对应位都是 `1` 时才返回 `1`,任何一位是 `0`,结果都是 `0`。如下表所示: | 第一个数值的位 | 第二个数值的位 | 结果 | | --- | --- | --- | | 1 | 1 | 1 | | 1 | 0 | 0 | | 0 | 1 | 0 | | 0 | 0 | 0 | #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-按位或)`|` 按位或 按位或操作就是将两个数值的每一位对齐,两个数值只要有一个位是 `1` 就返回 `1`,只在两个位都是 `0` 的情况下才返回 `0`。如下表所示: | 第一个数值的位 | 第二个数值的位 | 结果 | | --- | --- | --- | | 1 | 1 | 1 | | 1 | 0 | 1 | | 0 | 1 | 1 | | 0 | 0 | 0 | #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-按位异或)`^` 按位异或 按位异或与按位或的不同之处在于,两个数值只有一个 `1` 时才返回 `1`,如果对应的两位都是 `1` 或都是 `0`,则返回 `0`。 | 第一个数值的位 | 第二个数值的位 | 结果 | | --- | --- | --- | | 1 | 1 | 0 | | 1 | 0 | 1 | | 0 | 1 | 1 | | 0 | 0 | 0 | #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-左移)`<<` 左移 这个运算符会将数值的所有位向左移动指定的位数。例如: ~~~ var oldValue = 2; // 等于二进制的 10 var newValue = oldValue << 5; // 等于二进制的 1000000,十进制的 64 ~~~ 注意,左移不会影响操作数的符号位。换句话说,如果将 `-2` 向左移动 `5` 位,结果将是 `-64`,而非 `64`。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-有符号的右移)`>>` 有符号的右移 这个运算符会将数值向右移动,但保留符号位(即正负号标记)。 ~~~ var oldValue = 64; // 等于二进制的 1000000 var newValue = oldValue >> 5; // 等于二进制的 10 ,即十进制的 2 ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#-无符号的右移)`>>>` 无符号的右移 这个运算符会将数值的所有32位都向右移动。对正数来说,无符号右移的结果与有符号右移相同。 ~~~ var oldValue = 64; // 等于二进制的 1000000 var newValue = oldValue >>> 5; // 等于二进制的 10 ,即十进制的 2 ~~~ 无符号右移运算符会把负数的二进制码当成正数的二进制码。而且,由于负数以其绝对值的二进制补码形式表示,因此就会导致无符号右移后的结果非常之大。 ~~~ var oldValue = -64; // 等于二进制的 11111111111111111111111111000000 var newValue = oldValue >>> 5; // 等于十进制的 134217726 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#赋值运算符)赋值运算符 简单的赋值运算符由等于号 `=` 表示,其作用就是把右侧的值赋给左侧的变量,如下面的例子所示: ~~~ var num = 10; ~~~ 如果在等于号 `=` 前面再添加乘性运算符、加性运算符或位运算符,就可以完成复合赋值操作。这种复合赋值操作相当于是对下面常规表达式的简写形式: ~~~ var num = 10; num += 10; // 等同于 num = num + 10; ~~~ 每个主要算术运算符(以及个别的其他运算符)都有对应的复合赋值运算符。这些运算符如下所示: * 乘/赋值 `*=`; * 除/赋值 `/=`; * 模/赋值 `%=`; * 加/赋值 `+=`; * 减/赋值 `-=`; * 左移/赋值 `<<=`; * 有符号右移/赋值 `>>=`; * 无符号右移/赋值 `>>>=`。 设计这些运算符的主要目的就是简化赋值操作,使用它们不会带来任何性能的提升。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#条件运算符)条件运算符 `? :` 条件运算符应该算是 JavaScript 中最灵活的一种运算符了,而且它遵循与 Java 中的条件运算符相同的语法形式,如下面的例子所示: ~~~ variable = boolean_expression ? true_value : false_value; ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#逗号运算符)逗号运算符 逗号运算符多用于声明多个变量;但除此之外,逗号运算符还可以用于赋值。在用于赋值时,逗号运算符总会返回表达式中的最后一项,如下面的例子所示: ~~~ var num = (5, 1, 4, 8, 0); // num 的值为 0 ~~~ 由于 `0` 是表达式中的最后一项,因此 `num` 的值就是 `0`。虽然逗号的这种使用方式并不常见,但这个例子可以帮我们理解逗号的这种行为。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.5-Expression&Operators#关卡)关卡 ~~~ // 挑战一 var x=1; if(!!function f(){}){ x+=typeof f; } console.log(x); // ??? ~~~ ~~~ // 挑战二 (function f(f){ console.log(typeof f()); // ??? })(function(){return 1;}); ~~~ ~~~ // 挑战三 console.log(typeof 2*3); // ??? console.log(typeof 2+3); // ??? ~~~ ~~~ // 挑战四 var a=0,b=0; console.log(a+++b); // ??? console.log(a); // ??? console.log(b); // ??? ~~~ ~~~ // 挑战五 var a,b,c; a=b==c; console.log(a); // ??? ~~~ ~~~ // 挑战六 console.log(1 && 3); // ??? console.log(1 && "foo" || 0); // ??? console.log(1 || "foo" && 0); // ??? ~~~ ~~~ // 挑战七 var a=1; var b=(a=(2,4,6))+a++ console.log(b); // ??? ~~~ ~~~ // 挑战八 if (!("a" in window)) { var a = 1; } console.log(a); // ??? ~~~ ~~~ // 挑战九 var val = 'smtg'; console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing'); // ??? ~~~ ~~~ // 挑战九 console.log(1 + - + + + - + 1); ~~~
';

变量和数据类型

最后更新于:2022-04-01 21:00:34

# 变量和数据类型 当程序需要将值保存起来以备将来使用时,便将其赋值给一个变量,值的类型称作数据类型。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#变量)变量 JavaScript 的变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。定义变量时要使用关键字 `var` 来声明的,如下所示: ~~~ var message; ~~~ 这行代码定义了一个名为 `message` 的变量,该变量可以用来保存任何值(像这样未经过初始化的变量,会保存一个特殊的值`undefined`)。JavaScript 也支持直接初始化变量,因此在定义变量的同时就可以设置变量的值,如下所示: ~~~ var message = "hello"; ~~~ 此时,变量 `message` 中保存了一个字符串值 `"hello"`。像这样初始化变量并不会把它标记为字符串类型,因此,可以在修改变量值的同时修改值的类型。如下所示: ~~~ var message = "hello"; message = 100; // 有效的语句,不好的写法 ~~~ 在这个例子中,变量 `message` 一开始保存了一个字符串值 `"hello"`,然后该值又被一个数字值100取代。虽然我们不建议修改变量所保存值的类型,但这种操作在 JavaScript 中完全有效。 有一点必须注意,即使用 `var` 运算符定义的变量是的该作用域中的局部变量。也就是说,如果在函数中使用 `var` 定义一个变量,那么这个变量在函数退出后就会被销毁,例如: ~~~ function test(){ var message = "hello"; // 局部变量 } test(); console.log(message); // 产生错误 ~~~ 这里,变量 `message` 是在函数中使用 `var` 定义的,是局部变量。当函数被调用时,就会创建该变量并为其赋值。而在此之后,这个变量又会立即被销毁,因此例子中的下一行代码就会导致错误。不过,可以像下面这样省略 `var` 运算符,从而创建一个全局变量: ~~~ function test(){ message = "hello"; // 全局变量,不好的写法 } test(); console.log(message); // "hello" ~~~ 这个例子省略了 `var` 运算符,因而 `message` 就成了全局变量。这样,只要调用一次 `test()` 函数,这个变量就有了定义,就可以在函数外部的任何地方被访问到。 虽然省略 `var` 运算符可以定义全局变量,但这也不是推荐的做法,因为在局部作用域中定义全局变量很难维护,给未经声明的变量赋值在严格模式下会抛出 `ReferenceError` 错误。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#数据类型)数据类型 JavaScript 中有5种简单数据类型(也称为「基本数据类型」或「原始数据类型」):`Undefined`、`Null`、`Boolean`、`Number`、`String` 。还有1种复杂数据类型 `Object`,`Object` 本质上是由一组无序的名值对组成的。JavaScript 不支持任何创建自定义类型的机制,所有值最终都将是上述6种数据类型之一。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#typeof-运算符)`typeof` 运算符 鉴于 JavaScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型,`typeof` 就是负责提供这方面信息的运算符。对一个值使用 `typeof` 运算符可能返回下列某个字符串: * `"undefined"`,如果这个值未声明或已声明但未初始化。 * `"boolean"`,如果这个值是布尔值。 * `"string"`,如果这个值是字符串。 * `"number"`,如果这个值是数值。 * `"object"`,如果这个值是对象或 `null`。 * `"function"`,如果这个值是函数。 下面是几个使用 `typeof` 运算符的例子: ~~~ var message = "some string"; console.log(typeof message); // "string" console.log(typeof(message)); // "string" console.log(typeof 95); // "number" ~~~ 从以上例子可以看出,`typeof` 运算符既可以对变量使用,又可以对字面量使用。由于 `typeof` 是一个运算符而不是函数,因此例子中的圆括号尽管可以使用,但并不提倡。 `typeof null` 结果是 `"object"` 是历史遗留 Bug,在 ECMAScript 6中,曾经有提案为历史平反, 将 `type null` 的值纠正为`"null"`,但最后该提案被拒。理由是历史遗留代码太多,不如继续将错就错。 从技术角度讲,函数在 JavaScript 中是对象,不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过 `typeof` 运算符来区分函数和其他对象是有必要的。 > 扩展阅读「为什么 JavaScript 里面 `typeof null` 的值是 `"object"`?」 > [https://www.zhihu.com/question/21691758](https://www.zhihu.com/question/21691758) > > 扩展阅读「MDN 之 `typeof`」 > [https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof) > > 扩展阅读「JavaScript 检测原始值、引用值、属性」 > [http://shijiajie.com/2016/06/20/javascript-maintainable-javascript-validate1/](http://shijiajie.com/2016/06/20/javascript-maintainable-javascript-validate1/) > > 扩展阅读「JavaScript 检测之 basevalidate.js」 > [http://shijiajie.com/2016/06/25/javascript-maintainable-javascript-basevalidatejs/](http://shijiajie.com/2016/06/25/javascript-maintainable-javascript-basevalidatejs/) ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#undefined-类型)`Undefined` 类型 `Undefined` 类型只有1个值,即 `undefined`。使用 `var` 声明变量但未对其加以初始化时,这个变量的值就是 `undefined`,直接使用未声明的变量会产生错误。对未声明或已声明但未初始化的变量执行 `typeof` 运算符会返回 `"undefined"` 值,例如: ~~~ var message; // 这个变量声明之后默认取得了 undefined 值 // var age // 这个变量并没有声明 console.log(message); // "undefined" console.log(age); // 产生错误 console.log(typeof message); // "undefined" console.log(typeof age); // "undefined" ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#null-类型)`Null` 类型 `Null` 类型也只有1个值,即 `null`。它用来表示值的空缺。你可以认为 `undefined` 是表示系统级的、出乎意料的或类似错误的值的空缺,而 `null` 是表示程序级的、正常的或在意料之中的值的空缺。在下列场景中应当使用 `null`。 * 用来初始化一个变量,这个变量可能赋值为一个对象。 * 用来和一个已经初始化的变量比较,这个变量可以是也可以不是一个对象。 * 当函数的参数期望是对象时,作用参数传入。 * 当函数的返回值期望是对象时,作用返回值传出。 在下列场景中不应当使用 `null`。 * 不要使用 `null` 来检测是否传入了某个参数。 * 不要使用 `null` 来检测一个未初始化的变量。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#boolean-类型)`Boolean` 类型 `Boolean` 类型是 JavaScript 中使用得最多的一种类型,该类型只有两个字面值:`true` 和 `false`。需要注意的是,他们是区分大小写的,也就是说 `True` 和 `False`(以及其他的混合大小写形式)都不是 `Boolean` 值,只是标识符。 虽然 `Boolean` 类型的字面值只有两个,但 JavaScript 中所有类型的值都能使用 `if` 语句或 `Boolean()` 函数转换为对应的`Boolean` 值,例如: ~~~ var message = "Hello world!"; if (message){ console.log("Value is true."); // 被执行 } var messageAsBoolean = Boolean(message); console.log(messageAsBoolean); // true ~~~ 下表给出了各种数据类型及其对应的转换规则。 | 数据类型 | 转换为true的值 | 转换为false的值 | | --- | --- | --- | | Undefined | - | undefined | | Null | - | null | | Boolean | true | false | | String | 任何非空字符串 | ""(空字符串) | | Number | 任何非零数字值(包括无穷大) | 0和NaN | | Object | 任何对象 | - | ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#number-类型)`Number` 类型 `Number` 类型是 JavaScript 中最令人关注的数据类型,这种类型使用 IEEE 754 格式来表示整数和浮点数值(浮点数值在某些语言中也被称为双精度数值)。和其他编程语言不同,JavaScript 中的所有数字均用浮点数值表示。 > 扩展阅读「IEEE 754-1985」 > [https://en.wikipedia.org/wiki/IEEE_754-1985](https://en.wikipedia.org/wiki/IEEE_754-1985) #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#整数)整数 在 JavaScript 中进行算术计算时,所有以八进制和十六进制表示的数值最终都将被转换成十进制数值。例如: ~~~ var a = 10; // 十进制 var b = 023; // 八进制 var c = 0x12ac; // 十六进制 console.log(b); // 19 console.log(c); // 4780 ~~~ 八进制第一位必须是0,后面跟八进制序列0到7,如果超出了范围,则忽略前导0,后面的数值当做十进制解析,例如:089会被解析为89。(八进制字面量在严格模式下是无效的,会抛出错误。) 十六进制前两位必须是 0x 或 0X,后跟十六进制序列0~9、a~f(不区分大小写),如果超出了范围,则会报语法错误。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#浮点数)浮点数 所谓浮点数值,就是该数值中必须包含一个小数点,并且小数点后面必须至少有一位数字。虽然小数点前面可以没有整数,但我们不推荐这种写法。例如: ~~~ var a = 1.1; var b = 0.1; var c = .1; // 有效,但不推荐 ~~~ JavaScript 会不失时机的将浮点数转换成整数。例如: ~~~ var a = 5.; // 解析成整数5 var b = 5.0; // 解析成整数5 ~~~ 对于极大或者极小的数值,可采用科学技术法(也称e表示法)。JavaScript 会将那些小数点后面带有6个零以上的小于1的浮点数值转换为以e表示法表示的数值。例如: ~~~ var a = 3.14e7; // 等于31400000 var b = 3.14E-7; // 等于0.000000314 console.log(0.0000003); // 3e-7 ~~~ 浮点数值的最高精度是17位小数,但在进行算术计算时其精确度远远不如整数,例如: ~~~ console.log(0.1 + 0.2); // 0.30000000000000004 ~~~ 这个舍入误差会导致无法测试特定的浮点数值,因此,永远不要测试某个特定的浮点数值。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#正无穷负无穷)正无穷、负无穷 由于内存限制,JavaScript 能表示的数值范围从 `Number.MIN_VALUE` 到 `Number.MAX_VALUE`,并将超出范围的数转换成`Number.POSITIVE_INFINITY` 或 `Number.NEGATIVE_INFINITY`。0作为除数是不会报错的,正数除以0返回正无穷,负数除以0返回负无穷,0除以0返回`NaN`。例如: ~~~ console.log(Number.MAX_VALUE); // 最大数 1.7976931348623157e+308 console.log(Number.MIN_VALUE); // 最小数 5e-324 console.log(Number.POSITIVE_INFINITY); // 正无穷 Infinity console.log(Number.NEGATIVE_INFINITY); // 负无穷 -Infinity console.log( 1 / 0); // Infinity console.log(-1 / 0); // -Infinity ~~~ JavaScript 提供了 `isFinite()` 函数,来确定一个数是不是有穷的。例如: ~~~ console.log(isFinite(100)); // true console.log(isFinite(Infinity)); // false ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#nan)NaN `NaN`(not a number),是一个特殊的数值。之所以称它为「非数值」,是因为它不能参与算数运算,任何涉及 `NaN` 的操作都返回 `NaN`。并且 `NaN` 与任何值都不相等(包括自身)。例如: ~~~ console.log(typeof NaN); // "number" console.log(0 / 0); // NaN console.log(NaN - NaN); // NaN console.log(Infinity - Infinity); // NaN var a = NaN; console.log(a === a); // false ~~~ JavaScript 提供了 `isNaN()` 函数,来确定一个数是不是 `NaN`。例如: ~~~ console.log(isNaN(100)); // false console.log(isNaN("100")); // false console.log(isNaN(true)); // false console.log(isNaN("sss")); // true console.log(isNaN(NaN)); // true ~~~ #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#numberparseintparsefloat-转型函数)`Number()`、`parseInt()`、`parseFloat()` 转型函数 `isNaN()` 函数在接收到一个值之后,会尝试使用转型函数 `Number()` 将这个值转换为数值,转换规则如下: * `undefined` 转换为 `NaN`; * `null` 转换为 0; * `true` 转换为 `1`、`false` 转换为 `0`; * `number` 整数转换为十进制,小数不变; * `string` 如果只包含十进制数和小数,则返回对应的数值,如果只包含八进制数,则忽略前导0返回剩余部分,如果只包含十六进制,则返回十进制数,空字符串转换为0,其它字符串转换为 `NaN`; * `object` 先则调用对象的 `valueOf()` 方法,然后依照前面的规则转换返回的值。如果转换的结果是 `NaN`,则调用对象的`toString()` 方法,然后再次依照前面的规则转换返回的字符串值。 由于 `Number()` 转型函数在转换字符串时不够理想,因此还有两个专门用来转换字符串的函数 `parseInt()` 和 `parseFloat()`函数。 `parseInt()` 函数会忽略字符串前面的空格,直至找到第一个非空格字符,只要第一个非空格字符不是数字或者正负号,一律返回 `NaN`, 如果第一个非空格字符是数字字符,`parseInt()` 会继续解析第二个字符,直到解析完所有后续字符或者遇到了一个非数字字符。例如: ~~~ console.log(parseInt("")); // NaN(Number("")返回 0) console.log(parseInt("123S")); // 123 console.log(parseInt("12.4")); // 12 ~~~ `parseFloat()` 函数也会忽略字符串前面的空格,直至找到第一个非空格字符,只要第一个非空格字符不是数字或者正负号或者小数点,一律返回 `NaN`, 如果第一个非空格字符是上述字符之一,`parseFloat()` 会继续解析第二个字符,直到解析完所有后续字符或者遇到了一个非浮点数值。例如: ~~~ console.log(parseFloat("098.2")); // 98.2 console.log(parseFloat("123.23.23")); // 123.23 ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#string-类型)`String` 类型 `String` 类型用于表示由零或多个16位 Unicode 字符组成的字符序列,即字符串。字符串可以由双引号(")或单引号(')表示,因此下面两种字符串的写法都是有效的: ~~~ var firstName = "Nicholas"; var lastName = 'Zakas'; ~~~ JavaScript 中的这两种语法形式没有什么区别。用双引号表示的字符串和用单引号表示的字符串完全相同。不过,以双引号开头的字符串也必须以双引号结尾,而以单引号开头的字符串必须以单引号结尾。 `String` 数据类型包含一些特殊的字符字面量,也叫转义序列,用于表示非打印字符,或者具有其他用途的字符。例如:`\n`换行、`\t` 制表、`\b` 空格、`\r` 回车、`\f` 进纸、`\\` 斜杠、`\'` 单引号,在用单引号表示的字符串中使用、`\"` 双引号,在用双引号表示的字符串中使用。 转义字符可出现在字符串中的任意位置,且长度为1。如要在字符串中显示 `\` ,则必须使用 `\` 进行转义。例如: ~~~ console.log("\n\\".length); // 2 console.log("\\hello"); // "\hello"(长度为6) ~~~ 大部分值都可以使用继承而来的 `toString()`方法转换为字符串,但 `undefined` 和 `null` 值没有这个方法。对数值使用`toString()` 方法时,可以传入一个数字基数,以此输出对应进制的字符串值。例如: ~~~ console.log(true.toString()); // "true" var num = 10; console.log(num.toString()); // "10" console.log(num.toString(2)); // "1010" console.log(num.toString(8)); // "12" console.log(num.toString(16)); // "a" ~~~ 在不知道要转换的值是不是 `undefined` 或 `null` 的情况下,还可以使用转型函数 `String()`,这个函数能够将任何类型的值转换为字符串。`String()` 函数遵循下列转换规则: * 如果值有 `toString()` 方法,则调用该方法(没有参数)并返回相应的结果; * 如果值是 `undefined`,则返回 `"undefined"`; * 如果值是 `null`,则返回 `"null"`。 ~~~ var value; console.log(String(10)); // "10" console.log(String(true)); // "true" console.log(String(null)); // "null" console.log(String(value)); // "undefined" ~~~ ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#object-类型)`Object` 类型 JavaScript 中所有对象都继承自 `Object` 类型,每个对象都具有下列基本的属性和方法: * `constructor`:保存着用于创建当前对象的函数(构造函数)。 * `hasOwnProperty()`:用于检查给定的属性在当前对象实例中是否存在。 * `propertyIsEnumerable()`:用于检查给定的属性是否能够使用for-in语句来枚举。 * `isPrototypeOf()`:用于检查对象是否是传入对象的原型。 * `toString()` 方法:返回对象的字符串表示。 * `toLocaleString()`:返回对象的本地字符串表示。 * `valueOf()`:返回对象的字符串、数值或布尔值表示(通常与toString()方法的返回值相同)。 `Object` 本质上是由一组无序的名值对组成,「名称」部分是一个 JavaScript 字符串,「值」部分可以是任何 JavaScript 的数据类型(包括对象和方法)。这使用户可以根据具体需求,创建出相当复杂的数据结构。 以下两种方法都可以创建一个空对象,这两种方法在语义上是相同的。第二种更方便的方法叫作「对象字面量」法。这也是 JSON 格式的核心语法,一般我们优先选择第二种方法。例如: ~~~ var obj = new Object(); var obj = {}; // 好的写法 ~~~ 「对象字面量」也可以用来在对象实例中定义一个对象: ~~~ var obj = { name: "Carrot", "for": "Max", details: { color: "orange", size: 12 } } ~~~ 对象的属性可以通过链式(chain)表示方法进行访问: ~~~ obj.details.color; // orange obj["details"]["size"]; // 12 ~~~ 完成创建后,对象属性可以通过如下两种方式进行赋值和访问: ~~~ obj.name = "Simon" // 赋值 var name = obj.name; // 访问 obj["name"] = "Simon"; // 赋值 var name = obj["name"]; // 访问 ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.4-Variable&Types#关卡)关卡 ~~~ // 挑战一 console.log(typeof "undefined"); // ??? console.log(typeof null); // ??? ~~~ ~~~ // 挑战二 var message = "some string"; console.log(typeof massage); // ??? message = 10000; console.log(typeof message); // ??? ~~~ ~~~ // 挑战三 var a; var b = null; var c = {}; if(a && b && c){ console.log("true."); // ??? }else{ console.log("false."); // ??? } ~~~ ~~~ // 挑战四 console.log(typeof (0 / 0)); // ??? console.log(023 + 123); // ??? ~~~ ~~~ // 挑战五 console.log(Number("1234S")); // ??? console.log(parseInt("1234S")); // ??? ~~~ ~~~ // 挑战六 console.log(3.14E-7 === 0.000000314); // ??? console.log(0.1 + 0.6 === 0.7); // ??? console.log(0.1 + 0.7 === 0.8); // ??? console.log(NaN === NaN); // ??? ~~~ ~~~ // 挑战七 console.log("\right\now"); // ??? console.log("\right\now".length); // ??? console.log(010.toString(2)); // ??? ~~~ ~~~ // 挑战八 // 1、为 person、wife、child 对象新增 weight 属性,数值分别为 62、36、15。 // 2、为 person 对象新增二胎 child2 子对象,name 为 emma,其他属性自行发挥。 var person = { name: "stone", age: 30, wife: { name: "sohpie", age: 30 }, child:{ name: "tommy", age: 3 } } ~~~ > 挑战九,深度阅读下面两篇文章,提出你的疑问。 > > 「JavaScript 检测原始值、引用值、属性」 [http://shijiajie.com/2016/06/20/javascript-maintainable-javascript-validate1/](http://shijiajie.com/2016/06/20/javascript-maintainable-javascript-validate1/) > > 「JavaScript 检测之 basevalidate.js」 [http://shijiajie.com/2016/06/25/javascript-maintainable-javascript-basevalidatejs/](http://shijiajie.com/2016/06/25/javascript-maintainable-javascript-basevalidatejs/)
';

语法

最后更新于:2022-04-01 21:00:31

# 语法 JavaScript 的语法大量借鉴了 C 及其他类 C 语言(如 Java 和 Perl)的语法。因此,熟悉这些语言的开发人员在接受 JavaScript 更加宽松的语法时,一定会有种轻松自在的感觉。本章主要按照 ECMAScript 3介绍这门语言的基本概念,并就 ECMAScript 5的变化给出说明。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#字符集)字符集 JavaScript 程序是用 Unicode 字符集编写的,Unicode 是 ASCII 和 Latin-1 的超集,并支持地球上几乎所有在用的语言。ECMAScript 3要求 JavaScript 的实现必须支持 Unicode 2.1及后续版本,ECMAScript 5则要求支持 Unicode 3及后续版本。 > 扩展阅读「Unicode 与 JavaScript 详解」 > [http://www.ruanyifeng.com/blog/2014/12/unicode.html](http://www.ruanyifeng.com/blog/2014/12/unicode.html) ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#区分大小写)区分大小写 JavaScript 是区分大小写的。也就是说,关键字、变量、函数名和所有的标识符(identifier)都必须采取一致的大小写形式。比如,关键字 `while` 必须写成 `while`,而不能写成 `While` 或者 `WHILE`。同样,`online`、`Online`、`OnLine`、`ONLINE` 是4个不同的变量名。 但需要注意的是,HTML 并不区分大小写。由于它和客户端 JavaScript 联系紧密,因此这点区别很容易混淆。许多客户端 JavaScript 对象和属性与他们所表示的 HTML 标签和属性名相同。在 HTML 中,这些标签和属性名可以使用大写也可以是小写,而在 JavaScript 中则必须是小写。例如,在 HTML 中设置事件处理程序时,`onclick` 属性可以写成 `onClick`,但在 JavaScript 代码中,必须使用小写的 `onclick`。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#注释)注释 JavaScript 使用 C 风格的注释,包括单行注释和块级注释。单行注释以两个斜杠 `//` 开头,块级注释以一个斜杠和一个星号`/*` 开头,以一个星号和一个斜杠 `*/` 结尾。下面都是合法的 JavaScript 注释: ~~~ // 这里是单行注释 /* * 这里是块级注释 * 也叫多行注释 */ ~~~ 虽然上面注释中的第二和第三行都以一个星号开头,但这不是必须的,纯粹是为了提高注释的可读性(这种格式在企业级应用中极其常见)。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#字面量)字面量 所谓字面量(也可称直接量,Literal values),就是程序中直接使用的数据值。字面量只代表自身,不存储在特定位置。JavaScript 中的字面量有:字符串、数字、布尔值、对象、数组、函数、正则表达式,以及特殊的 `null` 值。 ~~~ "hello world" // 字符串 123 // 数字 1.2 // 小数 true // 布尔值 false // 布尔值 /javascript/gi // 正则表达式 null // 空 { name: 'stone', age: 20} // 对象 [ 1, 2, 3, 4, 5, 6, 7, 8 ] // 数组 function(){ console.log('good'); } // 函数 ~~~ > 扩展阅读「undefined不是字面量」 > [http://www.cnblogs.com/ziyunfei/archive/2012/11/11/2765096.html](http://www.cnblogs.com/ziyunfei/archive/2012/11/11/2765096.html) ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#严格模式)严格模式 在 ECMAScript 5引入了严格模式(strict mode)的概念。严格模式是为 JavaScript 定义了一种不同的解析与执行模式。在严格模式下,ECMAScript 3中的一些不确定的行为将得到处理,而且对某些不安全的操作也会抛出错误。要在整个脚本中启用严格模式,可以在顶部添加如下代码: ~~~ "use strict"; ~~~ 这行代码看起来像是字符串,而且也没有赋值给任何变量,但其实它是一个编译指示(pragma),用于告诉支持的 JavaScript 引擎切换到严格模式。这是为了不破坏 ECMAScript 3语法而特意选定的语法。 在函数内部的第一行包含这条编译指示,也可以指定函数在严格模式下执行: ~~~ function doSomething(){ "use strict"; // 函数体 } ~~~ 严格模式下,JavaScript 的执行结果会有很大不同,因此本课程会随时指出严格模式下的区别。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#标识符)标识符 所谓标识符,就是指变量、函数、属性的名字,或者函数的参数。JavaScript 标识符必须以字母、下划线(`_`)或美元符号(`$`)开始,后续的字符可以是字母、数字、下划线或美元符号(数字是不允许作为首字符出现的)。下面是合法的标识符: ~~~ my_variable_name, v12345, _dummy, $str888 ~~~ 标识符中的字母可以包含扩展的 ASCII 或 Unicode 字母字符(如 `π` 和 `∑`),但不推荐这样做。按照惯例,JavaScript 标识符采用驼峰大小写格式,也就是第一个字母小写,剩下的每个有意义的单词的首字母大写,例如: ~~~ firstSecond, myCar, doSomethingImportant ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#关键字和保留字)关键字和保留字 ECMAScript 3描述了一组具有特定用途的关键字,这些关键字可用于表示控制语句的开始或结束,或者用于执行特定操作等。按照规则,关键字是语言保留的,不能用作标识符。以下是 ECMAScript 3的全部关键字: ~~~ break delete function return typeof case do if switch var catch else in this void continue false instanceof throw while debugger finally new true with default for null try ~~~ ECMAScript 3还将 Java 的所有关键字都作为自己的保留字。尽管保留字还没有任何特定的用途,但他们有可能在将来被用作关键字: ~~~ abstract double goto native static boolean enum implements package super byte export import private synchronized char extends int protected throws class final interface public transient const float long short volatile ~~~ ECMAScript 5把非严格模式下的保留字缩减为: ~~~ class enum extends super const export import ~~~ ECMAScript 5在严格模式下的保留字为: ~~~ implements package public interface private static let protected yield ~~~ 注意,`let` 和 `yield` 是 ECMAScript 5新增的保留字,其他保留字都是 ECMAScript 3定义的。为了保证兼容性,任何时候都不建议使用 ECMAScript 5新增的保留字 `let` 和 `yield` 。 ECMAScript 还预定义了很多全局变量和函数,也应当避免把它们用作标识符: ~~~ arguments Error Math RegExp Array eval NaN String Boolean EvalError Number SyntaxError Date Function Object TypeError decodeURI Infinity parseFloat undefined decodeURIComponent isFinite parseInt URIError encodeURI isNaN RangeError encodeURIComponent JSON ReferenceError ~~~ JavaScript 的具体实现可能定义独有的全局变量和函数,每一种特定的 JavaScript 运行环境都有自己的一个全局属性列表,这一点是需要牢记的。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#可选的分号)可选的分号 和其他许多变成语言一样,JavaScript 使用分号(`;`)将语句分隔开。这对增强代码的可读性和整洁性是非常重要的。缺少分隔符,一条语句的结束就成了下一条语句的开始,反之亦然。如果语句各自独占一行,通常可以省略语句之间的分号(程序结尾或花括号 `}` 之前的分号也可以省略)。 ~~~ var sum = a + b // 即使没有分号也是有效的语句,不好的写法 var diff = a - b; // 有效的语句,好的写法 ~~~ 虽然语句结尾的分号不是必须的,但请任何时候都不要省略它。因为加上这个分号可以避免很多错误,开发人员也可以放心地通过删除多余的空格来压缩 JavaScript 代码。另外,加上分号也会在某些情况下增进代码的性能,因为这样解析解就不必再花时间推测应该在哪插入分号了。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.3-Syntax#关卡)关卡 请判断以下代码是否有效?如果有效请给出结果,如果无效请说明原因。 ~~~ // 挑战一 var class = 'person'; console.log(class); // ??? ~~~ ~~~ // 挑战二 var Class = 'person'; console.log(class); // ??? ~~~ ~~~ // 挑战三 var True = false; console.log(True); // ??? ~~~ ~~~ // 挑战四 var true = false; console.log(True); // ??? ~~~ ~~~ // 挑战五 var $_$ = 'stone'; console.log($_$); // ??? ~~~ ~~~ // 挑战六 var 00_name = 'stone'; console.log(00_name); // ??? ~~~ ~~~ // 挑战七 var Array = 'null'; console.log(Array); // ??? ~~~ ~~~ // 挑战八 var undefined = 'null'; console.log(undefined); // ??? ~~~ ~~~ // 挑战九 var result = 1 + 2 + 3 + 4 console.log(result); // ??? ~~~
';

初探

最后更新于:2022-04-01 21:00:29

# 初探 当学习一门新的编程语言的时候,应该边学边做,反复演练以加深理解。因此,你需要一个 JavaScript 解释器。幸运的是,每一个 Web 浏览器都包含一个 JavaScript 解释器。 可以通过在 HTML 文件里写一个 ` ~~~ > 在 HTML5 规范中,` ~~~ 包含在 `` 元素之间再包含额外的 JavaScript 代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。 ~~~ ~~~ 另外,通过 ` ~~~ > 在 HTML 中嵌入 JavaScript 代码虽然没有问题,但一般认为最好的做法还是尽可能使用外部文件来包含 JavaScript 代码。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.2-FirstExploration#元素的位置)元素的位置 按照惯例,所有的 ` ~~~ 可是这种做法意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成之后,才能开始呈现页面的内容(浏览器在遇到`` 元素时才开始呈现内容)。对于那些需要加载很多 JavaScript 代码的页面来说,会导致页面出现明显的延迟(浏览器窗口一片空白)。为了避免这个问题,最好的做法是把 ` ~~~ ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.2-FirstExploration#小结)小结 把 JavaScript 插入到 HTML 页面中要使用 ` 本页面用来测试 <script> 加载顺序~ ~~~
';

简介

最后更新于:2022-04-01 21:00:27

# 简介 JavaScript 是面向 Web 的编程语言,绝大多数现代网站都使用了 JavaScript,并且所有的现代 Web 浏览器(电脑,手机,平板)均包含了 JavaScript 解释器。 这使得 JavaScript 能够称得上史上使用最广泛的编程语言。 JavaScript 也是前端开发工程师必须掌握的三种技能之一:描述网页内容的 HTML、描述网页样式的 CSS、以及描述网页行为的 JavaScript。 JavaScript 是一门 动态的、弱类型的、面向对象的、解释型的 编程语言,非常适合面向对象和函数式的编程风格。JavaScript 的语法来自于 Java,它的一等函数(first-class function)来自于 Scheme,它的基于原型(prototype-based)的继承来自于 Self。但学习本课程不必去了解那些(Java/Scheme/Slef)语言或熟悉那些术语。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#javascript-起源)JavaScript 起源 > 时间:1995年 > > 人物:Brendan Eich(布兰登·艾奇) > > 背景:那个时候,绝大多数因特网用户都是用速度仅为28.8kbit/s的「猫」上网,为了完成简单的表单验证,必须把表单数据发送到服务器端才能确定用户是否没有填写某个必填域、是否输入了无效的值,每次操作需等待30秒以上才会有结果,这无疑是在慢性自杀。 > > 事件:当时走在技术革新最前沿的 Netscape 公司指派 Brendan Eich 开发一种客户端语言,用来处理这种简单的验证,它就是 JavaScript。JavaScript 原名 LiveScript,Netscape 为了搭上媒体热炒 Java 的顺风车,才把 LiveScript 改名为 JavaScript。(所以 Java 和 JavaScript 的关系,就相当于雷锋和雷峰塔的关系。) 自此以后,JavaScript 逐渐成为市面上常见浏览器必备的一项特色功能。如今,JavaScript 的用途早已不再局限于简单的数据验证,而是具备了与浏览器窗口及其内容等几乎所有方面交互的能力。今天的 JavaScript 已经成为一门功能全面的编程语言,能够处理复杂的计算和交互,拥有了闭包、匿名(lambda,拉姆达)函数,甚至元编程等特性。 JavaScript 从一个简单的输入验证器发展成为一门强大的编程语言,完全出乎人们的意料。应该说,它既是一门非常简单的语言,又是一门非常复杂的语言。说它简单,是因为学会使用它只需片刻功夫;而说它复杂,是因为要真正掌握它则需要数年时间。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#javascript-实现)JavaScript 实现 随着 JavaScript 的普及,微软公司在 IE 浏览器中也加入了名为 JScript 的 JavaScript 实现,由于没有标准规定 JavaScript 的语法和特性,导致市面上存在多个版本的 JavaScript 实现,且互不兼容,因此 JavaScript 标准化问题被提上了议事日程。 1997年,欧洲计算机制造商协会(ECMA,European Computer Manufactures Association)定义了名为ECMAScript(发音为`ek-ma-script`)的脚本语言标准,它是通用的,与平台无关的语言标准。自此以后,浏览器开发商开始致力于将 ECMAScript 作为各自 JavaScript 实现的基础。虽然基础相同,但具体实现在不同浏览器上却略有差异。 JavaScript 和 ECMAScript 通常被人们用来表达相同的含义,但 JavaScript 的含义却比 ECMAScript 中规定的要多得多。一个完整的 JavaScript 实现应该由下面三个不同的部分组成。 * 核心(ECMAScript) * 文档对象模型(DOM) * 浏览器对象模型(BOM) ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#核心ecmascript)核心(ECMAScript) ECMAScript 标准由 语法、数据类型、语句、关键字、保留字、运算符、对象 组成。它与 Web 浏览器没有任何依赖关系,并且这门语言本身并不包含输入和输出定义。ECMAScript 定义的只是这门语言的基础,而在此基础之上可以构建更完善的脚本语言。 Web 浏览器只是 ECMAScript 实现的宿主环境之一,其他宿主环境包括 Node 和 Adobe Flash。宿主环境不仅提供基本的 ECMAScript 实现,同时也会提供该语言的扩展(例如:DOM、BOM),这些扩展则利用 ECMAScript 的核心类型和语法提供更多更具体的功能。 JavaScript 是 Web 浏览器对 ECMAScript 标准的实现,ActionScript 是 Adobe Flash 对 ECMAScript 标准的实现。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#ecmascript-历史)ECMAScript 历史 > 1997年,ECMAScript 1版发布。 > > 1998年6月,ECMAScript 2版发布。 > > 1999年12月,ECMAScript 3版发布。 > > 2000年,ECMAScript 4开始酝酿,最终这个版本没有通过。 > > 2009年12月,ECMAScript 5版发布。 > > 2011年6月,ECMAscript 5.1版发布,成为国际标准。 > > 2015年6月,ECMAScript 6正式通过,成为国际标准。 > > 扩展阅读「[阮一峰](http://www.ruanyifeng.com/home.html) 的《ECMAScript 6入门》」 > [http://es6.ruanyifeng.com/#docs/intro](http://es6.ruanyifeng.com/#docs/intro) 3.0版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学3.0版的语法。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#文档对象模型dom)文档对象模型(DOM) 文档对象模型(DOM,Document Object Model)是用于 HTML 的应用程序编程接口(API),它把整个页面映射为一个多层节点结构。HTML 页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。看下面这个 HTML 页面: ~~~ Sample Page

Hello World!

~~~ 通过 DOM 创建的这个表示文档的树形图,开发人员获得了控制页面内容和结构的主动权。借助 DOM 提供的 API,开发人员可以轻松自如地删除、添加、替换或修改任何节点。 由于 Netscape 和 微软实现的 DOM 互不兼容,负责制定 Web 通信标准的 W3C(World Wide Web Consortium,万维网联盟)开始着手规划 DOM。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#dom-1级)DOM 1级: * DOM 核心:映射文档结构,简化对文档中任意部分的操作和访问。 * DOM HTML:在 DOM 核心的基础上,添加了针对 HTML 的对象和方法。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#dom-2级)DOM 2级: * DOM 视图:定义了跟踪不同文档视图的接口。 * DOM 事件:定义了事件和事件处理的接口。 * DOM 样式:定义了基于 CSS 为元素应用样式的接口。 * DOM 遍历和范围:定义了遍历和操作文档树的接口。 #### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#dom-3级)DOM 3级: * DOM 加载和保存:引入了以统一方式加载和保存文档的方法。 * DOM 验证:新增了验证文档的方法。 * DOM 核心扩展。 > 注意: > 1\. DOM 并不是只针对 JavaScript 的,很多别的语言也都实现了 DOM。 > 2\. DOM 0级标准是不存在的,它只是 DOM 历史坐标中的一个参照点而已。 ### [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#浏览器对象模型bom)浏览器对象模型(BOM) 浏览器对象模型(BOM,Browser Object Model)是用于浏览器的应用程序编程接口(API),它把整个浏览器窗口映射为一个对象。从根本上讲,BOM 只处理浏览器窗口和框架,但人们习惯上也把所有针对浏览器的 JavaScript 扩展算作 BOM 的一部分,例如: * 弹出新浏览器窗口的功能。 * 移动、缩放和关闭浏览器窗口的功能。 * 提供浏览器详细信息的 navigator 对象。 * 提供浏览器所加载页面的详细信息的 localtion 对象。 * 提供用户显示器分辨率详细信息的 screen 对象。 * 对 cookies 的支持。 * XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象。 BOM 最让人头疼的是没有相关的规范和标准,每个浏览器都有独有的实现,这个问题在 HTML5 中得到了解决,HTML5 致力于把很多 BOM 功能写入正式规范。 ## [](https://github.com/stone0090/javascript-lessons/tree/master/1.1-Introduction#小结)小结 JavaScript 是一种专为网页交互而设计的脚本语言,由下列3个不同的部分组成: * 核心(ECMAScript),由 ECMA-262 定义,提供核心语言功能。 * 文档对象模型(DOM),提供访问和操作网页内容的方法和接口。 * 浏览器对象模型(BOM),提供与浏览器交互的方法和接口。 JavaScript 的这3个组成部分,在当前5个主要浏览器(IE、FireFox、Chrome、Safari 和 Opera)中都得到了不同程度的支持。其中,所有浏览器对 ECMAScript 3版本的支持大体上都还不错,而对 ECMAScript 5的支持程度越来越高,但对 DOM 的支持则彼此相差比较多。对于已经正式纳入 HTML5 标准的 BOM 来说,尽管各浏览器都实现了某些众所周知的共同特性,但其他特性还是会因浏览器而异。 > 扩展阅读「Standard ECMA-262 5.1 Edition 」 > [http://www.ecma-international.org/ecma-262/5.1/](http://www.ecma-international.org/ecma-262/5.1/) > > 扩展阅读「ECMAScript 5浏览器兼容一览表」 > [http://caniuse.mojijs.com/Home/Html/item/key/es5/index.html](http://caniuse.mojijs.com/Home/Html/item/key/es5/index.html)
';

初级

最后更新于:2022-04-01 21:00:25

';

前言

最后更新于:2022-04-01 21:00:22

> 原文出处:https://github.com/stone0090/javascript-lessons ## 为何写作此课程 [stone](http://shijiajie.com/about/) 主要负责基于 Web 的企业内部管理系统的开发,虽然能够熟练地使用 JavaScript,但随着对 JavaScript 的理解越来越深,才发现自己尚未掌握其精髓。 2015年,公司请来 [大胡子姜志辉](http://weibo.com/zhihuijiang) 为我们培训 [JS Dota](http://ibagmarsoft.github.io/js_dota/) 课程,让我对 JavaScript 有了全新的认识,他一步步引导我们手写 jQuery、Backbone 等 JavaScript 主流框架,更是激发了我对 JavaScript 浓厚的兴趣。 俗话说「教是最好的学」,我是公司的兼职教员,正好借此机会梳理自己 JavaScript 知识体系,深入研究每个知识点,整理成培训课程分享给大家。 本课程之所以叫做《JavaScript 闯关记》,是因为每个章节都精心设计了挑战关卡,通过提供更多的实战机会,让大家可以循序渐进地、有目的地、有挑战地开展学习。
';