map()函数与parseInt()函数的隐形坑
- 例子:存在一个数组,数组中的每个元素都是Number类型的字符串[‘1’,‘2’, ‘3’, ‘4’],如果我们想要将数组中的元素全部转换为整数,应该怎么做?
我们可能会想到在Array的map()函数中调用parseInt()函数。像下面这样:
var arr = ['1', '2', '3', '4'];
var result = arr.map(parseInt);
console.log(result);
但是在运行后,得到的结果是[1, NaN, NaN, NaN],与我们期望的结果[1, 2, 3, 4]差别很大,其实这就是一个藏在map()函数与parseInt()函数中的隐形坑。
arr.map(parseInt)
上面的代码实际与下面的代码等效:
arr.map(function (val, index) {
return parseInt(val, index);
});
parseInt()函数接收的第二个参数实际为数组的索引值,所以实际处理的过程如下所示。
parseInt('1', 0); // 1
parseInt('2', 1); // NaN
parseInt('3', 2); // NaN
parseInt('4', 3); // NaN
任何整数以0为基数取整时,都会返回本身,所以第一行代码会返回“1”。
第二行代码parseInt(‘2’, 1),因为parseInt()函数对应的基数只能为2~36,不满足基数的整数在处理后会返回“NaN”;
第三行代码parseInt(‘3’, 2),表示的是将3处理为二进制表示,实际上二进制时只有0和1,3超出了二进制的表示范围,无法转换,返回“NaN”;
第四行代码parseInt(‘4’, 3),与第三行类似,4无法用三进制的数据表示,返回“NaN”。
因此我们在map()函数中使用parseInt()函数时需要注意这一点,不能直接将parseInt()函数作为map()函数的参数,而是需要在map()函数的回调函数中使用,并尽量指定基数,代码如下所示。
var arr = ['1', '2', '3', '4'];
var result = arr.map(function (val) {
return parseInt(val, 10);
});
console.log(result); // [1, 2, 3, 4]
isNaN()函数与Number.isNaN()函数
Number类型数据中存在一个比较特殊的数值NaN(Not a Number),它表示应该返回数值却并未返回数值的情况。
NaN存在的目的是在某些异常情况下保证程序的正常执行。例如0/0,在其他语言中,程序会直接抛出异常,而在JavaScript中会返回“NaN”,程序可以正常执行。
NaN的特点:
-
- 任何涉及NaN的操作都会返回“NaN”
-
- NaN与任何值都不相等,包含NaN自身
在判断NaN时,ES5提供了isNaN()函数,ECMAScript 6(后续简称ES6)为Number类型增加了静态函数isNaN()。
既然在ES5中提供了isNaN()函数,为什么要在ES6中专门增加Number.isNaN()函数呢?两者在使用上有什么区别呢?
这是因为isNaN()函数本身存在误导性,而ES6中的Number.isNaN()函数会在真正意义上去判断变量是否为NaN,不会做数据类型转换。只有在传入的值为NaN时,才会返回“true”,传入其他任何类型的值时会返回“false”。
可以通过下面的代码测试下:
isNaN(NaN); // true
isNaN(undefined); // true
isNaN({}); // true
isNaN(true); // false,Number(true)会转换成数字1
isNaN(null); // false,Number(null)会转换成数字0
isNaN(1); // false
isNaN(''); // false,Number('')会转换为成数字0
isNaN("1"); // false,字符串"1"可以转换成数字1
isNaN("JavaScript"); // true,字符串"JavaScript"无法转换成数字
// Date类型
isNaN(new Date()); // false
isNaN(new Date().toString()); // true
// Date是一种比较特殊的类型,当我们调用new Date()函数生成的实例并转换为数值类型时,
// 会转换为对应的时间戳,例如下面的代码
Number(new Date()); // 1543333199705
Number.isNaN(NaN); // true
Number.isNaN(undefined); // false
Number.isNaN(null); // false
Number.isNaN(true); // false
Number.isNaN(''); // false
Number.isNaN(123); // false
总结:
isNaN()函数与Number.isNaN()函数的差别如下:
- isNaN()函数在判断是否为NaN时,需要先进行数据类型转换,只有在无法转换为数字时才会返回“true”
- Number.isNaN()函数在判断是否为NaN时,只需要判断传入的值是否为NaN,并不会进行数据类型转换。
JavaScript常用判空方法
1. 判断变量为空对象
function isEmpty(obj) {
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
2. 判断变量为空数组
arr instanceof Array && arr.length === 0
3. 判断变量为空字符串
str == '' || str.trim().length == 0;
4. 判断变量为0或者NaN
!(Number(num) && num) == true;
new操作符到底做了什么?
new操作符在执行过程中会改变this的指向,所以在了解new操作符之前,我们先解释一下this的用法。
function Cat(name, age) {
this.name = name;
this.age = age;
}
console.log(new Cat('miaomiao',18)); // Cat {name: "miaomiao", age: 18}
输出的结果中包含了name与age的信息。事实上我们并未通过return返回任何值,为什么输出的信息中会包含name和age属性呢?其中起作用的就是this这个关键字了。
我们通过以下代码输出this,看看this具体的内容。
function Cat(name,age) {
console.log(this); // Cat {}
this.name = name;
this.age = age;
}
new Cat('miaomiao',18);
我们可以发现this的实际值为Cat空对象,后两句就相当于给Cat对象添加name和age属性,结果真的是这样吗?不如我们改写一下Cat函数。
function Cat(name,age){
var Cat = {};
Cat.name = name;
Cat.age = age;
}
console.log(new Cat('miaomiao',18)); // Cat {}
输出结果中并未包含name和age属性,这是为什么呢?
因为在JavaScript中,如果函数没有return值,则默认return this。而上面代码中的this实际是一个Cat空对象,name和age属性只是被添加到了临时变量Cat中。为了能让输出结果包含name和age属性,我们将临时变量Cat进行return就可以了。
function Cat(name, age) {
var Cat = {};
Cat.name = name;
Cat.age = age;
return Cat;
}
console.log(new Cat('miaomiao', 18)); // {name: "miaomiao", age: 18}
最后的返回值中包含了name和age属性。通过以上的分析,我们了解了构造函数中this的用法,那么它与new操作符之间有什么关系呢?
我们先来看看下面这行简单的代码,该代码的作用是通过new操作符生成一个Cat对象的实例。
var cat = new Cat();
从表面上看这行代码的主要作用是创建一个Cat对象的实例,并将这个实例值赋予cat变量,cat变量就会包含Cat对象的属性和函数。
其实,new操作符做了3件事情,如下代码所示。
1. var cat = {};
2. cat._ _proto_ _ = Cat.prototype;
3. Cat.call(cat);
第一行:创建一个空对象。
第二行:将空对象的__proto__属性指向Cat对象的prototype属性。
第三行:将Cat()函数中的this指向cat变量。
于是cat变量就是Cat对象的一个实例。
我们自定义一个类似new功能的函数,来具体讲解上面的3行代码。
function Cat(name, age) {
this.name = name;
this.age = age;
}
function New() {
var obj = {};
var res = Cat.apply(obj, arguments);
return typeof res === 'object' ? res : obj;
}
console.log(New('mimi', 18)); //Object {name: "mimi", age: 18}
返回的结果中也包含name和age属性,这就证明了new运算符对this指向的改变。Cat.apply(obj, arguments)调用后Cat对象中的this就指向了obj对象,这样obj对象就具有了name和age属性。
因此,不仅要关注new操作符的函数本身,也要关注它的原型属性。
我们对上面的代码进行改动,在Cat对象的原型上增加一个sayHi()函数,然后通过New()函数返回的对象,去调用sayHi()函数,看看执行情况如何。
function Cat(name, age) {
this.name = name;
this.age = age;
}
Cat.prototype.sayHi = function () {
console.log('hi')
};
function New() {
var obj = {};
var res = Cat.apply(obj, arguments);
return typeof res === 'object' ? res : obj;
}
console.log(New('mimi', 18).sayHi());
运行以上代码得到的结果如下所示:
Uncaught TypeError: New(...).sayHi is not a function
我们发现执行报错了,New()函数返回的对象并没有调用sayHi()函数,这是因为sayHi()函数是属于Cat原型的函数,只有Cat原型链上的对象才能继承sayHi()函数,那么我们该怎么做呢?
这里需要用到的就是__proto__属性,实例的__proto__属性指向的是创建实例对象时,对应的函数的原型。设置obj对象的__proto__值为Cat对象的prototype属性,那么obj对象就继承了Cat原型上的sayHi()函数,这样就可以调用sayHi()函数了。
function Cat(name, age) {
this.name = name;
this.age = age;
}
Cat.prototype.sayHi = function () {
console.log('hi')
};
function New() {
var obj = {};
obj._ _proto_ _ = Cat.prototype; // 核心代码,用于继承
var res = Cat.apply(obj, arguments);
return typeof res === 'object' ? res : obj;
}
console.log(New('mimi', 18).sayHi());
结果输出“hi”,方法调用成功。