原生 JavaScript 灵魂拷问 (二),你能全部答对吗?
前言
当下的前端开发,三大框架三分天下,框架的简单、强大让我们欲罢不能,使用原生 JavaScript
越来越少。
但我认为 JavaScript
作为每一个前端工程师的立身之本,不止要学会,还要学好、学精,学再多遍都不为过。
另一方面,前端面试中,越来越重视原生 JavaScript
的考察,其所占比例也越来越高。
因此我决定整理JavaScript
中容易忽视或者混淆的知识点,写一系列篇文章,以灵魂拷问的方式,系统且完整的带大家遨游原生 JavaScript
的世界,希望能给大家带来一些收获。
这是原生 JavaScript
灵魂拷问系列文章的第二部分,第一部分链接:
传送门: 原生 JavaScript 灵魂拷问,你能答上多少 (一)
JavaScript 之运算符篇
16. 你知道 ||
与 &&
的返回值规则吗?
短路效应:
&&
和||
都会发生短路&&
只有在两个操作数都为true
时,条件判断的结果才为true
,如果操作数一为false
,不会判断操作数二。||
两个操作数只要有一个为true
,条件判断的结果就为true
,因此操作数一为true
时,不会判断操作数二。返回值规则
||
和&&
首先会对操作数一执行条件判断,如果不是布尔值就先强制转换为布尔类型,然后再执行条件判断。对于
||
来说,如果条件判断结果为true
就返回第一个操作数的值,如果为false
就返回第二个操作数的值。&&
则相反,如果条件判断结果为true
就返回第二个操作数的值,如果为false
就返回第一个操作数的值。||
和&&
返回它们其中一个操作数的值,而非条件判断的结果(2<3)||(3<2)
返回值是多少?
17. 1 + - + + + - + 1
结果是多少?
+/-
号在 JavaScript
通常有三种用途:
普通加减法: 二元运算符
++/--
: 自增自减,一元运算符+/-
: 正负,一元运算符
上面表达式中没有涉及自增与自减的情况,一元运算符的优先级大于二元运算符,上述表达式执行顺序为:1 + (- (+ (+ (+ (- (+ 1)))))) ----> 1 + 1 = 2
18. 你能准确的做出自增与自减的题目吗?
++/--
在前,先加/减后用++/--
在后,先用后加/减
请回答:y
的值为?
咱们来解剖一下这个长的要死的表达式:
19. == 与 === 的区别,Object.is() 与 === 区别
== 与 === 区别
===
是严格相等,要求数据类型和值都要相等;==
只需要值相等。==
会发生隐式类型转换,===
不会发生隐式类型转换。
Object.is() 与 === 区别:
Object.is()
来进行相等判断时,一般情况下与 ===
相同,它处理了一些特殊的情况,比如 +0
和 -0
不再相等,两个 NaN
是相等的。
20. 你知道new
运算符的有两种优先级吗?
MDN
对 new
操作符的描述中,语法是:
([arguments])
意味着可以缺省,会存在 new constructor(...args)
和 new constructor
两种模式,并且前者的优先级高于后者。更详细的优先级见下图:
这个知识点非常重要,只有区分开了 new
带参列表和不带参列表,才能准确并且透彻的理解下面这道经典面试题。
21.(ES6+) 可选链接运算符(?.)和空值合并运算符(??)
说到运算符,提一下这两个最新的扩展符,开阔一下眼界。
可选链接运算符(?.)
当使用 ?.
时,若当前值为假值,会停止执行,返回 undefined
。
在日常编程中,如果要读取对象内部的属性,往往需要判断该对象是否存在,例如读取 message.body.user.firstName
,安全的写法
但如果有了 (?.) ,上面代码就可以简化为
空值合并运算符(??)
空值合并运算符 (??)
是一个逻辑操作符,当左侧的操作数为 null
或 undefined
时,才会返回右侧操作数结果,否则返回左侧结果
?? 和 || 有点像,但是 || 判断的是假值,?? 是判断 null 和 undefined
JavaScript 之字符串篇
22.String('1'), new String('1'),'1' 区别是什么?
在 JavaScript
中,我们有下面三种定义字符串的方式。
上面三种方式定义出的 '1'
,是否存在区别?我们来通过 ===
运算符和 typeof
运算符测试一下。
打印一下, new String('1')
的结构见下图:
可见 new String()
生成的字符串为对象类型, [[PrimitiveValue]]
存储字符串的原始值。
23.字符串是基本类型,那为什么可以调用字符串方法那?
这是 JavaScript
中的设计。JavaScript
为了便于基本类型操作,提供了三个特殊的引用类型(包装类),即 Number,String,Boolean
,它们的 [[PrimitiveValue]]
属性存储它们的本身值。基本类型的方法与属性是"借"来的,去向对应包装类"借"来的。
光说这些有可能有些难理解,咱们来举个例子:
其实 js 引擎内部会这样处理:
24.修改 string.length 大小能改变字符串长度吗?为什么?
先看一个示例:
很明显,修改 str.length
是无法做到修改字符串的长度的。
str
为原始值,调用 length
相当借用 new String(str).length
,修改的是 new String(str)
的 length
,跟原始值 str
无关。
扩展:修改 new String()生成字符串的 length 会生效吗?为什么?
如果将上面代码修改一下,str
是由 new String
产生,修改 length
属性会生效吗?
答案告诉我们,还是失败了。
new String()
生成的字符串是对象类型,为啥还不能使用 length
属性。那说明 length
属性,很有可能配置不可写,测试一下上述猜想:
由控制台的打印可知:new String()
生成的字符串的 length
属性不止是不可写,而且是不可枚举、不可配置的。
25.字符串截取函数 slice(),substring(),substr()的区别是什么?
基本使用
substring
: 方法返回一个字符串在开始索引到结束索引之间的一个子集,或从开始索引直到字符串的末尾的一个子集。
注意事项:
substring
截取字符串中[indexStart, indexEnd)
处字符串如果任一参数大于
stringName.length
,则被当作stringName.length
如果 indexStart 大于 indexEnd,则 substring 的执行效果就像两个参数调换了一样。见下面的例子。
substr: 方法返回一个字符串中从指定位置开始到指定字符数的字符。
注意事项
substr()
会从start
获取长度为length
字符(如果截取到字符串的末尾,则会停止截取)。如果
start
是正的并且大于或等于字符串的长度,则substr()
返回一个空字符串。若
start
为负数,则将该值加上字符串长度后再进行计算(如果加上字符串的长度后还是负数,则从0
开始截取)。如果
length
为0
或为负数,substr()
返回一个空字符串。如果length
省略,则将substr()
字符提取到字符串的末尾。
slice
: 方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。
注意事项
若
beginIndex
为负数,则将该值加上字符串长度后再进行计算(如果加上字符串的长度后还是负数,则从0
开始截取)。如果
beginIndex
大于或等于字符串的长度,则slice
() 返回一个空字符串。如果
endIndex
省略,则将slice()
字符提取到字符串的末尾。如果为负,它被视为strLength + endIndex
其中strLength
是字符串的长度。
三者区别
slice
和substring
参数都是起始索引和结束索引,但slice
参数可以是负数,substring
参数如果小于0
,则会被视为0
substr
与其他两个不同,两个参数为起始索引和要包含在生成的字符串中的字符的长度。
26.你真的理解透 JSON.stringify
了吗?
基本使用
JSON.stringify
可以将数组或者对象转化成 JSON
字符串。JSON.parse
可以将 JSON
字符串转化为数组或对象。基于这两个方法,可以产生很多用处,例如:
对象的深拷贝(存在缺点: 循环引用等)
localStorage
只能存取字符串格式的内容,因此存之前转换成JSON
字符串,取出来用时,在转化成数组或对象。
学透 JSON.stringfy
语法:
参数:
value
: 将要序列化成 一个JSON
字符串的值。replacer
(可选):如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;
如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的
JSON
字符串中;如果该参数为
null
或者未提供,则对象所有的属性都会被序列化。space
:指定缩进用的空白字符串,用于美化输出
(重要)九大特性:
特别要注意 1、3、5 条特性
布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
所有以
symbol
为属性键的属性都会被完全忽略掉,即便replacer
参数中强制指定包含了它们。对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
NaN
和Infinity
格式的数值及null
都会被当做null
。undefined
、任意的函数以及symbol
值:
出现在非数组对象的属性值中时在序列化过程中会被忽略
出现在数组中时会被转换成
null
单独转换时,会返回
undefined
转换值如果有
toJSON()
方法,该方法定义什么值将被序列化。Date
日期调用了toJSON()
将其转换为了string
字符串(同Date.toISOString()
),因此会被当做字符串处理。其他类型的对象,包括
Map/Set/WeakMap/WeakSet
,仅会序列化可枚举的属性非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
JavaScript 之数组篇
27.那些数组方法会修改原数组吗?那些不会?
这是频繁出现的考点,牛客的前端面试题中,该问题反复出现。
改变自身值的方法
不改变自身值的方法
28.shift
与 unshift
的返回值分别是什么?
unshift()
在数组开始处插入元素,shift()
删除数组第一个元素关于两者返回值,直接上测试代码:
由上述代码可知:unshift 返回插入元素后的新数组长度,shift 返回删除的元素值。
那么可以类比推理一下,push 返回的是插入元素的新数组长度,pop 返回删除的元素值。
29.new Array()
与 Array.of()
的区别是什么?
Array.of()
方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。这好像跟new Array
的功能极度接近,为什么ES6
添加了新方法那?
原因就在于 new Array(n)
,只有一个参数时,构造的并非只含 n
的数组,而是 1 * n
的数组,值全为 undefined
。具体看测试:
30.includes 与 indexOf 的区别是什么?
基本使用
indexOf
: 返回元素在array
中的位置,不存在就返回-1
includes
: 用来判断元素是否存在
区别
includes
可以检测NaN
includes
返回值为true/false
,在判断数组中是否存在某元素时,includes
的可读性更好
31.splice()
删除元素的注意事项
我来假设一个场景,大家就理解为什么会有此问题。
现在有这样一个数组 [1,-1,2,-1,-5]
,我想删除掉所有的负数,于是我就写出了下面的代码:
上面的代码感觉没什么错误,为啥 -2
没被删除掉?
我在 splice
执行后,打印一下当前 i
位置的元素值,大家应该就可以理解。
数组删除 -1
之后,当前的 i
值为 -2
,此次遍历结束,i++
,正好空过了 -2
元素。因此如果 splice 删除元素发生在数组中,一定要注意回调 i 的位置。
32. 你会使用数组高阶函数吗
传送门: 半小时,阿包带你学会手撕高阶函数
33. 你会手撕数组高阶函数吗?
传送门: 半小时,阿包带你学会手撕高阶函数
34.你知道 forEach 如何跳出循环吗?
break return
break return
都无法中断 forEach
循环,我们来挨着验证一下:
break
上述代码直接发生报错,可见 break
无法在 forEach
中使用。
return
输出结果
虽然有两个 val
未被打印,但循环还是进行了 4
次,return
无法中断 forEach
,只是类似于 continue
的功能。
try catch
try catch
可以中断 forEach
的,而且是唯一可以中断 forEach
的方式。
使用 try
监视代码块,在需要中断的地方抛出异常。具体见下面案例:
输出结果
官方推荐: every/some
除了抛出异常以外,没有办法中止或跳出 forEach()
循环。如果你需要中止或跳出循环,forEach()
方法不是应当使用的工具。
可以使用 some
和 every
来替代 forEach
函数: every
在碰到 return false
的时候,中止循环。some
在碰到 return true
的时候,中止循环。具体使用见下面案例:
35. 如何实现数组乱序
传送门: JavaScript专题之乱序
36. 如何实现数组去重
你知道多少种数组去重的方法吗?
使用双重 for
和 splice
使用 indexOf
或 includes
加新数组
sort
排序后,使用快慢指针的思想
sort
方法用于从小到大排序(返回一个新数组),其参数中不带以上回调函数就会在两位数及以上时出现排序错误(如果省略,元素按照转换为的字符串的各个字符的 Unicode
位点进行排序。两位数会变为长度为二的字符串来计算)。
ES6
提供的 Set
去重
Set
中的元素只会出现一次,即 Set
中的元素是唯一的。
使用哈希表存储元素是否出现(ES6
提供的 map
)
map
对象保存键值对,与对象类似。但 map
的键可以是任意类型,对象的键只能是字符串类型。
如果数组中只有数字也可以使用普通对象作为哈希表。
filter
配合 indexOf
这里有可能存在疑问,我来举个例子:
reduce
配合 includes
37. 如何实现数组扁平化
传送门: 面试官连环追问:数组拍平(扁平化) flat 方法实现
38. 类数组如何转化为数组?
Array.prototype.slice.call()
Array.from()
Array.from
是 ES6
新增的方法,它可以将**类数组对象和可遍历(iterable)**转变为真正的数组。
(...)
展开运算符
扩展运算符调用的是遍历器接口,如果一个对象没有部署此接口就无法完成转换。
上面咱们自己写的普通类数组就无法使用...运算符。
如果部署了遍历器接口,例如 arguments
类数组,便可以使用扩展运算符。
后语
我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。
如果喜欢小包,可以在 InfoQ 关注我,同样也可以关注我的小小公众号——小包学前端。
一路加油,冲向未来!!!
疫情早日结束 人间恢复太平
版权声明: 本文为 InfoQ 作者【战场小包】的原创文章。
原文链接:【http://xie.infoq.cn/article/9f4c8f7aae6bab36bacfc8c82】。文章转载请联系作者。
评论