每日30题(一)

190 阅读15分钟

1.谈谈变量提升

js在执行的时候,会创建两种上下文,全局上下文和函数上下文。上下文的执行又两个阶段,一是创建,二是执行。

创建的时候有三步:1.thisBinding 2.词法环境3.变量环境

我们要讲的变量提升就发生在变量环境中。用var声明的变量是存在变量环境中的,初始化的值是undefined。所以就会有变量提升这个问题。

拓展一下,变量环境其实也是一种特殊的词法环境。ES6提出的let,const是放在词法环境里面的,初始值为uninitlize,所以我们在没有声明的时候使用let会报错。

console.log(a) //undefined
var a =20
console.log(a) //Cannot access 'a' before initialization
let a =20

2.说说call,apply,bind的区别

都改变this指向。

bind返回一个函数。call和apply直接执行这个函数。

call和apply接收的第二个参数不同,call接收的参数是一个一个的,apply接收一个参数数组。

call比apply更加高效,因为省去了apply参数结构的过程。

3.实现一个call

Function.prototype.call_ = function(context){
    context= context?Object(context):window
    context.fn = this
    //参数不要忘记
    let args = [...arguments].slice(1) //第一个参数是this
    let result = context.fn(...args)
    delete context.fn
    return result
}

4.实现一个apply

apply实现的思路与call是一致的。不过第二个参数可以确定了。

Function.prototype.apply_ = function(context,args){
    context= context?Object(context):window
    context.fn = this
    //参数不要忘记
    let result;
    if(args){
        result = context.fn(...args)
    }else{
        result = context.fn()
    }
    delete context.fn
    return result
}

5.实现一个bind(bind还需要时间考虑记忆)

Function.prototype.bind_ = function(context){
    
    
}

6.简单说一下原型链

我是看了网上的一遍js万物诞生记来帮助记忆的

js的本质是null,就像所有的__prop__到最后一定是null.

然后null通过创造术创造了No1。我们可以理解__prop__就是创造术。No1以自己的原型生成了Object,同时以创造术创造了No2。No2以自己的原型生成了Function。同时创造了万物。并且规定所有生成的元素都可以通过__props__连接到自己。

基本上这张图包含了原型链的关系。

7.怎么判断对象类型

我们可以通过typeof 判断基本类型,除了null的类型

剩下的我们可以通过instanceof 判断对象的类型。

另外我们也可以通过Object.proptype.toString.call(obj)来获得一个[object Object]形式的字符串,后面的就是类型。通用的,比较方便。

8.箭头函数的特点

箭头函数就是没有this指向,里面如果有this就按照作用域链向上找。

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())

没有this,想上找就找到了a,a是函数调用,所以里面的this指向window。

还有一点,一旦箭头函数绑定了this就不会改变。

9.this指向

this的指向非常简单,就取决于函数调用的方式。

如果函数是方法调用,那函数内的this就指向这个调用者。

如果函数是函数调用,this指向window

new的this指向新的实例

如果有call,apply,bind就指向强制绑定的对象上。

10.async/await 的优缺点

async/await是ES7里面提出来的,作为异步处理的终极解决方案。本质是generator函数的语法糖。相比较于promise,它处理起异步请求显得更加同步化了,就像是在写同步代码一样,很简洁。

但是有一点,我们一般在await后面不要跟耗时的操作。因为代码执行到await就会等待异步请求返回。所以这时时阻塞的。

附一段代码,理解执行顺序

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

11.generator的原理

我们在function 后加上一个* ,这个函数就变成了生成器函数。

在生成器函数里我们可以处理异步请求。当函数执行到yeild的时候就会停下来。等待给一个next(),第一个next是不能传值的,同时返回的是一个对象,里面有执行next后的一个thenable的promise,还有一个状态,布尔值done。

我太菜了,就不实现generator了。

手写代码的时候要实现。

12. Promise的原理

Promise是ES6 提出来的一个东西,是为了解决异步请求的。

我们可以把Promise当成一个状态机。他有三个状态:PENDING ,FULFILLED,REJETED

有的人把fulfilled愿意写成resolved.Promise的状态是不可逆的,意思是只有pending是可变的。然后new Promise后会返回一个新的Promise实例。所以我们可以链式调用,解决了回调地狱的问题。

实现一个Promise,不好意思,我还是不会实现,不过我看了PromiseA+规范,里面只指定了一个then方法。对于catch,all,race好像都没提及。

手写代码的时候应该实现。

13.隐式类型转换

在开发过程中我们一定会遇到这个问题,我们先看一个题目

[] == ![]

这道题其实很常见了,运算方式从右向左,有!所以[]会转成true ,然后取反false

左边会转成Number ,0

所以会相等。

另外附一道题目

if([]==false){console.log(1)} //1
if({}==false){console.log(2)}
if([]){console.log(3)} //3
if([1]==[1]){console.log(4)}

最后附上转换规则

随机应变。

14.垃圾回收机制

我之前看过V8的垃圾回收机制,还有一些印象。v8的垃圾处理机制分为新生代和老生代对象的处理。

新生代对象的垃圾处理会将新生代空间分为from 与 to 两个空间,而且保证其中有一个空间是空的,然后逐一对对象进行是没有引用的检查。如果没有就recanvageGC。

执行过一次新生代GC之后,新生代对象就会转到老生代对象里面。

相比较于新生代对象来说,老生代里的对象更多,也更复杂。相应的,老生代的GC处理也更加复杂。会用到标记清除,与标记压缩。

标记清除就是从根节点上向上找。跟着分支,如果一个对象没有引用就会删除。删除之后还有碎片,要对碎片进行标记压缩,将存活的对象向一边移动,然后删除这一边。

记不太清了,浅显理解。

15.闭包

之前写过关于闭包的理解。

红宝石书里面对闭包的定义是如果一个函数能够访问另一个函数内的变量,这个函数就是闭包。

MDN里面对闭包的定义就是能够访问自由变量的函数就是闭包。对自由变量的定义是既不是形参传进来的,又不是函数定义的。自由变量是存在堆上的。

一般来说,函数执行完毕之后,函数内的局部变量就会被释放,但是如果这个变量被当前函数返回的函数或对象有引用的话就释放不了。

闭包常常用来保护私有变量,一般来说我们是暴露一个方法给调用者。另外,模块化开发也是用到闭包。

闭包还涉及到一个知识点,就是作用域。我们在第一个题目里面就说过,函数执行上下文在创建的时候会有一个词法环境,在里面有一个outer,记录了这个函数声明的时候的作用域链,所以说我们找作用域会找这个函数在哪里声明,而不是在哪里调用。

有一个优化的点,就是我们在使用闭包获取私有变量之后,要将这个引用置空,这样浏览器就知道变量用不到了,就会回收。

有一个很常见的题目


for(var i = 0;i<5;i++){
    setTimeout(()=>{
        console.log(i)
    },0)
}

会打印5个5 ,想要改成0-5,有三种方法。

1.改成立即执行函数

for(var i = 0;i<5;i++){
    (function(i){
        setTimeout(()=>{
            console.log(i)
        },0)
    })(i)
}

2.声明改用let

3.给setTimeout送一个初始值

for(var i = 0;i<5;i++){
    setTimeout(a=>{
        console.log(a)
    },0,i)
}

16.基本数据类型与引用类型的区别

基本数据类型存在栈上,引用数据类型存在堆上。

当然也不是绝对的,js这样处理是为了更高效的处理数据。

17.谈谈EventLoop

JS在执行的时候会有一个机制,要谈EventLoop就必须说一说同步任务与异步任务。

因为js是单线程的,所以就会顺序执行同步代码,遇到异步任务的时候是要将任务放到Task队列里面,当同步代码执行完毕的时候就会去Task取第一个任务执行,然后就会继续执行同步代码,组成了一个循环就是EventLoop。

既然说到了EventLoop又不能不说异步任务里面的宏任务与微任务。执行顺序是同步任务->微任务->宏任务。常见的宏任务有setTimeout,setIntval..,微任务有Promise,async/await。要准确的理解这个需要比较多的例子。

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

juejin.cn/post/684490…

juejin.cn/post/684490…

能看懂基本上就能基本了解EventLoop的执行过程。

18.setTimeout执行误差

js是单线程的,所以造成定时器误差的原因会有很多,比如回调函数的执行,也有可能是浏览器中的各种事件。

19.防抖

还没防抖,木易杨第七章就是关于防抖与节流的,估计要啃很久。

20.数组降维

let a =[1,[2,3,[4,[5],6],7]]
//定义一个降维函数
const flatMapDeep = arr=>
Array.isArray(arr)?
arr.reduce((a,b)=>[...a,...flatMapDeep(b)],[]):
[arr]
console.log(flatMapDeep(a))

21.实现一个深拷贝

JSON.parse(JSON.stringify(obj))

其实这个方法是最简单也最常用的,但是有一些缺点。一是会忽略undefined以及symbol。二是不能拷贝函数。三是不能处理循环引用的问题。

让我自己实现一个,我还真不会。

手写深拷贝函数。

22.typeof 与 instanceof 的区别

typeof 用来判断基本数据类型,instanceof用来判断引用数据类型。 注意:

typeof null ----->object

自己实现一个instanceof

function instance_of(left,right){
    if(typeof(left) !== 'object' || left === null){
        return false
    }
    let proto = left.__proto__
    while(true){
        if(proto === null){
            return false
        }
        if(proto === right.proptype) {
            return true
        }
        proto = proto.__proto__
    }
}

23.cookie与sessionStorage,localStorage的区别

  • cookie可以由服务端和js进行读写(没有设置httpOnly),storage是存在浏览器上的,只能由js读写。
  • cookie会带在请求头中,所以一般不会太大。而且如果使用cookie保存用户的登录状态,一定要加密。
  • cookie可以设置过期时间。setMaxAge(600),单位是s
  • 同域名的http和https共享cookie。对于浏览器来说localStorage存的多个tab页共享,sessionStorage不共享。

24.如何判断页面是否加载完成

Load 事件代表页面的html,css,js,图片都加载完成。

DOMContentLoaded代表html加载完成。(其他的没有)

25.跨域

由于浏览器的同源策略,所以不同的协议,域名,端口请求数据都会失败。

跨域的解决方案

  • jsonp jsonp其实就是前端用script标签发给一个请求,然后后台返回一个方法,立即执行,并把参数放在方法的参数里。缺点是只能发送get请求。

  • CORS cors是现在主流的解决跨域的方案。需要前后端配合实现。分为简单请求和复杂请求两类。

简单请求有Accept,Accept-Language,Content-Language,Content-Type只包含urlencode,form-data,plain这几个值,剩下的包含其他字段的都是复杂请求。

简单请求后端只要设置 set('Access-Control-Allow-Origin':'*'),如果需要携带cookie,前后端都要设置credentials。

比较常见的在webpack 打包的vue工程里,跨域是在webpack.config.js里配置的。

devServer下

porxy:{
        'test':{
            target:'localhost:1337',
            changeOrigin:true,
            secure:true
        }
    }

26.什么是事件代理

事件代理又称事件委托。利用事件冒泡,只指定一个事件处理,就可以处理一个类型的所有事件。

这可以作为性能优化的点。

<ul id="ul">
	<li>1</li>
	<li>2</li>
	<li>3</li>
	<li>4</li>
</ul>
<script>
	let ul = document.querySelector('#ul')
	ul.addEventListener('click', (event) => {
		console.log(event.target);
	})
</script>

其实我们在开发过程中经常用到,就像一个返回图标切的很小,我们一般都是在外层加一个div,然后给这个div加点击事件。

27.Service Worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

目前该技术主要是用来做缓存,提高首屏速度。

我对这个真没有特别深的理解。

28.浏览器缓存

浏览器缓存要作为前端性能优化一个很重的点,良好的缓存可以大大提高前端网页的加载速度。目前有强缓存和协商缓存两种方式,大致可以解决大部分的问题。

  • 强缓存

强缓存说明缓存期间不需要请求。

http1.0强缓存用Expires

Expires: Wed, 22 Oct 2018 08:41:00 GMT

http1.1强缓存用cache-control

cache-control: public, max-age=31536000
  • 协商缓存

如果缓存过期了,可以使用协商缓存,协商缓存需要发送请求。如果缓存有效,会返回status code 304

http1.0用Last-Modified 和 If-Modified-Since Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。 但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag

http1.1用ETag 和 If-None-Match

ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来

  • 选择合适的缓存策略
    • 对于某些不需要缓存的资源,可以用cache-control:no-store
    • 对于频繁变动的资源,可以用cache-control:no-cache并配合Etag使用,说明资源已经被缓存,但是会发送请求询问是否有改动
    • 对于代码文件来说一般使用cache-control: public, max-age=31536000,然后配合指纹处理,一旦文件名有变动就重新下载。

缓存大致如此。

29.浏览器的性能优化

  • 重绘和回流

之所以把重绘放在前面,是因为重绘是回流的一部分。

repaint(重绘):颜色,背景等等不会引起页面布局变化,只需要重新渲染

reflow(回流):当render 树的一部分或全部因为大小边距等问题需要重建。

所以回流一档会发生重绘,而且回流的成本要高很多。

所以以下的动作可能会导致性能问题

a.改变字体 b.添加删除样式 c.改变文字 d.改变盒模型 e.定位或浮动

另外我们补充一下,其实重绘和回流是和EventLoop有关的。(其实我不太懂)

  1. 当EventLoop执行完MicroTask后,会判断document是否需要刷新。因为浏览器的执行频率是60Hz,即16m执行一次。
  2. 然后判断是否有scroll和resize,有的话就去触发事件,所以scroll和resize也是至少16ms才会执行一次,并且自带节流。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面

减少回流

  1. 使用translate替代top
  2. 使用visibility替代display:none
  3. 不要使用table
  4. 不要把dom节点放在循环里面
  5. 动画速度越快,回流越多,可以考虑使用requestAnimationFrame
  • 图片优化
  1. 简单的图片可以用css实现
  2. 雪碧图
  3. 小图片可以用base64,png格式也可以,大图用jpeg
  • 其他文件优化
  1. css放header
  2. js放body底部,也可以放任意位置加上defer,表示会html解析完成后执行。
  3. 服务端开启文件压缩功能
  • CDN

    静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。对于 CDN 加载静态资源需要注意 CDN域名要与主站不同,否则每次请求都会带上主站的 Cookie。

  • 使用webpack优化项目

  1. tree shaking 移除无用代码
  2. 按照路由拆分代码,实现按需加载
  3. 给打包后的文件添加hash名,实现浏览器缓存文件
  4. 使用production模式,webpack会自动压缩代码。

30.webpack优化打包速度

  • 减小文件搜索范围
    • 通过别名
    • loader的test,include,exclude
  • Webpack4 默认压缩并行
  • Happypack 并发调用
  • Babel缓存编译