前端工程师自检清单及答案整理

4,854 阅读1小时+

前言

根据大神们整理的前端自检清单,自己整理一下答案,也方便自己学习。

image.png

一、JavaScript基础

变量和类型

1.JavaScript规定了几种数据类型

js目前共定义了8种数据类型,其中包括:Undefined,Null,Boolean,Number,String,Object, Symbol,BigInt

2.JavaScript对象的底层数据结构是什么

JavaScript基本类型数据都是直接按值存储在栈中的(Undefined、Null、不是new出来的布尔、数字和字符串),每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说 ,更加容易管理内存空间。

JavaScript引用类型数据被存储于堆中 (如对象、数组、函数等,它们是通过拷贝和new出来的)。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

3.Symbol类型在实际开发中的应用、可手动实现一个简单的Symbol

1.使用Symbol来替代常量

const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()

2.使用Symbol来作为对象属性名(key)

const PROP_NAME = Symbol()
const PROP_AGE = Symbol()
let obj = {
  [PROP_NAME]: "一斤代码"
}
obj[PROP_AGE] = 18

3.使用Symbol定义类的私有属性/方法

// a.js
const PASSWORD = Symbol()
class Login {
  constructor(username, password) {
    this.username = username
    this[PASSWORD] = password
  }

  checkPassword(pwd) {
      return this[PASSWORD] === pwd
  }
}
export default Login
// b.js
import Login from './a'
const login = new Login('admin', '123456')
login.checkPassword('123456')  // true
login.PASSWORD  // oh!no!
login[PASSWORD] // oh!no!
login["PASSWORD"] // oh!no!

由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的),因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。

4.注册和获取全局Symbol window中创建的Symbol实例总是唯一的,而我们需要的是在所有这些window环境下保持一个共享的Symbol。这种情况下,我们就需要使用另一个API来创建或获取Symbol,那就是Symbol.for(),它可以注册或获取一个window间全局的Symbol实例:

let gs1 = Symbol.for('global_symbol_1')  //注册一个全局Symbol
let gs2 = Symbol.for('global_symbol_1')  //获取全局Symbol
gs1 === gs2  // true

4.JavaScript中的变量在内存中的具体存储形式

基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

let a1 = 0; // 栈内存
let a2 = "this is string" // 栈内存
let a3 = null; // 栈内存
let b = { x: 10 }; // 变量b存在于栈中,{ x: 10 }作为对象存在于堆中
let c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3]作为对象存在于堆中

image.png

5.基本类型对应的内置对象,以及他们之间的装箱拆箱操作

内置对象: Object是 JavaScript 中所有对象的父对象 数据封装类对象:Object、Array、Boolean、Number 和 String 其他对象:Function、Math、Date、RegExp、Error。 特殊的基本包装类型(String、Number、Boolean) arguments: 只存在于函数内部的一个类数组对象 装箱: 把基本数据类型转化为对应的引用数据类型的操作,装箱分为隐式装箱和显示装箱 隐式装箱

let a = 'sun'
let b = a.indexof('s') // 0 // 返回下标

上面代码在后台实际的步骤为:

let a = new String('sun')
let b = a.indexof('s')
a = null

实现机制:

1.创建String类型的一个实例; 2.在实例上调用指定的方法; 3.销毁这个实例;

显示装箱 通过内置对象可以对Boolean、Object、String等可以对基本类型显示装箱 let a = new String('sun')

拆箱: 拆箱和装箱相反,就是把引用类型转化为基本类型的数据,通常通过引用类型的valueof()和toString()方法实现

let name = new String('sun')
let age = new Number(24)
console.log(typeof name) // object
console.log(typeof age) //  object
// 拆箱操作
console.log(typeof age.valueOf()); // number // 24  基本的数字类型
console.log(typeof name.valueOf()); // string  // 'sun' 基本的字符类型
console.log(typeof age.toString()); // string  // '24' 基本的字符类型
console.log(typeof name.toString()); // string  // 'sun' 基本的字符类型

6.理解值类型和引用类型

image.png

7.null和undefined的区别

null表示"没有对象",即该处不应该有值。典型用法是:

(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:

(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。

8.至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

1、typeof:(可以对基本类型做出准确的判断,但对于引用类型,用它就有点力不从心了)

typeof 返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function等6种数据类型。

2、instanceof

instanceof是用来判断A是否为B的实例时,表达式为:A instanceof B,如果 A是B的实例,则返回true; 否则返回false 在这里特别注意的是 instanceof检测的是原型

3 Object.prototype.toString

toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为[object,xxx],xxx是具体的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,… 基本上所有对象的类型都可以通过这个方法获取到。 4 constructor 查看对象对应的构造函数 construvtor在对应对象的原型下面,是自动生成的,当我们写一个构造函数的时候,程序自动添加,构造函数名.prototype.constructor = 构造函数名 image.png

9.可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

image.png

10.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法

0.1+0.2不等于0.3,是因为计算机进行计算时先转化成二进制,二浮点数用二进制表示时是无穷位的,IEEE754标准中用64位表示(1位用来表示符号位,11用来表示指数,52位表示尾数)会截断后面的位数,再转化成十进制,就有了误差。

最大数字:

对于整数,前端出现问题的几率可能比较低,毕竟很少有业务需要需要用到超大整数,只要运算结果不超过 Math.pow(2, 53) 就不会丢失精度。

对于小数,前端出现问题的几率还是很多的,尤其在一些电商网站涉及到金额等数据。解决方式:把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)

最安全的数字-Math.pow(2, 53)-1,到+Math.pow(2, 53)-1

原型和原型链

1.instanceof的底层实现原理,手动实现一个instanceof

function new_instance_of(leftVaule, rightVaule) { 
    let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
    leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
    while (true) {
    	if (leftVaule === null) {
            return false;	
        }
        if (leftVaule === rightProto) {
            return true;	
        } 
        leftVaule = leftVaule.__proto__ 
    }
}

2.实现继承的几种方式以及他们的优缺点

// 原型继承
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subProty =false;
}
SubType.prototype = new SuperType();
var instance = new SubType();
console.log(instance.getSuperValue())

// 借用构造函数
function SuperType(name) {
    this.name = name;
}
function SubType(){
    SuperType.call(this, 'demo');
    this.age = 18;
}
var instance = new SubType();
console.log(instance.name);
console.log(instance.age);

// 组合继承
function SuperType(name){
    this.name = name;
    this.colors = ['red'];
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
}
function SubType(name,age) {
    SuperType.call(this,name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    console.log(this.age);
}
var instance = new SubType('demo',18);
instance.sayAge();
instance.sayName();

// 原型式继承
function object(o) {
    function F(){};
    F.prototype = o;
    return new F();
}
var person = {
    name: 'tom'
}
var anotherPerson = object(person)
console.log(anotherPerson.name)

// 寄生式继承
function createAnother(original){
    var clone =Object.create(original);
    clone.sayHi = function () {
        console.log('hi');
    }
    return clone;
}
var person = {
    name: 'tom'
}
var anotherPerson = createAnother(person);
console.log(anotherPerson.name)
anotherPerson.sayHi();

// 寄生组合式继承
function SuperType(name) {
    this.name = name;
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
}
function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
function inheritPrototype(subType,superType){
    var prototype = Object.create(superType.prototype);
    prototype.constructor =subType;
    subType.prototype = prototype;
}
inheritPrototype(SubType,SuperType);
var person = new SubType('zhangsan',18);
person.sayName()

3.可以描述new一个对象的详细过程,手动实现一个new操作符

function Person (name,age){
    this.name = name;
    this.age = age;
    this.say = function () {
        console.log("I am " + this.name)
    }
}
function realizeNew(){
    let obj = {};
    let Con = [].shift.call(arguments);
    obj.__proto__ = Con.prototype;
    let result = Con.apply(obj,arguments);
    return typeof result === 'object'? result : obj
}
var person1 =realizeNew(Person,'张三')

4.理解es6 class构造以及继承的底层实现原理

作用域和闭包

1.理解词法作用域和动态作用域

词法作用域,函数的作用域在函数定义的时候就决定了(取决于函数定义的位置)
动态作用域,函数的作用域在函数调用的时候就决定了(取决于函数的调用) js采用的是词法作用域

2.理解JavaScript的作用域和作用域链

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。 ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了‘块级作用域’,可通过新增命令let和const来体现。

function outFun2() {
    var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

作用域链 在 JavaScript 中使用变量时,JavaScript 引擎将尝试在当前作用域中查找变量的值。如果找不到变量,它将查找外部作用域并继续这样做,直到找到变量或到达全局作用域为止。

如果仍然找不到变量,它将在全局作用域内隐式声明变量(如果不是在严格模式下)或返回错误。 ####3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题 执行上下文是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。

执行上下文的类型 执行上下文总共有三种类型
全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。

执行栈 执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。 首次运行JS代码时,会创建一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push到当前执行栈的栈顶。 根据执行栈LIFO规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文。

执行上下文的创建 执行上下文分两个阶段创建:
1)创建阶段;
2)执行阶段
创建阶段
1、确定 this 的值,也被称为 This Binding。
2、LexicalEnvironment(词法环境) 组件被创建。
3、VariableEnvironment(变量环境) 组件被创建。
This Binding:
在全局执行上下文中,this 的值指向全局对象,在浏览器中,this 的值指向 window 对象。 在函数执行上下文中,this 的值取决于函数的调用方式。如果它被一个对象引用调用,那么 this 的值被设置为该对象,否则 this 的值被设置为全局对象或 undefined(严格模式下)
词法环境
在词法环境中,有两个组成部分:
(1)环境记录(environment record)
(2)对外部环境的引用 环境记录是存储变量和函数声明的实际位置。 对外部环境的引用意味着它可以访问其外部词法环境。
变量环境:
它也是一个词法环境,其 EnvironmentRecord 包含了由 VariableStatements 在此执行上下文创建的绑定。 如上所述,变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。 在 ES6 中,LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )绑定。 在创建阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下) 这就是为什么你可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。

这就是我们所谓的变量提升。

4.this的原理以及几种不同使用场景的取值

一、this原理

this 既不指向函数自身,也不指函数的词法作用域,而是调用函数时的对象!

二、使用场景

一)普通函数的调用,this指向的是Window

var name = '卡卡';
function cat(){
    var name = '有鱼';
    console.log(this.name);//卡卡
    console.log(this);//Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
}
cat();

(二)对象的方法,this指的是该对象

1、一层作用域链时,this指的该对象

var name = '卡卡';
var cat = {
    name:'有鱼',
    eat:function(){
        console.log(this.name);//有鱼
    }
}
cat.eat();

2、多层作用域链时,this指的是距离方法最近的一层对象

var name = '卡卡';
var cat = {
    name:'有鱼',
    eat1:{
        name:'年年',
        eat2:function(){
            console.log(this.name);//年年
        }
    }
}
cat.eat1.eat2();

这里需要注意一个情况,如果cat.eat1.eat2这个结果赋值给一个变量eat3,则eat3()的值是多少呢?

var eat3 = cat.eat1.eat2;
eat3(); // 卡卡

答案是[卡卡],这个是因为经过赋值操作时,并未发起函数调用,eat3()这个才是真正的调用,而发起这个调用的是根对象window,所以this指的就是window,this.name=卡卡

(三)构造函数的调用,this指的是实例化的新对象

var name = '卡卡';
function Cat(){
    this.name = '有鱼';
    this.type = '英短蓝猫';
}
var cat1 = new Cat();
console.log(cat1);// 实例化新对象 Cat {name: "有鱼", type: "英短蓝猫"}
console.log(cat1.name);// 有鱼

(四)apply和call调用时,this指向参数中的对象

var name = '有鱼';
function eat(){
    console.log(this.name);
}
var cat = {
    name:'年年',
}
var dog = {
    name:'高飞',
}

eat.call(cat);// 年年
eat.call(dog);// 高飞

(五)匿名函数调用,指向的是全局对象

var name = '卡卡';
var cat = {
    name:'有鱼',
    eat:(function(){
        console.log(this.name);//卡卡
    })()
}
cat.eat;

(六)定时器中调用,指向的是全局变量

var name = '卡卡';
var cat = setInterval(function(){
    var name = '有鱼';
    console.log(this.name);// 卡卡
    clearInterval(cat);
},500);

总结:
①普通函数的调用,this指向的是window
②对象方法的调用,this指的是该对象,且是最近的对象
③构造函数的调用,this指的是实例化的新对象
④apply和call调用,this指向参数中的对象
⑤匿名函数的调用,this指向的是全局对象window
⑥定时器中的调用,this指向的是全局变量window

5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用

包就是能够读取其他函数内部变量的函数 它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

function f1() {
    var n = 999;
    nAdd = function () {
        n += 1
    }

    function f2() {
        alert(n);
    }
    return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。 为什么会这样呢?
原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。 这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

6.理解堆栈溢出和内存泄漏的原理,如何防止

1、内存泄露:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内部内存溢出 2、堆栈溢出:是指内存空间已经被申请完,没有足够的内存提供了

常见的手段是将一个变量置为null,该变量就会被下一轮垃圾回收机制回收。 常见的内存泄露的原因:

  • 全局变量引起的内存泄露
  • 闭包
  • 没有被清除的计时器 解决方法:
  • 减少不必要的全局变量
  • 严格使用闭包(因为闭包会导致内存泄露)
  • 避免死循环的发生

执行机制

1.JavaScript如何实现异步编程,可以详细描述EventLoop机制

image.png 任务队列实际上有两个,一个是宏任务队列,一个是微任务队列,当主线程执行完毕,如果微任务队列中有微任务,则会先进入执行栈,当微任务队列没有任务时,才会执行宏任务的队列。

2.宏任务和微任务分别有哪些

微任务包括: 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver, MessageChannel;只有promise调用then的时候,then里面的函数才会被推入微任务中 宏任务包括:setTimeout, setInterval, setImmediate, I/O;

3.使用Promise实现串行

function execute(tasks){
    return tasks.reduce((previousPromise, currentPromise)=>previousPromise.then(resultList=>{
        return new Promise(resolve=>{
            currentPromise().then(result=>{
                resolve(resultList.concat(result))
            }).catch(()=>{
                resolve(resultList.concat(null))
            })
        })
    },Promise.resolve([])))
}
const execute = (tasks = []) => {
    const resultList = [];
    for(task of tasks){
        try{
            resultList.push(await tasks())
        }catch(err){
            resultList.push(null);
        }
    }
    return resultList;
}

4.Node与浏览器EventLoop的差异

Node 10以前: 执行完一个阶段的所有任务 执行完nextTick队列里面的内容 然后执行完微任务队列的内容 Node 11以后: 和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。

语法和API

1.理解ECMAScript和JavaScript的关系

ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现

2.setInterval需要注意的点,使用settimeout实现setInterval

function  mySetInterval(fn,mil){
    function interval(){
        setTimeout(interval,mil);
        fn();
    }
    setTimeout(interval,mil)
}
mySetInterval(function(){console.log(1)},1000)

3.什么是防抖和节流?有什么区别?如何实现?

防抖
防抖就是n时间内函数只会执行一次,如果n时间内多次触发则会重新计算时间

 function debounce(fn){
          let timeout =null;
          return function(){
              clearTimeout(timeout);
              timeout =setTimeout(function(){
                  fn.apply(this,arguments)
              },500)
          }
      }
      function say(){
          console.log('防抖')
      }
      var myInput = document.getElementById('hello');
      myInput.addEventListener('input',debounce(say))

节流
n时间内只会执行一次

 function throttle(fn){
          let canFlag = true;
          return function(){
              if(!canFlag) return;
              canFlag = false;
              setTimeout(()=>{
                  fn.apply(this,arguments);
                  canFlag = true
              },1000)
          }
      }
      function say(){
          console.log('节流',new Date())
      }
      var myInput = document.getElementById('hello');
      myInput.addEventListener('input',throttle(say))

4.介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  • WeakSet 的成员只能是对象,而不能是其他类型的值
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中 WeakMap结构与Map结构类似,也是用于生成键值对的集合
  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
  • eakMap的键名所指向的对象,不计入垃圾回收机制

二、HTML和CSS

HTML

1.从规范的角度理解HTML,从分类和语义的角度使用标签

2.常用页面标签的默认样式、自带属性、不同浏览器的差异、处理浏览器兼容问题的方式

3.元信息类标签(head、title、meta)的使用目的和配置方法

4.HTML5离线缓存原理

5.可以使用Canvas API、SVG等绘制高性能的动画

CSS

1.CSS盒模型,在不同浏览器的差异

1. W3C 标准盒模型: 属性width,height只包含内容content,不包含border和padding。 2. IE 盒模型: 属性width,height包含border和padding,指的是content+padding+border。 css的盒模型由content(内容)、padding(内边距)、border(边框)、margin(外边距)组成。但盒子的大小由content+padding+border这几部分决定,把margin算进去的那是盒子占据的位置,而不是盒子的大小! 我们在编写页面代码时应尽量使用标准的W3C模型(需在页面中声明DOCTYPE类型),这样可以避免多个浏览器对同一页面的不兼容。 因为若不声明DOCTYPE类型,IE浏览器会将盒子模型解释为IE盒子模型,FireFox等会将其解释为W3C盒子模型;若在页面中声明了DOCTYPE类型,所有的浏览器都会把盒模型解释为W3C盒模型。

2.CSS所有选择器及其优先级、使用场景,哪些可以继承,如何运用at规则

3.CSS伪类和伪元素有哪些,它们的区别和实际应用

image.png image.png

4.HTML文档流的排版规则,CSS几种定位的规则、定位参照物、对文档流的影响,如何选择最好的定位方式,雪碧图实现原理

5.水平垂直居中的方案、可以实现6种以上并对比它们的优缺点

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .parent1{
            width: 200px;
            height: 200px;
            background: red;
            position: relative;
        }
        .parent2{
            display: table-cell;
            vertical-align: middle;
            text-align: center;
        }
        .parent5{
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .child1{
            width: 100px;
            height: 100px;
            position: absolute;
            left: 50%;
            top:50%;
            margin-left: -50px;
            margin-top: -50px;
            line-height: 100px;
            text-align: center;
        }
        .child2{
            position: absolute;
            left: 50%;
            top:50%;
            transform: translate(-50%,-50%);
        }
        .child3{
            position: absolute;
            left: 0;
            top: 0;
            right: 0;
            bottom: 0;
            margin: auto;
            width: 100px;
            height: 100px;
            line-height: 100px;
            text-align: center;
        }
        .child4{
            display: inline-block;
            width: 100px;
            height: 50px;
            overflow: scroll;
        }
        .child6{
            width: 100px;
            height: 100px;
            display: inline-block;
            text-align: center;
            vertical-align: middle;
            line-height: 100px;
        }
        .parent6{
            text-align: center;
        }
        .parent6::after{
            content: '';
            height: 100%;
            vertical-align: middle;
            display: inline-block;
        }
    </style>
</head>
<body>
    <p>方案一:知道宽度的情况下 absolute+margin负值</p>
    <div class="parent1">
        <div class="child1">child1</div>
    </div>
    <p>方案二:不知道宽度的情况下 absolute+transform</p>
    <div class="parent1">
        <div class="child2">child2</div>
    </div>
    <p>方案三:不知道宽度的情况下 absolute+margin:auto</p>
    <div class="parent1">
        <div class="child3">child3</div>
    </div>
    <p>方案四:多行文本垂直居中 table-cell vertical-align:middle</p>
    <div class="parent1 parent2">
        <div class="child4">多行文本垂直居中 table-cell vertical-align:middle多行文本垂直居中 table-cell vertical-align:middle多行文本垂直居中 table-cell vertical-align:middle</div>
    </div>
    <p>方案五:display:flex</p>
    <div class="parent1 parent5">
        <div class="child5">flex</div>
    </div>
    <p>方案六:伪元素</p>
    <div class="parent1 parent6">
        <div class="child6">伪元素</div>
    </div>
</body>
</html>

6.BFC实现原理,可以解决的问题,如何创建BFC

juejin.cn/post/684490…

7.可使用CSS函数复用代码,实现特殊效果

8.PostCSS、Sass、Less的异同,以及使用配置,至少掌握一种

9.CSS模块化方案、如何配置按需加载、如何防止CSS阻塞渲染

10.熟练使用CSS实现常见动画,如渐变、移动、旋转、缩放等等

11.CSS浏览器兼容性写法,了解不同API在不同浏览器下的兼容性情况

12.掌握一套完整的响应式布局方案

三、计算机基础

编译原理

1.理解代码到底是什么,计算机如何将代码转换为可以运行的目标程序

js是一门解释型语言(英语:Interpreted language),是一种编程语言。这种类型的编程语言,会将代码一句一句直接运行,不需要像编译语言(Compiled language)一样,经过编译器先行编译为机器码,之后再运行。这种编程语言需要利用解释器,在运行期,动态将代码逐句解释(interpret)为机器码,或是已经预先编译为机器码的的子程序,之后再运行。

2.正则表达式的匹配原理和性能优化

3.如何将JavaScript代码解析成抽象语法树(AST)

4.base64的编码原理

5.几种进制的相互转换计算方法,在JavaScript中如何表示和转换

网络协议

1.理解什么是协议,了解TCP/IP网络协议族的构成,每层协议在应用程序中发挥的作用

2.有哪些协议是可靠,TCP有哪些手段保证可靠交付

3.DNS的作用、DNS解析的详细过程,DNS优化原理

举例来说,www.example.com真正的域名是www.example.com.root,简写为www.example.com.。因为,根域名.root对于所有域名都是一样的,所以平时是省略的。

根域名的下一级,叫做"顶级域名"(top-level domain,缩写为TLD),比如.com、.net;再下一级叫做"次级域名"(second-level domain,缩写为SLD),比如www.example.com里面的.example,这一级域名是用户可以注册的;再下一级是主机名(host),比如www.example.com里面的www,又称为"三级域名",这是用户在自己的域里面为服务器分配的名称,是用户可以任意分配的。

总结一下,域名的层级结构如下。

主机名.次级域名.顶级域名.根域名

需要明确的是,每一级域名都有自己的NS记录,NS记录指向该级域名的域名服务器。这些服务器知道下一级域名的各种记录。

所谓"分级查询",就是从根域名开始,依次查询每一级域名的NS记录,直到查到最终的IP地址,过程大致如下。

1.从"根域名服务器"查到"顶级域名服务器"NS记录和A记录(IP地址)
2.从"顶级域名服务器"查到"次级域名服务器"NS记录和A记录(IP地址)
3.从"次级域名服务器"查出"主机名"IP地址

1.网络客户端就是我们平常使用的电脑,打开浏览器,输入一个域名。比如输入www.163.com,这时,你使用的电脑会发出一个DNS请求到本地DNS服务器。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。

2.查询www.163.com的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果。如果没有,本地DNS服务器还要向DNS根服务器进行查询。

3.根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。

4.本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。

5.最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

4.CDN的作用和原理

1.首先我们在地址栏键入一个网址,浏览器发现本地没有关于这个网址的 DNS 缓存,所以向网站的 DNS 服务器发起请求。

2.网站的 DNS 服务器设置了 CNAME,指向了某个 CDN 服务器,也就是我们常见的阿里云、腾讯云、Cloudflare 之类的,去请求 CDN 中的智能 DNS 均衡负载系统。

3.均衡负载系统解析域名,把对用户响应最快的节点返回给用户,然后用户向该节点发出请求。

4.如果是第一次访问该内容,CDN 服务器会向源站请求数据并缓存,否则的话,直接在缓存节点中找到该数据,将请求结果发给用户。 没有CNAME的情况:

有CNAME的情况:

5.HTTP请求报文和响应报文的具体组成,能理解常见请求头的含义,有几种请求方式,区别是什么

6.HTTP所有状态码的具体含义,看到异常状态码能快速定位问题

7.HTTP1.1、HTTP2.0带来的改变

juejin.cn/post/684490…

8.HTTPS的加密原理,如何开启HTTPS,如何劫持HTTPS请求

juejin.cn/post/684490…

9.理解WebSocket协议的底层原理、与HTTP的区别

juejin.cn/post/684490… ###设计模式

1.熟练使用前端常用的设计模式编写代码,如单例模式、装饰器模式、代理模式等

2.发布订阅模式和观察者模式的异同以及实际应用

3.可以说出几种设计模式在开发中的实际应用,理解框架源码中对设计模式的应用

四、数据结构和算法

JavaScript编码能力

1.多种方式实现数组去重、扁平化、对比优缺点

2.多种方式实现深拷贝、对比优缺点

3.手写函数柯里化工具函数、并理解其应用场景和优势

4.实现一个sleep函数

手动实现前端轮子

1.手动实现call、apply、bind

Function.prototype.myCall = function (context = window,...args) {
    context.fn = this;
    console.log(args)
    let result = context.fn(...args);
    delete context.fn;
    return result;
}
Function.prototype.myApply = function (context = window, arr) {
    context.fn = this;
    let result = !arr ? context.fn() : context.fn(...arr);
    delete context.fn;
    return result;
}
Function.prototype.myBind = function (context = window , ...arg) {
    context.fn = this;
    let bound = function () {
        let args = [...args].concat(...arguments);
        context.fn = this instanceof context.fn ? this : context;
        let result = context.fn(...args);
        delete context.fn;
        return result;
    }
    bound.prototype = new this();
    return bound;
}

2.手动实现符合Promise/A+规范的Promise、手动实现async await

function handlePromise(promise2, x, resolve, reject){
    if (promise2 === x) { //promise2是否等于x,也就是判断是否将自己本身返回
        return reject(new TypeError('circular reference')); //如果是抛出错误
    }
    if(x !==null && (typeof x==='object' || typeof x ==='function')) {
        let called; //called控制resolve或reject 只执行一次,多次调用没有任何作用。
        try{
            let then = x.then;
            if(typeof then === 'function') {
                then.call(x,y=>{
                    if(called) return;
                    called = true;
                    handlePromise(promise2,y,resolve,reject)
                },r => {
                    if(called) return;
                    called = true;
                    reject(r);
                })
            } else {
                reject(x);
            }
        }catch(err) {
            if (called) return;
            called = true;
            reject(err);
        }
    } else {
        reject(x);
    }
}
class Promise {
    constructor(executor){
        this.status = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];
        let resolve =  (value) => {
            if(this.status === 'pending') {
                this.value = value;
                this.status = 'resolved';
                this.onResolvedCallbacks.foEach(fn => fn(this.value));
            }
        }
        let reject = (reason) => {
            if(this.status === 'pending') {
                this.reason = reason;
                this.status = 'rejected';
                this.onRejectedCallbacks.foEach(fn => fn(this.reason));
            }
        }
        try{
            executor(resolve,reject)
        }catch(err){
            reject(err);
        }
        
    }
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : y => y;
        onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}
        let promise2; // 返回的新的promise
        if (this.status === 'pending') {
            this.onResolvedCallbacks.push()
            promise2 = new Promise((resolve,reject)=>{
                this.onResolvedCallbacks.push(()=>{
                    setTimeout(()=>{
                        try{
                            let x = onFulfilled(this.value);
                            handlePromise(promise2,x,resolve,reject)
                        }catch(err){
                            reject(err);
                        }
                    },0)
                    
                })
                this.onRejectedCallbacks.push(()=>{
                    setTimeout(()=>{
                        try{
                            let x = onRejected(this.reason);
                            handlePromise(promise2,x,resolve,reject)
                        }catch(err){
                            reject(err);
                        }
                    },0)
                    
                })
            })
        }
        if (this.status === 'resolved') {
            promise2 = new Promise((resolve,reject) =>{
                setTimeout(()=>{
                    try{
                        let x =onFulfilled(this.value);
                        handlePromise(promise2,x,resolve,reject)
                    }catch(err){
                        reject(err);
                    }
                },0)
                
            })
        }
        if(this.status === 'rejected') {
            onRejected(this.reason);
            promise2 = new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    try{
                        let x = onRejected(this.reason);
                        handlePromise(promise2,x,resolve,reject)
                    }catch(err){
                        reject(err);
                    }
                },0)
                
            })
        }
        return promise2;
    }
    catch(onRejected){ //在此处添加原型上的方法catch
        return this.then(null,onRejected);
    }
}
Promise.all = function (promiseArrs) { //在Promise类上添加一个all方法,接受一个传进来的promise数组
    return new Promise((resolve, reject) => { //返回一个新的Promise
        let arr = []; //定义一个空数组存放结果
        let i = 0;
        function handleData(index, data) { //处理数据函数
            arr[index] = data;
            i++;
            if (i === promiseArrs.length) { //当i等于传递的数组的长度时 
                resolve(arr); //执行resolve,并将结果放入
            }
        }
        for (let i = 0; i < promiseArrs.length; i++) { //循环遍历数组
            promiseArrs[i].then((data) => {
                handleData(i, data); //将结果和索引传入handleData函数
            }, reject)
        }
    })
}
Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(resolve, reject);
        }
    })
}
Promise.resolve = function (val) {
    return new Promise((resolve, reject) => resolve(val));
}
Promise.reject = function (val) {
    return new Promise((resolve, reject) => reject(val));
}
module.exports = Promise;

3.手写一个EventEmitter实现事件发布、订阅

class EventEmitter {
    constructor(){
        this.events = {}
    }
    on(eventName,callback){
        if(this.events[eventName]){
            this.events[eventName].push(callback)
        } else{
            this.events[eventName] = [callback]
        }
    }
    emit(eventName,...rest){
        if(this.events[eventName]){
            this.events[eventName].forEach(fn=>fn.apply(this,rest))
        }
    }
}
const event =new EventEmitter();
const handle = (...pyload) => console.log(pyload)
event.on('click',handle)
event.emit('click',1,2,3)

4.可以说出两种实现双向绑定的方案、可以手动实现

5.手写JSON.stringify、JSON.parse

window.JSON = {
    parse: function (str) {
        return eval('(' + str + ')')   //防止{}会返回undefined
    },
    stringify: function (str) {
        if (typeof str === 'number') {
            return Number(str);
        }
        if (typeof str === 'string') {
            return str
        };
        var s = '';
        console.log(Object.prototype.toString.call(str))
        switch (Object.prototype.toString.call(str)) {
            case '[object Array]':
                s += '[';
                for (var i = 0; i < str.length - 1; i++) {
                    if (typeof str === 'string') {
                        s += '"' + str[i] + '",'
                    } else {
                        s += str[i] + ','
                    }
                }
                if (typeof str[str.length - 1] == 'string') {
                    s += '"' + str[i] + '"'
                } else {
                    if (str[str.length - 1] == null) {
                        str[str.length - 1] = null;
                        s += 'null';
                    } else {
                        s += (str[str.length - 1] ? str[str.length - 1] : '')
                    }
                }
                s += "]";
                break;
            case '[object Date]':
                console.log(str.toJSON())
                s+='"'+(str.toJSON?str.toJSON():str.toString())+'"';
                break;
            case '[object Function]':
                s= 'undefined';
                break
            case '[object Object]':
                s+='{'
                for(var key in str) {
                    if(str[key] === undefined){
                        continue;
                    }
                    if(typeof str[key] === 'symbol' || typeof str[key] === 'function') {
                        continue;
                    }
                    if(typeof Object.prototype.toString.call(str[key]) === '[object RegExp]') {
                        continue;
                    }
                    s+=('"'+key+'":"'+str[key]+'",')
                } 
                s = s.slice(0,s.length-1);
                if(s===''){s+='{'}
                s+='}'
                break   

        }
        return s;
    }
}

6.手写一个模版引擎,并能解释其中原理

7 .手写懒加载、下拉刷新、上拉加载、预加载等效果

数据结构

1.理解常见数据结构的特点,以及他们在不同场景下使用的优缺点

2.理解数组、字符串的存储原理,并熟练应用他们解决问题

3.理解二叉树、栈、队列、哈希表的基本结构和特点,并可以应用它解决问题

4.了解图、堆的基本结构和使用场景

算法

1.可计算一个算法的时间复杂度和空间复杂度,可估计业务逻辑代码的耗时和内存消耗

2.至少理解五种排序算法的实现原理、应用场景、优缺点,可快速说出时间、空间复杂度

3.了解递归和循环的优缺点、应用场景、并可在开发中熟练应用

4.可应用回溯算法、贪心算法、分治算法、动态规划等解决复杂问题

5.前端处理海量数据的算法方案

五、运行环境

浏览器API

1.浏览器提供的符合W3C标准的DOM操作API、浏览器差异、兼容性

2.浏览器提供的浏览器对象模型 (BOM)提供的所有全局API、浏览器差异、兼容性

3.大量DOM操作、海量数据的性能优化(合并操作、Diff、requestAnimationFrame等)

4.浏览器海量数据存储、操作性能优化

5.DOM事件流的具体实现机制、不同浏览器的差异、事件代理

6.前端发起网络请求的几种方式及其底层实现、可以手写原生ajax、fetch、可以熟练使用第三方库

7.浏览器的同源策略,如何避免同源策略,几种方式的异同点以及如何选型

8.浏览器提供的几种存储机制、优缺点、开发中正确的选择

9.浏览器跨标签通信

浏览器原理

1.各浏览器使用的JavaScript引擎以及它们的异同点、如何在代码中进行区分

2.请求数据到请求结束与服务器进行了几次交互

TCP三次握手:
1、客户端发送syn包到服务器,等待服务器确认接收。
2、服务器确认接收syn包并确认客户的syn,并发送回来一个syn+ack的包给客户端。
3、客户端确认接收服务器的syn+ack包,并向服务器发送确认包ack,二者相互建立联系后,完成tcp三次握手。
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

【问题3】为什么不能用两次握手进行连接?

答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

   现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

3.可详细描述浏览器从输入URL到页面展现的详细过程

4.浏览器解析HTML代码的原理,以及构建DOM树的流程

5.浏览器如何解析CSS规则,并将其应用到DOM树上

6.浏览器如何将解析好的带有样式的DOM树进行绘制

7.浏览器的运行机制,如何配置资源异步同步加载

8.浏览器回流与重绘的底层原理,引发原因,如何有效避免

当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘CSS

  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class。
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolute或fixed的元素上。
  • 避免使用CSS表达式(例如:calc())。

JavaScript

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
  • 9.浏览器的垃圾回收机制,如何避免内存泄漏 垃圾收集机制的原理 垃圾收集器会按照固定的时间间隔,周期性的找出不再继续使用的变量,然后释放其占用的内存。

什么叫不再继续使用的变量?

不再使用的变量也就是生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用(闭包),那么该变量会被标记回收。

全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收。

工作原理:

当变量进入环境时(例如在函数中声明一个变量),将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。

工作流程:

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 去掉环境中的变量以及被环境中的变量引用的变量的标记。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

到2008年为止,IE、Chorme、Fireofx、Safari、Opera 都使用标记清除式的垃圾收集策略,只不过垃圾收集的时间间隔互有不同

避免内存泄漏的方法

  • 少用全局变量,避免意外产生全局变量
  • 使用闭包要及时注意,有Dom元素的引用要及时清理。
  • 计时器里的回调没用的时候要记得销毁。
  • 为了避免疏忽导致的遗忘,我们可以使用 WeakSet 和 WeakMap结构,它们对于值的引用都是不计入垃圾回收机制的,表示这是弱引用。 举个例子:
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"

复制代码这种情况下,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

####10.浏览器采用的缓存方案,如何选择和控制合适的缓存方案 缓存过程分析 浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图: image.png 由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

    以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。为了方便大家理解,这里我们根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强制缓存和协商缓存 。

强制缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:

  • 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如下图: image.png

  • 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存(暂不分析),如下图 image.png

  • 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果,如下图 image.png 当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高。 Expires Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。 Cache-Control 在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)

  • private:所有内容只有客户端可以缓存,Cache-Control的默认取值

  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定

  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

  • max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效 到了HTTP/1.1,Expire已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义,

由于Cache-Control的优先级比expires,那么直接根据Cache-Control的值进行缓存,意思就是说在600秒内再次发起该请求,则会直接使用缓存结果,强制缓存生效。

注:在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control相比于expires是更好的选择,所以同时存在时,只有Cache-Control生效。

浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?

  • 内存缓存(from memory cache):内存缓存具有两个特点,分别是快速读取和时效性: 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。 时效性:一旦该进程关闭,则该进程的内存则会清空。

  • 硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。

在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  • 协商缓存生效,返回304,如下 image.png
  • 协商缓存失效,返回200和请求结果结果,如下 image.png 同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。 Last-Modified / If-Modified-Since
  • Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间,如下。 image.png
  • If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件,如下。 image.png
  • Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下。 image.png
  • If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200,如下。 image.png ####总结 强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存,主要过程如下: image.png 参考文档:www.cnblogs.com/chenhuichao…

Node

1.理解Node在应用程序中的作用,可以使用Node搭建前端运行环境、使用Node操作文件、操作数据库等等

2.掌握一种Node开发框架,如Express,Express和Koa的区别

3.熟练使用Node提供的API如Path、Http、Child Process等并理解其实现原理

4.Node的底层运行原理、和浏览器的异同

5.Node事件驱动、非阻塞机制的实现原理

六、框架和类库

TypeScript

1.理解泛型、接口等面向对象的相关概念,TypeScript对面向对象理念的实现

2.理解使用TypeScript的好处,掌握TypeScript基础语法

3.TypeScript的规则检测原理

4.可以在React、Vue等框架中使用TypeScript进行开发

React

1.React和vue选型和优缺点、核心架构的区别

2.React中setState的执行机制,如何有效的管理状态

3.React的事件底层实现机制

4.React的虚拟DOM和Diff算法的内部实现

5.React的Fiber工作原理,解决了什么问题

6.React Router和Vue Router的底层实现原理、动态加载实现原理

7.可熟练应用React API、生命周期等,可应用HOC、render props、Hooks等高阶用法解决问题

8.基于React的特性和原理,可以手动实现一个简单的React

Vue

1.写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

不带有key,并且使用简单的模板,基于这个前提下,可以更有效的复用节点,diff速度来看也是不带key更加快速的,因为带key在增删节点上有耗时。这就是vue文档所说的默认模式。但是这个并不是key作用,而是没有key的情况下可以对节点就地复用,提高性能。这种模式会带来一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。VUE文档也说明了。还有就是key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度

Vue 官方文档:
key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
在做动态改变的时候,尽量不要使用 index 作为循环的 key,如果你用 index 作为 key,那么在删除第二项的时候,index 就会从 123 变为 12(而不是 13),那么仍有可能引起更新错误。

没有 Key 值的问题
如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。 针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

为什么循环需要添加唯一 Key值
给元素加了 Key 值之后,React/Vue 在做 Diff 的时候会进行差异化对比,即通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。 那么,如此高效的 diff 到底是如何运作的呢? 简单来说有以下几步:
1.对新集合的节点进行遍历,通过唯一 key 可以判断新老集合中是否存在相同节点。
2.如果存在相同节点,则进行移动操作,但在移动前,需要将当前节点在老集合中的位置与 lastIndex 进行比较,如果不同,则进行节点移动,否则不执行该操作。

这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。
这里给出一整图作为示例。

如上图所示,以新树为循环基准:

B 在老集合的下标为 BIndex=1,此时 lastIndex=0,这时,lastIndex < BIndex,不进行任何处理,并且取值 lastIndex=Math.max(BIndex, lastIndex)
A 在老集合的下标为 AIndex=0,此时lastIndex=1,这时,lastIndex > AIndex,这时,需要把老树中的 A 移动到下标为lastIndex的位置,并且取值 lastIndex=Math.max(AIndex, lastIndex)
D 在老集合的下标为 DIndex=3,此时lastIndex=1,这时,lastIndex < DIndex,不进行任何处理,并且取值 lastIndex=Math.max(DIndex, lastIndex)
C 在老集合的下标为 CIndex=2,此时lastIndex=3,这时,lastIndex > CIndex,需要把老树中的 C 移动到下标为lastIndex的位置,并且取值 lastIndex=Math.max(CIndex, lastIndex)
由于 C 已经是最后一个节点,因此 Diff 至此结束。
没有 key 值的更新问题

删除第二个,页面却仍然保留了第二个输入框内容 其流程如下:

既然 1 没有变,那么就就地复用之前的 1
既然 2 变成了 3,里面的子孙元素就地复用。有人不理解为什么子孙元素就地复用,那么是因为子孙元素的 data/state 属性不受 2 变成 3 的影响
既然 3 没了,那么连其子孙元素全部删除

2.虚拟DOM优点以及diff算法

虚拟 DOM 的好处

虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地一个 JS 对象中,最终将这个 JS 对象一次性 attch 到 DOM 树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上,操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,交由浏览器去绘制。 Diff操作
在实际代码中,会对新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象中。 下面我们创建一棵新树,用于和之前的树进行比较,来看看Diff算法是怎么操作的。 平层Diff,只有以下4种情况:

1、节点类型变了,例如下图中的P变成了H3。我们将这个过程称之为REPLACE。直接将旧节点卸载并装载新节点。旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做效率不高。但为了避免O(n^3)的时间复杂度,这样是值得的。这也提醒了开发者,应该避免无谓的节点类型的变化,例如运行时将div变成p没有意义。

2、节点类型一样,仅仅属性或属性值变了。我们将这个过程称之为PROPS。此时不会触发节点卸载和装载,而是节点更新。 3、文本变了,文本对也是一个Text Node,也比较简单,直接修改文字内容就行了,我们将这个过程称之为TEXT。
4、移动/增加/删除 子节点,我们将这个过程称之为REORDER。看一个例子,在A、B、C、D、E五个节点的B和C中的BC两个节点中间加入一个F节点。

我们简单粗暴的做法是遍历每一个新虚拟DOM的节点,与旧虚拟DOM对比相应节点对比,在旧DOM中是否存在,不同就卸载原来的按上新的。这样会对F后边每一个节点进行操作。卸载C,装载F,卸载D,装载C,卸载E,装载D,装载E。效率太低

如果我们在JSX里为数组或枚举型元素增加上key后,它能够根据key,直接找到具体位置进行操作,效率比较高。常见的最小编辑距离问题,可以用Levenshtein Distance算法来实现,时间复杂度是O(M*N),但通常我们只要一些简单的移动就能满足需要,降低精确性,将时间复杂度降低到O(max(M,N))即可。

多端开发

1.单页面应用(SPA)的原理和优缺点,掌握一种快速开发SPA的方案

2.理解Viewport、em、rem的原理和用法,分辨率、px、ppi、dpi、dp的区别和实际应用

3.移动端页面适配解决方案、不同机型适配方案

4.掌握一种JavaScript移动客户端开发技术,如React Native:可以搭建React Native开发环境,熟练进行开发,可理解React Native的运作原理,不同端适配

5.掌握一种JavaScript PC客户端开发技术,如Electron:可搭建Electron开发环境,熟练进行开发,可理解Electron的运作原理

6.掌握一种小程序开发框架或原生小程序开发

7.理解多端框架的内部实现原理,至少了解一个多端框架的使用

数据流管理

1.掌握React和Vue传统的跨组件通信方案,对比采用数据流管理框架的异同

2.熟练使用Redux管理数据流,并理解其实现原理,中间件实现原理

3.熟练使用Mobx管理数据流,并理解其实现原理,相比Redux有什么优势

4.熟练使用Vuex管理数据流,并理解其实现原理

5.以上数据流方案的异同和优缺点,不情况下的技术选型

七、前端工程

项目构建

1.理解npm、yarn依赖包管理的原理,两者的区别

yarn是经过重新设计的崭新的npm客户端,它能让开发人员并行处理所有必须的操作,并添加了一些其他改进。 运行速度得到了显著的提升,整个安装时间也变得更少 像npm一样,yarn使用本地缓存。与npm不同的是,yarn无需互联网连接就能安装本地缓存的依赖项,它提供了离线模式。这个功能在2012年的npm项目中就被提出来过,但一直没有实现。 允许合并项目中使用到的所有的包的许可证

2.可以使用npm运行自定义脚本

3.理解Babel、ESLint、webpack等工具在项目中承担的作用

4.ESLint规则检测原理,常用的ESLint配置

5.Babel的核心原理,可以自己编写一个Babel插件

image.png

和编译器类似,babel 的转译过程也分为三个阶段,这三步具体是:

1.解析 Parse 将代码解析生成抽象语法树( 即AST ),也就是计算机理解我们代码的方式(扩展:一般来说每个 js 引擎都有自己的 AST,比如熟知的 v8,chrome 浏览器会把 js 源码转换为抽象语法树,再进一步转换为字节码或机器代码),而 babel 则是通过 babylon 实现的 。简单来说就是一个对于 JS 代码的一个编译过程,进行了词法分析与语法分析的过程。

2.转换 Transform 对于 AST 进行变换一系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进行遍历,在此过程中进行添加、更新及移除等操作。

3.生成 Generate 将变换后的 AST 再转换为 JS 代码, 使用到的模块是 babel-generator。 而 babel-core 模块则是将三者结合使得对外提供的API做了一个简化。

6.可以配置一种前端代码兼容方案,如Polyfill

7.Webpack的编译原理、构建流程、热更新原理,chunk、bundle和module的区别和应用

8.可熟练配置已有的loaders和plugins解决问题,可以自己编写loaders和plugins

nginx

1.正向代理与反向代理的特点和实例

2.可手动搭建一个简单的nginx服务器、

3.熟练应用常用的nginx内置变量,掌握常用的匹配规则写法

4.可以用nginx实现请求过滤、配置gzip、负载均衡等,并能解释其内部原理

八、项目和业务

性能优化

1.了解前端性能衡量指标、性能监控要点,掌握一种前端性能监控方案

2.了解常见的Web、App性能优化方案

3.SEO排名规则、SEO优化方案、前后端分离的SEO

4.SSR实现方案、优缺点、及其性能优化

5.Webpack的性能优化方案

6.Canvas性能优化方案

7.React、Vue等框架使用性能优化方案

前端安全

1.XSS攻击的原理、分类、具体案例,前端如何防御

跨站脚本攻击是指通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击。 XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。

1.非持久型 XSS(反射型 XSS )

非持久型 XSS 漏洞攻击有以下几点特征:

  • 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据。
  • 攻击者需要诱骗点击,必须要通过用户点击链接才能发起
  • 反馈率低,所以较难发现和响应修复
  • 盗取用户敏感保密信息

为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情:

  • Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
  • 尽量不要从 URL,document.referrer,document.forms 等这种 DOM API 中获取数据直接渲染。
  • 尽量不要使用 eval, new Function(),document.write(),document.writeln(),window.setInterval(),window.setTimeout(),innerHTML,document.createElement() 等可执行字符串的方法。 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。 前端渲染的时候对任何的字段都需要做 escape 转义编码

2.持久型 XSS(存储型 XSS) 持久型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行

攻击成功需要同时满足以下几个条件:

  • POST 请求提交表单后端没做转义直接入库。
  • 后端从数据库中取出数据没做转义直接输出给前端。
  • 前端拿到后端数据没做转义直接渲染成 DOM。

持久型 XSS 有以下几个特点:

  • 持久性,植入在数据库中
  • 盗取用户敏感私密信息
  • 危害面广

如何防御 1) CSP CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP:

  • 设置 HTTP Header 中的 Content-Security-Policy
  • 设置 meta 标签的方式

2) 转义字符 用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

3) HttpOnly Cookie 这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时,将其属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie信息。

2.CSRF攻击的原理、具体案例,前端如何防御

1.CSRF攻击的原理 完成 CSRF 攻击必须要有三个条件:

  • 用户已经登录了站点 A,并在本地记录了 cookie
  • 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。
  • 站点 A 没有做任何 CSRF 防御

2.如何防御 防范 CSRF 攻击可以遵循以下几种规则:

  • Get 请求不对数据进行修改
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者 Token 1) SameSite 可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。

2) Referer Check HTTP Referer是header的一部分,当浏览器向web服务器发送请求时,一般会带上Referer信息告诉服务器是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律,如在提交表单的referer必定是在该页面发起的请求。所以通过检查http包头referer的值是不是这个页面,来判断是不是CSRF攻击。

但在某些情况下如从https跳转到http,浏览器处于安全考虑,不会发送referer,服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞,那么攻击者可以在其他网站注入恶意脚本,受害者进入了此类同域的网址,也会遭受攻击。出于以上原因,无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。

3) Anti CSRF Token 目前比较完善的解决方案是加入Anti-CSRF-Token。即发送请求时在HTTP 请求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值,会进行校验该请求当中的token和cookie当中的token值是否都存在且相等,才认为这是合法的请求。否则认为这次请求是违法的,拒绝该次服务。

这种方法相比Referer检查要安全很多,token可以在用户登陆后产生并放于session或cookie中,然后在每次请求时服务器把token从session或cookie中拿出,与本次请求中的token 进行比对。由于token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时,当某个页面消耗掉token后,其他页面的表单保存的还是被消耗掉的那个token,其他页面的表单提交时会出现token错误。

3.HTTP劫持、页面劫持的原理、防御措施

点击劫持的原理 用户在登陆 A 网站的系统后,被攻击者诱惑打开第三方网站,而第三方网站通过 iframe 引入了 A 网站的页面内容,用户在第三方网站中点击某个按钮(被装饰的按钮),实际上是点击了 A 网站的按钮。

如何防御 1)X-FRAME-OPTIONS X-FRAME-OPTIONS是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

该响应头有三个值可选,分别是

  • DENY,表示页面不允许通过 iframe 的方式展示
  • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
  • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

2)JavaScript 防御 对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。

if(top.location != self.location){
    top.location = self.location;
}

九、资源推荐

语言基础

计算机基础

数据结构和算法

运行环境

框架和类库

前端工程

项目和业务

学习提升

另外推荐我一直在关注的几位大佬的个人博客:

技术之外