现在去做前端面试题都是心虚的, 本来可以做对的题,想想好像有坑,然后答错了。举个例子:
Number([0]); // 0
[0] == true; // false
if ([0]) alert('ok'); // "ok" // 恩? 不都是 false 吗
复制代码
所以本文将尽可能多的去试图挖一下 javascript 数据类型方面的蹊跷。
- 数据相等
- 判等于
- 判大小
- -0 问题
- 数据类型判断
- if 判断
- typeof 判断
- instanceof 判断
- constructor 判断
- is 方法判断(如 isNaN())
- 其他判断
- Object.is() 方法
- key in obj 判断
- prototype 判断
- 强制数据类型转换
- 运算时自动转换(如 +'6')
- 对象式转换(如 Number([1]))
- 函数式转换
- 原型函数(如 toString())
- parse 系列函数(如 parseInt([1]))
- 参数的隐形转换
- 其他强行转换转换(如 JSON.stringify())
数据相等
这张图大伙应该很熟悉了,但其实这里面有些很诡异的问题,很迷很迷。
0 == '0'; // true
0 == []; // true
'0' == []; // false
复制代码
双等时也许是进行了类型转换的, 比如都转为数字或字符串后再进行的比较。
个人猜测转换的顺序 可能 如下:
undefined < null < Boolean < Number < String < Array
复制代码
它是一层层向下进行转换后进行比较的。
'0' == true // false
// 实则是 0 == true 的比较
复制代码
再比如
Boolean([0]); // true
[0] == true; // false
// 实际是 '0' == true 最后 0 == true 的比较
复制代码
<=
这类数值判断,也是类似的,但很快就发现, 以上猜测并不完善,还有更多一步前置的 Number
转换。
2 > true; // true
'1' > '2'; // false
复制代码
undefined == undefined; // true
undefined <= undefined; // false
// 因为 Number(undefined) 的结果是 NaN
复制代码
注意 [2] == [2]
当然是为 false
啦, 这个可 别混淆 了,以为也要去转化。
此处稍微提一下 -0
的存在,会造成 Infinity
与 -Infinity
的不同。 但我们多半会做分母不为零的判断的,恩大概会的吧。
0 === -0; // true
(1/-0) === (1/0); // false
复制代码
数据类型判断
if 判断
一般使用 if 大致会有以下五种情况,三目判断 和 并或非 也包含其中。
if (a <= b)
if (a)
if (a())
if (a = 1)
if (!a)
复制代码
如图所示,if 中结果即是
Boolean()
转化后的结果。
请再回味一番,切实记住 if 判断与等于判断的不同哟。
还以为 !a
的判断会有坑,试验下来舒了口气,并没有什么特别之处。
typeof 判断
这章好像要记住的和留意的东西也并不多,
typeof [] === 'object';
typeof NaN === 'number'
typeof null === 'object'
复制代码
却也是判断中稍有点难判的,所以才出现了 Array.isArray
和 isNaN
这样的方法存在。 为啥我试不出 typeof 为 array 的情况呀,很奇怪耶,是我记错了咩
还有像 Date
RegExp
arguments
等自然就是对象了,typeof
的坑相对要少很多。
instanceof 判断
用 [] instanceof Array
判数组真的很方便,但这块也还是有坑的。
'a' instanceof String // false
(new String('a')) instanceof String // true
复制代码
除此之外,还有原型链上的一点问题:
function Foo(){}
var foo = new Foo();
console.log(foo instanceof Foo); //true
Foo.prototype = new Aoo();
var foo2 = new Foo();
console.log(foo2 instanceof Foo) // true
console.log(foo2 instanceof Aoo) // true
复制代码
说实话,除了几个特例,用这个来判原型其实并不是很好的方法。 参考:www.ibm.com/developerwo…
constructor 判断
constructor
相比 instanceof
有一点优势,就是它不随 __proto__
的改变
function A(){};
var a = new A();
var b = new A();
a.__proto__ = {};
a instanceof A // false
b.constructor === A // true
复制代码
以往 es5 做继承时还要自己给 A.prototype.constructor
设新值, 有了 es6 的 class
后已经很简单了,用 constructor
来判原型也稳妥了起来。 至于基础数据类型嘛,也不太推荐用此方法。
is 方法判断
isFinite();
isNaN();
Number.isNaN();
Array.isArray();
复制代码
其他判断
Object.is() 判断
其实 Object.is() 是类似 === 的,但又有点不一样,它是真真正正的绝对相等。
+0 === -0 // true
Object.is(+0, -0) // false
NaN === NaN // false
Object.is(NaN, NaN) // true
复制代码
key in object 判断
还需稍微分清一下原型与实例即可,即 for-in
和 for-of
的区别。
'0' in [1, 2]; // true
'now' in Date; // true
'getFullYear' in Date; // false
复制代码
至于项目是使用以下哪种判断就见仁见智了。
if (Array.prototype.includes) {}
'includes' in [];
复制代码
prototype 判断
obj.hasOwnProperty(key)
和 obj.isPrototypeOf(obj2)
等相关方法,整理中
强制数据类型转换
运算式自动转换
+' 014' // 14
+'0x12' // 18
1 + '14' // '114'
1 + '0x12' // '10x12'
1 + +'14' // 15
'14' + 1 // '141'
1 + [1, 1]; // '11,1'
1 + {}; // '1[object Object]'
1 + null; // 1
1 +undefined; // NaN
复制代码
很鲜明,当有单独的运算符存在时(单加括号是不行滴), 会帮忙 Number
转换,否则 String
转换。 还请注意上例中后 4 种特殊的情况。
进行 ++
运算时并不会帮忙转换为数字,还容易报错。 所以使用时这里得留个心眼哟。
++'14' // ReferenceError
复制代码
还有两个特立独行的数字运算,即 Infinity
和 0
的正负号。
Infinity+Infinity; // Infinity
-Infinity+(-Infinity); // -Infinity
Infinity+(-Infinity); // NaN
+0+(+0); // 0
(-0)+(-0); // -0
(+0)+(-0); // 0
复制代码
再看一个绝对不会遇到的特例, {} + []
理论上应该是 '[object Object]' + ''
才对, 就算不是也应该是 NaN + 0
吧,结果我又猜错了。 遇事不决问百度,结果震惊了,这里的 {}
被当成空代码块了,+[]
自然就是 0
了。
[] + {}; // '[object Object]'
{} + []; // 0
复制代码
对象式转换
Number() 转换
Number
与 parseInt
的不同,将于下文 parseInt 系列方法 讲述
String() 转换
探讨一下 String
和 toString
的不同吧。
一方面是部分数据类型没有 toString
方法:
String(null); // 'null'
(null).toString(); // Uncaught TypeError
(undefined).toString(); // Uncaught TypeError
复制代码
另一方面是 toString
可以传个进制数的参(仅对数字类型有用)
(30).toString(16); // "1e"
('30').toString(16); // "30"
复制代码
至于 Date
Error
RegRxp
的字符串化,基本不会出啥幺蛾子。
用原型的 toString
来判数据类型也是种很巧妙常用的方法。
function typeOf(obj) {
var typeStr = Object.prototype.toString.call(obj).split(" ")[1];
return typeStr.substr(0, typeStr.length - 1).toLowerCase();
}
typeOf([]); // 'array'
复制代码
函数式转换
原型方法
toString
在上文已有介绍,但还得再区分一下数组的。
[1,[2,"abc","",0,null,undefined,false,NaN],3].toString();
// "1,2,abc,,0,,,false,NaN,3"
复制代码
也即是下例想表达的意思:
(null).toString(); // Uncaught TypeError
[null].toString(); // ''
复制代码
toString
与 valueOf
大致是相同的,但是否有不同,整理中...
再则 (1).toFixed
Date.parse
等,应该不会有啥常见错误。 只需注意那些是会 对入参进行隐形转换 的,下文 参数的隐形转换 将介绍
parseInt 系列方法
window.parseInt
和 Number.parseInt
是全等的,即完全相同。
主要来看 Number
与 parseInt
的不同,挺迷的, 它们并不是单纯的数据类型转化那么简单,举个例子:
Number(''); // 0
parseInt(''); // NaN
复制代码
parseInt
就很花哨,还会再多进行一些暗箱操作来判断和适配成数字。 可见,用 Number
转非整数时会是更好的选择。
parseInt(' 10 '); // 10 // 自动去空格,通用
parseInt('10.2'); // 10 // 数字后的全剔除,Number 和 parseFloat 没问题
parseInt('1e2'); // 1 // 区分不出科学计数法,Number 和 parseFloat 没问题
parseFloat('0x5'); // 0 // 区分不出进制,Number 和 parseInt 没问题
复制代码
当参数为数组时,当然也是先转 String
的咯, 而 parseInt
又能去除 ,
后的字符,所以就有下面的情况。
Number([1, 2]); // NaN
parseInt([1, 2]); // 1
复制代码
参数的隐形转换
比较典型的 isNaN
是先用 Number
转了一次,但 Number.isNaN
就没有。
isNaN('1x'); // true
Number.isNaN('1x'); // false
复制代码
这方面没做什么整理,遇到了再补吧。
'12'.replace(1, ''); // "2"
复制代码
其他强行转换
JSON.stringify()
JSON.parse(JSON.strigify())
深拷贝时可得注意了哟。 其实递归加对象解构来做深拷贝要更好一些哟。
JSON.stringify(Infinity); // 'null'
JSON.stringify(NaN); // 'null'
JSON.stringify(undefined); // undefined
JSON.stringify({a: undefined}); // '{}'
JSON.stringify({a: null}); // '{"a":null}'
JSON.stringify(() => {}); // 'undefined'
复制代码
encode 系列
encodeURI
方法不会对下列字符编码 ASCII字母、数字、~!@#$&*()=:/,;?+'
encodeURIComponent
方法不会对下列字符编码 ASCII字母、数字、~!*()'
所以 encodeURIComponent
比 encodeURI
编码的范围更大。
其他
Array.from('foo'); // ["f", "o", "o"]
Object.assign([1], [2,3]); // [2, 3]
复制代码
大致就是这些了,写完要自闭一会,整个过程充满了怀疑与揣测。 虽然做了较为系统的拆分,但还是得承认没写好,敬请期待后续吧。
我还有一个 BUG 库,不妨也分享出来一起看看吧。