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

前言

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

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以后:
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。

5.如何在保证页面运行流畅的情况下处理海量数据

语法和API

1.理解ECMAScript和JavaScript的关系

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

2.熟练运用es5、es6提供的语法规范,

3.熟练掌握JavaScript提供的全局对象(例如Date、Math)、全局函数(例如decodeURI、isNaN)、全局属性(例如Infinity、undefined)

4.熟练应用map、reduce、filter 等高阶函数解决问题

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

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

6.JavaScript提供的正则表达式API、可以使用正则表达式(邮箱校验、URL解析、去重等)解决常见问题

7.JavaScript异常处理的方式,统一的异常处理方案

二、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

https://juejin.im/post/5ecdd538518825433c13a044

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.三次握手和四次挥手详细原理,为什么要使用这种机制

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

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

5.CDN的作用和原理

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

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

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

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

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

设计模式

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

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

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

四、数据结构和算法

JavaScript编码能力

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

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

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

4.手写防抖和节流工具函数、并理解其内部原理和应用场景

5.实现一个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.请求数据到请求结束与服务器进行了几次交互

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

参考文档:https://www.cnblogs.com/chenhuichao/p/13299408.html

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.熟练使用Vue的API、生命周期、钩子函数

2.MVVM框架设计理念

3.Vue双向绑定实现原理、Diff算法的内部实现

4.Vue的事件机制

5.从template转换成真实DOM的实现机制

多端开发

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;
}

九、资源推荐

语言基础

计算机基础

数据结构和算法

运行环境

框架和类库

前端工程

项目和业务

学习提升

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

技术之外

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335