美文网首页
《你不知道的JavaScript(中卷)》

《你不知道的JavaScript(中卷)》

作者: sunxiaochuan | 来源:发表于2018-08-02 15:46 被阅读0次

    前言

    本文作为对本书的一些知识点的收集

    正文

    1. 类型

    ECMAScript 语言类型包括 Undefined、Null、Boolean、String、Number 和 Object。

    1. 内置类型
    • JavaScript 有七种内置类型:
      • 空值(null)
      • 未定义(undefined)
      • 布尔值( boolean)
      • 数字(number)
      • 字符串(string)
      • 对象(object)
      • 符号(symbol,ES6 中新增)

      除对象 object 之外,其他统称为“基本类型”。

    1. 小结

    JavaScript 有 七 种 内 置 类 型:nullundefinedbooleannumberstringobjectsymbol,可以使用 typeof 运算符来查看。

    变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
    很多开发人员将 undefinedundeclared 混为一谈,但在 JavaScript 中它们是两码事。

    undefined 是值的一种。undeclared 则表示变量还没有被声明过。
    遗憾的是,JavaScript 却将它们混为一谈,在我们试图访问 "undeclared" 变量时这样报错:ReferenceError: a is not defined,并且 typeofundefinedundeclared 变量都返回 "undefined"。

    然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的办法。

    2. 值

    1. 数组

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

    使用 delete 运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的 length 属性并不会发生变化。

    • 在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:
    var a = [ ];
    a[0] = 1;
    // 此处没有设置a[1]单元
    a[2] = [ 3 ];
    a[1]; // undefined
    a.length; // 3
    

    上面的代码可以正常运行,但其中的“空白单元”(empty slot)可能会导致出人意料的结果。a[1] 的值为 undefined,但这与将其显式赋值为 undefineda[1] = undefined)还是有所区别。

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

    在数组中加入字符串键值 / 属性并不是一个好主意。建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。

    1. 类数组
    • 有时需要将类数组(一组通过数字索引的值)转换为真正的数组
    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 );
    

    Array.from(..) 还有一些其他非常强大的功能

    1. 数字
    • 应该怎样来判断 0.1 + 0.20.3 是否相等呢?

    最常见的方法是设置一个误差范围值,通常称为“机器精度”(machine epsilon),对 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 !

    • 整数检测

    要检测一个值是否是整数,可以使用 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 类型只有一个值,即 undefinednull 类型也只有一个值,即 null。它们的名称既是类型也是值。

    undefinednull 常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:
    null 指空值(empty value)
    undefined 指没有值(missing value)
    或者:
    undefined 指从未赋值
    null 指曾赋过值,但是目前没有值

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

    • 特殊的数字 - NaN

    NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。

    判断一个值是否是 NaN,从 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-0 在相等比较时的表现有些特别。由于 NaN 和自身不相等,所以必须使用 ES6 中的 Number.isNaN(..)(或者 polyfill)。而 -0 等于 0(对于 === 也是如此,参见第4 章),因此我们必须使用 isNegZero(..) 这样的工具函数。
    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;
      };
    }
    

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

    1. 值和引用

    JavaScript 中没有指针,引用的工作机制也不尽相同。在 JavaScript 中变量不可能成为指向另一个变量的引用。

    JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系。

    JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

    简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括 nullundefined、字符串、数字、布尔和 ES6 中的 symbol

    复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则总是通过引用复制的方式来赋值 / 传递。

    • 函数参数相关的代码示例
    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 的指向,只能更改 ax 共同指向的值。

    • 如果要将 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]
    

    从上例可以看出,x.length = 0x.push(4,5,6,7) 并没有创建一个新的数组,而是更改了当前的数组。于是 a 指向的值变成了 [4,5,6,7]
    请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。

    1. 小结

    JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的行为特征不同,在将字符作为数组来处理时需要特别小心。
    JavaScript 中的数字包括“整数”和“浮点型”。

    基本类型中定义了几个特殊的值。
    null 类型只有一个值 nullundefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefinedvoid 运算符返回 undefined
    数 字 类 型 有 几 个 特 殊 值, 包 括 NaN( 意 指 “not a number”, 更 确 切 地 说 是 “invalid number”)、+Infinity-Infinity-0

    简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。
    JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。

    3. 原生函数

    JavaScript 的内建函数(built-in function),也叫原生函数(native function),如 StringNumber

    1. 常用的原生函数有:
    • String()
    • Number()
    • Boolean()
    • Array()
    • Object()
    • Function()
    • RegExp()
    • Date()
    • Error()
    • Symbol()——ES6 中新加入的!

    实际上,它们就是内建函数。

    • 代码示例
    var a = new String( "abc" );
    typeof a; // 是"object",不是"String"
    a instanceof String; // true
    Object.prototype.toString.call( a ); // "[object String]"
    

    通过构造函数(如 new String("abc"))创建出来的是封装了基本类型值(如 "abc")的封装对象。
    请注意:typeof 在这里返回的是对象(String)类型的子类型(object)。

    1. 将原型作为默认值

    Function.prototype 是一个空函数,RegExp.prototype 是一个“空”的正则表达式(无任何匹配),而 Array.prototype 是一个空数组。对未赋值的变量来说,它们是很好的默认值。

    function isThisCool(vals,fn,rx) {
      vals = vals || Array.prototype;
      fn = fn || Function.prototype;
      rx = rx || RegExp.prototype;
      return rx.test(
        vals.map( fn ).join( "" )
      );
    }
    isThisCool(); // true
    
    isThisCool(
      ["a","b","c"],
      function(v){ return v.toUpperCase(); },
      /D/
    ); // false
    

    这种方法的一个好处是 .prototypes 已被创建并且仅创建一次。相反,如果将 []
    function(){}/(?:)/ 作为默认值,则每次调用 isThisCool(..) 时它们都会被创建一次(具体创建与否取决于 JavaScript 引擎,稍后它们可能会被垃圾回收),这样无疑会造成内存和 CPU 资源的浪费。
    另外需要注意的一点是,如果默认值随后会被更改,那就不要使用 Array.prototype。上例中的 vals 是作为只读变量来使用,更改 vals 实际上就是更改 Array.prototype,而这样会导致前面提到过的一系列问题!

    1. 小结

    JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 StringNumberBoolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String.prototype.trim()Array.prototype.concat(..))。

    对于简单标量基本类型值,比如 "abc",如果要访问它的 length 属性或String.prototype 方法,JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。

    4. 强制类型转换

    1. 值类型转换

    也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在、动态类型语言的运行时(runtime)。

    然而在 JavaScript 中通常将它们统称为强制类型转换,我个人则倾向于用“隐式强制类型转换”(implicit coercion)和“显式强制类型转换”(explicit coercion)来区分。

    二者的区别显而易见:我们能够从代码中看出哪些地方是显式强制类型转换,而隐式强制类型转换则不那么明显,通常是某些操作产生的副作用。

    var a = 42;
    var b = a + ""; // 隐式强制类型转换
    var c = String( a ); // 显式强制类型转换
    

    对变量 b 而言,强制类型转换是隐式的;由于 + 运算符的其中一个操作数是字符串,所以是字符串拼接操作,结果是数字 42 被强制类型转换为相应的字符串 "42"
    String(..) 则是将 a 显式强制类型转换为字符串。
    两者都是将数字 42 转换为字符串 "42"。然而它们各自不同的处理方式成为了争论的焦点。

    1. 假值列表
    • undefined
    • null
    • false
    • +0、-0 和 NaN
    • ""
    1. 日期显式转换为数字

    一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为 Unix 时间戳,以微秒为单位(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间):

    var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
    +d; // 1408369986000
    

    我们常用下面的方法来获得当前的时间戳,例如:

    var timestamp = +new Date();
    

    相关文章

      网友评论

          本文标题:《你不知道的JavaScript(中卷)》

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