美文网首页工作生活
JS里的数据类型

JS里的数据类型

作者: Sur_lee | 来源:发表于2019-07-01 17:26 被阅读0次

    JS里的每一个值都属于一种数据类型,JavaScript中的数据类型共有7种:number(数值)、string(字符串)、boolean(布尔)、symbol(符号)、null、undefined、object(对象)。
    其中number、string和boolean是原始类型的值,object为合成类型;
    对象是复杂的数据类型,又可分为三个子类:

    • object (狭义的对象)
    • array(数组)
    • function(函数)

    1.null和undefined

    null和undefined都表示没有,但是null表示一个空对象,转为数值时为0,调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。undefined表示“此处无定义”的原始值,转为数字时为NaN。

    2.boolean(布尔值)

    布尔值只有“true”和“false”两个值,如果 JavaScript 预期某个位置应该是布尔值,下面六个值被转为false,其他值都视为true。

    • undefined
    • null
    • false
    • 0
    • NaN
    • “ ” 或’ ‘(空字符串)
      注意,空数组([])和空对象({})对应的布尔值,都是true。

    3.number(数值)

    3.1 数值精度

    JavaScript 内部实际的表示形式,精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,都可以精确表示。由于2的53次方是一个16位的十进制数值,所以简单的法则就是,JavaScript 对15位的十进制数都可以精确处理。大于2的53次方以后,多出来的有效数字(最后三位的111)都会无法保存,变成0。

    3.2 数值范围

    JavaScript 能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。
    如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回Infinity。如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0。

    3.3 数值的进制

    使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。

    • 十进制:没有前导0的数值。
    • 八进制:有前缀0o或0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
    • 十六进制:有前缀0x或0X的数值。
    • 二进制:有前缀0b或0B的数值。

    3.4 NaN

    NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
    需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number。

    3.5 parseInt()

    parseInt方法用于将字符串转为整数。

    • 如果字符串头部有空格,空格会被自动去除。
    • 如果parseInt的参数不是字符串,则会先转为字符串再转换。
    • 字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
    • 如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN。
    • 如果字符串以0x或0X开头,parseInt会将其按照十六进制数解析。
    • 如果字符串以0开头,将其按照10进制解析。

    3.6 parseFloat()

    parseFloat方法用于将一个字符串转为浮点数。

    • 如果字符串符合科学计数法,则会进行相应的转换。
    • 如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。
    • parseFloat方法会自动过滤字符串前导的空格。
    • 如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回NaN。

    3.7 isNaN()

    isNaN方法可以用来判断一个值是否为NaN。isNaN只对数值有效,如果传入其他值,会被先转成数值。出于同样的原因,对于对象和数组,isNaN返回true。但是,对于空数组和只有一个数值成员的数组,isNaN返回false。

    4. String(字符串)

    字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。
    字符串默认只能写在一行内,分成多行将会报错;如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。
    连接运算符(+)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。

    4.1转义

    • \0 :null
    • \b :后退键
    • \f :换页符
    • \n :换行符
    • \r :回车键
    • \t :制表符
    • \v :垂直制表符
    • \’ :单引号
    • \” :双引号
    • \ :反斜杠
      如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。

    4.2字符串和数组

    字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。

    var s = ‘hello’;
    s[0] // “h”
    s[1] // “e”
    s[4] // “o”
    
    // 直接对字符串使用方括号运算符
    ‘hello’[1] // “e”
    

    如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined。
    但是,字符串与数组的相似性仅此而已,字符串内部的单个字符无法改变和增删。
    length属性返回字符串的长度,该属性也是无法改变的。
    JavaScript 原生提供两个 Base64 相关的方法。

    • btoa():任意值转为 Base64 编码
    • atob():Base64 编码转为原来的值

    5.object(对象)

    对象是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。

    5.1键名

    • 对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。
    • 如果键名是数值,会被自动转为字符串。
    • 如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错。
    • 对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
    var obj = {
      p: function (x) {
        return 2 * x;
      }
    };
    
    obj.p(1) // 2
    
    • 属性可以动态创建,不必在对象声明时就指定。
    var obj = {};
    obj.foo = 123;
    obj.foo // 123
    

    5.2对象的引用

    如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。

    var obj1 = {};
    var obj2 = o1;
    
    obj1.a = 1;
    obj2.a // 1
    
    obj2.b = 2;
    obj1.b // 2
    

    此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。

    var obj1 = {};
    var obj2 = o1;
    
    obj1 = 1;
    obj2 // {}
    

    上面代码中,obj1和obj2指向同一个对象,然后obj1的值变为1,这时不会对obj2产生影响,o2还是指向原来的那个对象。
    但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。

    var x = 1;
    var y = x;
    
    x = 2;
    y // 1
    

    上面的代码中,当x的值发生变化后,y的值并不变,这就表示y和x并不是指向同一个内存地址。

    对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。

    5.3属性读取

    读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。

    var obj = {
      p: ‘Hello World’
    };
    
    obj.p // “Hello World”
    obj[‘p’] // “Hello World”
    

    如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。

    var foo = ‘bar’;
    
    var obj = {
      foo: 1,
      bar: 2
    };
    
    obj.foo  // 1
    obj[foo]  // 2
    

    上面代码中,引用对象obj的foo属性时,如果使用点运算符,foo就是字符串;如果使用方括号运算符,但是不使用引号,那么foo就是一个变量,指向字符串bar。
    方括号运算符内部还可以使用表达式。

    obj[‘hello’ + ‘ world’]
    obj[3 + 3]
    

    数字键可以不加引号,因为会自动转成字符串。但是,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。

    5.4属性的赋值

    点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。

    var obj = {};
    
    obj.foo = ‘Hello’;
    obj[‘bar’] = ‘World’;
    

    JavaScript 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。

    var obj = { p: 1 };
    
    // 等价于
    
    var obj = {};
    obj.p = 1;
    

    5.5属性的查看

    查看一个对象本身的所有属性,可以使用Object.keys()方法。

    var obj = {
      key1: 1,
      key2: 2
    };
    
    Object.keys(obj);
    // [‘key1’, ‘key2’]
    

    5.6属性的删除

    delete命令用于删除对象的属性,删除成功后返回true。

    var obj = { p: 1 };
    Object.keys(obj) // [“p”]
    
    delete obj.p // true
    obj.p // undefined
    Object.keys(obj) // []
    

    注意,删除一个不存在的属性,delete不报错,而且返回true。
    只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除
    另外,需要注意的是,delete命令只能删除对象本身的属性,无法删除继承的属性。

    5.7属性是否存在:in运算符

    in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false。它的左边是一个字符串,表示属性名,右边是一个对象。

    var obj = { p: 1 };
    ‘p’ in obj // true
    ‘toString’ in obj // true
    

    in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj本身并没有toString属性,但是in运算符会返回true,因为这个属性是继承的。
    这时,可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性。

    var obj = {};
    if (‘toString’ in obj) {
      console.log(obj.hasOwnProperty(‘toString’)) // false
    }
    

    5.8属性的遍历:for…in循环

    for…in循环用来遍历一个对象的全部属性。

    • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
    • 它不仅遍历对象自身的属性,还遍历继承的属性。
    var obj = {a: 1, b: 2, c: 3};
    
    for (var i in obj) {
      console.log(‘键名:’, i);
      console.log(‘键值:’, obj[i]);
    }
    // 键名: a
    // 键值: 1
    // 键名: b
    // 键值: 2
    // 键名: c
    // 键值: 3
    

    如果继承的属性是可遍历的,那么就会被for…in循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for…in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。

    var person = { name: ‘老张’ };
    
    for (var key in person) {
      if (person.hasOwnProperty(key)) {
        console.log(key);
      }
    }
    // name
    

    5.9with语句

    with (对象) {
      语句;
    }
    

    它的作用是操作同一个对象的多个属性时,提供一些书写的方便。

    // 例一
    var obj = {
      p1: 1,
      p2: 2,
    };
    with (obj) {
      p1 = 4;
      p2 = 5;
    }
    // 等同于
    obj.p1 = 4;
    obj.p2 = 5;
    
    // 例二
    with (document.links[0]){
      console.log(href);
      console.log(title);
      console.log(style);
    }
    // 等同于
    console.log(document.links[0].href);
    console.log(document.links[0].title);
    console.log(document.links[0].style);
    

    如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。因此,建议不要使用with语句,可以考虑用一个临时变量代替with。

    with(obj1.obj2.obj3) {
      console.log(p1 + p2);
    }
    
    // 可以写成
    var temp = obj1.obj2.obj3;
    console.log(temp.p1 + temp.p2);
    

    6.函数

    6.1 函数的声明

    JavaScript 有三种声明函数的方法。
    1.function命令:function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

    function print(s) {
      console.log(s);
    }
    

    2.函数表达式
    采用变量赋值的写法,这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。

    var print = function(s) {
      console.log(s);
    };
    

    采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

    var print = function x(){
      console.log(typeof x);
    };
    
    x
    // ReferenceError: x is not defined
    
    print()
    // function
    

    函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。
    3.Function构造函数

    var add = new Function(
      ‘x’,
      ‘y’,
      ‘return x + y’
    );
    
    // 等同于
    function add(x, y) {
      return x + y;
    }
    

    上面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,其他参数都是add函数的参数。
    你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。

    var foo = new Function(
      ‘return “hello world”;’
    );
    
    // 等同于
    function foo() {
      return ‘hello world’;
    }
    

    如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。

    6.2 圆括号运算符,return 语句和递归

    调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。

    function add(x, y) {
      return x + y;
    }
    
    add(1, 1) // 2
    

    函数体内部的return语句,表示返回。JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,return语句所带的那个表达式,就是函数的返回值。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined。
    函数可以调用自身,这就是递归(recursion)。

    function fib(num) {
      if (num === 0) return 0;
      if (num === 1) return 1;
      return fib(num - 2) + fib(num - 1);
    }
    
    fib(6) // 8
    

    上面代码利用递归计算得到斐波那契数列的第6个元素。

    6.3 函数名提升

    JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。但是,如果采用赋值语句定义函数,JavaScript 就会报错。

    f();
    
    function f() {}
    
    f();
    var f = function (){};
    // TypeError: undefined is not a function
    

    上面的代码等同于下面的形式。

    var f;
    f();
    f = function () {};
    

    上面代码调用f的时候,f只是被声明了,还没有被赋值,等于undefined,所以会报错。因此,如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。

    var f = function () {
      console.log(‘1’);
    }
    
    function f() {
      console.log(‘2’);
    }
    
    f() // 1
    

    6.4 name属性

    函数的name属性返回函数的名字:如果是通过变量赋值定义的函数,那么name属性返回变量名;如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名。

    var f3 = function myName() {};
    f3.name // ‘myName’
    

    上面代码中,f3.name返回函数表达式的名字。注意,真正的函数名还是f3,而myName这个名字只在函数体内部可用。

    6.5 length属性

    函数的length属性返回函数预期传入的参数个数。

    6.6 toString()

    函数的toString方法返回一个字符串,内容是函数的源码。
    对于那些原生的函数toString()方法返回function (){[native code]}

    6.7函数作用域

    1.作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域。ES6 又新增了块级作用域
    对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。

    var v = 1;
    
    function f() {
      console.log(v);
    }
    
    f()
    // 1
    

    在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。

    function f(){
      var v = 1;
    }
    
    v // ReferenceError: v is not defined
    

    函数内部定义的变量,会在该作用域内覆盖同名全局变量。
    对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
    2.函数内部的变量提升
    与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部
    3.函数本身的作用域
    函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

    6.8参数

    函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。
    1.参数的省略

    function f(a, b) {
      return a;
    }
    
    f(1, 2, 3) // 1
    f(1) // 1
    f() // undefined
    
    f.length // 2
    

    上面代码的函数f定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值就变为undefined。需要注意的是,函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
    但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined。
    2.传递方式

    • 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
    var p = 2;
    
    function f(p) {
      p = 3;
    }
    f(p);
    
    p // 2
    

    上面代码中,变量p是一个原始类型的值,传入函数f的方式是传值传递。因此,在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。

    • 如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
    var obj = { p: 1 };
    
    function f(o) {
      o.p = 2;
    }
    f(obj);
    
    obj.p // 2
    
    • 如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
      3.同名函数
      如果有同名的参数,则取最后出现的那个值。没有提供第二个参数,返回值值就变成了undefined。
      4.arguments 对象
      由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。
      arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
    var f = function (one) {
      console.log(arguments[0]);
      console.log(arguments[1]);
      console.log(arguments[2]);
    }
    
    f(1, 2, 3)
    // 1
    // 2
    // 3
    

    正常模式下,arguments对象可以在运行时修改。严格模式下,修改arguments对象不会影响到实际的函数参数。

    var f = function(a, b) {
      arguments[0] = 3;
      arguments[1] = 2;
      return a + b;
    }
    
    f(1, 1) // 5
    

    6.9闭包

    前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。但是,函数外部无法读取函数内部声明的变量。
    如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

    function f1() {
      var n = 999;
      function f2() {
        console.log(n);
      }
      return f2;
    }
    
    var result = f1();
    result(); // 999
    

    上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

    闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

    function createIncrementor(start) {
      return function () {
        return start++;
      };
    }
    
    var inc = createIncrementor(5);
    
    inc() // 5
    inc() // 6
    inc() // 7
    

    上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
    为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

    闭包的另一个用处,是封装对象的私有属性和私有方法。

    function Person(name) {
      var _age;
      function setAge(n) {
        _age = n;
      }
      function getAge() {
        return _age;
      }
    
      return {
        name: name,
        getAge: getAge,
        setAge: setAge
      };
    }
    
    var p1 = Person(‘张三’);
    p1.setAge(25);
    p1.getAge() // 25
    

    上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。
    由于外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

    6.10立即调用的函数表达式(IIFE)

    在 JavaScript 中,圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。
    有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
    通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

    // 写法一
    var tmp = newData;
    processData(tmp);
    storeData(tmp);
    
    // 写法二
    (function () {
      var tmp = newData;
      processData(tmp);
      storeData(tmp);
    }());
    

    上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。

    6.11eval命令

    • eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。
    • 如果参数字符串无法当作语句运行,那么就会报错。
    • 如果eval的参数不是字符串,那么会原样返回。
    • eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。不过,即使在严格模式下,eval依然可以读写当前作用域的变量。
    • 总之,eval的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法。
    • 凡是使用别名执行eval,eval内部一律是全局作用域。

    7.数组(array)

    1.数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。

    var arr = [‘a’, ‘b’, ‘c’];
    

    任何类型的数据,都可以放入数组。

    var arr = [
      {a: 1},
      [1, 2, 3],
      function() {return true;}
    ];
    
    arr[0] // Object {a: 1}
    arr[1] // [1, 2, 3]
    arr[2] // function (){return true;}
    

    如果数组的元素还是数组,就形成了多维数组。

    var a = [[1, 2], [3, 4]];
    a[0][1] // 2
    a[1][1] // 4
    

    2.数组的本质

    本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)。
    由于数组成员的键名是固定的(默认总是0、1、2…),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。
    上一章说过,对象有两种读取成员的方法:点结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构。

    var arr = [1, 2, 3];
    arr.0 // SyntaxError
    

    上面代码中,arr.0的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]表示(方括号是运算符,可以接受数值)。

    3.length属性

    JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232- 1)个,也就是说length属性的最大值就是 4294967295。

    • 只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1。
    • 数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。
    • length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值。
    var arr = [ ‘a’, ‘b’, ‘c’ ];
    arr.length // 3
    
    arr.length = 2;
    arr // [“a”, “b”]
    
    • 清空数组的一个有效方法,就是将length属性设为0。
    • 如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都会返回undefined。
    • 由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length属性的值。
    var a = [];
    
    a[‘p’] = ‘abc’;
    a.length // 0
    
    a[2.1] = ‘abc’;
    a.length // 0
    

    上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。

    • 如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
    var arr = [];
    arr[-1] = ‘a’;
    arr[Math.pow(2, 32)] = ‘b’;
    
    arr.length // 0
    arr[-1] // “a”
    arr[4294967296] // “b”
    

    上面代码中,我们为数组arr添加了两个不合法的数字键,结果length属性没有发生变化。这些数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符串

    4.in运算符

    检查某个键名是否存在的运算符in,适用于对象,也适用于数组。

    var arr = [ ‘a’, ‘b’, ‘c’ ];
    2 in arr  // true
    ‘2’ in arr // true
    4 in arr // false
    

    5.for…in 循环和数组的遍历

    for…in循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。

    var a = [1, 2, 3];
    
    for (var i in a) {
      console.log(a[i]);
    }
    // 1
    // 2
    // 3
    

    for…in不仅会遍历数组所有的数字键,还会遍历非数字键。

    var a = [1, 2, 3];
    a.foo = true;
    
    for (var key in a) {
      console.log(key);
    }
    // 0
    // 1
    // 2
    // foo
    

    数组的遍历可以考虑使用for循环或while循环。

    var a = [1, 2, 3];
    
    // for循环
    for(var i = 0; i < a.length; i++) {
      console.log(a[i]);
    }
    
    // while循环
    var i = 0;
    while (i < a.length) {
      console.log(a[i]);
      i++;
    }
    
    var l = a.length;
    while (l—) {
      console.log(a[l]);
    }
    

    上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。

    6.数组的空位

    当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。

    • 数组的空位不影响length属性。
    var a = [1, , 3];
    a.length // 3
    

    数组的空位是可以读取的,返回undefined。

    • 使用delete命令删除一个数组成员,会形成空位,并且不会影响length属性。
    • 数组的某个位置是空位,与某个位置是undefined,是不一样的。如果是空位,使用数组的forEach方法、for…in结构、以及Object.keys方法进行遍历,空位都会被跳过。
    var a = [, , ,];
    
    a.forEach(function (x, i) {
      console.log(i + ‘. ‘ + x);
    })
    // 不产生任何输出
    
    for (var i in a) {
      console.log(i);
    }
    // 不产生任何输出
    
    Object.keys(a)
    // []
    
    • 如果某个位置是undefined,遍历的时候就不会被跳过。
    var a = [undefined, undefined, undefined];
    
    a.forEach(function (x, i) {
      console.log(i + ‘. ‘ + x);
    });
    // 0. undefined
    // 1. undefined
    // 2. undefined
    
    for (var i in a) {
      console.log(i);
    }
    // 0
    // 1
    // 2
    
    Object.keys(a)
    // [‘0’, ‘1’, ‘2’]
    

    这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过。

    7.类似数组的对象

    如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。

    var obj = {
      0: ‘a’,
      1: ‘b’,
      2: ‘c’,
      length: 3
    };
    
    obj[0] // ‘a’
    obj[1] // ‘b’
    obj.length // 3
    obj.push(‘d’) // TypeError: obj.push is not a function
    

    上面代码中,对象obj就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。

    “类似数组的对象”的根本特征,就是具有length属性。只要有length属性,就可以认为这个对象类似于数组。但是有一个问题,这种length属性不是动态值,不会随着成员的变化而变化。

    var obj = {
      length: 0
    };
    obj[3] = ‘d’;
    obj.length // 0
    

    典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串。

    // arguments对象
    function args() { return arguments }
    var arrayLike = args(‘a’, ‘b’);
    
    arrayLike[0] // ‘a’
    arrayLike.length // 2
    arrayLike instanceof Array // false
    
    // DOM元素集
    var elts = document.getElementsByTagName(‘h3’);
    elts.length // 3
    elts instanceof Array // false
    
    // 字符串
    ‘abc’[1] // ‘b’
    ‘abc’.length // 3
    ‘abc’ instanceof Array // false
    

    上面代码包含三个例子,它们都不是数组(instanceof运算符返回false),但是看上去都非常像数组。

    相关文章

      网友评论

        本文标题:JS里的数据类型

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