web 前端面试题详细解析
1. 说说你对闭包的认识
请讲一下你对闭包的认识”——这道题几乎是前端面试必问的问题,今天我试着总结一下如何优雅的回答这道题
什么是闭包
一句话解释:
能够读取其他函数内部变量的函数。
稍全面的回答:
在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。
这里涉及到对函数作用域的认识:js 变量分为: 全局变量和局部变量;函数内部可以直接读取全局变量,而在函数外部自然无法读取函数内的局部变量;
原来 JavaScript 的闭包是这么回事这篇文职中作者详细的剖析了闭包.
闭包解决了什么问题
1. 可以读取函数内部的变量
2. 让这些变量的值始终保持在内存中。不会在函数调用后被清除
可以通过下面的代码来帮助理解上面所说的:
在这段代码中increment
实际上就是闭包函数myFunction
, 它一共运行了三次,第一次的值是 1,第二次的值是 2,第三次的值是 3。这证明了,函数addCounter
中的局部变量counter
一直保存在内存中,并没有在addCounter
调用后被自动清除。
闭包的应用场景
在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送 ajax 请求成功|失败的回调;setTimeout 的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。
下面是具体应用的栗子:
1. 老掉牙的取正确值问题
怎么取到每一次循环的正确值呢? 闭包这样用:
声明了 10 个自执行函数,保存当时的值到内部
2.使用闭包模拟私有变量
私有变量在 java 里使用 private 声明就可以了, 但是在 js 中还没有,但是我们可以使用闭包模拟实现。
匿名函数已经定义就立即执行, 创建出一个词法环境包含counter.increment
、counter.decrement
、counter.value
三个方法,还包含了两个私有项:privateCounter
变量和changeBy
函数。这两个私有项无法在匿名函数外部直接访问,必须通过匿名包装器返回的对象的三个公共函数访问。
闭包的缺点
1. 由于闭包会是的函数中的变量都被保存到内存中,滥用闭包很容易造成内存消耗过大,导致网页性能问题。解决方法是在退出函数之前,将不再使用的局部变量全部删除。
2. 闭包可以使得函数内部的值可以在函数外部进行修改。所有,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
2. 跨域问题有哪些处理方式
跨域解决方案
通过 jsonp 跨域
跨域资源共享(CORS)
nodejs 中间件代理跨域
nginx 反向代理中设置 proxycookiedomain
Ⅰ.通过 jsonp 跨域
通常为了减轻 web 服务器的负载,我们把js
、css
,img
等静态资源分离到另一台独立域名的服务器上,在 html 页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建 script,再请求一个带参网址实现跨域通信。
原生实现
服务器端返回如下(返回即执行全局函数)
jquery 方式实现
Ⅱ.跨域资源共享(CORS)
CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)跨域资源共享 CORS 详解。看名字就知道这是处理跨域问题的标准做法。CORS 有两种请求,简单请求和非简单请求。
简单请求
只要同时满足以下两大条件,就属于简单请求:
请求方法是以下三种方法之一:
HEAD
GET
POST
HTTP 请求头的信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
如果是简单请求, 后端处理即可, 前端什么也不用干; 这里注意的是如果前端要带 cookie, 前端也需要单独设置
原生 ajax (前端)
jquery (前端)
vue 中使用 axios (前端)
后端 node
可以借助koa2-cors
快速实现
Ⅲ.nodejs 中间件代理跨域
跨域原理: 同源策略是浏览器的安全策略, 不是 HTTP 协议的一部分。服务器端调用 HTTP 接口只是使用 HTTP 协议, 不会执行 js 脚本, 不需要检验同源策略,也就不存在跨域问题。
实现思路:通过起一个代理服务器, 实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头 cookie 中域名,实现当前域下 cookie 的写入
在 vue 框架下实现跨域
利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于 vue 渲染服务和接口代理服务都是 webpack-dev-server 同一个,所以页面与代理接口之间不再跨域,无须设置 headers 跨域信息了。后台可以不做任何处理。
webpack.config.js
部分配置
Ⅳ.nginx 反向代理中设置
和使用 node 中间件跨域原理相似。前端和后端都不需要写额外的代码来处理, 只需要配置一下 Ngnix
对于跨域还有挺多方式可以实现, 这里就不一一列举了
3. for...in 和 for...of 的区别
for...of 是 ES6 新引入的特性,修复了 ES5 引入的 for...in 的不足
for...in 循环出的是 key,for...of 循环出的是 value
for...of 不能循环普通的对象,需要通过和 Object.keys()搭配使用
推荐在循环对象属性的时候,使用 for...in,在遍历数组的时候的时候使用 for...of
4. new 一个对象, 这个过程中发生了什么
创建一个新对象,如:var obj = {};
新对象的 proto 属性指向构造函数的原型对象。
将构造函数的作用域赋值给新对象。(也所以 this 对象指向新对象)
执行构造函数内部的代码,将属性添加给 obj 中的 this 对象。
返回新对象 obj。
5. js 的防抖和节流是什么
防抖: 在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
使用场景:
给按钮加函数防抖防止表单多次提交。
对于输入框连续输入进行 AJAX 验证时,用函数防抖能有效减少请求次数。
简单的防抖(debounce)代码:
节流: 就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
区别:
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
6. 数组中常用的方法有哪些
开发中数组的使用场景非常多, 这里就简单整理总结一些常用的方法;从改变原有数据的方法、不改变原有数组的方法以及数据遍历的方法三方面总结。
改变原有数组的方法: (9 个)
splice() 添加/删除数组元素
sort() 数组排序
pop() 删除一个数组中的最后的一个元素
shift() 删除数组的第一个元素
push() 向数组的末尾添加元素
unshift()向数组开头添加元素
reverse()
ES6: copyWithin() 指定位置的成员复制到其他位置
ES6: fill() 填充数组
以上是 9 种会改变原数组的方法, 接下来是 6 种常用的不会改变原数组的方法
不改变原数组的方法(6 种)
join() 数组转字符串
cancat 合并两个或多个数组
ES6 扩展运算符...合并数组
indexOf() 查找数组是否存在某个元素,返回下标
ES7 includes() 查找数组是否包含某个元素 返回布尔
>1. indexOf 方法不能识别 NaN
>2. indexOf 方法检查是否包含某个值不够语义化,需要判断是否不等于-1,表达不够直观
slice() 浅拷贝数组的元素
>字符串也有一个 slice() 方法是用来提取字符串的,不要弄混了。
遍历方法
forEach:按升序为数组中含有效值的每一项执行一次回调函数。
1.无法中途退出循环,只能用 return 退出本次回调,进行下一次回调.
2.它总是返回 undefined 值,即使你 return 了一个值。
2. every 检测数组所有元素是否都符合判断条件
如果数组中检测到有一个元素不满足, 则整个表达式返回 false,且元素不会再进行检测
some 数组中的是否有满足判断条件的元素
>如果有一个元素满足条件,则表达式返回 true, 剩余的元素不会再执行检测
filter 过滤原始数组,返回新数组
map 对数组中的每个元素进行处理,返回新的数组
reduce 为数组提供累加器,合并为一个值
>reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,最终合并为一个值。
ES6:find()& findIndex() 根据条件找到数组成员
这两个方法都可以识别 NaN,弥补了 indexOf 的不足.
ES6 keys()&values()&entries() 遍历键名、遍历键值、遍历键名+键值
<br />
7. 怎么判断一个 object 是否是数组
方法一
使用 Object.prototype.toString 来判断是否是数组
这里使用 call 来使 toString 中 this 指向 obj。进而完成判断
方法二
使用 原型链 来完成判断
>基本思想: 实例如果是某个构造函数构造出来的那么 它的 __proto__
是指向构造函数的 prototype
属性
方法 3
利用 JQuery, 利用 JQuery isArray 的实现其实就是方法 1。
<br/>
8. 继承有哪些方式
ES6 中的 class 继承
原型继承
构造继承
寄生组合式继承
实例继承
简单介绍一下前两种方式, 后面几种继承方式大家可以自行上网查找.
ES6 中的 class 继承
细心的同学可能会发现, 在 Cat 类中没有构造函数, 这里有一个小的知识点,就是 ES6 的继承方法中如果子类没有写构造函数的话就一般默认添加构造。举个例子。
注意:如果我写了构造函数但是没有写 super 的话,或者 super 方法的参数不对等等,编译器都会报错。
原型继承
在 ES6 之前,也有很多继承的方法,其中一个很常用的方法就是使用原型继承。其基本方法就是一个父类的实例赋值给子类的原型。这个继承方式是通过__proto__
建立和子类之间的原型链,当子类的实例需要使用父类的属性和方法的时候,可以通过__proto__
一级级向上找;
缺点: 1. 子类实例时,无法向父类构造函数传参。
>2. 父类的私有属性被所有实例共享
<br/>
9. 说说 js 中 call,apply,bind 之间的关系
这又是一个面试经典问题, 也是 ES5 中众多坑中的一个,在 ES6 中可能会极大避免 this 产生的错误,但是为了一些老代码的维护,最好还是了解一下 this 的指向和 call、apply、bind 三者的区别.
bind,apply,call 三者都可以用来改变this
的指向, 下面分别对他们进行比较分析:
apply 和 call
二者都是 Function 对象的方法, 每个函数都能调用
二者的第一个参数都是你要指定的执行上下文
apply 和 call 的区别是: call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。
我们常常使用的验证是否是数组(前提是 toString()方法没有被重写过):
bind 与 apply、call 区别
我们发现bind()
方法还需要调用一次; 是由于 bind()
方法创建一个新的函数,我们必须手动去调用。
bind,apply,call 的共同和不同点:
三者都可以用来改变
this
的指向三者第一个参数都是 this 要指向的对象,也就是想指定的上下文,上下文就是指调用函数的那个对象。(点前的那个对象,没有就是全局 window)
三者都可以传参,但是 apply 是数组,而 call 是有顺序的传入
bind 是返回对应函数,便于稍后调用;apply 、call 则是立即执行
<br/>
10. Promise
前端面试过程中,基本都会问到 Promise,如果你足够幸运,面试官问的比较浅,仅仅问 Promise 的使用方式,那么恭喜你。事实上,大多数人并没有那么幸运, 很多面试官在 promise 这块都是由浅入深的提问.
了解 Promise 吗?
Promise 解决了什么问题?
Promise 如何使用?
Promise 常用的方法有哪些?它们的作用是什么?
Promise 在事件循环中的执行过程是怎样的?
1. 了解 Promise 吗?
所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理,让开发者不用再关注于时序和底层的结果。Promise 的状态具有不受外界影响和不可逆两个特点。
###### 2.Promise 解决了什么问题?
Promise 解决了回调地狱的问题, 提高代码的可读性以及解决信任度问题.
传统的回调有五大信任问题:
调用回调过早
调用回调过晚(或者没有被调用)
调用回调次数过多或过少
未能传递所需的环境和参数
涂掉可能出现的错误和异常
###### 3. Promise 如何使用?
ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。
下面代码创造了一个 Promise 实例。
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve
函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从Pending
变为Resolved
),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从Pending
变为Rejected
),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
4. Promise 常用的方法有哪些?它们的作用是什么?
Promise.prototype.then
Promise 实例具有then
方法,也就是说,then
方法是定义在原型对象Promise.prototype
上的。它的作用是为 Promise 实例添加状态改变时的回调函数。then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
Promise.prototype.catch
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
上面代码中,getJSON
方法返回一个 Promise 对象,如果该对象状态变为resolved
,则会调用then
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch
方法指定的回调函数,处理这个错误。
Promise.all
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例, 返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败。
Promise.race
Promise.race
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected
,则只返回 rejected
任务的结果。
5. Promise 在事件循环中的执行过程是怎样的
上面代码的执行顺序是: 我是 promise 任务、我是同步任务、resolved、我是延时任务。
Promise 新建后立即执行,立即 resolve
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时;setTimeout 在下一轮“事件循环”开始时执行。
总结
在面试中, 很多问题并没有真正的答案,至于知识点能掌握到什么样的程度,都需要靠自己不断的学习积累, 在开发中不断的使用也是加深对知识点理解的方式。由于个人精力有限,只是针对一些常遇到的面试题,做了一些浅显的答案解析,希望对大家有所帮助吧。
版权声明: 本文为 InfoQ 作者【小啵】的原创文章。
原文链接:【http://xie.infoq.cn/article/89b5dd35d34803de32eb04553】。文章转载请联系作者。
评论