美文网首页Web前端之路程序员
你不知道的JavaScript(中卷)|值

你不知道的JavaScript(中卷)|值

作者: xpwei | 来源:发表于2017-10-23 17:06 被阅读52次

    数组
    和其他强类型语言不同,在JavaScript中,数组可以容纳任何类型的值,可以使字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的):

    var a = [1, "2", [3]];
    a.length; // 3
    a[0] === 1; // true
    a[2][0] === 3; // true
    

    使用delete运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的length属性并不会发生变化。
    数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内):

    var a = [ ];
    a[0] = 1;
    a["foobar"] = 2;
    a.length; // 1
    a["foobar"]; // 2
    a.foobar; // 2
    

    这里有个问题需要特别注意,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当做数字索引来处理:

    var a = [ ];
    a["13"] = 42;
    a.length; // 14
    

    类数组
    有时需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如indexOf(..)、concat(..)、forEach(..)等)来实现。
    例如,一些DOM查询操作会返回DOM元素列表,它们并非真正意义上的数组,但十分类似。另一个例子是通过arguments对象(类数组)将函数的参数当做列表来访问(从ES6开始已废止)。
    工具函数slice(..)经常被用于这类转换:

    function foo() {
        var arr = Array.prototype.slice.call(arguments);
        arr.push("bam");
        console.log(arr);
    }
    foo("bar", "baz"); // ["bar","baz","bam"]
    

    如上所示,slice()返回参数列表(上例中是一个类数组)的一个数组复本。
    用ES6中的内置工具函数Array.from(..)也能实现同样的功能:

    ...
    var arr = Array.from( arguments );
    ...
    

    字符串
    JavaScript中字符串是不可变的,而数组是可变的。
    字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。

    c = a.toUpperCase();
    a === c; // false
    a; // "foo"
    c; // "FOO"
    b.push( "!" );
    b; // ["f","O","o","!"]
    

    数组有一个字符串没有的可变更成员函数reverse():

    a.reverse; // undefined
    b.reverse(); // ["!","o","O","f"]
    b; // ["f","O","o","!"]
    

    可惜我们无法“借用”数组的可变更成员函数,因为字符串是不可变的:

    Array.prototype.reverse.call( a );
    // 返回值仍然是字符串"foo"的一个封装对象
    

    一个变通(破解)的办法是先将字符串转换为数组,待处理完后再将结果转换回字符串:

    var c = a
        // 将a的值转换为字符数组
        .split("")
        // 将数组中的字符进行倒转
        .reverse()
        // 将数组中的字符拼接回字符串
        .join("");
    c; // "oof"
    

    这个方法的确简单粗暴,但对简单的字符串却完全适用。

    数字
    JavaScript只有一个数值类型:number(数字),包括“整数”和带小数的十进制数。JavaScript没有真正意义上的整数,JavaScript中的“整数”就是没有小数的十进制数。所以42.0即等同于“整数”42。

    数字的语法
    JavaScript中的数字常量一般用十进制表示。特别大和特别小的数字默认用指数格式显示,与toExponential()函数的输出结果相同:

    var a = 5E10;
    a; // 50000000000
    a.toExponential(); // "5e+10"
    var b = a * a;
    b; // 2.5e+21
    var c = 1 / a;
    c; // 2e-11
    

    tofixed(..)方法可指定小数部分的显示位数:

    var a = 42.59;
    a.toFixed( 0 ); // "43"
    a.toFixed( 1 ); // "42.6"
    a.toFixed( 2 ); // "42.59"
    a.toFixed( 3 ); // "42.590"
    a.toFixed( 4 ); // "42.5900"
    

    toPrecision(..)方法用来指定有效数位的显示位数:

    var a = 42.59;
    a.toPrecision( 1 ); // "4e+1"
    a.toPrecision( 2 ); // "43"
    a.toPrecision( 3 ); // "42.6"
    a.toPrecision( 4 ); // "42.59"
    a.toPrecision( 5 ); // "42.590"
    a.toPrecision( 6 ); // "42.5900"
    

    较小的数值
    二进制浮点数最大的问题(不仅JavaScript,所有遵循IEEE754规范的语言都是如此),是会出现如下情况:

    0.1 + 0.2 === 0.3; // false
    

    从数学角度来说,上面的条件判断应该为true,可结果为什么是false呢?
    简单来说,二进制浮点数中的0.1和0.2并不是十分精确,它们相加的结果并非刚好等于0.3,而是一个比较接近的数字0.30000000000000004,所以条件判断结果为false。
    那应该怎样来判断0.1+0.2和0.3是否相等呢?
    最常见的方法是设置一个误差范围值,通常称为“机器精度”,对JavaScript的数字来说,这个值通常是2^-52(2.220446049250313e-16)。
    从ES6开始,该值定义在Number.EPSILON中,我们可以直接拿来用,也可以为ES6之前的版本写polyfill:

    if (!Number.EPSILON) {
        Number.EPSILON = Math.pow(2, -52);
    }
    

    可以使用Number.EPSILON来比较两个数字是否相等(在指定的误差范围内):

    function numbersCloseEnoughToEqual(n1, n2) {
        return Math.abs(n1 - n2) < Number.EPSILON;
    }
    var a = 0.1 + 0.2;
    var b = 0.3;
    numbersCloseEnoughToEqual(a, b); // true
    numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false
    

    能够呈现的最大浮点数大约是1.798e+308(这是一个相当大的数字),它定义在Number.MAX_VALUE中。最小浮点数定义在Number.MIN_VALUE中,大约是5e-324,它不是负数,但无限接近于0!

    整数的安全范围
    数字的呈现方式决定了“整数”的安全范围远远小于Number.MAX_VALUE。
    能够被“安全”呈现的最大整数是2^53 - 1,即9007199254740991,在ES6中被定义为Number.MAX_SAFE_INTEGER。最小整数是-9007199254740991,在ES6 中被定义为Number.MIN_SAFE_INTEGER。
    有时JavaScript程序需要处理一些比较大的数字,如数据库中的64位ID等。由于JavaScript的数字类型无法精确呈现64位数值,所以必须将它们保存(转换)为字符串。
    好在大数值操作并不常见(它们的比较操作可以通过字符串来实现)。如果确实需要对大数值进行数学运算,目前还是需要借助相关的工具库。将来JavaScript也许会加入对大数值的支持。

    整数检测
    要检测一个值是否是整数,可以使用ES6中的Number.isInteger(..)方法:

    Number.isInteger( 42 ); // true
    Number.isInteger( 42.000 ); // true
    Number.isInteger( 42.3 ); // false
    

    也可以为ES6之前的版本polyfill Number.isInteger(..)方法:

    if (!Number.isInteger) {
        Number.isInteger = function (num) {
            return typeof num == "number" && num % 1 == 0;
        };
    }
    

    要检测一个值是否是安全的整数,可以使用ES6中的Number.isSafeInteger(..)方法:

    Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
    Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
    Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true
    

    可以为ES6之前的版本polyfill Number.isSafeInteger(..)方法:

    if (!Number.isSafeInteger) {
        Number.isSafeInteger = function (num) {
            return Number.isInteger(num) &&
                Math.abs(num) <= Number.MAX_SAFE_INTEGER;
        };
    }
    

    特殊数值
    不是值的值
    undefined类型只有一个值,即undefined。null类型也只有一个值,即null。它们的名称既是类型也是值。
    undefined和null常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:

    • null指空值
    • undefined指没有值
      或者:
    • undefined指从未赋值
    • null指曾赋过值,但是目前没有值
      null是一个特殊关键字,不是标识符,我们不能将其当做变量来使用和赋值。然而undefined却是一个标识符,可以被当做变量来使用和赋值。

    undefined
    在非严格模式下,我们可以为全局标识符undefined赋值(这样的设计实在是欠考虑!):

    function foo() {
        undefined = 2; // 非常糟糕的做法!
    }
    foo();
    function foo() {
        "use strict";
        undefined = 2; // TypeError!
    }
    foo();
    

    在非严格和严格两种模式下,我们可以声明一个名为undefined的局部变量。再次强调最好不要这样做!

    function foo() {
        "use strict";
        var undefined = 2;
        console.log(undefined); // 2
    }
    foo();
    

    void运算符
    undefined是一个内置标识符(除非被重新定义),它的值为undefined,通过void运算符即可得到该值。
    表达式void __没有返回值,因此返回结果是undefined。void并不改变表达式的结果,只是让表达式不返回值:

    var a = 42;
    console.log( void a, a ); // undefined 42
    

    所以,void 0、void 1 和undefined 之间并没有实质上的区别。
    void运算符在其他地方也能派上用场,比如不让表达式返回任何结果(即使其有副作用)。

    function doSomething() {
        // 注: APP.ready 由程序自己定义
        if (!APP.ready) {
            // 稍后再试
            return void setTimeout(doSomething, 100);
        }
        var result;
        // 其他
        return result;
    }
    // 现在可以了吗?
    if (doSomething()) {
        // 立即执行下一个任务
    }
    

    这里setTimeout(..)函数返回一个数值(计时器间隔的唯一标识符,用来取消计时),但是为了确保if语句不产生误报,我们要void掉它。
    很多开发人员喜欢分开操作,效果都一样,只是没有使用void运算符:

    if (!APP.ready) {
        // 稍后再试
        setTimeout(doSomething, 100);
        return;
    }
    

    特殊的数字
    不是数字的数字
    如果数学运算的操作数不是数字类型(或者无法解析为常规的十进制或十六进制数字),就无法返回一个有效的数字,这种情况下返回值为NaN。

    var a = 2 / "foo"; // NaN
    typeof a === "number"; // true
    

    换句话说,“不是数字的数字”仍然是数字类型。
    NaN是一个“警戒值”,用于指出数字类型中的错误情况,即“执行数字运算没有成功,这是失败后返回的结果”。

    var a = 2 / "foo";
    a == NaN; // false
    a === NaN; // false
    

    NaN是一个特殊值,它和自身不相等,是唯一一个非自反的值。而NaN!=NaN为true。
    可以使用内建的全局工具函数isNaN(..)来判断一个值是否是NaN。

    var a = 2 / "foo";
    isNaN( a ); // true
    

    然而操作起来并非这么容易。isNaN(..)有一个严重的缺陷,它的检查方式过于死板,就是“检查参数是否不是NaN,也不是数字”。但是这样做的结果并不太精确:

    var a = 2 / "foo";
    var b = "foo";
    a; // NaN
    b; "foo"
    window.isNaN( a ); // true
    window.isNaN( b ); // true——晕!
    

    从ES6开始我们可以使用工具函数Number.isNaN(..)。ES6之前的浏览器的polifill如下:

    if (!Number.isNaN) {
    Number.isNaN = function(n) {
    return (
    typeof n === "number" &&
    window.isNaN( n )
    );
    };
    }
    var a = 2 / "foo";
    var b = "foo";
    Number.isNaN( a ); // true
    Number.isNaN( b ); // false——好!
    

    实际上还有一个更简单的方法,即利用NaN不等于自身这个特点。NaN是JavaScript中唯一一个不等于自身的值。于是我们可以这样:

    if (!Number.isNaN) {
        Number.isNaN = function (n) {
            return n !== n;
        };
    }
    

    无穷数

    var a = 1 / 0; // Infinity
    var b = -1 / 0; // -Infinity
    

    计算结果一旦溢出为无穷数就无法再得到有穷数。换句话说,就是你可以从有穷走向无穷,但无法从无穷回到有穷。

    零值
    JavaScript有一个常规的0(也叫+0)和一个-0。

    var a = 0 / -3; // -0
    var b = 0 * -3; // -0
    

    加法和减法运算不会得到负零。
    负零在开发调试控制台中通常显示为-0,但在一些老版本的浏览器中仍然会显示为0。
    根据规范,对负零进行字符串化会返回“0”:

    var a = 0 / -3;
    // 至少在某些浏览器的控制台中显示是正确的
    a; // -0
    // 但是规范定义的返回结果是这样!
    a.toString(); // "0"
    a + ""; // "0"
    String( a ); // "0"
    // JSON也如此,很奇怪
    JSON.stringify( a ); // "0"
    

    有意思的是,如果反过来将其从字符串转换为数字,得到的结果是准确的:

    +"-0"; // -0
    Number( "-0" ); // -0
    JSON.parse( "-0" ); // -0
    

    负零转换为字符串的结果令人费解,它的比较操作也是如此:

    var a = 0;
    var b = 0 / -3;
    a == b; // true
    -0 == 0; // true
    a === b; // true
    -0 === 0; // true
    0 > -0; // false
    a > b; // false
    

    要区分-0和0,不能仅仅依赖开发调试窗口的显示结果,还需要做一些特殊处理:

    function isNegZero(n) {
        n = Number(n);
        return (n === 0) && (1 / n === -Infinity);
    }
    isNegZero(-0); // true
    isNegZero(0 / -3); // true
    isNegZero(0); // false
    

    抛开学术上的繁枝缛节不论,我们为什么需要负零呢?
    有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位用来代表其他信息(比如移动的方向)。此时如果一个值为0的变量失去了它的符号位,它的方向信息就会丢失。所以保留0值的符号位可以防止这类情况发生。

    特殊等式
    ES6中新加入了一个工具方法Object.is(..)来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:

    var a = 2 / "foo";
    var b = -3 * 0;
    Object.is( a, NaN ); // true
    Object.is( b, -0 ); // true
    Object.is( b, 0 ); // false
    

    对于ES6之前的版本,Object.is(..)有一个简单的polyfill:

    if (!Object.is) {
        Object.is = function (v1, v2) {
            // 判断是否是-0
            if (v1 === 0 && v2 === 0) {
                return 1 / v1 === 1 / v2;
            }
            // 判断是否是NaN
            if (v1 !== v1) {
                return v2 !== v2;
            }
            // 其他情况
            return v1 === v2;
        };
    }
    

    能使用==和===时就尽量不要使用Object.is(..),因为前者效率更高,更为通用。Object.is(..)主要用来处理那些特殊的相等比较。

    值和引用
    JavaScript中没有指针,引用的工作机制也不尽相同。在JavaScript中变量不可能成为指向另一个变量的引用。
    由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向:

    var a = [1,2,3];
    var b = a;
    a; // [1,2,3]
    b; // [1,2,3]
    // 然后
    b = [4,5,6];
    a; // [1,2,3]
    b; // [4,5,6]
    

    函数参数就经常让人产生这样的困惑:

    function foo(x) {
        x.push(4);
        x; // [1,2,3,4]
        // 然后
        x = [4, 5, 6];
        x.push(7);
        x; // [4,5,6,7]
    }
    var a = [1, 2, 3];
    foo(a);
    a; // 是[1,2,3,4],不是[4,5,6,7]
    

    我们向函数传递a的时候,实际是将引用a的一个复本赋值给x,而a仍然指向[1,2,3]。在函数中我们可以通过引用x来更改数组的值(push(4)之后变为[1,2,3,4])。但x=[4,5,6]并不影响a的指向,所以a仍然指向[1,2,3,4]。我们不能通过引用x来更改引用a的指向,只能更改a和x共同指向的值。
    如果要将a的值变为[4,5,6,7],必须更改x指向的数组,而不是为x赋值一个新的数组:

    function foo(x) {
        x.push(4);
        x; // [1,2,3,4]
        // 然后
        x.length = 0; // 清空数组
        x.push(4, 5, 6, 7);
        x; // [4,5,6,7]
    }
    var a = [1, 2, 3];
    foo(a);
    a; // 是[4,5,6,7],不是[1,2,3,4]
    

    我们无法自行决定使用值复制还是引用复制,一切由值得类型来决定。
    如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值:

    foo( a.slice() );
    

    slice(..)不带参数会返回当前数组的一个浅复本。由于传递给函数的是指向该复本的引用,所以foo(..)中的操作不会影响a指向的数组。
    相反,如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递。

    function foo(wrapper) {
        wrapper.a = 42;
    }
    var obj = {
        a: 2
    };
    foo(obj);
    obj.a; // 42
    

    这样看来,如果需要传递指向标量基本类型值的引用,就可以将其封装到对应的数字封装对象中。
    与预期不同的是,虽然传递的是指向数字对象的引用复本,但我们并不能通过它来更改其中的基本类型值:

    function foo(x) {
        x = x + 1;
        x; // 3
    }
    var a = 2;
    var b = new Number(a); // Object(a)也一样
    foo(b);
    console.log(b); // 是2,不是3
    

    原因是标量基本类型值是不可更改的(字符串和布尔也是如此)。如果一个数字对象的标量基本类型值是2,那么该值就不能更改,除非创建一个包含新值得数字对象。
    x=x+1中,x中的标量基本类型值2从数字对象中拆封(或者提取)出来后,x就神不知鬼不觉地从引用变成了数字对象,它的值为2+1等于3。然而函数外的b仍然指向原来那个值为2的数字对象。
    相对而言,前面用obj作为封装对象的办法可能更好一些。这并不是说数字等封装对象没有什么用,只是多数情况下我们应该优先考虑使用标量基本类型。

    想念,如果会有声音。我想,那是悲伤的哭泣

    相关文章

      网友评论

        本文标题:你不知道的JavaScript(中卷)|值

        本文链接:https://www.haomeiwen.com/subject/qdhiuxtx.html