Web开发面试的那些题-JavaScript篇
Web 开发者面试题集第一篇,关于 JavaScript。
1 Null 和 Undefined 的区别
先执行一下基本类型检测
|
|
从字面上看。两个值都表示某种东西的"缺失"。
将 object 数据类型进行 true,false 转换的时候,唯一一个为 false 的就是 null,null 表示的是引入对象的一种"缺失",也可以说是空对象引用。最好理解的是,比如document.getElementById('myEle')
,假设这个元素根本不存在,那么返回的就是 null。
在 JavaScript 里面,null 除非我们自己定义,然后就是上面提到的一种情况之外,我暂时没能想到还有哪里会隐式出现 null。而 undefined 就不同,在 console 里面我见到最多的一个错误便是
|
|
往往在于我们没有对拿到的值是否为 undefined 进行判断,进而在 undefined 上继续取下一个属性,从而抛出错误,从这一点来看,undefined 不经意间出现还是挺多的,有下面几种常见情况
|
|
由于 undefined 出现情况很多,而且大多都是我们不良编程习惯导致,或者是不经意间发生,所以我们一般不会显式把一个变量声明为 undefined,这样会造成二义性,上面的第 5 点使用 typeof 进行类型检测就是二义性之一,还有一种二义性如下
|
|
所以我们习惯上初始化一个变量为 null,而且使用全等操作符(避免相等操作符发生类型转换)
还需要知道,Undefined 数据类型的唯一值就是 undefined,Null 数据类型的唯一值就是 null
最后,关于 JS 中为什么要定义两种类型来表示"缺失",以及他们的历史来源,建议读一下阮一峰老师的这篇文章,评论处有一些讨论,我觉得还是挺有意思的
2 JS 中有哪些数据类型
首先我们要知道 JS 变量是松散类型的,可以保存任何数据类型
5 中简单数据类型(也称基本数据类型):Undefined, Null, Boolean, String, Number(后三种可以封装成为 Object)
1 种复杂数据类型:Object
ES6 新数据类型:Symbol
使用 typeof 进行类型检测,有七种返回情况: “undefined”, “object”(Array & null), “boolean”, “string”, “number”, “symbol”, “function”,值得注意以下几种特殊情况
|
|
Boolean 数据类型的转换规则(这个和题目无关,但是记住很有用)
数据类型 | true | false |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | “"(空字符串) |
Number | 任何非零数值(包括正负无穷) | 0 和 NaN |
Object | 任何对象 | null |
Undefined | n/a(或 N/A),not applicable, 意思是“不适用” | undefined |
3 Array 检测有几种方法
使用 instanceof, 例如
console.log(arr instanceof Array) // true
使用自身的 constructor 属性, 例如
console.log(arr.constructor === Array) // true
使用 ES6 的
Array.isArray(arr)
检测使用对象原生 toString()方法判断:
Object.prototype.toString.call(arr) === "[object Array]"
,注意这里不是使用Array.toString()
,这个方法会将数组里的元素调用toString()
后的结果以”,“为间隔拼接成字符串返回
注意上面的前两种方法判断不同 document 或者 iframe 下的 Array 时会失败,因为跨 iframe 实例化的对象不能共享原型链,是不同的对象,所以最好的解决办法是自己结合后两个方法写一个判断数组的函数
|
|
4 对象属性遍历的方法
- 使用
for(let prop in obj){}
可以遍历对象属性,这种方法既可以遍历自有属性也可以遍历继承自原型的属性,只要属性的[[Enumerable]]
特性为true
,对于直接在对象上定义的属性,这个特性默认为true
- 如果只想遍历实例属性,可以使用
Object.keys(obj)
或者Object.getOwnPropertyNames(obj)
,两者均返回一个数组,数组的每一项是 obj 的 key 值,在此基础上使用forEach()
即可遍历。两者区别在于前者只会遍历可枚举的自身属性,而后者不可枚举的自身属性也能遍历 - 使用
Reflect.ownKeys(obj)
,该方法除了具有getOwnPropertyNames()
功能外,还能遍历以 Symbol 作为 key 值的对象属性,而前面两种都不能遍历 Symbol()
|
|
注意,使用Reflect.ownKeys(obj)
相当于Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))
5 JavaScript 中 0.1+0.2 为什么不等于 0.3
关于这个问题,我这篇文章中已经进行了深入的探讨
6 mouse leave 和 mouse out 事件的区别
主要区别在于 mouseleave 事件不冒泡,而 mouseout 事件冒泡;类似的还有 mouseenter 和 mouseover
主要看外层的 mouseout 事件,完整地移动过外层 div,会触发其 mouseout 三次
- 第一次触发因为进入了内层,此时相当于移开了外层,被触发
- 第二次因为离开了内层,由于内层的 mouseout 事件冒泡,被触发
- 第三次因为真真实实离开了外层,被触发
第一次触发一开始我不是很理解,查了 MDN 文档的相关解释才懂,下面三条加粗语句分别代表上述三种情况
The mouseout event is fired when a pointing device (usually a mouse) is moved off the element that has the listener attached or off one of its children. Note that it is also triggered on the parent when you move onto a child element, since you move out of the visible space of the parent.
7 一个区域内的多张图片,怎么判断他们全部加载完成
当时还不知道 Promise,甚至对异步也是一知半解的时候遇到这个问题,错的当然也是很离谱
使用 Promise 结合 Promise.all()可以判断图片是否全部加载完成。我这里是使用创建 img 标签插入到 DOM 中判断,也可以在 document.DOMContentLoaded()中判断已经在 DOM 节点中的 img 是否加载完成,道理类似
也可以直接在我的CodePen上运行
|
|
8 XSS 是什么,怎么防止 XSS
XSS(Cross-Site-Scripting),跨站脚本攻击,也叫做脚本注入
当服务器完全信赖客户端提交的数据时,就可能发生脚本注入。例如,当用户提交表单时,提交了一段 script 代码,服务器将这段代码存储起来,下次其他用户访问时,这段代码被加载
在代码中我们可以获取用户 cookie,并将其发送到我们自己的服务器,例如下面就是一段简单的脚本
|
|
当下次别的用户访问时,这段代码被记载,一旦用户不小心点击到伪装图片,cookie 就会被发送到我们的主机
从上面来看,防治 XSS 有两种主要方式
防止特殊的字符出现,这些字符主要是对于 HTML 文档有特殊意义的字符
客户端表单数据值类型检测和验证
服务器对用户提交的表单数据进行严格验证
主要是将相应的符号转换成 HTML 实体字符,像
<
或者>
这些字符是不允许出现在文本中的,因为他们对于 HTML 文档来说有特殊意义。如果我们要在 HTML 文档中展示这些字符,应该使用它们的转义字符,例如<
转义字符为<
,所以客户端或者服务器应该将提交上来的这些字符进行编码,或者过滤掉这些字符让服务器将重要的 cookie 标记为
http-only
,也就是在 response header 中设置set-cookie: xxx;HttpOnly
这里分别使用 jQuery 和原生 JS 实现对特殊字符的加密和解密
|
|
9 JS 中定义变量的 var, let, const 有什么区别
var
是 ES5 中定义变量的方式,定义的变量只有全局作用域和函数作用域之分
ES6 引入了let
和const
,前者定义的变量有了块级作用域的概念。后者表示定义一个常量,这里的常量用 C 语言来说,类似于 C 的指针,定义一个指针为常量,只是说这个指针不能指向别的内存地址(不能指向别的对象),但是其自身内存地址的内容是可以访问和修改的
10 数组去重的方法
数组去重的方式网上太多了,总结起来就三大类,首先直接遍历,不使用数组的其他方法;然后可以使用数组的方法进行去重,或者使用 ES6 的 Set 和 Map 数据结构;最后扩展一下,考虑下其他数据类型的去重结果
- 使用原始方法
|
|
- 使用数组方法(splice),会修改原数组
|
|
使用数组方法(indexOf+filter)
关于这两个方法也可以只用其一,搭配其他方法,或者自己写循环,但是原理差不多
|
|
使用数组方法(sort+filter),会修改原数组
这两个方法也可以只选其一,搭配其他方法使用,或者自己写循环,但是原理差不多
|
|
- ES6 Map(Map.prototype.set()返回原 Map)
|
|
- ES6 Set
|
|
- 其他数据类型使用上述方法去重的检验结果
|
|
方法 | 结果 |
---|---|
原始方法 | NaN 和String {"1"} 不能去重 |
splice | NaN 和String {"1"} 不能去重,undefined 为empty |
filter+indexOf | String {"1"} 不能去重,NaN 全被过滤 |
filter+sort | NaN 不能去重,undefined 全被过滤,1, String {"1"}, "1" 无法正确判断 |
Map | String {"1"} 不能去重 |
Set | String {"1"} 不能去重 |
以上的结果只要关注几个点
new String {"1"}
和new String {"1"}
并不是同一个对象,如果非要把他们当成同一对象,我们可以使用对象的hasOwnProperty(typeof arr[i] + arr[i])
来判断,如果没有就新增一个 key,但是我还是认为上面的两个是不同的对象实例要注意
console.log(NaN===NaN) // false
,所以造成有些方法不能去重,有些筛选机制直接过滤,但是在 Map 和 Set 中,即使这两个不相等,但是会把他们当成相同的东西看待最后就是 sort 方法,MDN 给出的解释非常详细
The
sort()
method sorts the elements of an array in place and returns the array. The sort is not necessarily stable. The default sort order is according to string Unicode code points.sort()
对于1, String{"1"}, "1"
来说是一视同仁的,因此在此基础上使用filter()
判断时和三者在原数组中的顺序有关。不能准确去重
11 DOM 节点的深度遍历和广度遍历
广度遍历(BFS)比较简单,类似于二叉树的层次遍历,使用队列模拟当前一层,每出队列一个节点,则将其加入到最终结果数组里,并且将其的子节点全部入队,直到队列为空
|
|
当然,我们也可以不出队列,只入队列,使用index
记录下当前访问到的节点,每访问完就将其子节点全部入队,直到全部节点都被访问
|
|
深度遍历稍微复杂一点,我想到的是从根节点开始,每次访问其第一个子节点,直到某个节点没有子节点,此时将该元素从临时数组pop
出来,访问其兄弟节点(如果访问不到则继续pop
),直到访问到一个存在的兄弟节点,并把它作为当前节点,重复步骤。那么何时结束呢?刚才说到访问不到兄弟节点会一直pop
,当把第一个根节点 pop 出来的时候,也就访问完毕了,可以返回结果数组
|
|
关于节点访问,我这里为了简单起见都称作节点了。但是要记住 DOM 元素和 DOM 节点是不同的,准确来说以上的应该都是 DOM 元素,因为 DOM 节点还包括了文本节点,注释节点等等
要注意children
和childNodes
,firstElementChild
和firstChild
,nextElementSibling
和nextSibling
的区别,前者访问到的是元素,例如children
返回的是HTML Collection
。后者访问到的是节点,例如childNodes
返回的是NodeList
,这两种类型都是类数组类型,可以使用Array.from
转换成数组