• (万文)最全、最细前端面试问题总结(答题思路分析、答案解析)



    theme: fancy

    😱前言

    已经进入秋招了,作者本人也在着急的准备面试,凭着之前的面试经验,我总结了一份完整的面试题,其中分析了答题思路和答案,还有一些手写题,希望可以帮到所有参加面试的小伙伴!!!

    可能有些地方不严谨,写的也比较急,所以希望大家可以评论留言批评指正!!!

    写作不易,我已经毫无保留分享自己的毕生所学,那你是不是应该毫不吝啬的给个赞吧!!!

    持续更新中,还没有总结完。。。

    😊(冲!!!)ES6 篇

    😃1. 请你说下为什么ES6中引入了 const,let

    回答思路:

    • 没有 const、let 之前有什么弊端
    • const、let解决了什么问题

    扩展问题:

    • let、const 存储方式

    开始回答

    1. 在没有 const、let之前,我们使用 var 来定义变量,因此我们的作用域只分为两种,也就是全局作用域和块级作用域,因此,我们在使用像for或者if这种关键词时,会有很大的隐患,可能造成变量冲突。
      比如说:
    • for: 正常来说,我们在 for 循环中定义其他变量,在结束后 for 中定义的变量应该被销毁,不会修改全局变量,但是由于没有块级作用域的限制,使得本应该销毁的变量没有被销毁。

    • if: 正常我们在fun函数中会打印x的值1,但是由于var变量提升,并且没有块级作用域的限制,虽然if中的代码不会执行,但是变量 x 会覆盖fun外部的值,导致x打印undefined

        var i = 5;                               
        for(var i = 3; i < 10; i ++){            
            // 其他操作                                 
        }                                        
        console.log(i) 
        ---------------------------
        var x = 1
        function fun (){
            console.log(x)
            if(false){
                var x = 1
            }
            console.log(x)
        }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    1. 在引入let、const之后,我们引入了新的概念,块级作用域,很好的解决了变量提升和无块级作用域带来的变量冲突的问题。那是如何解决的呢?我们引入新的问题。

    扩展回答:

    1. 在js代码执行之前会经历编译阶段,而在编译阶段会确定当前执行上下文所需要的信息,变量环境,词法环境,this,作用域链,其中,变量环境用来存放 varfunction定义的变量,并且初始化值为undefined地址值,而在执行前变量已经定义好了,也就是所谓的变量提升。而constlet定义的变量存储在词法环境中,在词法环境中维护一个小型栈,按照块的形式,将每一个块级变量压入栈顶,执行后弹出,并且不会初始化,因此在一个块级作用域中提前访问变量会报错,也就是所谓的暂时性死区

    变量查找顺序:先从词法环境中从栈顶到栈底,然后到变量环境中查找,之后沿着作用域链查找。

    相关题型:

    • 给代码看输出

    注意点varfunction 变量提升,并且function优先于var,const 、let块级作用域中的暂时性死区。

    😃2. 请你说下Promise?

    回答思路

    • 首先回答 Promise 是什么?
    • Promise 为什么出现,没出现前有什么问题,出现后解决了什么问题?

    拓展问题:

    • Promise 执行机制,微任务宏任务
    • Promise的缺陷,async/await 语法糖的出现

    开始回答:

    1. 首先,从字面意思来理解,Promise 期约, 是一个用来执行将来要发生的或者即将要发生的事件的对象,它自身有三种状态,pending, fulfilledrejected,同一时刻只能有一种状态,一旦状态改变,则不能再更改,也不可逆。通过构造函数的方式使用,传入一个执行器回调函数,以此来决定Promise的执行状态。之后通过then方法来决定执行什么样的操作,并且then方法返回一个值会自动包装为Promise从而实现对象的链式调用。(之后可以介绍下其他方法 race, all, resolve, reject
    2. Promise 出现之前,我们在书写异步代码时,通过回调的方式来拿到异步返回的值,代码抒写逻辑不连续,除此之外,如果下一次的执行需要依赖上一次的执行结果,会导致代码嵌套,如果嵌套次数太多,就造成了新的问题,回调地狱,使得代码难看难以维护,Promise的出现,通过 then方法,解决了函数嵌套的问题,then方法的链式调用,也将回调地狱的问题迎刃而解。

    上面回答,只是简单的介绍了下Promise的用法,没有深入的去讲 Promise ,有能力的话我们应该扩展去讲它周边的知识

    扩展:

    1. Promise执行机制

    Promise除了解决异步回调的问题之外,还有一个特性,就是它的执行时机,微任务,和微任务对应的还有宏任务,说到这我们不得不讲下浏览器的异步实现机制,为了解决 js 单线程(可能涉及问题:js为什么是单线程?)同步执行的效率问题,我们引入了异步执行机制,而异步执行依赖于 v8引擎 中的消息队列和事件循环机制,也就是js在执行过程中遇到异步任务时,不会立即执行,而是将该事件存放到消息队列当中,而消息队列中存放的任务也就是我们所谓的宏任务,然后继续执行js代码,当所有同步代码执行完毕之后,通过事件循环,也就是一个循环代码for或者while来不停的从消息队列中取出一个事件来执行,这就是异步任务的执行机制。消息队列有一个缺点,就是所有任务都是按顺序执行,因此,如果我们需要执行一些时间粒度小的任务,比如监听DOM的变化去做相应的业务逻辑的时候,再使用消息队列的话会造成严重的效率问题,因为消息队列是先进先出的结构,在该事件添加到消息队列尾部时,消息队列内部可能已经有很多任务了,所以宏任务的执行效率不高,这是就引入了新的概念微任务,在宏任务中包含一个微任务队列,来存放需要执行效率高的事件,在每次宏任务执行完之后,不会着急执行下一个宏任务,而是将该宏任务中的微任务执行完毕,再去执行下一个宏任务

    • js
      • 添加微任务:Promise, MutationObserver
      • 添加宏任务:script,定时器,I/O,交互事件
    • node
      • 添加微任务:: nextTick
    2. async/await 语法糖

    Promise虽然可以链式调用来解决回调地狱,但是还是不够完美,依然是回调的方式书写代码,为了更加符合代码逻辑,推出了async/await语法糖,来用同步的方式书写异步代码。使用 async 修饰的函数代表异步函数,会自动包装返回一个Promise类型的对象,await 关键字用来暂停执行逻辑。通过try/cache来捕获异常,介绍完后,我们来说下它的执行原理。
    执行原理:(自执行的生成器 + 协程)

    • 生成器(Generator):yield/* ,通过调用 * 函数来返回一个对象,其中有next,return,trow方法。next 方法用来恢复*函数的执行,yield来暂停 *函数的执行。详细使用大家请自己查阅,这里只做大致介绍。
    • 协程:协程是一种比线程更加轻量级的存在,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

    通过*函数来创建一个协程,但是该协程不会立刻执行,当调用next方法时,将控制权交给该协程,开始执行,遇到yield关键字后,停止协程,并将控制权返回给父协程,并把 yield 后的值传递给父协程,父协程通过next(value)传值给该子协程。这就是生成器可以实现暂停执行的原理。

    Promise 相关手写题:

    说明:Promise底层实现了微任务的调用,我们没办法用代码实现,只能通过js模拟执行

    1. Promise 的实现
            const PENDING = 'pending';
            const FULFILLED = 'fulfilled';
            const REJECTED = 'rejected';
            class MyPromise {
                constructor(excutor) {
                    this.state = PENDING
                    this.value = null
                    this.err = null
                    this.onFulfilledCallback = null
                    this.onRejectedCallback = null
                    excutor(this.Resolve, this.Reject)
                }
                Resolve = val => {
                    if (this.state == PENDING) {
                        this.state = FULFILLED
                        this.value = val
                        this.onFulfilledCallback && this.onFulfilledCallback(this.value)
                    }
                }
                Reject = err => {
                    if (this.state == PENDING) {
                        this.state = REJECTED
                        this.value = err
                        this.onRejectedCallback && this.onFulfilledCallback(this.err)
                    }
                }
                then = (onFulfilled, onRejected) => {
                    if (this.state == FULFILLED) {
                        const res = onFulfilled(this.value)
                        return MyPromise.resolve(res)
                    }
                    if (this.state == REJECTED) {
                        const err = onRejected(this.err)
                        return MyPromise.reject(err)
                    }
                    if (this.state == PENDING) {
                        this.onFulfilledCallback = onFulfilled
                        this.onRejectedCallback = onRejected
                    }
    
                }
                static resolve(val) {
                    return new MyPromise((res, rej) => {
                        res(val)
                    })
                }
                static reject(err) {
                    return new MyPromise((res, rej) => {
                        rej(val)
                    })
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    1. Promise.race
                static race(promiseList) {
                    return new MyPromise((res, rej) => {
                        const len = promiseList.length
                        for (let i = 0; i < len; i++) {
                            promiseList[i].then(res, rej)
                        }
                    })
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. Promise.all
                static all(promiseList) {
                    return new MyPromise((res, rej) => {
                        const len = promiseList.length, result = []
                        for (let i = 0; i < len; i++) {
                            promiseList[i].then(value => {
                                result.push(value)
                                if (len == result.length) res(result)
                            }, err => {
                                rej(err)
                            })
                        }
    
                    })
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. Promise.allSettled
                static allSettled(promiseList) {
                    return new MyPromise((res, rej) => {
                        const len = promiseList.length
                        const result = []
                        for (let i = 0; i < len; i++) {
                            promiseList[i].then(value => {
                                result.push({ state: 'fulfilled', value })
                            }, err => {
                                result.push({ state: 'rejected', value: err })
                            })
                        }
                        if (len == result.length) {
                            res(result)
                        }
                    })
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. Promise 控制高并发
            function limitRequest(request = [], limit = 3) {
                const callback = []
                return new Promise((res, rej) => {
                    let count = 0, len = request.length
                    while (limit > 0) {
                        start()
                        limit--;
                    }
                    function start() {
                        count++;
                        const req = request.shift()
                        req().then(value => {
                            callback.push(value)
                            console.log(value)
                        }).finally(() => {
                            if (count == len) {
                                res(callback)
                            } else {
                                start()
                            }
                        })
                    }
                })
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    1. 自执行生成器
            const co = function (generator) {
                const gen = generator()
                return new Promise((res, rej) => {
                    const run = function (val = undefined) {
                        const { value, done } = gen.next(val)
                        if (!done) {
                            Promise.resolve(value).then(ret => run(ret))
                        } else {
                            res(value)
                        }
                    }
    
                    run()
                })
            }
    
            const Async = function* () {
                const x = yield 4;
                console.log(x)
                const y = yield Promise.resolve(2)
                console.log(y)
                return 7
            }
            
            co(Async)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    1. 输出执行顺序题,自己寻找

    😃三. 你知道Set和Map吗?

    回答思路:

    • Set和Map 的特点,有什么API
    • 有什么不足之处,引入了WeakMap 和 WeakSet

    扩展问题

    • Map和object比较,性能对比
    • 垃圾回收机制(放在浏览器模块讲)

    开始回答

    1. Set 是一种类数组的数据结构,它有一个重要的特点就是Set中不会重复的存储一个值。Map是一种类似object的数据结构,通过键值对的方式进行存储,但是Map的键值可以是任意类型。Set和Map都是可迭代的。
      Api的的介绍就不多说了,自己可以查阅一下。
    2. Set和Map有一个问题就是对引用类型的数据是强引用,如果不主动使用 delete删除数据,这段引用类型数据会永远存在于内存中,不会被垃圾回收机制回收,因为Set和Map一直保持对该引用类型数据的引用。因此引入了WeakMap和WeakSet,因为是为了解决对引用类型数据的强引用类型,所以,WeakMap和WeakSetSet和Map不同的是,键名只能是引用类型,不可以是字符串或者数组等类型,WeakMap和WeakSet对数据是弱引用,因此不会阻止垃圾回收机制回收。因为其弱引用类型,所以也没用keys,values,entire,size的api。

    案例
    正常来讲,btn所指向的内存中会保存该dom,当 btn=null后会断开btn同保存dom内存的连接会回收该段内存,由于Map的强引用,即时连接断开,保存该dom的内存也不会被回收。

       let btn = document.getElementById('button')
        let map = new Map([
            [btn, { count: 0 }]
        ])
      btn = null
    
    • 1
    • 2
    • 3
    • 4
    • 5

    扩展回答

    1. Map和object 对比:
      • object 中的数据是有数序的,根据键值,非负数按顺序存储,字符串按插入顺序存储,而Map是根据插入顺序存储
      • object 会对键值做转换,将数组转为字符串类型,而Map中不会,‘0’0是不一样的键值
      • object 是不可迭代的,而Map是可迭代的
        性能对比:Map是一种散列表的数据结构,所以其取值和插入的时间复杂度是O(1),因此频繁的取值和插入使用Map的性能更高。

    散列表:
    是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构,保存在数组中,而数组是一块连续内存,可以直接根据地址值访问,因此查询速度更快,插入更快。当我们使用Map保存数据时,Map会先将我们传入的key经过哈希函数,生成一个哈希值(整数),而这个哈希值在内存中映射着我们传入的value值,因此他的访问速度很快。而Set是一种key和value相同的散列表,因此不可以重复。

    根据能力继续扩展(自己查找)

    • 解决哈希冲突
    • hash算法:md5

    😃四. 说下Proxy和Reflect

    答题思路:

    • 简述ProxyReflect 的作用
    • 说下Object.defineProperty 缺点,Proxy的优势

    扩展回答

    • Proxy的应用:vue3响应式原理

    开始回答

    1. 首先Proxy根据字面意思来看,它是一个对象的代理,代理就意味着我们想要为一个对象添加或者删除一些属性的时候,不能直接操作该对象,我们只需要操作代理对象就可以,通过代理对象做一些拦截就可以监测到我们对该对象的行为。Proxy通过构造函数的形式调用,接收两个参数,一个是需要代理的对象target,另一个是一些代理的行为handler
      Reflect是伴随Proxy使用的一个对象,它本身实现了同Proxy的 handler 相同的全部方法,它就像是我们在修改 handler 的默认行为时的一个正确指引,无论我们怎样对handler做修改,通过Reflect总能保持正确的输出。其上的API请自己查阅。

    2. Proxy出现之前,我们有一个与之相同功能的实现方式,通过Object.defineProperty来为对象的某个键值实现代理,实现数据劫持,但是它有一个严重的弊端,是为对象的键值实现数据劫持,因此没有添加的键值不会实现,这也是vue2中响应式原理实现的一个缺陷,新添加的属性不会实现响应式,必须通过$set方法来单独实现响应式,而vue3通过Proxy和Reflect实现对对象的代理,也就是响应式原理,完美的解决了vue2的问题。这也就是Proxy的优势。

    😃五. 了解箭头函数吗?

    答题思路:

    • 箭头函数有什么特点
    • 箭头函数与普通函数有什么区别

    扩展问题

    • 箭头函数和普通函数 this指向
    • 改变this指向的方法以及他们的区别

    开始回答

    1. 箭头函数类似于匿名函数,没有名字,并且不需要使用function来定义,是一种很简洁的定义函数的方法,如果箭头函数只有一个参数的话可以省略小括号,如果函数体只有一个return语句的话,可以省略return和花括号,因此它使用起来是更加方便的,很适合来简化回调函数。

       const fn = args => args
      
      • 1
    2. 使用起来虽然方便,但是同普通函数来比,少去了很多特性。

      • 箭头函数不能当作构造函数使用,由于箭头函数的轻量,它本身是没有原型对象prototype这个属性的,而构造函数的作用就是创建一个对象,并将该对象的原型指向函数的原型对象,因此它不可以作为构造函数使用。
      • 箭头函数 中没有 this,在箭头函数中使用this时,它的this指向是由他的父级上下文决定的
      • 箭头函数中没有 arguments对象
      • 箭头函数不可以使用*/yield

    扩展回答:

    1. 普通函数的this指向是动态的,默认绑定为当前执行上下文。只有两种情况,要么指向调用该函数的对象,要么指向window
      • 普通调用:this 指向window
      • 使用对象调用:this 指向 对象
            const obj = {
                fn(){
                    console.log(this)
                }
            }
            const f = obj.fn
            f()      // 指向 window
            obj.fn() // 指向 obj
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    箭头函数this指向父级上下文中的 this,由于箭头函数本身没有this,所以它的this绑定的是fn函数的this

            const obj = {
                fn() {
                    const s = () => {
                        console.log(this)
                    }
                    s()
                }
            }
            const fn = obj.fn
            fn() // window
            obj.fn() //obj
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 改变this指向的方法有
      • apply:改变this指向,以数组的形式传递参数
      • call:改变this指向,以单独的方式传递参数
      • bind:改变this指向,返回一个被绑定thisfn,以单独的方式传递参数

    相关手写题:

    1. call
            Function.prototype.myCall = function (obj = window, ...args) {
                const key = Symbol()
                obj[key] = this
                obj[key](...args)
                delete obj[key]
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. apply
            Function.prototype.myApply = function (obj = window, args = []) {
                const key = Symbol()
                obj[key] = this
                obj[key](...args)
                delete obj[key]
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. bind
            Function.prototype.myBind = function (obj = window, ...args) {
                const fn = this
                return function () {
                    fn.call(obj, ...args)
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    😘(加油!!!坚持!!!)浏览器篇

    😊一. 请你说下浏览器的发展(单进程浏览器和多进程浏览器)。

    答题思路:

    • 早期浏览器的缺点
    • 现代浏览器的优势

    扩展问题

    • 什么是线程、进程
    • 进程间的通信方式

    开始回答:

    1. 单进程浏览器:早期,我们的浏览器是采用单进程架构的,意思就是,所有的任务都在一个进程中执行,网络请求、js运行环境、渲染引擎、页面、垃圾回收、用户交互、浏览器插件等。多个线程在一个独立的进程中工作,共享所有资源,因此浏览器插件可以访问操作系统上的任意资源,所以单进程浏览器是不安全的;由于js和页面渲染等代码都是执行在一个线程上的,如果我们打开多个页面,多个页面的js和渲染机制抢夺一个线程,因此单进程浏览器是不流畅的;再加上如果一个线程出错,会导致整个进程奔溃,进而导致整个浏览器退出无法工作,造成我们打开多个页面,有一个页面出错导致所有页面和浏览器一起关闭,所以单进程的浏览器是不稳定。\
    2. 多进程浏览器
      现代浏览器采用多进程架构,完美的解决了单进程不流畅、不稳定、不安全的问题。现代浏览器包括一个浏览器主进程,主要负责页面的显示、用户交互、子进程管理、存储等操作;一个GPU进程负责绘制UI页面和css3D效果,一个网络进程主要负责网络资源的加载和请求,多个渲染进程主要作用是将js,css,html转变为一个可视的页面,为什么是多个呢,因为谷歌浏览器默认为每一个页面开启一个渲染进程,出去安全考虑,渲染进程运行在沙箱模式下,因此js不可以直接访问操作系统资源;多个插件进程,用来运行浏览器的插件,不同的插件执行在不同的进程,防止某个插件出错干扰其他插件和页面。

    扩展回答

    1. 进程:进程就是一个程序的运行实例,是资源分配的最小单位,可以理解为是为该程序运行专门创建的一块内存,用来存放程序运行的代码,运行的数据,还有一个执行任务的主线程。进程之间的资源是相互独立的,当一个进程关闭后,操作系统会回收进程所咋用的全部内存。
      线程:线程是依附于进程的,不能独立存在,是cpu调度的最小单位,可以理解为线程是在进程上工作的,不同线程可以并行工作,不同线程之间共享所在进程的资源,一个线程出错会导致整个进程的奔溃。

    😊二. 请你说下浏览器的渲染过程

    回答思路

    • 没有什么思路,该问题也可以同浏览器中输入url到页面渲染发生了什么?一起作答。我就从url开始讲起了。

    开始回答

    资源请求阶段

    • 首先,输入内容到地址栏之后,会判断输入的是关键字还是完整的url,如果是关键字,浏览器会使用浏览器的默认搜索引擎,合成带搜索关键字的URL,然后浏览器进程将该URL发送到网络进程
    • 网络进程会查找本地是否缓存该网站资源,如果缓存中没有找到,那么就进入浏览器的请求流程,发送该URLDNS服务器获取域名的服务器IP地址,之后开始与服务器建立TCP连接,而这里就要经历TCP的三次握手(详细内容放在TCP模块讲)成功建立连接,如果是https站点还要进行安全验证(放在http模块讲),之后浏览器构成请求的请求行请求头等信息,并携带本地存储的跟该域名相关的Cookie添加到请求头中,一起发送到服务器,服务器接收信息后,将响应数据,包括响应行、响应头、响应体发送给网络进程,等网络进程接收到响应头信息后,发起提交文档信息,通知渲染进程网络进程建立管道准备通信。同时开始解析响应头,判断是否需要重定向如果状态码为301、302,通过Content-Type判断此次响应体的文件格式,来决定如何处理响应体资源,是下载资源,还是显示图片,还是显示页面。等渲染进程拿到响应体之后,渲染进程返回确认提交的消息,浏览器进程接收到之后,会更新浏览器界面状态。之后准备渲染页面。

    渲染阶段

    • 渲染进程接收到响应体后,开始进入渲染流程,首先HTML解析器将html解析为浏览器可以识别的DOM树,然后到了样式计算阶段
    • 样式计算分为三步
      1. 将css转换为浏览器可以识别的结构styleSheets
      2. 将样式表中的属性值标准化
      3. 计算处DOM树中每个节点的样式,计算过程中遵循CSS继承层叠的两个规则,层叠是合并来自多个源的属性的算法
        之后进入布局阶段
    • 布局阶段,我们通过DOM树styleSheets来合成布局树,来确定DOM元素的几何位置,首先先遍历DOM树的可见元素,将其添加到布局树上,像head这种标签,还有display:none的元素不会出现在布局树中,然后根据styleSheets计算出DOM的几何位置。一切都准备好之后,进入分层阶段。
    • 分层阶段,分层其实就是创建不同的图层,虽然浏览器页面看起来是二维的页面,但是实际上是三维的,根据z-indexing属性做z轴排序,生成一颗对应的图层树,但是并不是每个DOM都会生成一个单独的图层,只有具有层叠样式属性的DOM才会创建一个图层,而没有的属性附属于它的父节点的图层,所有图层重叠起来,也就构成了我们的浏览器页面。
    • 图层绘制阶段,当所有准备工作做好之后,我们就可以开始绘制,渲染进程将我们的绘制过程拆分成一个个小的绘制指令,然后按照顺序组成一个待绘制列表,然后交给合成线程实现真正的绘制
    • 合成线程,合成线程拿到绘制指令后,不会直接开始绘制,由于我们的页面可能很大,但是视口是有限的,因此合成线程会将图层划分为图块,然后按照视口附近的图块优先生成位图,而这个生成过程是在栅格化线程池中进行,将我们的图块栅格化处理,就是将图块转换为位图
    • 栅格化,栅格化过程都会使用GPU来加速生成,将生成的位图保存在GPU内存中
    • 合成显示,一旦所有图块都被栅格化后,合成线程会生成一个绘制指令提交给浏览器进程,浏览器接收到指令后根据该绘制指令将内容绘制到显存中,最后将显存显示到屏幕上。

    😊三. 请你说下浏览器的垃圾回收机制

    回答思路

    • 栈内存的回收
    • 堆内存的回收

    开始回答

    1. 我们都知道,js 在运行中会维护一个执行栈,每当执行一个函数,就会往执行栈中压入一个执行上下文,用来保存一些当前执行函数的数据,而函数中的原始变量会保存在栈中,而引用类型数据是保存在堆中的,变量中保存的是堆中的地址。栈顶就是维护着我们当前正在执行的函数,当函数执行完之后,有一个出栈操作,出栈也就意味着销毁当前函数的执行上下文,也就回收了该函数保存在栈中的所有内存。
    2. v8 会把堆内存分为两个部分,一部分用来存放生命周期短的对象,也就是新生代,另一部分用来存放生命周期长的对象,也就是老生代。老生代的内存比新生代大很多,老生代有1400MB,新生代有32MB。
      • 新生代: 我们先来说说新生代的垃圾回收。v8将新生代的内存分为两个部分,from区域和to区域,我们的数据统一保存在from区域,而to区域是空闲区域,因为新生代中大部分都是生命周期短的对象,所以在执行垃圾回收时我们将还存活的对像存放到to区域更高效,然后将from中的内存全部回收,之后将fromto区域调换,就可以继续完成垃圾回收,同时,为了防止新生代内存溢出,v8提供了一套晋升机制,将经历两次存在的内存存放到老生代区域。
      • 老生代:采用标记清除法,因为老生代大多是生命周期长的对象,所以我们去处理生命周期短的更高效,通过遍历调用栈,查看内存中的哪些对象没有相应的变量引用了,那就将其标记,之后将已经标记的内存释放,也就完成了垃圾回收,但是这样一来导致内存中出现很多不连续的片段,与是又采用标记整理,将所有存活的对象都移动到一端,然后清理掉端边界以外的内存,进而实现垃圾回收。但是还有一个问题,js是运行在渲染进程的主线程上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿,新生代内存较小影响不大,但是老生代的内存空间较大,可能在回收过程中造成页面卡顿,为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 执行交替进行,直到标记阶段完成。

    😊四.浏览器是如何执行一段js代码的?

    回答思路

    • 现在执行:将代码转为字节码
    • 过去执行:将代码转为机器码,又什么不足
    • 执行代码中的优化
      开始回答
    1. js 本身是解释型语言,再运行之前,需要先经过解释器处理,因为计算机是不能直接读懂开发人员所写的代码的,所以再执行之前都要先翻译成计算机能懂的语言,无论编译型语言还是解释型语言都是如此。首先,解释器需要将我们写的源代码生成AST语法树执行上下文, 先对源代码进行分词,进行词法分析就是将代码拆分成token,token就是指语法上不可再分的最小单位字符或者字符串,之后进行解析语法分析生成AST,有了AST执行上下文之后,解释器根据AST生成字节码,然后执行。
    2. 在过去,V8引擎中并没有字节码,而是通过编译器直接将AST转为机器码,因为机器码的执行效率是非常高的,所以js运行也很快,但是有一个缺点,机器码占用的内存太大,为了解决占用内存的问题,改为了字节码,字节码可以远远减少系统内存的使用。
    3. 生成字节码之后,解释器开始逐条解释执行,在执行过程中,会对代码进行优化,就是JIT即时编译技术,在执行字节码同时,收集代码信息,一段代码执行次数多了之后会把该段代码标记为热点代码,将该段代码的字节码转变为机器码并且保存,当下次再执行时,直接执行机器码,大大的提升了执行效率。

    😊五.请你说下浏览器安全方面的知识。

    回答思路

    • 浏览器的同源策略以及解决方案
    • XSS跨站脚本攻击
    • CSRF攻击
    • 安全沙箱
    • https 数据安全传输

    开始回答

    1. 浏览器同源策略
    • DOM方面来说,同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。如果说有两个页面属于同一个源下A页面和B页面,我们通过A页面打开B页面,那么这种情况,我们就可以在B页面中操作A页面的DOM,甚至在B页面中将A页面的dom全部删除。我们在B页面中可以通过window.opener来操作A页面的window对象。而如果A页面打开的B页面是属于不同网站的,那么这种操作就是禁止的。
    • 数据方面来说,同源策略限制了不同源的站点访问当前站点的Cookie、LocalStorage等数据。
    • 网络层面来说,浏览器的同源策略禁止请求其他站点返回的数据资源,意味着禁止跨源资源共享。
      但是同源策略使得浏览器具有极大的不灵活性,因此可以通过一些方式来解决这些跨域问题。
      跨域问题的解决方案
    1. DOM:我们了解到,由于同源策略,不同源的js脚本无法对当前的DOM对象进行操作,但是浏览器使用一个机制来为我们提供方便,postMessage接口。
      从下边代码中可以看到,通过A页面打开了一个新的页面B,同时通过监听windowmessage,来限制该窗口可以接收哪些源的数据进行操作,而在B页面中,通过opener拿到A页面的window,然后使用opener.postMessage来向A页面发送数据,同时传入一个URL来限制哪些源可以接收该窗口传递的数据,通过两个窗口的配合,完美的解决浏览器的同源机制带来的限制。
    // A页面
        window.open('http://localhost:3000/h', '_blank', '')
        
        window.addEventListener('message', event => {
            if (event.origin == 'http://localhost:3000') {
                console.log(event.data)
            }
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    // B页面
            const A = window.opener
    
            const script = `
            const btn = document.createElement('button')
            btn.innerText = '哈哈'
            document.body.append(btn)
            `
            
            A.postMessage(script,'http://localhost:3001')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 数据: 通过postMessage我们也解决了数据传输问题。
    2. 网络资源请求:
      • 使用Access-Control-Allow-Origin响应头来限制解决跨域访问的问题,当发生跨域请求时,我们的请求会正常的发送到服务器端并且拿回数据,只不过浏览器出于安全考虑禁止用户访问该数据,因此在发生一些复杂请求时,会先发起一个预检请求options,来询问服务器哪些源可以访问该资源,服务器通过返回携带Access-Control-Allow-Origin的响应头来告诉浏览器,该源可以访问该资源,进而解决跨域请求。
      • 使用JSONP来解决跨域请求问题:其实该方法就是利用script和img标签的特性,因为浏览器通过标签加载任何源的脚本资源和图片资源是没有安全限制的,因此利用该特性可以解决跨域问题,但是只能发起Get请求。我用script脚本举例:
    client.js
    origin: http://localhost:3000/
    const getData = function(data){
        console.log(data)
    }
    
    const script = document.createElement('script')
    script.src = 'http://localhost:3001/get?name=getData'
    
    node.js
    origin: http://localhost:3001/get
    const name = req.url.split('?')[1].split('=')[1]
    const data = '' // 假如是数据库拿出的数据
    res.setHeaders('Content-Type', 'application/javascript')
    res.end(`name(${data})`)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 使用反向代理来解决: 由于安全策略只是限制浏览器访问不同源资源,因此我们可以将不同源的服务器使用nginx反向代理到本地,当我们再次发起请求时,不会直接请求跨域服务器,而是先请求代理服务器,然后代理服务器发送请求到跨域的服务器,拿回数据后,再由代理服务器返回给本地。使用反向代理是解决跨域请求的最好方式
    2. XSS 跨站脚本攻击

    我们先来说说什么是XSS攻击,其实根据其名称跨站脚本攻击,我们就可以知道,是通过执行脚本来做一些恶意操作,比如获取用户的cookie等信息,监听用户行为,或者通过修改DOM假冒登录窗口获取用户密码等。那说了半天浏览器安全策略,那这还是不安全呀,其实这并不是浏览器不安全,而实我们写代码时留下的漏洞。我们来看看恶意脚本是怎么注入的。

    1. 储存型攻击:这种攻击是将恶意执行的脚本保存到用户的数据库中的,比方说有一个网站有一个留言功能,当用户留言后不做任何处理把留言信息保存到数据库,然后将留言展示到网页上,这就留下了漏洞,我们可以通过留言一个script脚本,做任何操作,一旦打开留言面板访问到我们留言的人,都会执行这段恶意脚本,这就是储存型攻击
    2. 反射型攻击:反射型攻击是利用url来加入恶意脚本,比方说有一个页面有一个搜索功能,他的请求接口为这样http://xxx.com/queryName='石头山',服务器响应搜索不到后会在页面中显示找不到石头山相关结果,因此这也留下了漏洞,当我把请求接口改成这样http://xxx.com/queryName=