【前端基础-面试题】前端八股文

578 阅读11分钟

JavaScript基础

1. 说一说JS的数据类型有哪些,区别是什么?

回答思路: Number String Boolean Undefine Null Symbol Bigint Object 8种

JS的数据类型可以分为基础数据类型引用数据类型两种,其中基础数据类型包括Number String Boolean Undefine Null BigInt Symbol 7种,引用数据类型通常用Object代表,普通对象 数组 日期对象 Math数学函数都属于Object。

基础数据类型和引用数据类型的本质区别在于它们在内存中的存储方式不同。基础数据类型直接存储在内存栈中的简单数据段,占用空间小,属于被频繁使用的数据。引用数据类型 在栈中存储了指针,指针指向堆内存中该实体的起始地址,当解释器寻找到引用值的时候,会检索其在栈中的地址,取的地址后堆中获得地址。

Symbol是ES6新出的一种基础数据类型,不能使用new Symbol()创建数据,只能使用Symbol()创建数据,由于Symbol数据的唯一性,所以可以用来当做object的key,同时用Symbol做key不能使用for获得获取到这个key需要使用object.getOwnPro-pertySymbols(obj)获得objet中key类型是Symbol的key值。

BigInt也是ES6新提出的一种基础数据类型,用来解决大数运算问题,Jsnumber只能表示 -(2^53-1)和2^53-1 之间的整数,超出这个范围就会出现精度问题,bigInt数据类型支持范围更大的整数值以及任意精度表示整数的能力。

2. 说一说JavaScript有几种方法判断变量的类型?

回答思路 typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、constructor(用于引用数据类型)

标准回答 JavaScript有4种方法判断变量的类型,分别是typeof、instanceof、Object.prototype .toString.call()(对象原型链判断方法)、constructor (用于引用数据类型)

  • typeof: 常用于判断基本数据类型,对于引用数据类型除了function返回function,其余全部返回object
  • instanceof: 主要用于区分引用数据类型,检测方法是检测的类型在当前实例的原型链上 ,用其检测出来的结果都是true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的undefined, null, symbol检测不出来
  • constructor: 用于检测引用数据类型, 检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。
  • Object.prototype.toString.call(): 适用于所有类型的判断检测,检测方法是Object. prototype.toString.call(数据) 返回的是该数据类型的字符串。 这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是Object.prototype.toString.call()这种方法。

加分回答

  • instanceof的实现原理: 验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回true,未找到返回false。
  • Object.prototype.toString.call()原理: Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果

3. 说一说你对闭包理解?

回答思路: 变量背包、作用域链、局部变量不销毁、函数体外访问函数内部变量、内存溢出、内存泄漏、形成块级作用域、柯里化、构造函数中定义特权方法、Vue数据响应式Observer。

闭包特性: 一个函数和词法环境的引用绑定在一起的组合就是闭包。比如一个函数A return一个内部函数B,被return出去的函数B能够在外部访问A函数内部的变量,这样就行成了一个B函数的一个变量背包。A函数执行结束之后,这个变量背包不会被销毁,并且这个变量背包在A函数外部只能通过B函数访问

闭包原理: 当前作用域可以访问上一作用域的变量。

闭包解决的问题: 能够让函数作用域中的变量在函数执行结束之后不被销毁,同时能够让函数外部的可以访问函数内部的变量。

闭包带来的问题: 由于垃圾回收机制不能将闭包中的变量销毁,于是就会造成内存泄漏,内存泄漏积累多了就会导致内存溢出。

闭包的应用: 能够模仿块级作用域,防抖,节流,能够实现柯里化,在构造函数中定义特权方法、Vue中数据响应式Observer中使用闭包等。


柯里化: 比如说咱们实现一个函数,可以不停的往里面传String,知道传入句号,结束之后返回所有String拼接的结果。就是将变量背包当做一个小仓库,每次的计算结果放到这个小仓库里面什么时候,想取的时候,就把仓库面的内容给取出来。

function fn1() {
    let arr = []
    // **  重要:返回函数 concat  **
    return function concat() {
        // 拿到参数数组
        const arg = Array.prototype.slice.call(arguments)
        // 将参数存到外层作用域下的arr中
        // 由于闭包的原因,每次concat执行时,arr都会保持上一次操作结果
        arr = arr.concat(arg)
        // 接到终止参数,则返回拼接字符串
        if(~arg.indexOf('.')){
            const result = arr.join('')
            console.log(result)
            return result
        }
    }
}

// **  重要:这里 strConcat === concat  **
const strConcat = fn1()
strConcat('H')
strConcat('e', 'll')
strConcat('o', ' ', 'W')
strConcat('o')
strConcat('rl')
strConcat('d','.') // 输出 Hello Word.

防抖: 防抖是如果我一直操作,那么我一直不触发,就比如说窗口resize,什么时候不resize了,什么时候触发。

function debounce(fun,dealy){
    var timer=null;
    return function (){
        if(timer){
           clearTimeout(timer)
        }
        timer=setTimeout(()=>{
            fun(...arguments);
        },dealy)
    }
}

节流: 节流是我操作完之后,立即触发一次,并且以后一段时间之内不再出发。

function throttle(fun,delay){
    var timer;
    return function (){
        if(!timer){
            fun(...arguments)
            timer=setTimeout(()=>{
                timer=null;
            },delay)
        }
    }
}

Vue中数据响应式Observer中使用闭包: 50行代码的MVVM,感受闭包的艺术

注意观察 observe(data) 函数,这个函数就是每次循环就会产生一个闭包,通过for循环遍历对象的每一个属性。闭包的实质就是内层作用域的对地址暴露,我们我用get和set方法巧妙的暴露了函数内部的观察者Observer。

// 为响应式对象 data 里的每一个 key 绑定一个观察者对象
observe(data){
    Object.keys(data).forEach(key => {
        // 对每一个数据都有一个观察者
        let obv = new Observer()
        data["_"+key] = data[key]
        // 通过 getter setter 暴露 for 循环中作用域下的 obv,闭包产生
        Object.defineProperty(data, key, {
            get(){
            //每次所有使用这个数据的DOM节点添加到观察这队列里面,准备试图更新
                Observer.target && obv.addSubNode(Observer.target);
                return data['_'+key]
            },
            //视图更新,将object队列面的数据进行更新
            set(newVal){
                obv.update(newVal)
                data['_'+key] = newVal
            }
        })
    })
}

4.伪数组和数组的区别?

回答思路: 伪数组的类型object不能使用数组方法,可以获取长度、可以使用for in进行遍历,

标准回答:

  • 伪数组它的类型不是Array,而是Object,而数组类型是Array。可以使用的length属性查看长度,也可以使用[index]获取某个元素,但是不能使用数组的其他方法,也不能改变长度,遍历使用for in方法。

  • 伪数组的常见场景:

    • 函数的参数arguments
    • 原生js获取DOM:document.querySelector('div')
    • jquery获取DOM:$(“div”)等
  • 伪数组转换成真数组方法

    • Array.prototype.slice.call(伪数组)
    • [].slice.call(伪数组)
    • Array.from(伪数组) 转换后的数组长度由 length 属性决定。索引不连续时转换结果是连续的,会自动补位。

5.说一说Promise是什么与使用方法?

回答思路: pendding、rejected、resolved、微任务、then、catch、Promise.resolve()、Promise.reject()、Promise.all()、Promise.any()、Promise.race()

标准回答

  • Promise的作用:Promise是异步编程解决方式,解决了回调地狱问题,让代码的可读性更高,更容易维护
  • Promise使用:Promise是ES6提供的一个构造函数,Promise构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数 resolverejectresolve将Promise的状态由等待变为成功,将异步操作的结果作为参数传递过去;reject则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。实例创建完成后,可以使用then方法分别指定成功或失败的回调函数,也可以使用catch捕获失败,then和catch最终返回的也是一个Promise,所以可以链式调用。
  • Promise的特点
    1. 对象的状态不受外界影响(Promise对象代表一个异步操作,有三种状态)。 - pending(执行中) - Resolved(成功,又称Fulfilled) - rejected(拒绝) 其中pending为初始状态,fulfilledrejected为结束状态(结束状态表示promise的生命周期已结束。
    2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。 Promise对象的状态改变,只有两种可能(状态凝固了,就不会再变了,会一直保持这个结果):
      • Pending变为Resolved
      • 从Pending变为Rejected。
    3. resolve 方法的参数是then中回调函数的参数,reject 方法中的参数是catch中的参数。
    4. then 方法和 catch方法 只要不报错,返回的都是一个fullfilled状态的promise。
  • Promise的其他方法
    • Promise.resolve(): 返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。
    • Promise.reject(): 返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法。
    • Promise.all(): 返回一个新的promise对象,该promise对象在参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。
    • Promise.any(): 接收一个Promise对象的集合,当其中的一个 promise 成功,就返回那个成功的promise的值。
    • Promise.race(): 当参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

6.说一说JS实现异步的方法?

回答思路: 回调函数、事件监听、观察者模式、setTimeout、Promise、Generators/yield、async/await

标准回答 异步指的是程序执行顺序和代码顺序是不一致的,所有异步任务都是在同步任务执行结束之后,从任务队列中依次取出执行。

  • 回调函数是异步操作最基本的方法,比如AJAX回调,回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。 此外它不能使用 try catch 捕获错误,不能直接 return Promise包装了一个异步调用并生成一个
  • Promise实例,当异步调用返回的时候根据调用的结果分别调用实例化时传入的resolve 和 reject方法,then接收到对应的数据,做出相应的处理。Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,缺点是无法取消 Promise,错误需要通过回调函数捕获。
  • Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数是一个状态机,封装了多个内部状态,可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。优点是异步语义清晰,缺点是手动迭代Generator 函数很麻烦,实现逻辑有点绕。
  • async/await是基于Promise实现的,async/awt使得异步代码看起来像同步代码,所以优点是,使用方法清晰明了,缺点是awt 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 awt 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。
  • 加分回答 JS 异步编程进化史:callback -> promise -> generator/yield -> async/await。 async/awt函数对 Generator 函数的改进,体现在以下三点:
    • 内置执行器。 Generator 函数的执行必须靠执行器,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
    • 更广的适用性。 yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 awt 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
    • 更好的语义。 async 和 awt,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,awt 表示紧跟在后面的表达式需要等待结果。 目前使用很广泛的就是promise和async/awt

7.说一说事件循环Event loop,宏任务与微任务?

回答思路 任务挂起、同步任务执行结束执行队列中的异步任务、执行script标签内部代码、setTimeout/setInterval、ajax请求、postMessage,MessageChannel、setImmediate、I/O(Node.js)Promise、MutationObserver、Object.observe、process.nextTick(Node.js)每个宏任务中都包含了一个微任务队列。

标准回答

  1. 浏览器的事件循环:执行js代码的时候,遇见同步任务,直接推入调用栈中执行,遇到异步任务,将该任务挂起,等到异步任务有返回之后推入到任务队列中,当调用栈中的所有同步任务全部执行完成,将任务队列中的任务按顺序一个一个的推入并执行,重复执行这一系列的行为。
  2. 异步任务又分为宏任务和微任务。
    • 宏任务: 任务队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。宏任务包含:执行script标签内部代码、setTimeout/setInterval、ajax请、postM-essage、MessageChannel、setImmediate,I/O(Node.js)
    • 微任务: 等宏任务中的主要功能都完成后,渲染引擎不急着去执行下一个宏任务,而是执行当前宏任务中的微任务 微任务包含:Promise、MutationObserver、Object.observe(兼容性差)、process.nextTick(Node.js)
  3. 加分回答:浏览器和Node 环境下,microtask 任务队列的执行时机不同 - Node端,microtask 在事件循环的各个阶段之间执行 - 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

MutationObserver
作用: 到通知。DOM变动不会马上触发,需要等所有DOM操作家属之后才会触发。

特点:

  • 异步触发,DOM发生变化之后并不会立即触发,而是加入微任务队列里面去。
  • 批处理,DOM每次发生变化,就会生成一条变动记录。这个变动记录对应一个MutationReco-rd对象,该对象包含了与变动相关的所有信息。Mutation Observer处理的是一个个Mutation-Record对象所组成的数组。
  • type: 观察的变动类型(attribute、characterData或者childList)。
  • target: 发生变动的DOM节点。
  • addedNodes: 新增的DOM节点。
  • removedNodes: 删除的DOM节点。
  • previousSibling: 前一个同级节点,如果没有则返回null。
  • nextSibling: 下一个同级节点,如果没有则返回null。
  • attributeName:发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性。
  • oldValue: 变动前的值。这个属性只对attribute和characterData变动有效,如果发生childList变动,则返回null。

使用:

  1. 首先初始化一个MutationObserver这样一个实例var observer = new Mutation Observer(callback) 接受一个会调函作为参数
  2. 然后调用用这个实例的observer.oberser(targetNode,config),传递两个参数一个观察的目标节点一个是配置选项option

bserve 方法中 options 参数有已下几个选项:

  • childList:设置 true,表示观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化。
  • attributes:设置 true,表示观察目标属性的改变。
  • characterData:设置 true,表示观察目标数据的改变。
  • subtree:设置为 true,目标以及目标的后代改变都会观察。
  • attributeOldValue:如果属性为 true 或者省略,则相当于设置为 true,表示需要记录改变前的目标属性值,设置了 attributeOldValue 可以省略 attributes 设置。
  • characterDataOldValue:如果 characterData 为 true 或省略,则相当于设置为 true,表示需要记录改变之前的目标数据,设置了 characterDataOldValue 可以省略 characterData 设置。
  • attributeFilter:如果不是所有的属性改变都需要被观察,并且 attributes 设置为 true 或者被忽略,那么设置一个需要观察的属性本地名称(不需要命名空间)的列表。
  1. 设置callback() 回调函数,声明节点发生变化之后,需要作何处理mutations表示节点发生变化的情况。
//该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例,
var observer = new MutationObserver(function (mutations, observer) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});
  1. 使用案例
//选择一个需要观察的节点
var targetNode = document.getElementById('root')

// 设置observer的配置选项
var config = { attributes: true, childList: true, subtree: true }

// 当节点发生变化时的需要执行的函数
var callback = function (mutationsList, observer) {
    for (var mutation of mutationsList) {
        if (mutation.type == 'childList') {
            console.log('A child node has been added or removed.')
        } else if (mutation.type == 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.')
        }
    }
}

// 创建一个observer示例与回调函数相关联
var observer = new MutationObserver(callback)

//使用配置文件对目标节点进行观测
observer.observe(targetNode, config)
targetNode.setAttribute('class','div')
targetNode.appendChild(document.createElement('div'))
// 停止观测
observer.disconnect()

7.说一Node.js和浏览器事件循环的不同?

回答思路: node11版本一下是执行完所有的宏任务,再执行所有的微任务,node11以上和浏览器是宏任务和微任务同步执行。


function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}

test()

// 以上代码在node11以下版本的执行结果(先执行所有的宏任务,再执行微任务)
// start
// end
// children1
// children2
// children3
// children2-1
// children3-1

// 以上代码在node11及浏览器的执行结果(顺序执行宏任务和微任务)
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1

7.说一说原型和原型链?

什么是原型: 每个原型都有一个__proto__指针指向他的原型对象,他的原型对象也有这样指针指向他的原型对象,这样就行成了原型链。

为什么要有原型 原型可以理解为对象的公共部分,比如我要游戏里的两个人物,她们除了武器不一样,其余的全都相同,比如血量啊,皮肤颜色啊等等,,这时候,我们就可以将这些相似的属性抽象成一个相似的原型对象。使我们的代码块开起来更加简洁,同时能够占用较少的内存。

原型的应用: 通过原型实现继承,Object.prototype.toString.call()判断数据类型。


题目1: 对new的一种错误理解,b.__proto__==A.prototype 这个仅仅在A.prototype没有变化的时候成立,因为两者的的引用指针指向堆同一个区域,但是一旦执行A.prototype = {n: 2,m: 3} 这时候A.prototype不再指向原有的堆内存地址了,而是指向了新开辟的堆内存,但是原有的堆内存依旧存在,因为b.__proto__依旧指向原有的堆内存地址。所以打印b.n的输出undefined,为什么是undefined而不是报错,变量未定义,因为他是a对象里面的一个属性。

var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();
console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);

题目2: 这个题考察的是构造函数f.constructor,f.constructor==F is true F.constructor =Function is true。 f的原型是 f-->Object F的原型是 F--> Function.prototype-->Object 。因此f.b()是函数未定义。

var F = function() {};

Object.prototype.a = function() {
  console.log('a');
};

Function.prototype.b = function() {
  console.log('b');
}

var f = new F();

f.a();
f.b();

F.a();
F.b();

题目3: 这个题跟上个题基本差不多,但是不一样的地方在与这里面的foo.b他输出的是undefine,以为他调用的对象里面的一个属性,而上一个题调用的是一个函数。

var foo = {},
F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';
console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);

8.说一说JS中的new操作符发做了什么事?

  1. 创建一个空对象 var instance=new Object;
  2. 将空对象的原型指向构造函数的原型
  3. 改变this指向,将构造函数作用域赋值给这个新对象,并调用构造函数为这个新对象赋值 一般情况下,构造函数是没有返回值的,有返回值的情况下,如果是基础数据类型,则忽略返回值,如果是引用数据类型,new 操作符无效,返回这个object。
  4. 返回这个新对象。
function Person(name){
    this.name = name;
}
let me = new Person('快乐每一天');

9 数组问题


['1', '2', '3'].map(parseInt) what & why ?

不要被map的本身的方式常用写法给搞呆了,map本身就是传递一个方法,这个方法接受两个参数,value ,key,有问题吗,parseInt也是接受两个参数,一个数值 value,一个几进制N,然后将这个数转化为为N进制的整数。

[1, NaN, NaN]

  • parseInt(‘1’, 0) //radix 为 0 时,且 string 参数不以“0x”和“0”开头时,按照 10 为基数处理。这个时候返回 1
  • parseInt(‘2’, 1) //基数为 1(1 进制)表示的数中,最大值小于 2,所以无法解析,返回 NaN
  • parseInt(‘3’, 2) //基数为 2(2 进制)表示的数中,最大值小于 3,所以无法解析,返回 NaN

已知如下数组:(携程)

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

10 说一说HsitoryRouter和HashTouter区别

得分点 window.onhashchange history.pushState window.onpopstate

标准回答 HashRouter和 HistoryRouter的区别:

  1. history和hash都是利用浏览器的两种特性实现前端路由,history是利用浏览历史记录栈的API实现,hash是监听location对象hash值变化事件来实现。

  2. HashRouter的原理:通过window.onhashchange方法获取新URL中hash值,再做进一步处理。

  3. HistoryRouter的原理:通过history.pushState 使用它做页面跳转不会触发页面刷新,使用window.onpopstate 监听浏览器的前进和后退,再做其他处理。

  4. history的url没有'#'号,hash反之。需要url更优雅时,可以使用history模式。 需要兼容低版本的浏览器时hash能兼容到IE8,history只能兼容到IE10,建议使用hash模式。需要添加任意类型数据到记录时,可以使用his tory模式。

  5. 相同的url,history会触发添加到浏览器历史记录栈中,hash不会触发,history需要后端配合,如果后端不配合刷新新页面会出现404,hash不需要。

  6. 加分回答 hash模式下url会带有#,


Hash 模式和history 模式的区别

1.实现方式不同:hash是通过监听hashchange()事件实现的,前端js根据hash地址做响应的操作。history模式是利用浏览历史记录栈来实现的,通过history的pushState方法或者repalceState方法并且监听popState事件来实现的。

2.Vue采用的hash模式,hash模式为什么需要#,因为没有#直接访问地址,当一个窗口#后面的内容改变时,就会触发window.onhashchange()。

3.History模式,通过pushState和replaceState(),将访问的连接加入到当前窗口的历史记录里面,不会刷新页面。当时当用户点击前进后退按钮的时候,就会触发popState事件,这时候我们就可以操作了。

4.但是history模式,在刷新页面的时候,由于没有访问地址中没有#,所以会直接向服务器发送请求,这个时候就需要服务端进行拦截处理请求。

Array.from(new Set(arr.flat(Infinity))).sort((a, b) => {
  return a - b;
});

11. 说一说JS继承模式的优缺点 (未读)

回答思路 原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、ES6 Class

标准回答

  • 原型链继承: 让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。
    • 优点:写法方便简洁,容易理解。
    • 缺点:在父类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享。同时在创建子类型的实例时,不能向超类型的构造函数中传递参数
   // 父类
   function Parent() {
     this.name = ['父类']
     this.introduce = function () {
       console.log("my name is" + this.name)
     }
   }
   // 子类
   function Child() {
     this.childname = ['子类']
   }

   // 核心代码:
   Child.prototype = new Parent()
   var child1 = new Child()
   console.log(child1)
  • 借用构造函数继承: 在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。
    • 优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
    • 缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
    // 父类
    function Parent() {
      this.name = ['父类']
      this.introduce = function () {
        console.log("父类上的introduce方法")
      }
    }
    Parent.prototype.sayhi = function () {
      console.log('父类原型上的sayhi方法');
    }
    // 子类
    function Child() {
      this.childname = ['子类']
      Parent.call(this)
    }
 
    var child1 = new Child()
    child1.introduce()
    child1.sayhi()
  • 组合继承: 将原型链和借用构造函数的组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。
    • 优点就是解决了原型链继承和借用构造函数继承造成的影响。
    • 缺点是无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
    // 父类
    function Parent() {
      this.name = ['父类']
      this.introduce = function () {
        console.log("父类上的introduce方法")
      }
    }
    Parent.prototype.sayhi = function () {
      console.log('父类原型上的sayhi方法');
    }
    // 子类
    function Child() {
      this.childname = ['子类']
      Parent.call(this) // 第二次调用Parent
    }
 
    Child.prototype = new Parent() // 第一次调用Parent
 
    var child1 = new Child()
    console.log(child1);
  • 原型式继承: 在一个函数A内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上,函数A是对传入的对象执行了一次浅复制。ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。

    • 优点是:不需要单独创建构造函数。
    • 缺点是:属性中包含的引用值始终会在相关对象间共享
  • 寄生式继承: 寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

  • 优点:写法简单,不需要单独创建构造函数。

  • 缺点:通过寄生式继承给对象添加函数会导致函数难以重用

   var Parent = {
     name: ['父类属性'],
     sayhi: function () {
       console.log(this.name);
     }
   }

   var child1 = Object.create(Parent)
   var child2 = Object.create(Parent)
   child1.name[0] = 'child1属性'
   child2.name[0] = 'child2属性'
   console.log(child1);
  • 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
    • 优点是:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;
    • 缺点是:代码复杂
     // 父类
    function Parent() {
      this.name = ['父类']
      this.introduce = function () {
        console.log("父类上的introduce方法")
      }
    }
    Parent.prototype.sayhi = function () {
      console.log('父类原型上的sayhi方法');
    }
    // 子类
    function Child() {
      this.childname = ['子类']
      Parent.call(this) // 核心代码
    }
    Child.prototype = Object.create(Parent.prototype) // 核心代码
 
    const child1 = new Child()
    const child2 = new Child()
    child1.name[0] = 'child1'
    child2.name[0] = 'child2'
    console.log(child1);
    console.log(child1.name);
    console.log(child2.name);
  • 加分回答 ES6 Class实现继承
    • 原理:原理ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。
    • 优点:语法简单易懂,操作更方便。
    • 缺点:并不是所有的浏览器都支持class关键字
 class Person { //调用类的构造方法 
     constructor(name, age) { this.name = name this.age = age } //定义一般的方法 
     showName() { 
         console.log("调用父类的方法") 
         console.log(this.name, this.age);
         } 
} 
 let p1 = new Person('kobe', 39)
 console.log(p1) //定义一个子类 
 class Student extends Person { 
     constructor(name, age, salary) { 
         super(name, age)//通过super调用父类的构造方法
         this.salary = salary } 
     showName() {//在子类自身定义方法 console.log("调用子类的方法") 
         console.log(this.name, this.age, this.salary);
     } 
 } 
 let s1 = new Student('wade', 38, 1000000000) 
 console.log(s1)
 s1.showName()

12. 说一说this指向

回答思路 全局执行上下文、函数执行上下文、this严格模式下undefined、非严格模式window、构造函数新对象本身、普通函数不继承this、箭头函数无this,可继承

标准回答

  1. this关键字由来:在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。
  2. this存在的常见的场景有三种全局执行上下文函数执行上下文和eval执行上下文,eval这种不讨论。
    • 全局执行环境中无论是否在严格模式下,(在任何函数体外部)this 都指向全局对象。
    • 在函数执行上下文中访问this,函数的调用方式决定了 this 的值。在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window,通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
    • 普通函数this指向:
      • 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
      • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
      • new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。嵌套函数中的 this 不会继承外层函数的 this 值。
      • 箭头函数this指向:箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。

加分回答 箭头函数因为没有this,所以也不能作为构造函数,但是需要继承函数外部this的时候,使用箭头函数比较方便

var myObj = { 
name : "闷倒驴", 
showThis:function()
{ console.log(this);
// myObj 
var bar = ()=>{
this.name = "王美丽";
console.log(this) // myObj 
} 
bar(); } }; 
myObj.showThis(); 
console.log(myObj.name); //
"王美丽" console.log(window.name); // 

13. 说一说箭头函数

回答思路 没有this、this是从外部获取、不能使用new、没有arguments、没有原型和super

标准回答 箭头函数相当于匿名函数,简化了函数定义。

  1. 箭头函数有两种写法,当函数体是单条语句的时候可以省略{}和return。另一种是包含多条语句,不可以省略{}和return。
  2. 箭头函数最大的特点就是没有this,所以this是从外部获取,就是继承外部的执行上下文中的this,由于没有this关键字所以箭头函数也不能作为构造函数, 同时通过 call()apply() 方法调用一个函数时,只能传递参数(不能绑定this),第一个参数会被忽略。 箭头函数也没有原型和super。
  3. 不能使用yield关键字,因此箭头函数不能用作 Generator 函数。不能返回直接对象字面量。

加分回答

箭头函数的不适用场景:

  • 定义对象上的方法 当调用 dog.jumps 时,lives 并没有递减。因为 this 没有绑定值,而继承父级作用域。
var dog = {
    lives: 20, jumps: () => {
        this.lives--;
    }
}
  • 不适合做事件处理程序 此时触发点击事件,this不是button,无法进行class切换。
var button = document.querySelector('button');
button.addEventListener('click', () => {
    this.classList.toggle('on');
});

箭头函数函数适用场景:

  • 简单的函数表达式,内部没有this引用,没有递归、事件绑定、解绑定,适用于map、filter等方法中,写法简洁。
var arr = [1, 2, 3];
var newArr = arr.map((num) => num * num)
  • 内层函数表达式,需要调用this,且this应与外层函数一致时。
let group = {
    title: "Our Group", 
    students: ["John", "Pete", "Alice"], 
    showList() {
        this.students.forEach(student => alert(this.title + ': ' + student));
    }
};
group.showList();

14.说一说作用域和执行上下文

什么是作用域? 作用域是指程序源代码中定义变量的区域,作用域规定了当前执行代码对变量的访问权限。函数的作用域是函数创建的决定的,函数创建时的嵌套就形成了作用于链。

什么是执行上下文? 执行上下文可以理解为当前执代码的运行环境,执行上下文,其中执行上下文中最重要的就是thisValue和文本环境,文本环境相当于点名册,将变量名,类名,函数都登记到文本环境中。执行上下文又分为全局执行上下文和函数执行上下文。

什么时候创建执行上下文?

  1. 当我们进入到全局代码
  2. 进入function函数
  3. 进入eval函数执行的代码
  4. 进入module代码都会创建新的执行上下文,

怎么创建一个执行上下文? 创建一个全局执行上下文主要分为以下几个步骤:

    1. 创建全局执行上下文,并加入栈顶
    1. 分析代码,找变量声明
    • 找所有非函数的var声明,当然也包括for循环里面和while循环里面
    • 找到所有顶级的函数声明
    • 找到所有let class 和 const声明
  • 3 名字重复处理
    • let const class 他们之间的名字不能重复
    • let const class和var function之间的名字不能重复
    • var function的名字可以重复,function优先级要大于var
  • 4 名字绑定
    • 登记var 初始化为undefine (变量提升)
    • 登记function 初始化函数对象 (预解析)
    • 登记let const class 不进行初始化 (临时死区)

CSS基础

1. 说一说CSS的选择器?

  • 类选择器 ID选择器 标签选择器
  • 属性选择器(配合正则表达式)
  • 层级选择器 组选择器
  • 伪类选择器 伪元素选择器

2. 说一说样式优先级的规则是什么?

回答思路: !important、行内样式、嵌入样式、外链样式、id选择器、类选择器、标签选择器、复合选择器、通配符、继承样式

标准回答 CSS样式的优先级应该分成四大类

  • 第一类!important,无论引入方式是什么,选择器是什么,它的优先级都是最高的。
  • 第二类 引入方式行内样式的优先级要高于嵌入和外链,嵌入和外链如果使用的选择器相同就看他们在页面中插入的顺序,在后面插入的会覆盖前面的。
  • 第三类 选择器,选择器优先级:id选择器>(类选择器 | 伪类选择器 | 属性选择器 )> (后代选择器 | 伪元素选择器 )> (子选择器 | 相邻选择器) > 通配符选择器 。
  • 第四类 继承样式,是所有样式中优先级比较低的。
  • 第五类 浏览器默认样式优先级最低。
  • 加分回答 使用!important要谨慎
    • 一定要优先考虑使用样式规则的优先级来解决问题而不是 !important,只有在需要覆盖全站或外部 CSS 的特定页面中使用 !important
    • 为了避免全局变量污染,最好不要轻易使用!important,可以借助模块变成,比如css module来解决优先级问题并且还能有效解决相同类名的变量污染问题。

3.说一说盒模型?

回答思路 标准盒模型、怪异盒模型、box-sizing:border-box、盒模型大小

标准回答 CSS盒模型定义了盒的每个部分包含 margin, border, padding, content 。根据盒子大小的计算方式不同盒模型分成了两种,标准盒模型和怪异盒模型。

  • 标准模型,给盒设置 widthheight,实际设置的是 content box。paddingborder 再加上设置的宽高一起决定整个盒子的大小。
  • 怪异盒模型,给盒设置 widthheight,包含了paddingborder ,设置的 widthheight就是盒子实际的大小
  • 默认情况下,盒模型都是标准盒模型 设置标准盒模型:box-sizing:content-box 设置怪异盒模型:box-sizing:border-box

4.说一下重绘、重排区别如何避免?

回答思路: 改变元素尺寸、重新计算布局树、重排必定重绘、重绘避开了重建布局树和分层、GPU加速、脱离文档流、样式集中改变

标准回答:

  • 重排 :当DOM的变化影响了元素的布局(元素的的位置和尺寸大小),浏览器需要重新计算元素的位置信息,将其安放在界面中的正确位置,这个过程叫做重排。
  • 重绘:当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,所以重绘跳过了创建布局树和分层的阶段。
  • 重排需要重新计算布局树,重绘不需要,重排必定发生重绘,但是涉及到重绘不一定要重排。涉及到重排对性能的消耗更多一些。
  • 触发重排的方法
    • 页面初始渲染,这是开销最大的一次重排
    • 添加/删除可见的DOM元素 -改变元素位置
    • 改变元素尺寸,比如边距、填充、边框、宽度和高度等
    • 改变元素内容,比如文字数量,图片大小等
    • 改变元素字体大小 -改变浏览器窗口尺寸,比如resize事件发生时 -
    • 激活CSS伪类(例如::hover
    • 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow
    • 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等
  • 避免重排的方式
    • 样式集中改变
    • 使用 absolute 或 fixed 脱离文档流
    • 使用GPU加速:transform
  • 加分回答 GPU的过程是以下这几步 :
    1. 获取DOM并将其分割成多个层(renderLayer)
    2. 将每个层栅格化,并独立的绘制进位图中
    3. 将这些位图作为纹理上传至GPU
    4. 复合多个层来生成最终的屏幕图像(最后的layer)
    5. 开启了GPU加速的元素被独立出来,不会再影响其他dom的布局,因为它改变之后,只是相当于被贴上了页面。

5. 说一说服务端渲染

回答思路 服务器端生成HTML直接返回给浏览器、减少网络传输、首屏渲染快、对搜索引擎友好

标准回答 SSR是Server Side Render简称;

  • 页面上的内容是通过服务端渲染生成的,浏览器直接显示服务端返回的html就可以了
  • 和它对应的是,CSR是Client Side Render简称;客户端在请求时,服务端不做任何处理,直接将前端资源打包后生成的html返回给客户端,此时的html中无任何网页内容,需要客户端去加载执行js代码才能渲染生成页面内容,同时完成事件绑定,然后客户端再去通过ajax请求后端api获取数据更新视图。
  • 服务端渲染的优势
    • 减少网络传输,响应快,用户体验好,首屏渲染快,对搜索引擎友好,搜索引擎爬虫可以看到完整的程序源码,有利于SEO。
  • Vue项目中实现服务端渲染方法 Vue在客户端渲染中也是采用一定方法将虚拟DOM渲染为真实DOM的,那么服务端的渲染流程也是通过虚拟DOM的编译来完成的,编译虚拟DOM的方法是renderToString。在Vue中,vue-server-renderer 提供一个名为 createBundleRenderer 的 API,这个API用于创建一个 render,并且自带renderToStr-ing方法。
  • 加分回答 使用服务器端渲染 (SSR) 时还需要有一些权衡之处:
    • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
    • 涉及构建设置和部署的更多要求。 与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
    • 更多的服务器端负载。 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略 。

5. 说一说前端性能优化的手段

回答思路 图片压缩和文件压缩、雪碧图/精灵图、节流防抖、HTTP缓存、本地缓存、Vue的keep-alive缓存、ssr服务器端渲染、懒加载、对dom查询进行缓存、将dom操作合并

标准回答 前端性能优化分为两类,一类是文件加载更快,另一类是文件渲染更快。

  • 加载更快的方法:
    • 让传输的数据包更小(压缩文件/图片):图片压缩和文件压缩DNS预解析
    • 减少网络请求的次数:缓存(HTTP缓存、本地缓存、Vue的keep-alive缓存等),雪碧图/精灵图、节流防抖,
  • 渲染更快的方法:
    • 提前渲染:ssr服务器端渲染
    • 避免渲染阻塞:CSS放在HTML的head中 JS放在HTML的body底部
    • 避免无用渲染:懒加载
    • 减少渲染次数:对dom查询进行缓存、将dom操作合并、使用减少重排的标签

加分回答 雪碧图的应用场景一般是项目中不常更换的一些固定图标组合在一起,比如logo、搜索图标、切换图标等。 电商项目中最常用到的懒加载,一般在查看商品展示的时候通常下拉加载更多,因为商品数据太多,一次性请求过来数据太大且渲染的时间太长。 说一说性能优化有哪些性能指标,如何量化?

6.说一说性能优化有哪些指标,如何量化?

回答思路 加载速度、第一个请求响应时间、页面加载时间、交互动作的反馈时间、帧率FPS、异步请求完成时间 Lighthouse、Throttling 、Performance、Network、WebPageTest

参考这篇文章

主要参考这篇文章

主要参考这个视频

常用的性能优化指标

网站的核心性能指标主要包括三个方面,最大内容绘制(Largest Content paint,LCP),首次输入延迟(First Input Delay,FID),累计布局偏移(Cumulative layout shift,CLS)。分别对应加载体验、交互性和视觉稳定性。

  1. LCP,在视窗内可见的最大内容的渲染时间。这类的内容指文本、图像(包括背景图)、svg元素或非空白的canvas元素。详细说明及计算:Largest Contentful Paint。

图片.png

  1. FID 衡量的是从用户第一次与页面进行交互(单击链接,点击按钮或使用自定义的JavaScript驱动的控件)到浏览器实际上能够开始处理事件处理程序的时间。

图片.png

  1. CLS: 测量在页面的整个生命周期中发生的意外布局移动的所有单个布局移动得分的总和当可见元素的位置从一个渲染帧改变到下一个渲染帧时,就会发生布局移动(layout shift)。而只有现有可见元素的起始位置变动才算是布局移动,如果新插入一个元素或者元素不改变位置只改变大小等,只要它没有引起其他可见元素的位置变动,就不算入布局移动中。

Lighthouse中的性能指标

  1. First Contentful Paint(FCP)

  2. Speed Index(SI)

  3. Largest Contentful Paint (LCP)

  4. Time to Interactive (TTI)

  5. Total Blocking Time (TBT)

  6. Cumulative Layout Shift (CLS)

6.说一说js的async和defer?

回答思路 加载JS文档和渲染文档可以同时进行、JS代码立即执行、JS代码不立即执行、渲染引擎和JS引擎互斥

标准回答

  • 普通: 浏览器会立即加载JS文件并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行
  • async: 加上async属性,加载JS文档和渲染文档可以同时进行(异步),当JS加载完成,JS代码立即执行,会阻塞HTML渲染。
  • defer: 加上defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),当HTML渲染完成,才会执行JS代码。
  • 加分回答 渲染阻塞的原因: 由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉

7.说一说BFC?

回答思路 块级格式化上下文、独立的渲染区域、不会影响边界以外的元素、形成BFC条件、float、position、overflow、display

标准回答

  1. BFC(Block Formatting Context)块级格式化上下文,是Web页面一块独立的渲染区域,内部元素的渲染不会影响边界以外的元素。

  2. BFC布局规则

    • 内部盒子会在垂直方向,一个接一个地放置。
    • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。
    • 每个盒子(块盒与行盒)的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
    • BFC的区域不会与float box重叠。
    • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
    • 计算BFC的高度时,浮动元素也参与计算。
    • BFC形成的条件
      • float 设置成 left right -position absolute或者fixed
      • overflow 不是visible,为 autoscrollhidden
      • displayflex或者inline-block
    • BFC解决能的问题:清除浮动

加分回答

  • BFC的方式都能清除浮动:,但是常使用的清除浮动的BFC方式只有overflow:hidden,原因是使用float或者position方式清除浮动,虽然父级盒子内部浮动被清除了,但是父级本身又脱离文档流了,会对父级后面的兄弟盒子的布局造成影响。如果设置父级为display:flex ,内部的浮动就会失效。所以通常只是用overflow: hidden清除浮动。
  • IFC(Inline formatting contexts):内联格式上下文。IFC的高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响),IFC中的line box一般左右都贴紧整个IFC,但是会因为float元素而扰乱。
  • GFC(GrideLayout formatting contexts :网格布局格式化上下文。当为一个元素设置display值为grid的时候,此元素将会获得一个独立的渲染区域。 FFC(Flex formatting contexts):自适应格式上下文。display值为flex或者inline-flex的元素将会生成自适应容器。

浏览器问题

1. 说一说cookie sessionStorage localStorage 区别?

回答思路: 从三者的写入方式、生命周期、存储位置、存储大小、数据共享、应用场景、请求是否携带这几个方面来回答。

标准回答: cookie sessionStorage localStorage三者都是浏览器本地存储的方式,她们的区别主要以下几个方面。

  • 写入方式: localStorage和sessionStorage是由前端写入,cookie是服务端写入。
  • 生命周期: localStorage写好了之后会一直存放在本地,除非手动清除,sessionStorage页面关闭之后会自动清楚,cookie的生命周期是服务端写入的时候就设置好的。
  • 存储空间: loacalStorage和sessionStorage的存储空间比较大,大约5M,cookie的存储空间比较小,大约4K左右。
  • 数据共享: localStorage、sessionStorage、cookie数据共享会遵循同源策略,sessionStorage还限制在同一个页面。
  • 是否携带: 当浏览器向服务器发送请求时会携带cookie中的数据,但是localSorage和sessioStorage不会。
  • 应用场景: 由于存在以上这些区别,所以它们的应用场景也各不相同,cookie一般应用于存储登录信息验证 sessionId或者token,localStorage常用于存储不易变动的东西,减轻服务器压力,session-Storage常用于检测用户是否刷新进入该界面,比如音乐播放器的进度条恢复功能。(刷新页面的情况下sessionStorage不会消失)

了解Cookie

  • Cookie的起源 cookie的本事是弥补了浏览的无状态协议的一个不足,服务端只要设置了setCookie这个header,之后浏览器就会把这个cookie写到我们浏览器里面存起来,然后当访问服务器的时候就会带上这个浏览器。
  • 设置Cookie cookie的一般格式如下:
Set-Cookie: "name=value;domain=.domain.com;path=/;expires=Sat, 11 Jun 2016 11:29:42 GMT;HttpOnly;secure"
属性说明
name=value键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型(name 不区分大小写。如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。
domain指定 cookie 所属域名,默认是当前域名
*path指定 cookie 在哪个路径(路由)下生效,默认是 '/' 。如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read
expires过期时间(GMT时间格式),在设置的某个时间点后该 cookie 就会失效。如果客户端和服务器时间不一致,使用expires就会存在偏差。一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
max-agecookie 有效期,单位秒。如果为正数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。优先级高于 expires
HttpOnly如果给某个 cookie 设置了 httpOnly 属性,则无法通过JS脚本读写该 cookie 的信息,但还是能通过 Application(控制台) 中手动修改 cookie,所以只是在一定程度上可以防止 CSRF 攻击,不是绝对的安全
secure该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。当 secure 值为 true 时,cookie 在 HTTP 中是无效的。

2. 说一说token能放cookie中吗?

问题解释: 一般token放在什么位置,前端获取token后一般放在localStorage、SessionStorage和cookie ,token当然可以放在cookie中。这里面有一个登录验证的过期时间问题,token有一个过期时间(token里面可以带 expiredTimestamp 解密后对比服务器时间可以限制token有效时间,cookie有也一个过期时间 ,两者不一致可能会产生死循环。

为了使用户登录体验更友好,之前做了个自动登录的优化。

实现方式为:在用户登录成功时,将 token 存入 cookie ;当用户下次来到本网站,读取 cookie 中的 token 时,自动登录。退出登录时则清空 token。

看起来很简单,而且对用户很友好 但是今天发现了一个漏洞,我本地 cookie 有效时间存的是30天 ,如果后端 token 的有效期和我存的时间不一致会发生什么?当用户某次自动登录时,响应拦截器发现 token 过期了,会把用户踢到登录页,然后登录页发现 cookie 中存了 token ,又会自动登录进去;进入系统后响应拦截器发现 token 过期了,又将用户踢去登录页...

死循环就这样发生了

怎么解决呢,很简单,在响应拦截器 401(与后台约定登录失效的返回码)时清除 token ,这样就不会存在本地存有过期 token 的问题了。 自动登录与 token 过期

标准回答: 能,token一般是用来判断用户是否登录的, 它内部包含的消息有:uid(用户唯一标识符)、time(当前时间的时间戳)、sign签名,token可以存放在cookie中,token是否过期应该有后端来判断,不应该由前端来判断,所以token存放在cookie中只要不设置cookie的过期时间就可以了,如果token失效,就让后端返回固定的状态表示(401)表明token失效,需要重新登录,再重新登录的时候,再重新设置cookie中的token就可以了。

加分回答: 说一说验证流程

  • 1.客户端是输入用户名和密码,发送给服务端
  • 2.服务点接受登录信息,并进行验证,验证成功之后,生成token并发送给浏览器。
  • 3.浏览器接受token,并将其存储在cookie或者localStorage中。
  • 4.客户端每次请求时候携带token进行验证。
  • 5.服务端获取获取浏览器发送的token,进行解析验证,如果验证成功,返回接收数据,验证失败,返回token过期状态。

Oauth2.0登录原理

当我们在A网站上面想要使用B网站的资源时,最常见的都是第三方登录,我们需要登录B网站的账号,这个时候就会产生信任问题,B网站不想向A网站暴露用户登录密码,所以就出现Oauth来解决这个问题。

Oauth使用access token,B网站设置两个服务器,一台资源服务器,一台鉴权服务器。如果用户想要访问A网站的信息,A网站的服务器会访问B网站的鉴权服务器,返回B网站的登录界面,登录成功之后鉴权服务器会向A服务器发送一个access token,为了保证安全性,access token设置了有效期,而且有效期一般比较短暂,然后A网站服务器那这个access token去访问B网站的资源服务器。

Access Token 与 Refresh Token

Access Token有一定有效期。这是因为,access token 在使用的过程中可能会泄露。给 access token 限定一个较短的有效期可以降低因 access token 泄露而带来的风险。

然而引入了有效期之后,客户端使用起来就不那么方便了。每当 access token 过期,客户端就必须重新向用户索要授权。这样用户可能每隔几天,甚至每天都需要进行授权操作,非常影响用户体验

于是它来了,Oauth2.0 引入了 refresh token 机制。 refresh token的作用是用来刷新access token。鉴权服务器提供一个刷新接口,返回新的access token。refresh token有效期很长,为了保证保证安全 oauth 2.0 引入了两个措施。

  • Oauth2.0 要求refresh token必须存储在应用方的服务上,绝不能存储在客户端,调用refresh接口的时候,一定要是服务器到服务器。
  • Oauth2.0 引入了client_id,client_secret。即每一个应用都会被分配到一个client_id和一个对应的client_secret。应用必须吧client_secret妥善保存在服务器上,绝对不能透露,刷新access token的时候,需要验证这个client_secret.

Access token 刷新接口如下。总结一波,我用一个把钥匙开一把锁,这个要是就是access token,这个锁就是服务资源,但这个这把钥匙有效期的只有2分钟,2分钟之后就要找管理员重新要一把锁。很麻烦,这个时候管理员说,我给你一个通行证,你保存好,放到保险柜里面,有效期是一个月,有这个通行证,如果你的要是过期了,你念个一个咒语,你就能有一个新钥匙,不用每次都来找我。

POST /refresh
   参数:
    refresh token
    client_id
    signatrue 签名,由 sha256(client_id + refresh_token + client_secret) 得到
   返回:新的 access token

3. 说一说浏览器垃圾回收机制?

回答思路: 栈垃圾回收、新生代老年代、Scavenge算法、标记清除算法、标记整理算法、全停顿、增量标记。

  • 浏览器的垃圾回收机制根据数据存储方式不同可以分为栈垃圾回收堆垃圾回收

  • 栈垃圾回收的方式比较简单,当一个函数执行结束之后,JavaScript引擎会销毁该函数保存在栈中的执行上下文

  • 堆垃圾回收,当函数执行结束,栈空间处理完成,但是堆空间中的数据虽然没有被引用但是还存储在堆空间里面,需要垃圾回收器将对空间中的垃圾数据进行回收。为了使垃圾回收效果更好,根据对象的生命周期不同,采用不同的垃圾回收算法。

    • V8引擎会把堆分为两个区域---新生区和老年区。新生代中存储存活时间比较短的对象,老年区里存储生存时间比较长的对象。新生区采用Scavenge算法老年区采用标记清楚和标记整理算法
      • Scavenge算法: 对象区域和空闲区域
      • 引用计数算法
      • 标记清除算法
      • 标记整理算法
      • 增量标记算法

4. 浏览器的页面渲染机制?

回答思路 dom树、stylesheet、布局树、分层、光栅化、合成

标准回答

  1. 浏览器拿到HTML,先将HTML转换成dom树
  2. 再将CSS样式转换成stylesheet,
  3. 根据dom树和stylesheet创建布局树,
  4. 对布局树进行分层,为每个图层生成绘制列表,再将图层分成图块,
  5. 紧接着光栅化将图块转换成位图
  6. 最后合成绘制生成页面。

图片.png

浏览器渲染流程(下) 分层、绘制、合成

加分回答:

分层的目的: 避免整个页面渲染,把页面分成多个图层,尤其是动画的时候,把动画独立出一个图层,渲染时只渲染该图层就ok,transform,z-index等,浏览器会自动优化生成图层。

光栅化目的: 页面如果很长但是可视区很小,避免渲染非可视区的样式造成资源浪费,所以将每个图层又划分成多个小个子,当前只渲染可视区附近区域。


浏览器渲染一个网页,大致分为一下几个步骤:

  1. 解析html文档生成dom树,同时解析css生成cssom树,将dom树和cssom树合并,生成渲染树。Dom树表示页面的结构,cssom树表示节点应该如何绘制。

  2. 根据渲染树,浏览器可以计算出网页中有那些节点,各节点的css以及从属关系,这阶段就是根据盒子模型,每一个标签的长宽,位置,来确定布局,所以这个叫做重排,也就是回流。脱离文档流其实就是脱离渲染树【起型】

  3. 根据渲染树进行绘制,在绘制阶段,调用渲染器的print()方法会在浏览器上绘制内容其内容,浏览器的绘制是由浏览器的后端ui组件来完成的(这个就是重绘)【雕琢】

5. 说一下浏览器输入URL发生了什么?

回答思路: DNS解析、TCP握手、HTTP缓存、重定向、服务器状态码、渲染引擎和JS引擎互斥、渲染过程、浏览器进程、网络进程、渲染进程。

标准回答

  1. 输入地址,浏览器查找域名的 IP 地址。 浏览器向该IP地址的web 服务器发送一个 HTTP 请求,在发送请求之前浏览器和服务器建立TCP的三次握手,判断是否是HTTP缓存,如果是强制缓存且在有效期内,不再向服务器发请求,如果是HTTP协商缓存向后端发送请求且和后端服务器对比,在有效期内,服务器返回304,直接从浏览器获取数据,如果不在有效期内服务器返回200,返回新数据。 请求发送出去服务器返回重定向,浏览器再按照重定向的地址重新发送请求。 如果请求的参数有问题,服务器端返回404,如果服务器端挂了返回500。 如果有数据一切正常,当浏览器拿到服务器的数据之后,开始渲染页面同时获取HTML页面中图片、音频、视频、CSS、JS,在这期间获取到JS文件之后,会直接执行JS代码,阻塞浏览器渲染,因为渲染引擎和JS引擎互斥,不能同时工作,所以通常把Script标签放在body标签的底部。 渲染过程就是先将HTML转换成dom树,再将CSS样式转换成stylesheet,根据dom树和stylesheet创建布局树,对布局树进行分层,为每个图层生成绘制列表,再将图层分成图块,紧接着光栅化将图块转换成位图,最后合成绘制生成页面。

6. 说一说跨域是什么?如何解决跨域问题?

回答思路 同源限制、协议、域名、端口、CORS、node中间件、JSONP、postmessage

标准回答

跨域 当前页面中的某个接口请求的地址和当前页面的地址如果协议、域名、端口其中有一项不同,就说该接口跨域了。

跨域限制的原因:浏览器为了保证网页的安全,出的同源协议策略。

图片.png 跨域解决方案

  1. cors: 目前最常用的一种解决办法,通过设置后端允许跨域实现。
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS, POST");

  2. node中间件nginx反向代理跨域限制的时候浏览器不能跨域访问服务器,node中间件和nginx反向代理,都是让请求发给代理服务器,静态页面面和代理服务器是同源的,然后代理服务器再向后端服务器发请求,服务器和服务器之间不存在同源限制。

  3. JSONP利用的原理是script标签可以跨域请求资源,将回调函数作为参数拼接在url中。后端收到请求,调用该回调函数,并将数据作为参数返回去,注意设置响应头返回文档类型,应该设置成javascript。

  4. postmessage:H5新增API,通过发送和接收API实现跨域通信。

7. 说一说虚拟dom?

是什么

解决什么问题

如何解决问题

React问题

1.React 生命周期的各个阶段是什么?

在目前16.X(X>3)的 React 中,使用 componentWillMount, componentWillReceiveProps, and componentWillUpdate 这三个方法会收到警告。React 团队计划在 17.0 中废弃掉这几个 API。

为什么删除以上三个API,采用 getDerivedStateFromProps 替代?大致的意思是由于react使用Fiber架构,这一套异步中断机制,低优先级的任务会被高优先级的任务打断,可能会导致生命周期函数执行多次。这是不好的,所以react采用静态方法的方式,让我们获取不到this,来写一些副作用的代码,从而避免对声明周期的滥用。

我们知道 React 的更新流程分为:render 阶段和 commit 阶段。componentWillMount、compo-nentWillReceiveProps、componentWillUpdate 这三个生命周期钩子都是在 render 阶段执行的。

在 fiber 架构被应用之前,render 阶段是不能被打断的。当页面逐渐复杂之后,就有可能会阻塞页面的渲染,于是 React 推出了 fiber 架构。在应用 fiber 架构之后,低优先级任务的 render 阶段可以被高优先级任务打断。

而这导致的问题就是:在 render 阶段执行的生命周期函数可能被执行多次

像 componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个生命周期钩子,如果我们在其中执行一些具有副作用的操作,例如发送网络请求,就有可能导致一个同样的网络请求被执行多次,这显然不是我们想看到的。

而 React 又没法强迫开发者不去这样做,因为怎么样使用 React 是开发者的自由,所以 React 就新增了一个静态的生命周期 getDerivedStateFromProps,来解决这个问题。

用一个静态函数 getDerivedStateFromProps 来取代被废弃的几个生命周期函数,这样开发者就无法通过 this 获取到组件的实例,也不能发送网络请求以及调用 this.setState。它就是强制开发者在 render 之前只做无副作用的操作,间接强制我们无法进行这些不合理不规范的操作,从而避免对生命周期的滥用。

详情请点这里

注:红色为废弃的API,绿色为新加的API。

图片.png


初始化阶段: 1. getDefaultProps(): 设置默认props,ES6也可以用defaultProps设置组件默认属性。

2. getInitalState(): ES6可以使用constructor,此时可以访问this.state和this.props。

3. componentWillMount(): 组件虚拟dom转化成真实dom挂载到指定标签下之前调用,以后组件更新不调用,整个组件生命周期只调用一次。

4. render(): react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在这个步骤,此时不能更改state。

5. componentDidMount(): 组件渲染之后调用,只调用一次。


更新阶段:

6. componentwillReceiveProps(nextProps): 组件初始化时不调用,组件接受新的props时调用。注意,如果父组件的渲染导致了子组件渲染,计时props没有改变,该生命周期也会被调用。这个方法会比较新旧两个props是否相同,如果不同再将新的props更新到就state上去,但是他会破坏state的单一数据源,使得组件变得不可预测

6.getDerivedStateFromProps(nextProps,prevState): 替代了componentWillUpdate.它是一个静态方法,接收 propsprops 和 statestatestate 两个参数。它会在调用 render 方法之前被调用,不管是在初始挂载时还是在后续组件更新时都会被调用。

它的调用时机和 componentWillMount、componentWillUpdate、componentWillReceiveProps 一样都是在 render 方法被调用之前,它可以作为 componentWillMount、componentWillUpdate 和 component-WillReceiveProps 的替代方案。

当然,它的作用不止如此,它可以返回一个对象,用来更新 state,就像它的名字一样,从 props 中获取衍生的 state。如果不需要更新 state 则可以返回 null。

7. shouldComponentUpdate(nexrProp,nextState): react性能优化非常重要的一环。组件接受新的state或者props时调用,此时可以比较新的state、props和现在的state和props是否相同,如果相同则组织更新,这样就不需要让新的dom树和旧的dom树进行对比,节省大量性能。

9. render(): 渲染。

8. componentWillUpdate():,更新前调用,此时可以修改state。

需要注意的是:这个生命周期函数是类的静态方法,并不是原型中的方法,所以在其内部使用 this 访问到的不是组件实例。

此生命周期钩子不常用,如果可以的话,我们也尽可能不会使用它。

8. getSnapshotBeforeUpdate(): 此生命周期函数在最近一次渲染提交至 DOM 树之前执行,此时 DOM 树还未改变,我们可以在这里获取 DOM 改变前的信息,例如:更新前 DOM 的滚动位置。此时已经渲染完,已经render结束。

它接收两个参数,分别是:prevProps、prevState,上一个状态的 props 和上一个状态的 state。它的返回值将会传递给 componentDidUpdate 生命周期钩子的第三个参数。

使用场景:需要获取更新前 DOM 的信息时。例如:需要以特殊方式处理滚动位置的聊天线程等。

10. componentDidUpdate(prevProps,prevState): 组件初始化不调用,组件更新结束后调用,此时可以获得dom节点,,可以拿到更新之前的props和state。


销毁阶段:

11. componentWillUnmounted: 组件将要卸载时调用,一些时间监听器以及一些定时器需要在此清除。 age?)

2.SetState()同步还是异步?

回答思路 是同步也是异步、合成事件、生命周期函数、原生事件、定时器

标准回答

setState在合成事件和生命周期函数中是异步的,在原生事件和定时器中都是同步的。

加分回答: setState本身不分同步或者异步,而是取决于是否处于batch update中。组件中的所有函数在执行时临时设置一个变量isBatchingUpdates = true,当遇到setState时,如果isBatchingUpdates是true,那么setState就是异步的,如果是false,那么setState就是同步的。

那么什么时候isBatchingUpdates会被设置成false呢?

  1. 当函数运行结束时isBatchingUpdates = false
  2. 当函数遇到setTimeout、setInterval时isBatchingUpdates = false
  3. 当dom添加自定义事件时isBatchingUpdates = false

3.说一说React的diff算法?(未读)

  • 单节点diff
    • 单节点只是当前新的jsx对象只有一个节点,但是老的fiber树有多少对象这个我不知道。
    • 从同层级的老fiber节点中找出key值和type都相等的老节点,如果该老fiber节点存在,则复用他,然后删除剩余的节点,否则重新生成一个新的fiber节点(这也就意味着以这个节点为根的子树需要重新生成)。
  • 多节点diff,主要分为两个循环
    • 第一轮循环主要是新旧节点同为对比。 找到第一个无法复用的位置节点后,以最后一个可以复用的旧节点的位置作为后续判断节点是够能够插入的基准位置值(该值后续可能会变),然后跳出循环。经历了第一遍循环过后会出现三种情况;
      • 新节点遍历完成,老节点没遍历完。将老节点基准值之后的节点标记删除。
      • 老节点遍历完成,新节点没遍历完。将新节点节点基准值之后的节点标记插入。
      • 老节点和新节点都没有遍历完成。这个时候比较复杂,需要将剩余老节点放到一个map里面去,然后开启第二轮循环。
    • 第二轮循环是遍历剩余的新节点,遍历时,新节点都从map中寻找有没有自己能复用的老节点(ke和type相同即可复用),如果map中存在就可复用,然后将该老节点从map中移除,否则就重新生成,如果老节点被复用了,就会将该老节点原来的位置和第一次循环确立的基准位置进行比较,如果老节点的位置在基准值的右边时,说明复用该老节点的新节点无需移动,但是基准位置需要更新为老节点的位置,如果老节点的位置在基准值的左边的时候,说明复用该老节点的新节点需要重新插入,基准位置值不变二轮循环结束,只需要将map中剩余老节点的位置标志为删除即可。

4.ReactRouter基本用法是什么?

回答思路 路由的模式有两种、hash模式、history模式、路由的动态传参、重定向、高阶路由组件。

标准回答

  • react的路由保证了界面和URL的同步,拥有简单的API和强大的功能。react中的路由模式有两种,分别是:hash路由和history路由。
    • 首先用析构的方法引入需要用到的路由方式,需要注意的是路由所有的配置都必须被包裹在hash路由或者history路由里面。
    • 然后在路由标签内先再配置Route标签,它的参数有:path,路由匹配的路径。component,路由匹配成功之后渲染的组件。
    • react中路由的跳转使用Link标签,它的参数to指路由匹配的路径,也需要引入。NavLink标签和Link的区别就是渲染成a标签之后会自带一个class属性,对应的是NavLink标签的active属性。
    • react路由中有高阶路由组件withRouter,它和普通路由一样需要引入,主要作用是增加了路由跳转的方式,可以调用history方法进行函数中路由的跳转。
    • react中路由的动态传值是一个重点,{/:属性名}和{/属性名/值}搭配的方式进行传值,在需要接收参数的组件通过this.props.match.params来进行接收。react中路由的query传值是通过问号的方法将参数拼接在url之后,在需要接收参数的组件通过url.parse(this.props.location.search).query获取参数。
    • 路由的重定向需要用组件Redirect来完成,参数to是目标组件。
    • 路由的懒加载需要从react中引入Suspense和lazy,引入组件时通过lazy(() => import())来引入,使用Suspense标签将Route包裹起来即可。

加分回答: react中路由模式分为hash路由和history路由。

  • hash路由的原理是window.onhashchange监听,url前面会有一个#号,这个就是锚点,每个路由前面都会有#锚点,特点是#之后的数据改变不会发起请求,因此改变hash不会重新加载页面。
  • history路由的原理是window.history API,在HTML5中新增了pushState和replaceState方法,这两个方法是在浏览器的历史记录栈上做文章,提供了对历史记录做修改的功能,虽然更改了url但是不会向服务器发起请求。history模式虽然去掉了hash模式的#锚点,但是它怕刷新,因为刷新时是真实的请求。而hash模式下,修改的是#锚点之后的信息,浏览器不会将#锚点之后的数据发送到服务器,所以没有问题。

5.React组件间传值的方法有哪些?

回答思路 props、context、发布/订阅。

标准回答 React中组件之间的传值方法有很多,按照不同的组件间关系可以把组件传值的方法分为父子组件传值跨级组件传值非嵌套关系组件传值

  • 父子组件常用的传值方法是当父组件给子组件传值时通过props,子组件向父组件传值通过回调函数来传值。
  • 跨级组件常用的传值方法是props一层一层的传,或者使用context对象,将父组件设置为生产者而子组件都设置对应的contextType即可。
  • 非嵌套关系组件传值的方法是使用共同的父级组件进行props传值,或者通过context传值,推荐使用发布/订阅的自定义事件传值。

加分回答 React中组件间传值方法有很多,按照不同的组件间关系可以把组件间传值的方法分为:

  • 父子组件传值:父子组件传值是最常见的应用场景也是非常简单的一种通信方式,父组件通过向子组件传递props,子组件得到props之后做处理就行。而子组件向父组件传值需要通过回调函数触发,具体操作是父组件将一个函数作为props的属性传递给子组件,子组件通过this.props.xxx()调用父组件的函数,参数根据需要自己进行搭配即可。
  • -跨级组件传值:跨级的组件之间传值无非就是重复多个父子组件传值的过程。一般跨级的传值方式有两种,分别是:
    • 一层一层的传递props:写法很复杂,耦合程度也很高,如果两个组件之间隔了很多层,那么也会影响中间组件的性能,开销大。不过这种方法也是可以的,如果组件之间的层级不是非常多,可以考虑使用这个方法。
    • context对象:context相当于一个全局的变量,是一个大的容器,可以把需要传递的数据放在这个容器中,不论嵌套多深都可以轻易的使用。
      • 具体操作是创建一个context对象,需要输入默认值。在父组件中使用生产者标签将需要传值的所有子组件包裹起来。
      • 子组件通过指定contextType获取到这个context对象,直接调用this.context即可获得值。
  • 非嵌套关系组件传值:就是没有任何包含关系的组件之间的传值,包括了兄弟组件。对于肺嵌套关系组件传值一般用两种方法:
    • 通过相同的父组件传值:子组件通过回调函数的形式将数据传给父组件,父组件直接通过属性将数据传递给子组件。
    • context:利用共同的父组件context对象进行传值
    • 通过发布/订阅进行传递:也可以说是自定义事件。重点是在子组件的componentDid-Mount生命周期通过声明一个自定义事件,然后在componentWillUnmount生命周期组件销毁时移除自定义事件。

6 说一说redux?

什么Redux redux提供一个可预测状态管理容器,可以让我们构建一致化的应用并且易于测试。

**Redux解决的问题** 随着单页面应用开大瑜伽复杂,js需要

redux的特性

redux面试题

redux的基本使用

7.说一说React中hooks的优缺点是什么?

回答思路 可读性强、组件层级变得更浅、不再需要考虑class组件中this指向、部分生命周期不支持

标准回答

优点:

  • 代码的可读性强,在使用hooks之前比如发布/订阅自定义事件被挂载在componentDidM-ount生命周期中,然后需要在componentWillUnmount生命周期中将它清楚,这样就不便于开发者维护和迭代。在使用hooks之后,通过useEffect可以将componentDidMount生命周期、componentDidUpdate生命周期和componentWillUnmount生命周期聚合起来 ,方便代码的维护。
  • 状态逻辑复用,在使用hooks之前通常使用高阶组件HOC的方法来复用多个组件公共的状态,增强组建的功能,这样肯定是加大了组件渲染的开销,损失了性能。但是在hooks中可以通过自定义组件useXxx()的方法将多个组件之间的共享逻辑放在自定义hook中,就可以轻松的进行数据互通。
  • 不再需要考虑class组件中this指向的问题,hook在函数组件中不需要通过this.state或者this.fn来获取数据或者方法。

缺点:

  • hooks的useEffect只包括了componentDidMountcomponentDidUpdate还有compon-entWillUnmount这三个生命周期,对于getSnapshotBeforeUpdatecomponentDidC-atch等其他的生命周期没有支持。

加分回答 使用useEffect时候里面不能写太多依赖项,将各个不同的功能划分为多个useEffect模块,将各项功能拆开写,这是遵循了软件设计的“单一职责模式”。如果遇到状态不同步的情况,使用手动传递参数的形式。如果业务复杂,就使用Component代替hooks,hooks的出现并不是取代了class组件,而是在函数组件的基础上可以实现一部分的类似class组件功能


1. 自定义hook


const useWindowSize = () => {
    const [size, setSize] = useState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    })


    useEffect(() => {
        window.addEventListener('resize', onResize);
        return (() => {
            window.removeEventListener('resize', onResize);
        })
    }, [])
    const onResize = useCallback(() => {
        setSize({
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight,
        })
    }, [])
    return {
        size
    }
}

const CustomHook = () => {
    const {size} = useWindowSize();
    return (
        <div id="container">
            <h1>屏幕大小</h1>
            <p>width:{size.width} height:{size.height} </p>
        </div>
    )

}
export default CustomHook

8.说一说useRef()?

useRef返回一个对象,这个对象只有一个current属性,并且他的地址一保持不变,并且useRef不会使页面重新渲染。

作用

  • 1. 获取DOM元素节点。可以通过在dom节点上天界ref属性,并将属性值设置为我们定义好的useRef返回的对象,就可以通过useRef该节点的dom对象。

  • 2. 获取子组件的实例。因为函数组件没有实例,所以只能通过为类组件添加ref属性来获得子组件的实例,但是我们可以通过forwardRef将子组件的函数包起来,通过传递Ref对象的形式来获取字节点的DOM对象,对子组件的元素进行操作。

  • 3. 渲染周期之间共享数据存储。 我们组件函数每次执行,我们称之为一次渲染每一次渲染函数内部都拥有自己独立的props和state,当在jsx中调用代码中的state进行渲染时,每次渲染都会获得各自作用域中的props和state,比如说当我们点击一个按钮是的当前的state加1,这个是后我们出发一个函数两面中获取这个state,并且在这个两秒期间我们再让这个state+1,这个时候试图上的state是2,但是我们获取到state是1,但是如果我们将这个状态同步到refState中就不会出现这样一个问题,因为他的内存地址不不变的,相当于一个全局变量。

2. 模拟生命周期

  • useEffect(()=>{},[]) 空数组时只在元素挂载的时候执行一次,模拟ComponentDidMount ed
  • useEffect(()=>{},[a,b,c]) 数组中任意以属性改变,页面每渲染一次就会触发一次回调函数,模拟ComponentDidUpdated
  • useEffect(()=>{return() => {console.log('该组件要销毁了');}}) return后面表示该组件销毁时执行的回调函数,模拟 ComponentWillunmounted

Vue问题

1. 说一说vue2.0双向绑定原理?

回答思路: Object.defineProperty、getter、setter

  • Vue响应式: 组件数据发生变化,视图立即发生变化。

  • 响应式原理: Vue 来采用数据劫持结合发布者-订阅者模式的方式实现数据的响应式,通过Object.defineProperty来劫持数据的setter,getter,在数据变动时发布消息给订阅者,当获取属性值的时候触发getter方法,调用订阅者的add方法将依赖于该数据的DOM节点添加到订阅者的对列里面,当修改数据的时候,触发setter方法,调用订阅者的upate方法,恒信队列中的所有dom节点数据为当前最新值。

  • Object.defineProperty的缺点:

    • defineProperty是只能监听一个属性,不能对整个对象进行劫持。要想对对象中的所有属性进行监听,必须使用循环遍历的方式,如果数据量过大会导致栈溢出

    • defineProperty不能监听新增和删除属性,不能正确监听数组对象。


响应式原理: MVVM的响应式就是试图更新和数据更新绑定在一起。由于数据和试图的变化是一对多关系,所以VUE采用的发布订阅模型,所有vue需要对data中的每一个属性都添加一个Observer来观察数据,并将依赖该数据的视图DOM节点放到Observer内部的一个队列中,一旦数据更新,就会遍历队列中所有元素触发视图更新。

class Vue{
    constructor(opt){
        this.opt = opt
        this.observe(opt.data)
        let root = document.querySelector(opt.el)
        this.compile(root)
    }
    // 为响应式对象 data 里的每一个 key 绑定一个观察者对象
    observe(data){
        Object.keys(data).forEach(key => {
            // 对每一个数据都有一个观察者
            let obv = new Observer()
            data["_"+key] = data[key]
            // 通过 getter setter 暴露 for 循环中作用域下的 obv,闭包产生
            Object.defineProperty(data, key, {
                get(){
                //每次所有使用这个数据的DOM节点添加到观察这队列里面,准备试图更新
                    Observer.target && obv.addSubNode(Observer.target);
                    return data['_'+key]
                },
                //视图更新,将object队列面的数据进行更新
                set(newVal){
                    obv.update(newVal)
                    data['_'+key] = newVal
                }
            })
        })
    }
    // 初始化页面,遍历 DOM,收集每一个key变化时,随之调整的位置,以观察者方法存放起来    
    compile(node){
        [].forEach.call(node.childNodes, child =>{
            if(!child.firstElementChild && /{{(.*)}}/.test(child.innerHTML)){
                let key = RegExp.$1.trim()
                child.innerHTML = child.innerHTML.replace(new RegExp('\{\{\s*'+ key +'\s*\}\}', 'gm'),this.opt.data[key])
                // 这个代码就是为每一个每一个数据添加监听节点
                Observer.target = child
                this.opt.data[key]
                Observer.target = null
            }
            else if (child.firstElementChild)
                this.compile(child)
        })
    }
}
// 常规观察者类
class Observer{
    constructor(){
        this.subNode = []
    }
    addSubNode(node){
        this.subNode.push(node)
    }
    update(newVal){
        this.subNode.forEach(node=>{
            node.innerHTML = newVal
        })
    }
}

2. 说一说vue3.0双向绑定原理?

回答思路 proy数据拦截,劫持整个对象返回一个新对象。

  • Proxy数据拦截: Vue3.0 是通过Proxy实现的数据双向绑定,Proxy是ES6中新增的一个特性,proxy主要功能返回一个新的proxy对象对原始兑现进行拦截,通过返回的proxy对象我们可以外界的访问进行过滤和改写,比如说重写gettersetter方法,除此之外proxy提供了对对象的13种劫持方式,同时ES6另外一个Reflect对象也提供了对对象的13中操作方式,两因此两者可以配合使用。使用方式事new Proxy(obj,handle)传入两个参数,一个是原始对象,一个是包含操作方法的对象。
  • Proxy的优势Object.defineProperty的主要缺点有三个不能监听整个对象只能监听属性,监听整个对象需要循环递归遍历、不能监听增加和删除操作、不能有效监听数据下标的变化。proxy完美解决这个问题,proxy可以监听真个对象,并且提供十三种劫持方式,此外proxy也能监听数组,因为proxy将数据转化成了伪数组对象。但是两者的共同点是都不支持嵌套,都需要逐层遍历。

3. 说一说Diff算法

回答思路 patchpatchVnodeupdateChildren、vue优化时间复杂度为**O(n) **

标准回答 Diff算法比较过程

  • 第一步:patch函数中对新老节点进行比较 如果新节点不存在就销毁老节点 如果老节点不存在,直接创建新的节点 当两个节点是相同节点的时候,进入 patctVnode 的过程,比较两个节点的内部
  • 第二步:patchVnode函数比较两个虚拟节点内部 如果两个虚拟节点完全相同,返回 当前vnode 的children 不是textNode,再分成三种情况
    • 有新children,没有旧children,创建新的
    • 没有新children,有旧children,删除旧的
    • 新children、旧children都有,执行updateChildren比较children的差异,这里就是diff算法的核心 当前vnode 的children 是textNode,直接更新text
  • 第三步:updateChildren函数子节点进行比较
    • 第一步 头头比较。若相似,旧头新头指针后移(即 oldStartIdx++ && newStartIdx++),真实dom不变,进入下一次循环;不相似,进入第二步。
    • 第二步 尾尾比较。若相似,旧尾新尾指针前移(即 oldEndIdx-- && newEndIdx--),真实dom不变,进入下一次循环;不相似,进入第三步。
    • 第三步 头尾比较。若相似,旧头指针后移,新尾指针前移(即 oldStartIdx++ && newEndIdx--),未确认dom序列中的头移到尾,进入下一次循环;不相似,进入第四步。
    • 第四步 尾头比较。若相似,旧尾指针前移,新头指针后移(即 oldEndIdx-- && newStartIdx++),未确认dom序列中的尾移到头,进入下一次循环;不相似,进入第五步。
    • 第五步 都不相同。 若节点有key且在旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移(即 newStartIdx++);否则,vnode对应的dom(vnode[newStartIdx].elm)插入当前真实dom序列的头部,新头指针后移(即 newStartIdx++)。 但结束循环后,有两种情况需要考虑:
      • 新的字节点数组(newCh)被遍历完(newStartIdx > newEndIdx)。那就需要把多余的旧dom(oldStartIdx -> oldEndIdx)都删除,上述例子中就是c,d
      • 旧的字节点数组(oldCh)被遍历完(oldStartIdx > oldEndIdx)。那就需要把多余的新dom(newStartIdx -> newEndIdx)都添加。

4. 说一说 Vue 列表为什么加 key?

回答思路 性能优化、diff算法节点比对、保证节点的唯一性、key不能是index

标准回答

  • 更准确: 能够避免就地服用的情况,因为diff算法认为如果两个节点标签类型一样,并且key一样,那么就认为这两个节点相同,如果不设置key就会出现就低复用的情况。
  • 更快: 因为key的唯一性,我们可以通过建立map的方式提高查找效率,而不是通过最原始遍历方式。
  • key也不能是li元素的index,因为假设我们给数组前插入一个新元素,它的下标是0,那么和原来的第一个元素重复了,整个数组的key都发生了改变,这样就跟没有key的情况一样了