阅读 112

「面试基础小册」数据类型及其延伸

前言

「面试基础小册」系列正式开写。主要是对一些基础相关的知识进行归纳整理与拓展。后续还有更多,敬请期待

本文讲述的是 javascript 的类型相关知识,并且对此进行延伸。

问题预警:

  • Javascript 的数据类型有哪些?
  • null 与 undefinded 的区别?
  • null 不是对象,为何 typeof null 结果为 object ?
  • 手写数据类型判断函数?
  • 深拷贝与浅拷贝?
  • 类型转化与快速转化?
  • 快速取整与位运算

Javascript 的数据类型有哪些?


JavaScript 一共有 8 种数据类型,其中有 7 种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6 新增,表示独一无二的值) 和 BigInt(ES10 新增)

一种引用数据类型——Object,里面包含 Function、Array、Date 等。

  • null 与 undefinded 的区别?

undefined 代表的含义是未定义, null 代表的含义是空对象(但又不是对象)。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。还有一个是:Number 转换的值不同,Number(null) 输出为 0, Number(undefined) 输出为 NaN

  • null 不是对象,为何 typeof null 结果为 object ?

虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object

这里使用了 typeof 函数去判断类型,那么在 javascript 如何准确的判断一个变量的类型呢?

Javascript 数据类型判断


typeof 能够判断六种基本数据类型(除了 null)以及所有的引用数据类型(还能从中判断出 function),但是无法判断 object 的细分类型,如 Array,Date, Error, RegExp 等等。

这里我们用到 Object.prototype.toString 这个方法,我们知道 ({name: 'super'}).toString() === "[object Object]",但是 [1,2].toString()=== '1,2', 万物皆对象,都继承了 object,我们窥探数组内部,发现这里的 toString() 不是调用 Object.prototype.toString(),数组内部对这个方法进行重写了。

试着用 Object.prototype.toString()方法去转化它

var toString = Object.prototype.toString;
toString.call([1,2]) // "[object Array]"
toString.call(1) // "[object Number]"
...
复制代码

利用这个特性,就可以写出我们的类型判断函数

function type(obj) {
    var toString = Object.prototype.toString;
    var toType = {};
    var typeArr = [
        'Undefined',
        'Null',
        'Boolean',
        'Number',
        'String',
        'Object',
        'Array',
        'Function',
        'Date',
        'RegExp',
        'Error',
        'Arguments',
    ];
    typeArr.map(function (item, index) {
        toType['[object ' + item + ']'] = item.toLowerCase();
    });

    return typeof obj !== 'object' ? typeof obj : toType[toString.call(obj)];
}
复制代码

这些数据是如何储存的?


基本数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。

引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

基本数据类型和引用数据类型储存的位置不同,从而引出了深浅拷贝的问题

深拷贝与浅拷贝


  • 所谓浅拷贝,就是拷贝一个对象里的基本数据类型属性和引用数据类型属性的指针地址
  • 所谓深拷贝,就是拷贝一个对象里的所有类型属性,且与原始对象独立开来不受其变动影响

它们的实现方式有所不同:

  • 浅拷贝:如 Array.prototype.slice(), Array.from(), Object.assign() 等都属于是浅拷贝

  • 深拷贝:主要有两种方法—— JSON.parse(string) 和 递归赋值

    • JSON.parse(string)

      JSON.parse(string) 需先用 JSON.stringify(obj) 将对象转化为字符串,因为转化成了字符串,存在了栈内存后再解析成一个新的对象,因此不存在堆内存地址引用的问题。但是它也存在着以下几个问题:

      1. 他无法实现对函数 、RegExp 等特殊对象的克隆
      2. 会抛弃对象的 constructor, 所有的构造函数会指向 Object
      3. 对象有循环引用, 会报错

    • 递归实现

    递归方法可以解决 JSON.parse(string) 存在的问题

function deepClone(data) {
    if (!data || !(data instanceof Object) || typeof data === 'function') {
        return data;
    }
    var constructor = data.constructor;
    var result = new constructor();
    for (var key in data) {
        //不能把原型链上的一起拷贝了
        if (data.hasOwnProperty(key)) {
            result[key] = deepClone(data[key]);
        }
    }
    return result;
}
复制代码

但是这种递归也存在一个问题,就是属性内部引用的问题,如:

// 无限循环递归没有终止条件导致栈溢出
let obj = {
    first: {
        name: 'suporka',
        age: 26,
        children: null,
    },
};
obj.first.children = obj.first;
let objClone = deepClone(obj); // Uncaught RangeError: Maximum call stack size exceeded

// 内部的一个属性引用了另外属性,这个引用不会复制
let obj2 = {
    first: {
        name: 'suporka',
        age: 26,
        children: null,
    },
    second: {
        name: 'suporka2',
        age: 26,
        children: null,
    },
};
obj2.first.children = obj2.second;
let objClone2 = deepClone(obj2);
obj2.second.name = 'super';
objClone2.first.children.name;
// suporka,其实我们想要的是obj2.first.children 和 obj2.second 指向同一个地址,但是递归会重新创建一个新的对象
复制代码

因此我们要创建一个数组去存放这些引用类型的地址

let arr = [];
function deepClone(data) {
    if (!data || !(data instanceof Object) || typeof data === 'function') {
        return data;
    }
    var constructor = data.constructor;
    var result = new constructor();
    for (var key in data) {
        //不能把原型链上的一起拷贝了
        if (data.hasOwnProperty(key)) {
            if (arr.indexOf(data) === -1) {
                result[key] = deepClone(data[key]);
                arr.push(data);
            } else {
                return arr[arr.indexOf(data)];
            }
        }
    }
    return result;
}
复制代码

不同数据类型之间如何转化?


在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值(调用 Boolean()方法)
  • 转换为数字(调用 Number()、parseInt()和 parseFloat()方法)
  • 转换为字符串(调用.toString()或者 String()方法)

注意: null 与 undefinded 没有 toString()方法

原始值 转化目标 结果
number boolean 除了+-0, NaN 都为 true
string boolean 除了空字符串都为 true
undefinded null boolean false
引用类型 boolean true

原始值 转化目标 结果
number string 转化为对应的数字字符串 0 => '0'
boolean string true => 'true', false => 'false'
function string function() {} => 'function() {}'
Symbol string Symbol(23) => 'Symbol(23)'
Array string [1,2,3] => '1,2,3'
Object string {name: 123} => "[object Object]"

原始值 转化目标 结果
string number 转化为对应的数字 '0' => 0, 'a' => NaN
Array number 空数组为0,其他为 NaN
其他引用类型 number NaN
null number 0
Symbol number 报错

上面所述的均为显式转化,下面介绍隐式转化。数据类型在遇到 算术运算符(+、-、*、/、++、–、%…) 或者关系运算符(>、<、==、!=…) 时会进行默认的类型转化,其转化规则是怎样子的?

1、 算术运算符(+、-、*、/、++、–、%…)

  • 若 + 两边存在一个字符串,将另一个也转为字符串进行字符串拼接。
  • 若 + 两边存在一个引用类型,将两者转为字符串进行字符串拼接
  • 若 + 两边由boolean或null或number组成,都转为数值类型

2、关系运算符(>、<、==、!=…)

  • ===、!==:同时对比类型和值,两个都为真才返回真

拓展:是否 === 判断两者是否一致 就完全靠谱?

也是不一定的,例如 0 === -0 就为 true,NaN === NaN 为 false,判断两个变量是否完全相等可以使用 ES6 新增的 API,Object.is(0, -0),Object.is(NaN, NaN)就可以准确区分。

  • ==、!=: 若两边均为对象,对比它们的引用是否相同
  • 逻辑非(!): 将其后变量或表达式转为布尔值
  • 对比字符串:从头至尾扫描逐个比较每个字符的unicode码,直到分出大小
  • 其他情况下,两边均转为数值类型

注意:NaN与任何值都不相同,与任何值比较都返回false

接下来看一道题目:

题目: 为何 [] == ![] 结果为 true,而 {} == !{} 却为 false

首先了解一下 "==" 类型转化的规则:

1、如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false 转换为 0,而 true 转换为 1;

2、如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值

3、如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()(boolean 对象方法)方法或者 toString()方法,用得到的基本类型值按照前面的规则进行比较

null 和 undefined 是相等的

4、要比较相等性之前,不能将 null 和 undefined 转换成其他任何值

5、如果有一个操作数是 NaN,则相等操作符返回 false ,而不相等操作符返回 true。重要提示:即使两个操作数都是 NaN,相等操作符也返回 false 了;因为按照规则, NaN 不等于 NaN (NaN 不等于任何值,包括他本身)

6、如果两个操作数都是对象,则比较它们是不是同一个对象,如果两个操作数都指向同一个对象,则相等操作符返回 true;否则,返回 false

7、 !可将变量转换成 boolean 类型,null、undefined、NaN 以及空字符串('')取反都为 true,其余都为 false。

现在开始分析题目

[] == ![];
// 先转化右边 ![],
// `!`可将变量转换成 boolean 类型,null、undefined、NaN 以及空字符串('')取反都为 true,其余都为 false// 所以 ![] => false => 0

// 左边 [], 因为[].toString() 为空字符串,所以 [] => ''

// 综上, '' == 0, 为 true
复制代码
{} == !{}
// 先转化右边 !{},
// `!`可将变量转换成 boolean 类型,null、undefined、NaN 以及空字符串('')取反都为 true,其余都为 false// 所以 !{} => false => 0

// 左边 ({}).toString() => "[object Object]"

// 综上, "[object Object]" == 0, 为 false
复制代码

算术运算符的妙用

上面提及到了 算术运算符 有类型转化的作用,因此可在业务开发过程中用之进行类型的快速转化,常见的有:

// 快速转化为 Number 类型
let num = '15';
num = +num; // 15

let bool = true;
bool = +bool; // 1
复制代码
// 快速转化为 String 类型
let num = 15;
num = num + ''; // '15'

let bool = true;
bool = +bool; // "true"
复制代码

从快速转换类型联想到快速取整,那么快速取整有哪些方法呢?

var a = ~~2.33 // ~是按位非,就是每一位取反,~~常用来取整

var b= 2.33 | 0 // 或运算

var c= 2.33 >> 0
复制代码

这里便涉及到了位运算。

位运算

位运算是将数字转化为二进制进行运算得出结果

1. 异或

符号:^

规则:相同位不同的会保留 1,相同的会置为 0

var a = parseInt('111111', 2) // a = 63
var b = parseInt('100010', 2) // b = 34

console.log(a ^ b) // 29

// 111111
// 100010  按照规则算异或,得
// 011101  转化为十进制刚好是 29
复制代码

特点和用途

  • 任何数异或自己=把自己置 0;

根据这一点也可以判断两数是否相等,或者可以去除重复的数(这种只限于找出数组中单独存在的一个数)

  • 实现两个值的交换,而不必使用临时变量。
a = a^b;   //a=10100111
b = b^a;   //b=10100001
a = a^b;   //a=00000110
复制代码
  • 实现加密解密
var a = 63 // 原始值
var key = 34 // 秘钥
var a_encryption = a ^ key // 加密原始值,得到29
var a_origin = a_encryption ^ key // 解密得到 63
复制代码

2. 按位取反

符号:~

~ 是按位取反运算,~~ 是取反两次

规则: 1 变为 0,0 变为 1

如 3 的二进制: "00000011"

取反运算结果为: 11111100

特点和用途

  • 用于表示负数

负数的二进制表示由该数的相反数取反后再加1,即 -3 可以表示为

'00000011' => '11111100' + 1 = '11111101'11111101
复制代码
  • 用于取整

因为位运算的操作值要求是整数,其结果也是整数,所以经过位运算的都会自动变成整数, 所以带小数的整数经过两次取整后便舍去了原本的小数部分

~~3.565 = 3
复制代码

3. 与(&)或(|)运算

与运算:只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0。

或运算:对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。

4. 符号移动

左移:将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。

有符号右移:将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。

无符号右移:将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。


总结

类型是 javascript 最基础的知识,但是最基础的东西也可以延伸出很多知识点和考点。这篇文章比较基础,可作查漏补缺只用,如果本文对你有所帮助,请您不吝点赞,也可以关注我的公众号号:小皮咖

本文使用 mdnice 排版