「面试基础小册」系列正式开写。主要是对一些基础相关的知识进行归纳整理与拓展。后续还有更多,敬请期待
本文讲述的是 javascript 的类型相关知识,并且对此进行延伸。
JavaScript 一共有 8 种数据类型,其中有 7 种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6 新增,表示独一无二的值) 和 BigInt(ES10 新增)
一种引用数据类型——Object,里面包含 Function、Array、Date 等。
undefined 代表的含义是未定义, null 代表的含义是空对象(但又不是对象)。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。还有一个是:
Number
转换的值不同,Number(null)
输出为0
,Number(undefined)
输出为NaN
虽然
typeof null
会输出object
,但是这只是JS
存在的一个悠久Bug
。在JS
的最初版本中使用的是32
位系统,为了性能考虑使用低位存储变量的类型信息,000
开头代表是对象然而null
表示为全零,所以将它错误的判断为object
。
这里使用了 typeof 函数去判断类型,那么在 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)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
基本数据类型和引用数据类型储存的位置不同,从而引出了深浅拷贝的问题。
它们的实现方式有所不同:
• 递归实现 递归方法可以解决 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 中类型转换只有三种情况,分别是:
注意: 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、 算术运算符(+、-、*、/、++、–、%…)
2、关系运算符(>、<、==、!=…)
拓展:是否 === 判断两者是否一致 就完全靠谱? 也是不一定的,例如 0 === -0 就为 true,NaN === NaN 为 false,判断两个变量是否完全相等可以使用 ES6 新增的 API,Object.is(0, -0),Object.is(NaN, NaN)就可以准确区分。
注意: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,相同的会置为 0
var a = parseInt('111111', 2) // a = 63
var b = parseInt('100010', 2) // b = 34
console.log(a ^ b) // 29
// 111111
// 100010 按照规则算异或,得
// 011101 转化为十进制刚好是 29
复制代码
特点和用途
根据这一点也可以判断两数是否相等,或者可以去除重复的数(这种只限于找出数组中单独存在的一个数)
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
复制代码
符号:~
~
是按位取反运算,~~
是取反两次
规则: 1 变为 0,0 变为 1
如 3 的二进制: "00000011"
取反运算结果为: 11111100
特点和用途
负数的二进制表示由该数的相反数取反后再加1,即 -3 可以表示为
'00000011' => '11111100' + 1 = '11111101' 即 11111101
复制代码
因为位运算的操作值要求是整数,其结果也是整数,所以经过位运算的都会自动变成整数, 所以带小数的整数经过两次取整后便舍去了原本的小数部分
~~3.565 = 3
复制代码
与运算:只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0。
或运算:对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。
左移:将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。
有符号右移:将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。
无符号右移:将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。
类型是 javascript 最基础的知识,但是最基础的东西也可以延伸出很多知识点和考点。