1.什么是闭包 用它的优点和缺点?
通俗来讲,闭包就是子函数可以调用父函数的变量,但是这种说法不严谨,更严格一点的定义是:函数对象可以通过作用域链相互联系起来,函数体内部的变量可以保存在函数作用域范围内
,它的优点是:函数嵌套函数,有利于封装,命名变量不冲突,而且子函数可以调用父函数的参数和变量,不过它也有缺点,那就是:变量和参数不会被浏览器的垃圾回收机制回收,降低程序性能,在IE下还可能会导致内存泄漏,这个问题可以用手动清空引用的方式,也就是设置Null来解决。
注意:这里面试官可能会接着闭包继续往下问,比如引起闭包的原因(涉及到作用域链,详情请移步这里),看程序说出结果(提前刷题),闭包中的this指向问题(高三182页),以及内存泄漏和垃圾回收机制(第11题)等。
2.怎么解决回调函数嵌套的问题?以及底层工作原理?手写一个promise
回调函数就是指将一个函数作为参数,传递给另一个函数,等那个函数执行完以后,再执行传进去的这个函数。(这句与问题无关,可以不提,仅做笔记)
首先ES6中的Promise就是专门为解决为解决回调函数问题而设计的,它是一个对象,包含三个状态和两个方法,当状态改变的时候,就调用相应的方法,然后通过.then()
来接收并执行后续操作,还有Promise.race()
、Promise.all()
等一系列方法,它可以规范化回调,并且解决信任问题。
但是Promise
也存在缺陷,例如它无法中断执行,而且不设置回调函数的话,就无法抛出异常,如果层级很深的话,依旧会存在嵌套问题,所以解决回调地狱,还可以使用Generator
。
Generator
中的链式请求,可以随时控制请求的执行和中断,比如让正在执行的A先暂停,转去执行B,然后再返回来继续执行A。它需要在Funtion
和函数名之间加个*
号,在异步操作需要暂停的地方,用yield
注明,然后使用next()
继续下一步,直到碰到下一个yield
或者return
语句,也可以使用for...of
语句来自动遍历,用throw()
方法来获取捕获到的异常。(Generator
的缺点没找到)
最后还有一种async/ await
方法,它其实就是Generator
的语法糖,将原本的*
替换成async
,写在函数前面,然后把await
写在async
函数里的,相当于告诉函数“先回去等会儿”,等await
后面的异步操作执行完毕后,再继续过来执行,async
返回的是一个Promise
对象,可以用then()
方法来继续后面的操作,参数就是return
出来的内容,而且相比之前的方法,async/ await
有更好的语义和适用性。
(注意:await
后面紧跟着的一般是一个Promise
对象,如果不是,会强制转换成一个立即resolve
的Promise
对象,同时要熟悉它的错误机制,这个比较重要)
手写Promise
代码如下:
function myPromise(obj){
this.state = "pending"
this.value = undefined
this.error = undefined
this.resolveCallbackArr = []
this.rejectCallbackArr = []
var that = this
var resolve = function(val){
that.value = val
that.state = "fulfilled"
that.resolveCallbackArr.forEach(function(fn){
fn()
})
}
var reject = function(err){
that.error = err
that.state = "rejected"
that.rejectCallbackArr.forEach(function(fn){
fn()
})
}
obj(resolve,reject)
}
myPromise.prototype = {
then: function(onResolve,onReject){
var that = this
var promise2 = new myPromise(function(resolve,reject){
if(that.state == 'fulfilled'){
onResolve(that.value)
}
if(that.state == 'rejected'){
onReject(that.error)
}
if(that.state == 'pending'){
that.resolveCallbackArr.push(function(){
var x = onResolve(that.value)
console.log(x)
rePromise(x,resolve,reject)
})
that.rejectCallbackArr.push(function(){
onReject(that.error)
})
}
})
return promise2
}
}
function rePromise(x,resolve,reject){
if(typeof x == 'object'){
x.then(function(res){
resolve(res)
},function(err){
reject(err)
})
}else{
resolve(x)
}
}
测试代码:
var promise = new myPromise(function(resolve,reject){
setTimeout(function(){
resolve(200)
},1000)
// reject(500)
})
promise.then(function(res){
alert(res)
return new myPromise(function(resolve,reject){
setTimeout(function(){
resolve('200-2')
},1000)
})
},function(err){
alert(err)
}).then(function(res){
alert(res)
return '200-3'
},function(err){
alert(err)
}).then(function(res){
alert(res)
},function(err){
alert(err)
})
3.从输入一个URL到页面加载的过程?
我理解的一共是有八个步骤:
1. 首先输入url地址,敲击回车
2. 这个时候浏览器会先在缓存里查找有没有对应的IP地址
3. 如果没有的话,开始检查本地hosts文件里是否有网址映射关系,如果没有,就在DNS解析器缓存里,以递归的方式进行查找,如果还是没有,则继续向上,在本地DNS服务器里,以根域服务器 => 顶级域 => 第二层域 => 子域的顺序进行迭代查询,最后找到域名对应的IP地址
4. 然后客户端就可以和对应IP的服务器之间进行连接了,这里涉及到TCP的三次握手,详细过程如下所示(重在理解):
5. 建立连接后,客户端开始想服务器发送
http
请求,包括起始行、请求头和请求主体6. 服务端接收到请求后,对数据进行处理,然后以
http
的Response
对象格式返回给客户端,包括状态码、响应头和响应报文7. 之后浏览器开始处理接收到的页面文档,首先将
HTML
解析成DOM
树,然后将CSS
解析成样式结构体,最后将两者结合生成render tree
(注1:这里可能会问具体的解析过程,简单来说就是
CSS
不会阻塞解析,但会阻塞渲染render tres
,而JS
则会阻塞浏览器解析HTML
文档)(注2:这里还可能涉及到
回流和重绘
的问题)8. 最后数据传输完成,可以关闭连接,这就涉及到了TCP的四次挥手(可以只提一嘴,不必展开细说),详细过程如下所示(重在理解): TCP的四次挥手.png
4.清除浮动几种方式?原理和适用场景
清除浮动常见的有两种方式:
1. 使用clear:both;
属性清除浮动,也就是使用伪元素,在包含浮动的父元素(例如class="clearfix"
)加入如下代码:
//非IE浏览器下:
.clearfix:after{
content: '';
display: block;
height: 0;
clear: both;
}
//IE浏览器下:
.clearfix {
*zoom: 1
}
即可清除浮动,其原理是在被清除浮动的元素的上边和下面添加足够的垂直外边距
2. 用overflow: hidden;
触发父元素变成BFC,代码如下:
.clearfix {
overflow: hidden;
}
这样也可以清除浮动,其原理是利用BFC,让浮动元素也参与父元素高度的计算
(注意:这里面试官可能会问BFC
的定义、原理及相关内容,这样就可以顺带扯到margin
的父子拖拽问题)
5.为什么有跨域?简述几种跨域方式
引起跨域问题的是浏览器的同源策略,(有这一句够了,也可以继续往下补充)浏览器为了防止XSS
和CSFR
的攻击,于是设置了同源策略,它规定页面的脚本只能访问位于同一个源下的数据,所谓同源是指协议、域名、端口号三者相同,否则浏览器就报错。
解决跨域的方式有很多:
1. 首先可以使用CORS,也就是跨域资源共享,它由Server
来进行设置,客户端在正式通信前,会先发送一次“预检”请求,如果请求的域名在后台的许可名单之中,会返回一个肯定答复,浏览器就可以正式发送请求了。它有简单请求和非简单请求两种。
2. 还有可以通过nginx
反向代理来进行跨域
3. 还可以开启谷歌浏览器的DeBug
模式,在本地开发时进行跨域,这也是我在项目中最常用到的方式,因为公司的项目在正式上线后,都位于同一域名下,不会存在跨域问题的
4. 最后我还了解一种JSONP
方法,它的核心原理是利用了所有具有src
属性的HTML
标签可以跨域访问脚本这一特性,具体来说,就是
a. 先在客户端注册一个回调函数,比如callback
b. 然后动态的创建一个script
标签,将其src
值设置为请求地址,同时在后面添加参数和回调函数名,也就是callback
c. 之后服务端对这个请求进行处理,并返回callback(data)
,data
是服务端返回给前端的数据
d. 最后客户端接收到返回的这段js脚本,因为之前注册过callback
这个函数,所以会立即执行函数体,这样就完成了跨域
不过JSONP
也有一定的局限性,就是它只支持get
请求(注意:这里可能会引导面试官往get
、post
请求,或者http
协议上问),而且同样也需要Server
的支持。
嗯...我知道的跨域方式就只有这么多(羞涩~)
6.判断数据类型几种方式?及bug和解决方法
JS中一共有七种数据类型,一种是引用类型——Object
,还有六种基本数据类型,分别是Number
、String
、Boolean
、Null
、Undifined
,以及ES6新增的Symbol
所有的数据类型都可以用typeof(var)
来检测,它返回的是一个字符串,但是对于复杂数据类型来说,不管是数组Array
、日期Date
,或者是普通对象,这种方法返回的都是Object
,无法更详细的区分,所以可以用(var) instranceof (type)
方法来判断,但是这种方法在iframe
下会产生bug
,而且这种方法也无法准确判断Function
和Object
的类型,因为既可以说函数是个构造方法,也可以说方法是一个对象,万物皆对象嘛~所以区分它们两个的时候,应该使用(var).Constructor == (type)
,也就是构造器方法,不过这种方法在类继承的时候同样有可能产生bug
。
最后还有一种万能的方法,可以判断所有数据类型,而且也没有任何Bug
[内心窃喜脸~],就是用Object
原型对象上的toString
来判断,具体写法是Object.prototype.toString.call(var) == '[Object (type)]'
,这样无论什么类型,都可以准确知道它的类型了。
7.手写一个webpack配置(package.json、webpack.config.js)
8.排序和去重的算法至少能各自手写两种(要求性能最高的)
9.Generator、async的用法和区别(自个封装npm)
可以和上述第二题合并解答,一同添加至知识体系中,这里不再重复赘述
npm什么的等会儿再说啦!!!
10.手写3种以上Es5的面向对象继承,以及说说class类继承的this指向问题?
我知道的面向对象继承一共有六种方式,其中最常用的是组合继承方式,它涉及到了原型继承和构造函数继承。
1. 原型链继承 这种方式其实就是通过原型和原型链的方式,让一个原型对象和另一个类型的实例相等,来实现继承,写法如下:
function Father(){} //定义父类
Father.prototype.say = function(){ //父类方法
alert("我是父类的方法")
}
function Son(name,age){ //定义子类,有name和age属性
this.name = name
this.age = age
}
Son.prototype = new Father() //实现继承
Son.prototype.sayName = function(){ //子类方法
alert(this.name)
}
//后面可以实例化对象,如:
var son = new Son("亚当",23)
son.say() //我是父类的方法
son.sayName() //亚当
这种方法有很多缺陷,比如1. 无法确定实例和原型的关系,2. 用字面量添加方法时会重写原型连,导致继承无效,3. 而且如果属性中存在引用类型的话,多个实例访问时都会指向同一块内存地址,相互之间存在影响,4. 创建子类的实例时也无法向父类中传递参数。
2. 构造函数继承 这种方式是通过call()
或者apply()
调用父类的构造函数,代码如下:
function Father(name){ //定义子类,有name属性
this.name = name
this.sayName = function(){
alert(this.name)
}
}
function Son(age){ //定义子类,有age属性
Father.call(this,"亚当")
this.age = age
}
var son = new Son()
son.sayName() //亚当
这种方法可以再构造时向父类传递参数,也可以正常使用引用类型的数据,但是它将所有方法都放在了构造函数里,每次实例化都会创建一个一模一样的函数,复用性太差,而且父类里面定义的方法,在子类中也不可见,所以基本不会单独使用
3.组合继承 组合的意思其实就是将前两种方法结合起来,把每个实例独有的属性放在构造函数里,将共享的方法放在原型链中,这样每个对象既有各自独立的属性,又有可以共享的方法,代码如下:
function Father(name){
this.name = name
}
Father.prototype.sayName = function(){
alert(this.name)
}
function Son(age){
Father.call(this,"亚当")
this.age = age
}
Son.prototype = new Father()
Son.prototype.constructor = Son
Son.prototype.sayAge = function(){
alert(this.age)
}
组合继承是使用最广泛的一种方式,不过它同样存在不足之处,那就是无论什么情况下,都会调用两次父类的构造函数,一次是在创建子类通过原型继承父类的时候,另一次是在子类型构造函数内部,用call()
或者apply()
调用父类到时候,这样就创建了两次同名的属性,只不过后面这次把前面的属性覆盖了而已。
所以基于这种缺陷,后来就又有了寄生式组合继承的方法,它可以弥补组合继承的不足,而这个方法又涉及到寄生式继承和原型式继承,不过后面这几种方法不太常用,就不详细说了。
(注意:如果只了解前三种方法,说到组合继承就可以了,也不要说组合继承的不足,否则引出后面的几种方式无异于给自己挖坑)
简单说一下后几种,1. 原型式继承就是Object.creat()
方法,可以传入两个参数,存在引用类型方面的缺陷;2. 寄生式继承用于封装函数的继承过程,在内部可以进行某些自定义处理,需要先用到原型式继承,它无法做到函数复用;3. 寄生式组合继承,其原理是通过构造函数来继承属性,通过原型链的混成形式来继承方法,简单来说,就是不用每次都调用父类型的构造函数,而是先拷贝一个原型的副本下来,之后继承的都是这个副本,这样就可以只调用父类型一次,实现继承,不过这种方式过于繁琐,所以除非必要,一般还是使用组合继承的方式。
(注:ES6的类继承,以及相关的this
指向问题还没写,之后再补)
11.说说垃圾回收机制,还有内存泄漏什时候会出现以及解决方法
JS的垃圾回收机制有两种,第一种是标记清除,这也是目前主流的垃圾收集算法,它的思想是给当前不使用的值加上标记,然后再回收它们的内存;还有一种引用计数方法,这种算法的思想是根据跟踪记录所有值被引用的次数,引用数为0时,即收回内存,JS引擎目前都不再使用这种算法,但是IE中访问非原生JS对象,比如DOM
元素时,这种算法仍然可能导致问题。
内存泄漏是在IE浏览器下,使用闭包操作DOM元素的时候产生的,具体来说,如果闭包的作用域中保存着一个HTML元素,它会创建一个循环引用,导致该元素的引用数至少也是1,永远无法被销毁。
内存泄漏可以通过手动消除引用的方式来解决,就是先将调用的DOM元素保存在一个副本中,在闭包中只引用这个副本来解除循环引用,然后再将包含DOM对象的变量设置为null
,这样就能解除DOM对象的引用,确保能正常回收其占用的内存。
12.用定时器写时钟 解决误差的问题
定时器存在误差,这个涉及到了浏览器的线程问题,每个浏览器都包含一个JS处理线程,和一个定时触发线程,JS脚本在单线程的执行过程中如果遇到了定时器,会先将其放入一个任务队列,等待JS线程处理完当前操作之后,再转过来执行队列里的任务,所以这一小段时间,便是产生误差的原因。
所以在使用定时器,比如做时钟效果的时候,可以用当前时间 - 开始时间,来取得中间的时间差,然后根据时间差来动态计算指针划过的刻度,这样就可以减少因为执行过程所带来的误差值。
网友评论