JavaScript的数据类型是每一个前端开发者必须要掌握的内容,也是最基础最重要的角色之一,关于JavaScript数据类型你知道多少?
JavaScript一共有7种数据类型:String
、Number
、Null
、Undefined
、Boolean
、Object
、Symbol
(ES6新增)。他们可以分为两大类:值类型和引用类型。在文章最后会讲解他们的区别。
undefined
undefined
未找到的意思,它的值只有一个,也就是undefined
,我们可以通过以下几种方式得到undefined
:
- 已用一个已声明但未初始化的变量
- 一个没有返回值的函数
- 执行void表达式
- 引用未定义的对象属性
- 全局常量
window.undefined
或undefined
var a;
var obj = {
a: 'A',
}
var fn = function (){
var b = 2;
}
console.log(a); // undefined
console.log(void 0); // undefined
console.log(obj.b); // undefined
console.log(fn()); // undefined
window.undefined // undefined
如何判断一个变量的值是否为undefined呢?
前面提到了undefined
就是未找到的意思,那现在有下面几种方案看是否可行:
(1)逻辑取非转为布尔值值判断
var a;
if (!a) {
// 具体操作
}
(2)直接与undefined
进行比较
var a;
if (a === undefined) {
// 具体操作
}
(3)判断数据类型是否等于undefined
字符串
var a;
if (typeOf a === "undefined") {
// 具体操作
}
上面3种方案只有第三种是可行的,第一种,若a的值为null、空字符串、数字0时取非后值都为true。第二种使用三个“=”是可以的,但是如果a未定义时会抛出错误。所以建议使用第三种方案。
Null
Null
与undefined
类似,它也只有一个唯一的值,那就是null,表示为空的意思。如果我们将null与undefined作弱比较,可以发现他们是相等的。
null == undefined // true
null === undefined // false
null
为JavaScript的保留字关键字,undefined
是一个常量,因此null不能作为变量名。
var undefined = 1 // undefined
var null = 1 //Uncaught SyntaxError: Unexpected token 'null'
Boolean
boolean
只有两个值:true和false
,常用于判断语句中。例如下面这个例子,在一个数组中筛选出偶数:
var arr = [2, 4, 5, 1, 6, 7];
var odd = [];
arr.map((item) => {
if (item % 2 === 0) {
odd.push(item);
}
})
console.log(odd); // [2, 4, 6]
他们之间可以通过取非相互转换:
var f = true;
console.log(f); // true
console.log(!f); // false
console.log(!!f); // true
Number
number
表示数字,在JavaScript中不会区分一个数字具体是一个整数还是一个小数,又或者是一个浮点数。他们都属于number
类型。number有两个特殊的值:NaN
和 Infinity
。
NaN(Not a Number)
通常在计算失败的时候会得到该值。要判断一个变量是否为 NaN,则可以通过 Number.isNaN 函数进行判断。Infinity
是无穷大,加上负号 “-” 会变成无穷小,在某些场景下比较有用,比如通过数值来表示权重或者优先级,Infinity
可以表示最高优先级或最大权重。
在JavaScript中对`number进行一些计算时可能会得到一些非期望的结果,如0.1 + 0.3
console.log(0.1 + 0.3); // 0.30000000000000004
这是因为JavaScript在计算时,JavaScript引擎会先将十进制的数转为二进制,计算完成后再转为二进制。在进制转换过程中,如果小数是循环的话,那么就会出现误差。再比如把3开方后在平方后的结果也不等于3。
Math.pow(Math.pow(3, 1/2), 2) // 2.9999999999999996
要想解决精度误差问题,有两种方案:
(1)计算前先把小数转为整数,计算后再转为整数
console.log((0.1 * 10 + 0.3 * 10) / 10); // 0.3
(2)使用precision
进行四舍五入,以定点表示法或指数表示法表示的一个数值对象的字符串表示,四舍五入到 precision
参数指定的显示数字位数。
parseFloat((0.1 + 0.2).toPrecision(12)) // 0.3
在涉及到number计算的场景下,我们常会用到一些方法,对这些方法熟记于心才可以在不同的场景下灵活的运用。
// 取绝对值
Math.abs(x);
// 返回一个数的立方根
Math.cbrt(x)
// 一个数向上取整后的值
Math.ceil(x);
// 一个数向下取整后的值
Math.floor(x)
// 零到多个数值中最大值
Math.max([x[, y[, …]]])
// 零到多个数值中最小值
Math.min([x[, y[, …]]])
// 一个 0 到 1 之间的伪随机数
Math.random()
// 四舍五入后的整数
Math.round(x)
// 一个数的符号,得知一个数是正数、负数还是
Math.sign(x)
// ......
String
字符串类型,最常见的数据类型之一。我们可以通过.length
获取字符串的长度,也可以通过substr方法从一段字符串中解决我们所需要的部分,还可以使用split方法把字符串转为一个数组。
var str = 'abcdefg'
console.log(str.length) // 8
console.log(str.substr(2, 5)) // bcdef
console.log(str.split('')) // ["a", "", "b", "c", "d", "e", "f", "g"]
Symbol
Symbol
是 ES6 中引入的新数据类型,它表示一个唯一的常量,通过 Symbol 函数来创建对应的数据类型,创建时可以添加变量描述,该变量描述在传入时会被强行转换成字符串进行存储。
var a = Symbol('1')
var b = Symbol(1)
a.description === b.description // true
var c = Symbol({id: 1})
c.description // [object Object]
var _a = Symbol('1')
_a == a // false
Symbol
的最大特点就是它所定义的值永远都是唯一的,常用与的场景有,避免常量值重复、避免对象属性重复等。
看下面这个例子,有一个函数,对所传入的对象要添加一个name属性:
function fn(person) {
person.name = 'adc';
console.log(person); // { name: 'abc' }
}
fn({
name: 'aaa',
age: 22
});
可以看到当传入的对象含有name属性时,会把原有的name覆盖掉,这并不是我们想要的结果。为了避免对象属性的重复覆盖,可以使用symbol
很好的解决这个问题:
function fn(person) {
const name = Symbol('name');
person[name ]= 'adc';
console.log(person); // { name: 'aaa', [Symbol(name)]: 'abc' }
}
fn({
name: 'aaa',
age: 22
});
数据类型的检测
我们已经知道了JavaScript有这些数据类型,在开发中,往往需要判断某个变量的类型来进行一些后续的操作,有哪些方法可以检测数据的数据类型呢?
(1)typeof
const a = 1;
const b = '1';
const c = null;
const d = undefined;
const e = false;
const f = [1, 2, 3];
const g = { name: 'abc' };
const h = new Date();
const i = new RegExp();
console.log(typeof a); // number
console.log(typeof b); // string
console.log(typeof c); // object
console.log(typeof d); // undefined
console.log(typeof e); // boolean
console.log(typeof f); // object
console.log(typeof g); // object
console.log(typeof h); // object
console.log(typeof i); // symbol
上面的代码中,我们定义了不同类型的常量,打印出了通过typeof方法所检测的值,可以看到对于number
、string
、undefined
、boolean
和symbol
数据类型是可以检测数来的,但是对于null
、Array
、Object
等类型的结果都是object
。也就是说typeof只可以检测出值类型的数据,无法分辨出引用类型的数据。
(2)instanceof
instanceof用来判断A是否是B的实例,如果是则返回true
,不是则返回false
。
console.log([] instanceof Array); //true
console.log({} instanceof Object); //true
console.log(new Date() instanceof Date); //true
console.log(new Date() instanceof Object); //true
console.log(new RegExp() instanceof Object); //true
但是这种方式还是比较有局限性,instanceof
只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型,而且 instanceof
对于引用类型的支持很好,但他是无法对原始类型进行判断,所以一般都是在 typeof
判断为object
时才使用instanceof
。
(3)constructor
该方法可以打印出目标实例所属的类,函数的 constructor
是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype
后,原有的 constructor
引用会丢失,constructor
会默认为 Object
。
console.log([].constructor == Array); //true
console.log({}.constructor == Object); //true
console.log("1".constructor == String); //true
console.log((1).constructor == Number); //true
console.log(true.constructor == Boolean); //true
console.log(Symbol().constructor == Symbol); //true
console.log(new Date().constructor == Date); //true
console.log(new RegExp().constructor == RegExp); //true
但这种方式无法检测null
和undefined
,他们是无效的对象,因此不会存在constructor
。
(4)Object.prototype.toString.call()
toString
是Object
的一个方法,在Object
上调用该方法会返回[object Object]
,其中第二个Object就是他的数据类型。但是对于其他类型需要使用call / apply
才可以返回正确的数据类型。
const a = 1;
const b = '1';
const c = null;
const d = undefined;
const e = false;
const f = [1, 2, 3];
const g = { name: 'abc' };
const h = new Date();
const i = new RegExp();
const j = Symbol('name');
const k = function(){};
console.log(Object.prototype.toString.call(a)); //[object Number]
console.log(Object.prototype.toString.call(b)); //[object String]
console.log(Object.prototype.toString.call(c)); //[object Null]
console.log(Object.prototype.toString.call(d)); //[object Undefined]
console.log(Object.prototype.toString.call(e)); //[object Boolean]
console.log(Object.prototype.toString.call(f)); //[object Array]
console.log(Object.prototype.toString.call(g)); //[object Object]
console.log(Object.prototype.toString.call(h)); //[object Date]
console.log(Object.prototype.toString.call(i)); //[object RegExp]
console.log(Object.prototype.toString.call(j)); //[object Symbol]
console.log(Object.prototype.toString.call(k)); //[object Function]
类型的相互转换
JavaScript 是一种弱类型的语言,相对于其他高级语言有一个特点,那就是在处理不同数据类型运算或逻辑操作时会强制转换成同一数据类型。如果我们处理不好转换问题,在开发过程中可能会引发很多bug。
(1)强制转换
所谓强制转换,就是我们通过一些方法主动的去转换数据类型。
- 转为String:
.toString()
、String()
、JSON.stringify
- 转为Number:
Number()
、parseInt()
- 转为Boolean:
Boolean()
- 转为对象:
JSON.parse()
(2)隐式转换
所谓隐式转换,是在我们进行一些操作时,针对于不同的情况,JavaScript会自动的对一些数据的数据类型进行装换。常发生于一下情况:
运算相关的操作符包括 +、-、+=、++、* 、/、%、<<、& 等。
数据比较相关的操作符包括 >、<、== 、<=、>=、===。
逻辑判断相关的操作符包括 &&、!、||、三目运算符。
下面列举了一下例子,这里就不再一一分讲解了:
console.log(1 + 1 + '1'); //21
console.log(1 + '1' + 1); //111
console.log(1 + true); //2
console.log(1 + false); //1
console.log(1 + null); //1
console.log(1 + undefined); //NaN
console.log(null + undefined); //NaN
console.log('a' + undefined); //aundefined
console.log('a' + null); //anull
console.log(undefined + 'b'); //undefinedb
console.log(false == 0); //true
console.log(false == null); //false
console.log(false == undefined); //false
console.log(!null); //true
console.log(!undefined); //true
console.log(!12); //false
console.log(!'false'); //false
值类型与引用类型
在上面提到了很多次值类型和引用类型,那么他们究竟有什么区别?
var a = 1;
var b = {
name: 'abc',
age: 22
};
var c = a;
var d = b;
console.log(c); // 1
console.log(d); // { name: 'abc', age: 22 }
a = 2;
b.name = 'def';
console.log(c); // 1
console.log(d); // { name: 'def', age: 22 }
在上面的例子中,声明了两个变量a和b,a为Number
类型,b为Object
类型。然后把a赋值给变量c,b赋值给变量d。对a和b的值都进行改变,然后打印出c和d,得到的结果是:c的值还是原来的值,而d的值是改变后的b的值。明明赋值在改变值的前面进行的,为什么会出现这种情况呢?
这与不同数据类型的存储方式不同有关,值类型是以栈的方式存储的,而引用类型是存储在堆中的。当值类型赋值给其他变量时,会在栈中生成一个新的空间,其值与原值相同,但是他们之间是相互独立的,没有任何联系。
保存与复制的是指向对象的一个指针,当堆中的值改变后,指向该堆的变量值都会一起改变。
接着上面的那个例子,改变d的age属性,可以看到b的age也发生了改变。
d.age = 18;
console.log(b); // { name: 'def', age: 18 }
浅拷贝与深拷贝
- 由于引用类型在赋值时只传递指针,这种拷贝方式称为浅拷贝。
- 而创建一个新的与之相同的引用类型数据的过程称之为深拷贝。
实现深拷贝的方式:
(1)通过把一个对象转为一个字符串后再转为对象进行复制的方式
var obj = {
name: 'a',
path: 'a',
}
var copy = JSON.parse(JSON.stringify(obj));
obj.path = 'b';
console.log(copy); // { name: 'a', path: 'a' }
这种方式不能undefined
、function
、RegExp
等类型
(2) 通过Object.assign(target, source)
创建一个新的对象
var obj = {
a: 1,
b: 2,
c: 3
}
var copy = Object.assign({}, obj);
copy.b = 5;
console.log(obj.b); // 2
console.log(copy.b); // 5
这种方式看起来也可以,但是它对只有一层的数据有效,如下面这种大于一层的结构就不行了:
var obj2 = {
a: 1,
b: 2,
c: ['A', 'B', 'C']
}
var copy2 = Object.assign({}, obj2);
copy2.c[0] = 'D';
console.log(obj2.c); // [ 'D', 'B', 'C' ]
console.log(copy2.c); // [ 'D', 'B', 'C' ]
(3)递归拷贝
对于大于一层的情况,可以使用递归的方法逐层拷贝。这里要注意一点,使用typeof
判断null的类型时返回的也是'object'
function deepClone(target) {
let result;
if (typeof target === 'object') {
if (Array.isArray(target)) {
result = [];
for (let i in target) {
result.push(deepClone(target[i]))
}
} else if(target===null) {
result = null;
} else if(target.constructor===RegExp){
result = target;
}else {
result = {};
for (let i in target) {
result[i] = deepClone(target[i]);
}
}
} else {
result = target;
}
return result;
}
你能看到最后,也许说明我的文章对你有所帮助
如果你觉得还不错的话,可以关注我的个人公众号前端筱园,不错过我的每一篇推送
也欢迎访问我的个人网站:www.dengzhanyong.com