JS从入门到掉坑 (一) --- 基本数据类型

673 阅读19分钟

前言,我们常说的 js , 是由于 ECMAScript, BOM, DOM,这三个部分组成的。今天我们主要介绍的是ECMAScript当中的基本数据类型

基本数据类型

众所周知在我们的 ECMAScript 中有6种简单的数据类型:

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Symbol (ECMAScript 6 新定义)

当然后还我们的复杂数据类型 Object(Array,Date,RegExp, Function)或是叫做引用类型 ,它的本质是由一组无序的名值对组成的,可以这么说在 ECMAScript 当中所的值最终都会被以上的7种数据类型来表示,


Undefined 与 Null 类型

UndefinedNull 两个都是只有一个值的类型,Null 表示一个空对象的指针,而我们的 Undefined 则是继承自 Null,有时我们可以说他们是等价的如下面的

var msg;
console.log(msg == undefined);  // true
console.log(msg == null);  // true
console.log(undefined == null);  // true
console.log(undefined === null);  // false

它们的不同处在于,Undefined表示是一个表示"无"的原始值或者说表示"缺少值",就是此处应该有一个值,但是还没有定义。当尝试读取时会返回 undefined,例如变量被声明了,但没有赋值时,就等于undefined。,而我们的Null表示这东西压根都没存在,是一个对象(空对象, 没有任何属性和方法),所以就会下面的情况:

var a;
console.log(a); // undefined
console.log(b); // 报错了

我们在来看下面的

var a;
console.log(typeof a); // undefined
console.log(typeof b); // undefined

console.log(delete a); // false
console.log(delete b); // true

console.log(typeof null == typeof undefined); // false

Number(undefined); // NaN
Number(null); // 0

突然发现一个现象为啥这里又不报错了?这是因为在规定里面,对于尚未声明的变量,只能进行两种操作是合法的,就是用typeof操作符检测其数据的类型,当然你使用delete进行操作也是合法,但是使用delete操作并没有什么意思,本来都不存在嘛,而且在严格模式下使用delete是会报错的。特别注意typeof null 得到的结果是一个Object,而typeof undefined得到的结果是undefined

而在我们平时使当中,我们没有必要把一个变量的值是显示的设置为undefined,因为在声明这个变量的时候就自动为我们的隐式的设置值了。但是我们的null则只要是要保存的变量但是还没有真的保存下来的时候,我们应该明确的让该变量初始化的时候为null


Boolean类型

boolean类型只存在两个值,truefalse

TrueFalse这个大写的并不是我们的boolean值,只是标识符。

console.log(Boolean('')); // false 非空字符串转的结果都为true
console.log(Boolean(0)); // false 任何非零数字包含无穷大都为true
console.log(Boolean(NaN)); // false
console.log(Boolean(null)); // false 别的对象都为true

Number类型

我们在Number这个类型的时候,我们先看一下下面的代码

console.log(0.5 + 0.15 == 0.65); // true
console.log(0.1 + 0.2 == 0.3); // false

为啥会出现上面的这种情况?,原来我们的Number类型使用的是IEEEF754格式来表示整数各浮点数值,也可以叫做双精度数值。那什么是IEEEF754,在介绍它的时候我们先要了解ECMAScript中的对于一些极大或是极小数值,采用的是科学技术法,用e表示法就是我们以前数学学的有幂指数,请看下面:

var num = 3.125e7;
console.log(num); // 31250000
var floatNum = 3e-6;
console.log(floatNum); // 0.000003

这里存在一个浮点数值的精度问题,它目前最开精度是17位小数,所在进行浮点数进行算术运算的时其精确度远不如整数, 下面我们要来解释为什么()

先来看看IEEEF754是怎么存数据的:

指数位可以通过下面的方法转换为使用的指数值:

浮点数表示的值的形式由 e 和 f 确定:

我们都知道所的代码都会转成二进制进行运算

将 0.1 使用转换为二进制

  • 我们就可以得到```0.1与0.2``所表示的值
  • 由于小数位 f 仅储存 52bit, 储存时会将超出精度部分进行"零舍一入"
值类型 小数位(储存范围内) 小数位(储存范围外)
无限精确值 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001...
实际储存值 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 -

由于计算加减时不会对指数位进行位运算,这里不计算指数位的表示,直接使用数字表示最终的指数值所以0.1,0.2 的表示如下:

浮点数数值 符号位 S 指数值 E 小数位 f
0.1 0 -4 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
0.2 0 -3 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

我们在计算浮点数相加时需要先进行“对位”,将较小的指数化为较大的指数,并将小数部分相应右移:

0.1→(−1)^0×2^{−3}×(0.11001100110011001100110011001100110011001100110011010)_2
0.2→(−1)^0×2^{−3}×(1.1001100110011001100110011001100110011001100110011010)_2

所以可以转换为:

0.1+0.2=(−1)^0×2^{−2}×(1.0011001100110011001100110011001100110011001100110100)_2
0.1 + 0.2 === (-1)**0 * 2**-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); // true

补充说明:

  1. 数值范围

    由于我们的内存的限制,ECMAScript并不能保存世界上所有的数值,所以会存一个最小与最大的保存值,分别为5e-324, 1.7976931348623157e+308,当然这两个数不查一下你是记不住的,所以ECMAScript为我们提供了常量Number.MIN_VALUE, Number.MAX_VALUE,如果在某一次计算超过所谓的边界时则为自动转为Infinity-Infinity就是我们的数学里常说正无穷与负无穷。我们可以使用isFinite()判断一个数是否为有穷。我们也可以通过常量Number.POSITIVE_INFINITYNumber.NEGATIVE_INFINITY直接得到我们的正与负无穷值。当然这个很少见,不过在进行一些极小数与极大数计算时,我们最好还是要做一个检测。

  2. NaN

    首先NaN表示非数值的一个特殊的值,只要是用于本来应该返回一个操作数,结果并没有返回,这样就不会报错可以继续运行。我们刚开始学数据除法的时候我们数学老师就给我们说0不能做为除数。ECMASCript中,为避免除数为0会出错导致代码停止执行,规定任意数值除以0会返回NaN,而不会影响代码的继续执行。而我们的NaN有两个特点:

    1. 任何涉及到NaN操作的都会返回NaN
    2. NaN与任意值都不相等,包括自身。

    我们可以使用isNaN来判断,isNaN这是一个好玩的方法,请看下面的代码:

    isNaN(NaN); // true
    isNaN(111); // false
    isNaN('111'); // false
    isNaN('00001111'); // false
    isNaN('00001111a'); // true
    isNaN('0x3f'); // false
    isNaN('0x3j'); // true
    isNaN('Number'); // true
    isNaN(Number); // true
    isNaN(true); // false
    isNaN([]); // false
    isNaN({}); // true
    isNaN(null); // false
    isNaN(undefined); // true
    

    我们可以这么理解isNaN在接到一个值之后,会尝试将这个值转为数值,不是数值的值也会直接转换成数值如'111',而任何不能转换为数值的就是该方法就会返回一个true,同样对我们的字符串在转成数值的时候也有一个有趣的现象,如'0011',会转 成11,如果在是包含有效的十六进制的数也会转成对应的十进制数。

    但你又会发现一个奇怪的事情关于我们复杂类型就是引用类型Object当中的[]{}得到的结果居然不一样,不都是对象类型嘛,怎么会出这样的问题,这里面就是涉及到对象当中的valueOf()与toString()方法,在对对象进行判断时先调用valueOf()方法,判断该值是否可以转换为数值,不能刚调用toString()方法,在来判断值是否为数值。

    [].toString(); // '' 
    ({}).toString(); // "[object Object]"
    
  3. 数值进制

    请看下面代码所示:

    var intNum = 63; // 十进制 63 
    var baNum = 077; // 八进制 63
    var slNum = 0x3f; // 十六进制 63
    

    不过在进行算数运算的时候八进制与十六进制都会被转成十进制数值,注意,在严格模式下,八进制的写法会报错。

  4. Number(), parseInt()与parseFloat()之间的相同与区别 上面三个都可以实现类型转换为我们的Number类型,请看下面:

    Number(null); // 0
    parseInt(null); // NaN
    Number(''); // 0
    parseInt(''); // NaN
    Number('1234true'); // NaN
    parseInt('1234true'); // 1234
    parseInt('01234true'); // 1234
    parseInt('63'); // 63;
    parseInt('077'); // 77; 注意在`ECMAScript3`当中会当做八制处理 结果为63
    parseInt('0x3f'); // 63
    parseInt('63.22'); // 63
    parseFloat(null); // NaN
    parseFloat('63.22'); // 63.22
    

    通过上面的例子我们可以看出使用'Number()'来转换数值并不理想,达不到我们预期,我们的'parseInt()'与'parseFloat()'两个方法一个是解析为整数一个解析为浮点数。parseInt()第二个方法是为解析的基数就是我们是按十进制、二进制、八进制、十 六进制来解析如下:

    parseInt('0xAF', 16); // 175
    parseInt('AF', 16); // 175
    parseInt('AF'); // NaN
    parseInt('100',2); // 4
    parseInt('100',8); // 64
    parseInt('100',10); // 100
    parseInt('100',16); // 256
    

    parseInt()默认使用是的十进制,而parseFloat()方法没有第二个参数,只会按十进制来解析,所以在解析我们的十六进制的时候会解析成为0。如果小数点为0或是没有小数点则转为整型。

    parseFloat('1234.00true'); // 1234
    parseFloat('01234.33.33true');// 1234.33;
    parseFloat('0xAF'); // 0
    parseFloat('1.234e7'); // 12340000
    parseFloat('1.234e-7'); // 1.234e-7
    

String类型

字符串类型也是我们常用的一些类型,在ECMAScript中的字符串是不可变,也就是说一个字符串一但被创建,它的值就是不能改变的。如是要改变某一个变量保存的字符串,首先要销毁原来的字符串,然后用另外一个包含新值的字符串去填充该变量。

var str  = 'ECMA';
str = str + 'Script';

这上面这第二行代码实际的操作过程是:

  1. 创建一个能容纳10个字符的新字符串
  2. 在创建的新的字符串中填充ECMAScript
  3. 销毁原来的字符串ECMAScript

在转换为字符串的时候都会调用到toString()方法,该方法可以接收一个参数,用于输出数值的基数

var num = 100;
num.toString(); // "100"
num.tostring(2); // "1100100"
num.tostring(8); // "144"
num.tostring(10); // "100"
num.tostring(16); // "64"

注意,我们的nullundefined是没有toString(),在我们不知道是否为null或undefined的时候,我们可以使用String()如下:

var num1;
String(10); // "10"
String(true); // "true"
String(null); // "null"
String(num1); // "undefined"

留一思考题后面我在讲解Object类型可以找到解释:

var str1 = "string";
str1.color = "red";
console.log(str1.color); // undefined

var str2 = new String("string");
str2.color = "red";
console.log(str2.color); // red

Symbol类型

Symbol是我们ECMAScript5中新增加的一个类型,字面的意思就是唯一,独一无二的,在声明Symbol类型是使用Symbol(),记住不能带new

var a = new Symbol(); // error 
var b = Symbol();

关于Symbol的详细介绍阮一峰老师的ES6

我来简单的说一下关于它的使用,关于更多的使用请看上面的软老师的

var per1 = {
    name: '隔壁表哥',
    desc: '能力很强',
}

var per2 = {
    desc: '自我学习很好'
}

console.log({...per1,...per2}) // {name: "隔壁表哥", desc: "自我学习很好"}

var per1 = {
    name: '隔壁表哥',
    [Symbol('desc')]: '能力很强',
}

var per2 = {
     [Symbol('desc')]: '自我学习很好'
}

console.log({...per1,...per2}) // {name: "隔壁表哥", Symbol(desc): "能力很强", Symbol(desc): "自我学习很好"}


复杂引用Object类型

ECMAScript中对象其实不是一组数据与功能的集合,使用new Object()来创建一个对象,当然在你就简单的创建一个Object类型的实例是没什么用了的,但是Object类型是所有它的实例的基础,Object类型所具有的属性与方法也存在具体的对象中的,比如[],{}等。 可以这么理解,每一个Object类型的实例都具有new Object()的所有属性与方法。

  1. constructor: 构造函数,也就是相同于与我们的Object(),保存着用于创建当前对象的函数。
  2. hasOwnProperty(propertyName): 用于检查给定的属性在当前的对象实例中是否存在。
  3. isProtorypeOf(Object): 用于检查传入的对象是否是传入对象的原型。
  4. propertyIsEnumerable(propertyName): 用于检测给定的属性是否能够迭代。
  5. toLocaleString(): 返当前执行环境下对象的字符串表示。
  6. toString(): 返回对象的字符串表示。
  7. valueOf(): 返回对象的字符串、数值、布尔值表示。

我们常说的复杂引用数据类型Object类型常的有Array,Object,Date,RegEXP,Function,为啥叫做引类型请看

var a = {name: '隔壁表哥', age: '66' };
var b = a;
b.name = '不是表哥是老王';
console.log(a); // {name: "不是表哥是老王", age: "66"}
  1. 基本的Object类型 简单的来看一下Object类型的,可以使用:

    var obj1 = new Object();
    var obj2 = new Object; // 不推荐
    var obj3 = {};
    

    补充我们访问变量里面的一个某一个属性的时候可以使用:

    var person = {
        name: '隔壁表哥',
        age: '66'
    }
    person.name; // '隔壁表哥'
    person['name']; // '隔壁表哥'
    

    上面的两种形式用于访问对象属性的方法没有任何区别,但是方括号的形式可以能过变量的形式来访问属性。还可以避免一些物特殊的键值,当然,我们一般会推荐使用.来获取。

    var propertyName = "name";
    person[propertyName]; // '隔壁表哥'
    person['first_name'] = '小小';
    person['second name'] = '小小小';
    
  2. Array

    Array是也是除Object之外,常用的复杂对象类型了,它可以存放任何类型的数据。

    var arr1 = new Array();
    var arr2 = [];
    
    • 数组的长度
    var colors = ['red','yellow', 'blue', 'green'];
    var names = [];
    colors.length; // 4
    names.length; // 0
    
    colors.length = 3;
    console.log(colors); // ['red','yellow', 'blue'];
    
    colors.length = 4;
    console.log(colors); // ['red','yellow', 'blue', ];
    console.log(colors[3]); // undefined
    

    通过这发现我们的length属性居然不是一个只读属性,是可以修改数组的。理论上数组可以存放4,294,967,295个项。

    • 检测数组

      常用检查检测是否为数组

      value instanceof Array; 
      Array.isArray(value); // 这是ECMAScript5 为我们的提供的,当然为了做到兼容可以采用下面的方式
      if(typeof Array.isArray==="undefined") {
         Array.isArray = function(arg) {
          return Object.prototype.toString.call(arg) === "[object Array]"
         }; 
      }
      
    • 转换方法 前面我们有介绍我们Object实例当中有toLocaleString()toString()valueOf()这几个方法,以及数组的join()

        var colors = ['red','yellow', 'blue', 'green'];
        colors.valueOf(); // ["red", "yellow", "blue", "green"] 返回的还是当前的数组
        colors.toString(); // "red,yellow,blue,green"
        返回数组每个值的字符串形式拼接而成的一个以逗号分割的字符串,类似与调用了Array.join()方法
        colors.toLocaleString(); // "red,yellow,blue,green"
        colors.join(','); // "red,yellow,blue,green"
      
    • 数组的增与删

      ECMAScript提供了二个操作末尾的方法push()pop(),以及二个操作头部的方法shift()unshift(),还有一个更灵活的方法splice(),上面的方法都会修改原来的数组。

       var colors = ['red','yellow', 'blue'];
       
       colors.push('green'); // 4 push()返回当前的数组的长度
       colors.pop(); // 返回尾部删除项,并返回当前删除的项
       
       colors.unshift('orange'); //  返回当前数组的长度
       colors.shift(); // 删除头部第一项,并返回当前删除的项
       
       colors.splice(0,1); // 要删除的第一项的位置和要删除项数 ,返回被删除的项目
       colors.splice(0,0,'red','orange') //批定第三个参数则表示在指定位置添加,注意我们的第二个参数为0,表示删除的项为0  
       console.log(colors); // ["red", "orange", "yellow", "blue"]
       colors.splice(0,1,'purple') // 表示又删新增加元素
       console.log(colors); // ["purple", "orange", "yellow", "blue"]
      
    • 数组位置查找

    ECMAScript5为我们提供了两个位置方法:indexOf()lastIndexOf(),这两个方法都是用于查找元素的位置的,不过一个是从数组的头开始查找,一个是从数组的尾开始查找。它们都接收两个参数,一个表示查找项,和一个查找的位置。如果没有查找到则返回-1

    var numbers = [1,2,3,4,5,6,7,8,9]
    numbers.indexOf(4); // 3
    numbers.lastIndexOf(4); // 5
    numbers.indexOf(4,4); // 5
    numbers.lastIndexOf(4,4); // 3
    
    • 数组的迭代 除了我们的for循环,for in,for of以外,我们的ECMAScript为我们提供了以下几个方法

      • every(): 给定一个函数,对数组的每一项进行判断,每一项返回为true,则返回true;
      • some(): 给定一个函数,对数组的每一项进行判断,有一项返回为true,则返回true;
      • filter(): 给定一个函数,对数组的每一项进行判断,返回符合条件的,数组合集;
      • map(): 给定一个函数,对数组的每一项进行处理,返回处理之后的数组;
      • forEach(): 给定一个函数,对数组每一项进行处理,没有返回值;
      • reduce(): 给定一个函数,对数组每一项进行处理,返回处理之后的数组;
      • reduceRight(): 给定一个函数,对数组从最后一项开始进行处理,返回处理之后的数组;
      var numbers = [1,2,3,4,5,4,3,2,1]
      
      var everyNum = numbers.every((item,index,array)=>{ 
          return item > 2
      })
      console.log(everyNum); // false
      
      var someNum = numbers.some((item,index,array) => {
          return item > 2
      })
      console.log(someNum); // true
      
      var filterNum = numbers.filter((item,index,array)=>{
          return item > 2
      })
      console.log(filterNum); // [3, 4, 5, 4, 3]
      
      var mapNum = numbers.map((item,index,array)=>{
          return item * 2
      })
      console.log(mapNum); // [2, 4, 6, 8, 10, 8, 6, 4, 2]
      
      numbers.forEach((item,index,array)=>{
          console.log(item);
      })
      
      var sum1 = numbers.reduce((prev,cur,index,array)=>{ // prev,表示上一次执行的结果,cur当前项
          return prev + cur
      })
      console.log(sum1); // 25
      
       var sum2 = numbers.reduceRight((prev,cur,index,array)=>{
          return prev + cur
      })
      console.log(sum1); // 25
      
  3. Date

    Date这个类型的大家都常用到的过多的介绍我也不多说了,常用的工具类moment,dayjs,推荐大家使用这个两个这里我就不多说了,常用的查一下很简单也很方便。

  4. RegExp

    ECMAScript能过RegExp类型来支持正则表达式。表达式如下:

    var demo = / pattern / flags;
    

    pattern这个表示我们的匹配模式,后面的flags表示标志,标识有:全局模式(global) g,不区分大小写模式(case-insensitive) i, 多行模式(multiline) m。 同时也提供了exec()test(),这两个方法,前一个返回匹配到的结果,后一个则表示是否匹配到了。

  5. Function

    Function可能是ECMAScript当中最有意思的,首先我们要明确一点由于函数是对象所以函数名实际上是一个指向对象的指针,不会与某个函数绑定。同时要理解一下,函数声明与函数表达式,两着的区别就不得说到变量与函数的提升,js是一个弱类型的动态语言,在执行的js的时候,解析器会首先扫描所有变量与函数,并将它们提升到最顶部进行执行,关于这里的执行机制,可以在后面的讲解。

    // 函数声明
    function sum (num1 ,num2) {
        return sum1 + sum2
    }
    // 函数表达式
    var sum = function(num1 , num2){
        return sum1 + sum2
    }
    

    关于两者的区别如下

    console.log(sum(10,10)) // 20 
    function sum(sum1,sum2){
     return sum1 + sum2   
    }
    
    console.log(sum1(10,10)) // error
    var sum1 = function sum(sum1,sum2){
     return sum1 + sum2   
    }
    

    补充一点,当使用别人的写的函数方法时,不知道需要传递多少个参数,可以使用functionName.length,可以得该函数所能接收的参数。首先关于函数的参数名并不是一定必须的,因为在函数内部默认存在一个arguments的数组用 于接收函数所接收到的全部参数。

  6. 基本简单类型的包装

    前面介绍过,我们的简单数据类型并不是对象,但是ECMAScript提供了特殊的引用类型,也就是基本包装类型Number,String,Boolean,我们来看看下面的:

    var str1 = 'long string';
    var str2 =  str1.indexOf(s);
    

    我们知道基本数据类型不是对象,所以也就不存在方法了。但是String, Number, Boolean类型的数据都有自己的操作方法,我们要来说一下整个类型包装的过程。

    var str1 = new String('long string'); // 第一步 创建对象
    var str2 = str.indexOf(s); // 第二步 使用对象的属性或是方法
    str1 = null; // 第三步 销毁对象
    

    Object类型与基本包装类型主要区别是在于生存周期的问题,这个就可以解释上面的所提到的colors的问题。

    var num = '255';
    var number = Number(num); 
    typeof number; // "number"
    var numberObj = new Number(num);
    typeof numberObj; // 'object'
    
    var falseObj = new Boolean(false);
    console.log(falseObj && true); // true
    
    var falseS = false;
    console.log(falseS && true); // false
    

    补充说明关于substr,substring,slice一个简单区别

    var str = 'hello world';
    
    str.slice(3); // lo world
    str.substring(3); // lo world
    str.substr(3); // lo world
    
    str.slice(3,5); // lo
    str.substring(3,5); // lo
    str.substr(3,5); // lo wo
    
    str.slice(-3); // rld;
    str.substring(-3); // hello word
    str.substr(-3); // rld
    
    str.slice(3, -3); // lo wo
    str.substring(3,-3) // hel
    str.substr(3,-3)// ''
    

    首先这三个方法都不会修改字符串本身。

     1. 在指定同一个参数`3`的时候返回的结果一致。
     
     2. 在指定两个参数`3和5`时,`slice()`与`substring()`返回结果相同,
     从第3个到第5个,但是`substr`返回的结果就不一样了,
     因为`substr`接收的第二参数表示在返回的字符的个数。
     
     3. 传递一个负数参数`-3`时`slice()`会与当前的字符长度相加
     `slice(-3)`等价于`slice(11 + (-3))`,
     `substr()`方法与`slice()`相似,`substr(-3)`等价于`substr((11 + (-3)), 0)`,
     而`substring()`则会把所的负数转为`0`,`substring(-3)`等价于`substring(0)`。
     
     4. 传递第二个参数为负数时`3,-3`,
     `slice(3, -3)`等价于`slice(3, 11 + ( - 3))`,
     `substring(3, -3)`等价于`substring(3,0)`,
     `substr(3,-3)`等价于`substr(3,0)`
    

思考

NaN < 3; // false
NaN >= 3; // false
NaN == NaN;  // false
"Blue" > "Apple"; // true
"Blue" > "apple"; // false
0 == true; // false
1 == true; // true
2 == true;   // false
[] == false; // true
[] == ![];   // true
0.3 + 0.6 == 0.9; // false

一个是number另外一个是string比较时时,会尝试将string转换为number,尝试将boolean转换为number0或1Object类型比较时会将Object转换成numberstring,取决于另外一个对比量的类型