JavaScript高级程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

5.4 单例内置对象

ECMA-262对内置对象的定义是“任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括Object、Array和String。本节介绍ECMA-262定义的另外两个单例内置对象:Global和Math。

5.4.1 Global

Global对象是ECMAScript中最特别的对象,因为代码不会显式地访问它。ECMA-262规定Global对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成Global对象的属性。本书前面介绍的函数,包括isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是Global对象的方法。除了这些,Global对象上还有另外一些方法。

1.URL编码方法

encodeURI()和encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器。有效的URI不能包含某些字符,比如空格。使用URI编码方法来编码URI可以让浏览器能够理解它们,同时又以特殊的UTF-8编码替换掉所有无效字符。

ecnodeURI()方法用于对整个URI进行编码,比如"www.wrox.com/illegal value.js"。而encodeURIComponent()方法用于编码URI中单独的组件,比如前面URL中的"illegal value.js"。这两个方法的主要区别是,encodeURI()不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号,而encodeURIComponent()会编码它发现的所有非标准字符。来看下面的例子:

    let uri = "http://www.wrox.com/illegal value.js#start";
    // "http://www.wrox.com/illegal%20value.js#start"
    console.log(encodeURI(uri));
    // "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
    console.log(encodeURIComponent(uri));

这里使用encodeURI()编码后,除空格被替换为%20之外,没有任何变化。而encodeURI-Component()方法将所有非字母字符都替换成了相应的编码形式。这就是使用encodeURI()编码整个URI,但只使用encodeURIComponent()编码那些会追加到已有URI后面的字符串的原因。

注意 一般来说,使用encodeURIComponent()应该比使用encodeURI()的频率更高,这是因为编码查询字符串参数比编码基准URI的次数更多。

与encodeURI()和encodeURIComponent()相对的是decodeURI()和decodeURIComponent()。decodeURI()只对使用encodeURI()编码过的字符解码。例如,%20会被替换为空格,但%23不会被替换为井号(#),因为井号不是由encodeURI()替换的。类似地,decodeURIComponent()解码所有被encodeURIComponent()编码的字符,基本上就是解码所有特殊值。来看下面的例子:

    let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start";
    // http%3A%2F%2Fwww.wrox.com%2Fillegal value.js%23start
    console.log(decodeURI(uri));
    // http:// www.wrox.com/illegal value.js#start
    console.log(decodeURIComponent(uri));

这里,uri变量中包含一个使用encodeURIComponent()编码过的字符串。首先输出的是使用decodeURI()解码的结果,可以看到只用空格替换了%20。然后是使用decodeURIComponent()解码的结果,其中替换了所有特殊字符,并输出了没有包含任何转义的字符串。(这个字符串不是有效的URL。)

注意 URI方法encodeURI()、encodeURIComponent()、decodeURI()和decodeURI-Component()取代了escape()和unescape()方法,后者在ECMA-262第3版中就已经废弃了。URI方法始终是首选方法,因为它们对所有Unicode字符进行编码,而原来的方法只能正确编码ASCII字符。不要在生产环境中使用escape()和unescape()。

2.eval()方法

最后一个方法可能是整个ECMAScript语言中最强大的了,它就是eval()。这个方法就是一个完整的ECMAScript解释器,它接收一个参数,即一个要执行的ECMAScript(JavaScript)字符串。来看一个例子:

    eval("console.log('hi')");

上面这行代码的功能与下一行等价:

    console.log("hi");

当解释器发现eval()调用时,会将参数解释为实际的ECMAScript语句,然后将其插入到该位置。通过eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意味着定义在包含上下文中的变量可以在eval()调用内部被引用,比如下面这个例子:

    let msg = "hello world";
    eval("console.log(msg)");   // "hello world"

这里,变量msg是在eval()调用的外部上下文中定义的,而console.log()显示了文本"hello world"。这是因为第二行代码会被替换成一行真正的函数调用代码。类似地,可以在eval()内部定义一个函数或变量,然后在外部代码中引用,如下所示:

    eval("function sayHi() { console.log('hi'); }");
    sayHi();

这里,函数sayHi()是在eval()内部定义的。因为该调用会被替换为真正的函数定义,所以才可能在下一行代码中调用sayHi()。对于变量也是一样的:

    eval("let msg = 'hello world';");
    console.log(msg);   // Reference Error: msg is not defined

通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在eval()执行的时候才会被创建。

在严格模式下,在eval()内部创建的变量和函数无法被外部访问。换句话说,最后两个例子会报错。同样,在严格模式下,赋值给eval也会导致错误:

    "use strict";
    eval = "hi";   // 导致错误

注意 解释代码字符串的能力是非常强大的,但也非常危险。在使用eval()的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对XSS利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。

3.Global对象属性

Global对象有很多属性,其中一些前面已经提到过了。像undefined、NaN和Infinity等特殊值都是Global对象的属性。此外,所有原生引用类型构造函数,比如Object和Function,也都是Global对象的属性。下表列出了所有这些属性。

4.window对象

虽然ECMA-262没有规定直接访问Global对象的方式,但浏览器将window对象实现为Global对象的代理。因此,所有全局作用域中声明的变量和函数都变成了window的属性。来看下面的例子:

    var color = "red";
    function sayColor() {
      console.log(window.color);
    }
    window.sayColor(); // "red"

这里定义了一个名为color的全局变量和一个名为sayColor()的全局函数。在sayColor()内部,通过window.color访问了color变量,说明全局变量变成了window的属性。接着,又通过window对象直接调用了window.sayColor()函数,从而输出字符串。

注意 window对象在JavaScript中远不止实现了ECMAScript的Global对象那么简单。关于window对象的更多介绍,请参考第12章。

另一种获取Global对象的方式是使用如下的代码:

    let global = function() {
      return this;
    }();

这段代码创建一个立即调用的函数表达式,返回了this的值。如前所述,当一个函数在没有明确(通过成为某个对象的方法,或者通过call()/apply())指定this值的情况下执行时,this值等于Global对象。因此,调用一个简单返回this的函数是在任何执行上下文中获取Global对象的通用方式。

5.4.2 Math

ECMAScript提供了Math对象作为保存数学公式、信息和计算的地方。Math对象提供了一些辅助计算的属性和方法。

注意 Math对象上提供的计算要比直接在JavaScript实现的快得多,因为Math对象上的计算使用了JavaScript引擎中更高效的实现和处理器指令。但使用Math计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。

1.Math对象属性

Math对象有一些属性,主要用于保存数学中的一些特殊值。下表列出了这些属性。

这些值的含义和用法超出了本书的范畴,但都是ECMAScript规范定义的,并可以在你需要时使用。

2.min()和max()方法

Math对象也提供了很多辅助执行简单或复杂数学计算的方法。

min()和max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数,如下面的例子所示:

    let max = Math.max(3, 54, 32, 16);
    console.log(max);   // 54
    let min = Math.min(3, 54, 32, 16);
    console.log(min);   // 3

在3、54、32和16中,Math.max()返回54, Math.min()返回3。使用这两个方法可以避免使用额外的循环和if语句来确定一组数值的最大最小值。

要知道数组中的最大值和最小值,可以像下面这样使用扩展操作符:

    let values = [1, 2, 3, 4, 5, 6, 7, 8];
    let max = Math.max(...val);

3.舍入方法

接下来是用于把小数值舍入为整数的4个方法:Math.ceil()、Math.floor()、Math.round()和Math.fround()。这几个方法处理舍入的方式如下所述。

❑ Math.ceil()方法始终向上舍入为最接近的整数。

❑ Math.floor()方法始终向下舍入为最接近的整数。

❑ Math.round()方法执行四舍五入。

❑ Math.fround()方法返回数值最接近的单精度(32位)浮点值表示。

以下示例展示了这些方法的用法:

    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.fround(0.4));   // 0.4000000059604645
    console.log(Math.fround(0.5));   // 0.5
    console.log(Math.fround(25.9)); // 25.899999618530273
    console.log(Math.floor(25.9));   // 25
    console.log(Math.floor(25.5));   // 25
    console.log(Math.floor(25.1));   // 25

对于25和26(不包含)之间的所有值,Math.ceil()都会返回26,因为它始终向上舍入。Math.round()只在数值大于等于25.5时返回26,否则返回25。最后,Math.floor()对所有25和26(不包含)之间的值都返回25。

4.random()方法

Math.random()方法返回一个0~1范围内的随机数,其中包含0但不包含1。对于希望显示随机名言或随机新闻的网页,这个方法是非常方便的。可以基于如下公式使用Math.random()从一组整数中随机选择一个数:

    number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)

这里使用了Math.floor()方法,因为Math.random()始终返回小数,即便乘以一个数再加上一个数也是小数。因此,如果想从1~10范围内随机选择一个数,代码就是这样的:

    let num = Math.floor(Math.random() * 10 + 1);

这样就有10个可能的值(1~10),其中最小的值是1。如果想选择一个2~10范围内的值,则代码就要写成这样:

    let num = Math.floor(Math.random() * 9 + 2);

2~10只有9个数,所以可选总数(total_number_of_choices)是9,而最小可能的值(first_possible_value)是2。很多时候,通过函数来算出可选总数和最小可能的值可能更方便,比如:

    function selectFrom(lowerValue, upperValue) {
      let choices = upperValue - lowerValue + 1;
      return Math.floor(Math.random() * choices + lowerValue);
    }
    let num = selectFrom(2,10);
    console.log(num);   // 2~10 范围内的值,其中包含2 和10

这里的函数selectFrom()接收两个参数:应该返回的最小值和最大值。通过将这两个值相减再加1得到可选总数,然后再套用上面的公式。于是,调用selectFrom(2,10)就可以从2~10(包含)范围内选择一个值了。使用这个函数,从一个数组中随机选择一个元素就很容易,比如:

    let colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
    let color = colors[selectFrom(0, colors.length-1)];

在这个例子中,传给selecFrom()的第二个参数是数组长度减1,即数组最大的索引值。

注意 Math.random()方法在这里出于演示目的是没有问题的。如果是为了加密而需要生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用window.crypto. getRandomValues()。

5.其他方法

Math对象还有很多涉及各种简单或高阶数运算的方法。讨论每种方法的具体细节或者它们的适用场景超出了本书的范畴。不过,下表还是总结了Math对象的其他方法。

即便这些方法都是由ECMA-262定义的,对正弦、余弦、正切等计算的实现仍然取决于浏览器,因为计算这些值的方式有很多种。结果,这些方法的精度可能因实现而异。