美文网首页
中卷(1.2)

中卷(1.2)

作者: 风声233 | 来源:发表于2018-08-12 21:22 被阅读10次

    内容大纲:
    js 中的数组是通过数字索引的一组任意类型的值。字符串和数组相似,但是它们的行为特征不同, 将字符串作为数组来处理时需要特别小心。js 中的数字包括“整数”和“浮点型”。
    基本类型中定义了几个特殊的值。
    null 类型只有一个值 nullundefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefined。void 运算符返回 undefined。
    数字类型有几个特殊的值,包括 NaN+Infinity-Infinity-0
    简单标量基本类型值通过值复制来赋值 / 传递,而复合值通过引用复制来赋值 / 传递。

    数组

    JS 数组可以容纳任何类型的值,而且不需要预先设定大小

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

    数组通过数字进行索引,但有趣的是他们也是 对象,所以也可以包含字符串键值和属性(但这些并不计算在数组的长度内):

    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(..) 等)来实现。

    字符串

    字符串经常成为字符数组。字符串的内部究竟有没有使用数组并不好说,但 js 中的字符串和字符数组并不是一回事,最多只是看上去相似而已。
    字符串和数组的确很相似,他们都是类数组,都有 length 属性以及 indexOf(..) 和 concat(..) 方法

    var a = "foo";
    var b = ["f","o","o"];
    
    a.length; //3
    b.length; //3
    
    a.indexOf("o"); // 1
    b.indexOf("o"); // 1
    
    var c = a.concat("bar"); // "foobar"
    var d = b.concat(["b","a","r"]); // ["f","o","o","b","a","r"];
    
    a === c; // false
    b === d; // false
    
    a; // "foo"
    b; // ["f","o","o"]
    

    但这并不意味着它们都是“字符数组”,比如:

    a[1] = "0";
    b[1] = "0";
    
    a; // "foo"
    b; // ["f","0","o"]
    

    Js 中字符串是不可变的,而数组是可变的。并且 a[1] 在 js 中并非总是会和法语发,在老版本的 IE 中就不允许(现在可以了)。正确的方法应该是 a.charAt(1)。

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

    c = a.toUpperCase();
    a === c; //false
    a; // "foo"
    c; // "FOO"
    

    许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”
    数组中的非变更方法来处理字符串:

    a.join; // undefined
    a.map; //undefined
    
    var c = Array.prototype.join.call( a, "-" );
    var d = Array.prototype.map.call( a, function(v){
      return v.toUpperCase() + ".";
    } ).join( "" );
    
    c; // "f-o-o"
    d; // "F.O.O."
    

    另一个不同点在于字符串反转( js 常见面试题)。数组有一个字符串没有的可变更成员函数 reverse();

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

    可惜我们无法“借用”数组的可变更成员函数,因为字符串是不可变的。
    一个变通的方法是先将字符串转换为数组,待处理完后再将其结果转换回字符串:

    var c = a.split("").reverse.join("");
    
    c; // "oof";
    

    这种方法对简单的字符串完全适用,但对复杂字符(Unicode,如星号、多字节字符等)的字符串不适用。

    数字

    特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。
    由于数字只可以使用 Number 对象进行封装,因此数字值可以调用 Number.prototype 中的方法。例如,toFixed(..) 方法可指定小数部分的显示位数

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

    请注意上面的输出结果实际上是给定数字的字符串形式。

    而 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"
    

    上面的方法不仅适用于数字变量,也适用于数字字面量。不过对于 . 运算符要特别注意,因为它是一个有效的数字字符,会被优先识别为数字字面量的一部分,然后才是对象属性访问运算符。

    //无效语法:
    42.toFixed( 3 ); // SyntaxError
    
    //下面的有效:
    (42).toFixed( 3 ); // "42.000"  
    

    42.toFixed( 3 ) 是无效语法,因为 . 被视为常量 42. 的一部分,所以没有 . 属性访问运算符来调用 toFixed 方法。
    42..toFixed( 3 )42 .toFixed( 3 ) 则没有问题,但也比较少见。

    比较较小的数值

    二进制浮点数最大的问题(不仅 js ,所有遵循 IEEE 754 规范的语言都是如此),是会出现如下的情况:
    0.1 + 0.2 === 0.3; // false
    简单来说,二进制浮点数中的 0.1 和 0.2 并不是十分精确,他们相加的结果并非刚好等于 0.3。

    问题是,如果一些数字无法做到十分精确,是否意味着数字类型毫无作用呢?答案是否定的。
    在处理带有小数的数字时需要特别注意。很多程序只需要处理整数,最大不超过百万或者百亿,此时使用 js 的数字类型时绝对安全的。

    那么应该怎么判断 0.1 + 0.2 和 0.3 是否相等呢?

    最常见的方法时设置一个误差范围值,通常成为“机器精度”(machine epsilon),对 js 的数字来说,这个值通常是 2^-52。
    从 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
    
    整数的安全范围

    有时 js 程序需要处理一些比较大的数字,如数据库中的64位 ID 等。由于 js 的数字类型无法精确呈现64位数值,所以必须将它们保存(转换)位字符串。

    整数检测

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

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

    要检测一个值是否是安全的整数,可以使用 ES6 中的 Number.isSafeInteger(..) 方法,也可以为 ES6 之前的版本写 polyfill:

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

    两个不是值的值: undefined 和 null。
    它们的名称既是类型也是值。
    两者之间有一些细微的差别,例如:

    • null 指控制(empty value)
    • undefined 指没有值(missing value)

    或者:

    • undefined 指从未赋过值
    • null 指曾赋过值,但是目前没有值

    null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。

    void 运算符

    undefined 是一个内置标识符,它的值为 undefined,通过void运算符可以得到该值。

    表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值:

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

    void运算符在某些地方上也能配上用场,比如不让表达式返回任何结果(即使其有副作用)。
    例如:

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

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

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

    特殊的数字

    1.不是数字的数字:NaN

    NaN 意指“不是一个数字”(not a number),这个名字容易引起误会。将它理解为“无效数值”可能更准确些。
    NaN是一个特殊的之,它和自身不相等,是唯一一个非自反的值。而 NaN != NaN 为 true。
    那么我们该如何判断它呢?

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

    然而 isNaN(..) 有一个严重的缺陷,它的检查方式过于死板,例如:

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

    这个 bug 自 js 问世以来一直存在。
    从 ES6 开始我们可以使用工具函数 Number.isNaN(..)。ES6 之前的浏览器的 polyfill 如下:

    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 是 js 中唯一一个不等于自身的值。

    if(!Number.isNaN){
      Number.isNaN = function(n){
        return n !== n;
      }
    }
    
    2.无穷数
    var a = 1 / 0; // Infinity
    var b = -1 / 0; // -Infinity
    

    js 使用有限数字表示法,所以和纯粹的数字运算不同,js 的运算结果有可能溢出,此时结果为 Infinity 或者 -Infinity。例如:

    var a = Number.MAX_VALUE;
    a + a; // Infinity
    

    那么无穷除以无穷呢?因为从数学运算和 js 语言的角度来说, Infinity/Infinity 是一个未定义操作,结果为 NaN。

    有穷正数除以 Infinity 呢?结果为 0。有穷负数除以 Infinity 呢?答案是 -0。

    3.零值
    var a = 0 / -3; // -0
    var b = 0 * -3; // -0
    

    加法和减法运算不会得到负零

    根据规范,对负零进行字符串化会返回 “0”;

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

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

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

    -0 的比较操作:

    -0 == 0; // true
    -0 === 0; // true
    0 > -0; // false
    

    如何判断 -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 的变量失去了他的符号位,它的方向信息就会丢失。所以保留 0 值得符号位可以防止此情况发生。

    特殊等式

    ES6 中新加入了一个工具方法 Object.is(..) 来判断两个值是否相等,可以用来处理 NaN 和负零的情况:

    Object.is( NaN, NaN ); // true
    Object.is( -0, -0 ); // true
    Object.si( 0, -0 ); // false
    

    对于 ES6 之前的版本,有一个简单的 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(..),因为前者效率更高,更为通用,后者主要用来处理那些特殊的相等比较。

    值和引用

    var a = 2;
    var b = a; // b是a的值的一个复本
    b++;
    a; // 2
    b; // 3
    
    var c = [1,2,3];
    var d = c; // d是[1,2,3]的一个引用
    d.push( 4 );
    c; // [1,2,3,4]
    d; // [1,2,3,4]
    
    • 简单值(即标量基本类型值)总是通过值赋值的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。
    • 复合值 ——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递

    由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。

    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]
    

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

    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 b = new Number( 2 ); // Object(a) 也一样
    foo( b ); 
    b; // 是2,不是3!
    

    原因是标量基本类型值是不可更改的(字符串和布尔也是如此)。如果一个数字对象的标量基本类型值是2,那么该值就不能更改,除非创建一个包含新值的数字对象。

    阅读下一篇

    相关文章

      网友评论

          本文标题:中卷(1.2)

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