原生 JavaScript 灵魂拷问,你能答上多少 (一)

前言
当下的前端开发,三大框架三分天下,框架的简单、强大让我们欲罢不能,使用原生 JavaScript 越来越少。
但我认为 JavaScript 作为每一个前端工程师的立身之本,不止要学会,还要学好、学精,学再多遍都不为过。
另一方面,前端面试中,越来越重视原生 JavaScript 的考察,其所占比例也越来越高。
因此我决定整理JavaScript中容易忽视或者混淆的知识点,写一系列篇文章,以灵魂拷问的方式,系统且完整的带大家遨游原生 JavaScript 的世界,希望能给大家带来一些收获。
JS 类型之问——概念与检测篇
1.JS 中的数据类型有哪些?
- 基本数据类型:共有 7 种 
Symbol : ES6 引入的一种新的原始值,表示独一无二的值,主要为了解决属性名冲突问题。
Bigint :ES2020 新增加,是比 Number 类型的整数范围更大。
- 引用数据类型:1 种 
2.你真的懂 typeof 吗?
- typeof的作用?
- 区分数据类型,可以返回 7 种数据类型: - number、string、boolean、undefined、object、function,以及- ES6新增的- symbol
- typeof能正确区分数据类型吗?
- 不能。对于原始类型,除 - null都可以正确判断;对于引用类型,除- function外,都会返回- "object"
- typeof注意事项
- typeof返回值为- string格式,注意类似这种考题:- typeof(typeof(undefined)) -> "string"
- typeof未定义的变量不会报错,返回- "undefiend"
- typeof(null) -> "object": 遗留已久的- bug
- typeof无法区别数组与普通对象:- typeof([]) -> "object"
- typeof(NaN) -> "number"
- 习题 
答案
3.什么是 instanceof?你能模拟实现一个 instanceof 吗?
- instanceof判断对象的原型链上是否存在构造函数的原型。只能判断引用类型。
- instanceof常用来判断- A是否为- B的实例
- 模拟实现 - instanceof
思想:沿原型链往上查找
测试:
4.如何区分数组与对象?使用 instanceof 判断数组可靠吗?
- ES6提供的新方法- Array.isArray()
- 如果不存在 - Array.isArray()呢?可以借助- Object.prototype.toString.call()进行判断,此方式兼容性最好
- instanceof判断
判断方式
instanceof 判断数组类型如此之简单,为何不推荐使用那?
instanceof 操作符的问题在于,如果网页中存在多个 iframe ,那便会存在多个 Array 构造函数,此时判断是否是数组会存在问题。
更详细的内容可以参考博文:JavaScript为啥不用instanceof检测数组
5.如何判断一个数是否为 NaN?
NaN 有个非常特殊的特性, NaN 与任何值都不相等,包括它自身
鉴于这个独特的特性,可以手撕一个比较简单的判断函数
- 全局函数 - isNaN方法:不推荐使用。- MDN对它的介绍是:- isNaN函数内包含一些非常有趣的规则。
但为了避免一些面试官出一些冷门题目,咱们来稍微了解一下 isNaN 的有趣机制:会先判断参数是不是 Number 类型,如果不是 Number 类型会尝试将这个参数转换为 Number 类型,之后再去判断是不是 NaN
举个例子:
isNaN 的结果很大程度上取决于 Number() 类型转换的结果,关于 Number 的转换结果,后面会专门有一部分来介绍。
- Number.isNaN(推荐使用)
与 isNaN() 相比,Number.isNaN() 不会自行将参数转换成数字,只有在参数是值为  NaN 的数字时,才会返回 true。
6.如何实现一个功能完善的类型判断函数?
Object.prototype.toString.call([value]) ,可以精准判断数据类型,因此可以根据这个原理封装一个自己的 type 方法。
JS 类型之问——类型转换篇
7.toString 和 valueOf 方法有什么区别?
- 基础:这两个方法属于 - Object对象,是为了解决- JavaScript值运算与显示的问题。为了更适合自身功能,很多- JavaScript内置对象都重写了这两个方法。
- toString(): 返回当前对象的字符串形式;- valueOf(): 返回该对象的原始值
- 各个类型下两个方法返回值情况对比 
 
 - 调用优先级 
- 隐式转换时会自动调用 - toString和- valueOf方法,两者优先级如下:
- 强制转化为字符串类型时,优先调用 - toString方法
- 强制转换为数值类型时,优先调用 - valueOf方法
- 使用运算符操作符情况下, - valueOf优先级高于- toStirng
- 对象的类型转换见下一问。 
8.你知道对象转换成原始值是什么流程吗 (ToPrimitive)?
对象转换成原始类型,会调用内置的 [ToPrimitive]函数
(参考博客: 从ECMA规范彻底理解 JavaScript 类型转换)
- ToPrimitive方法接受两个参数,一个是输入的值- input,一个是期望转换的类型- PreferredType
- 如果未传入 - PreferredType参数,让- hint等于- 'default',后面会将- hint修改为- 'number'
- 如果 - PreferredType是- hint String,让- hint等于- 'string'
- 如果 - PreferredType是- hint Number,让- hint等于- 'number'
- 返回 - OrdinaryToPrimitive(input, hint)
- OrdinaryToPrimitive(input, hint)
- 如果 - hint是- 'string',那么就将- methodNames设置为- toString、valueOf
- 如果 - hint是- 'number',那么就将- methodNames设置为- valueOf、toString
methodName存储的就是当前preferredType下的调用优先级,如果全部调用完毕仍然未转化为原始值,会发生报错。
9.你能做出下面这个题吗?
有了第七问和第八问的知识,这个题目就不难了。 JavaScript 对象的键必须是字符串,因此分别需要将对象 a 和 b 转换为 string 类型。具体转换流程:
对象 a 和 b 转换后的结果都是 [object Object],obj 对象上只添加了一个属性 [object Object]。
答案
10.你能理清类型转换吗?
首先需要知道:在JavaScript中,只有三种类型的转换
- 转换为 - Number类型:- Number() / parseFloat() / parseInt()
- 转化为 - String类型:- String() / toString()
- 转化为 - Boolean类型:- Boolean()
因此遇到类型转换问题,只需要弄清楚在什么场景之下转换成那种类型即可。
转换为 boolean
- 显式: - Boolean方法可以显式将值转换为布尔类型
- 隐式:通常在逻辑判断或者有逻辑运算符时触发( - || && !)
boolean 类型只有 true 和 false 两种值。
除值 0,-0,null,NaN,undefined,或空字符串("") 为 false 外,其余全为 true
转化为 string
- 显式: - String方法可以显式将值转换为字符串
- 隐式: - +运算符有一侧操作数为- string类型时
转化为 string 类型的本质:需要转换为 string 的部分调用自身的 toString 方法(null/undefined 返回字符串格式的 null 和 undefined)
当被转换值为对象时,相当于执行
ToPrimitive(input, 'hint String')
转化为 number
- 显式: - Number方法可以显式将值转化为数字类型
Number 的具体规则,ES5 规范中给了一个对应的结果表
 
 - String: 空字符串返回- 0,出现任何一个非有效数字字符,返回- NaN
- 隐式: - number的隐式类型转换比较复杂,对需要隐式转换的部分执行- Number:
- 比较操作( - <, >, <=, >=)
- 按位操作( - | & ^ ~)
- 算数操作( - + - * / %) 注意:+的操作数存在字符串时,为 string 转换
- 一元 - +-操作
11.== 的隐式转换规则
- ==: 只需要值相等,无需类型相等;- null, undefined在- ==下互相等且自身等
- ==的转换规则:
 
 在上面的表格中,ToNumber(A) 尝试在比较前将参数 A 转换为数字。ToPrimitive(A) 将参数 A 转换为原始值( Primitive )。
12.1 + {} 与 {} + 1的输出结果分别是什么?
通过上面的学习,当对象与其他元素相加时,对象会调用 toPrimitive 转化为原始值:
- 执行 - toPrimitive,未传入- PreferredType,- methodNames为- [valueOf, toString]
- 执行 - ({}).valueOf,返回对象本身- {},不是原始值
- 继续执行 - ({}).toString(),返回- "[object Object]",返回结果为原始值,转换结束
此时 1 + {},右侧为 string 类型,将 1 进行 ToString() 转化为 "1" ,最后字符串连接,结果为 "1[object Object]"
注意: {} + 1 输出的结果会和 1 + {} 一样吗?
{} 在 JavaScript 中,不止可以作为对象定义,也可以作为代码块的定义。js 引擎会把 {} + 1 解析成 1 个代码块和 1 个+1,最终输出结果为 1
答案
13.[]与{}的相加的结果是多少?
[] + {}
数组是特殊的对象,需要调用 toPrimitive,转换为原始值
- 执行 - toPrimitive,未传入- PreferredType,- methodNames为- [valueOf, toString]
- 执行 - [].valueOf,返回数组本身
- 执行 - [].toString,返回空字符串- ''
空对象不做赘述。
答案
[] + []
类似 1 两个空数组都执行 toPrimitive,返回两个空字符串。
答案
{} + []
类似于 {} + 1,{} + [] 相当于 {}; + [],一元 + 强制将 "" 隐式转换为0,最终结果为0
答案
{} + {}
对于这个题,我先公布一下答案,之后说一下我的疑问。
答案
疑问
为什么 JavaScript 引擎没有将前面的 {} 解释成代码块?
友情提示:由于
{}可以解释为代码块的形式,有些需要注意的地方,举个栗子:
空对象调用方法时:
{}.toString()会报错
箭头函数返回对象时:
let getTempItem = id => { id: id, name: "Temp" }会报错
14.你能灵活运用 parseInt 与 parseFloat 吗
- parseInt:从数字类开始看,看到非数字类为止,返回原来的数。(小数点也属于非有效数字)
- parseInt(string, radix)还有第二个参数- radix表示要解析数字的基数,取值为- 2~36(默认值为- 10)
- parseFloat与- parseInt类似,只不过它返回浮点数。从数字类开始看,看到除了第一个点以外的非数字类为截止,返回前面的数。
网红题:['1','2','3'].map(parseInt)
这个网红题考察的就是 parseInt 有两个参数。 map 传入的函数可执行三个参数:
['1','2','3'].map(parseInt)相当于执行了以下三次过程:
- parseInt('1', 0, ['1','2','3']): radix 为 0 时,默认取 10,最后返回 1
- parseInt('2', 1, ['1','2','3']): radix 取值为 2~36,返回 NaN
- parseInt('3', 2, ['1','2','3']): radix 取值为 2,二进制只包括 0,1,返回 NaN
15.如何让 if(a == 1 && a == 2) 条件成立?
valueOf 的应用
后语
我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。
如果喜欢小包,可以在 infoQ 关注我,可以关注战场小包,同样可以关注我的小小公众号——小包学前端。
一路加油,冲向未来!!!
版权声明: 本文为 InfoQ 作者【战场小包】的原创文章。
原文链接:【http://xie.infoq.cn/article/3763493dd37c4c714858478a5】。文章转载请联系作者。












 
    
评论