《一名【合格】前端工程师的自检清单》【自检ing】

424 阅读52分钟

前言

感谢原作者提供这样一份优秀的自检清单,写这篇的文章的主要目的是按照清单上的知识检测自己还有哪些不足和提升,同时建立自己的知识体系

原文章地址: 一名【合格】前端工程师的自检清单

1、JavaScript基础

前端工程师吃饭的家伙,深度、广度一样都不能差。

1.1变量和类型

1.1.1、JavaScript规定了几种语言类型

JavaScript中语言类型通过堆栈储存方式的不同分为简单类型(也称为原始类型)和复杂类型(也称为引用类型)

简单类型:stringnumberbooleannullundefinedsymbol(ES6新增,表示独一无二的值)

复杂类型:object

1.1.2、JavaScript对象的底层数据结构是什么

JavaScript使用的是 堆(Heap)和栈(Stack)

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

实际项目上没用用过,对此了解甚少,为了避免误导,这里我推荐阮大大的阮一峰-ES6标准入门-Symobl

1.1.4、JavaScript中的变量在内存中的具体存储形式

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

简单类型存储结构如下图:

image.png

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

复杂类型存储结构如下图: image.png

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

装箱:

把基本数据类型转化为对应的引用数据类型,装箱分为隐式装箱显示装箱

隐式装箱":

let a = 'sun'
let b = a.indexof('s') // 0 // 返回下标
        
// 上面代码在后台实际的步骤为:
let a = new String('sun')
let b = a.indexof('s')
    a = null

在上面的代码中,a是基本类型,它不是对象,不应该具有方法,js内部进行了一些列处理(装箱), 使得它能够调用方法。在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。实现机制:

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

显示装箱 通过内置对象可以对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' 基本的字符类型

1.1.6、理解值类型和引用类型

基本类型值是指简单的数据段,如 Undefined、Null、 Boolean、 Number 和 String. 这 5 种基本数据类型是按值访问的。

引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。

1.1.7、nullundefined的区别

undefined 只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值,undefined是一个假值

let message; // 这个变量被声明了,只是值为 undefined
// age 没有声明
if (message) {
    // 这个块不会执行
}
if (!message) {
    // 这个块会执行
}

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回"object"的原因 undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下面的例子所示:

console.log(null == undefined); // true
console.log(null === undefined); // false

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

1、typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

  • 对于基本类型,除 null 以外,均可以返回正确的结果。
  • 对于引用类型,除 function 以外,一律返回 object 类型。
  • 对于 null ,返回 object 类型。
  • 对于 function 返回 function 类型。

2、instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。在这里需要特别注意的是:instanceof 检测的是原型。所以在obj instanceof Object中,无论参数obj是数组还是函数都会返回true针对数组的这个问题,ES5 提供了 Array.isArray() 方法

3、toString() 是 Object 的原型方法,调用该方法,可以统一返回格式为 “[object Xxx]” 的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。我们来看一下代码。

Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // 同上结果,加上call也ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

4、 constructorprototype对象上的属性,指向构造函数。根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的,同样的这个也只能输出构造函数

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

数值转换

有三个函数可以将非数值转换为数值:Number()、parseInt()、parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。

隐式类型转换

  1. 涉及类型转换最多的两个运算符是+和==。+可以是字符串相加,也可以是数字相加,在操作符中存在字符串时,优先转换为字符串。
  2. −∗/- * /−∗/ 只针对Number类型,所以转换的结果只能是Number类型。

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

  • 精度丢失原因,说是JavaScript使用了IEEE 754规范,二进制储存十进制的小数时不能完整的表示小数
  • 能够表示的最大数字Number.MAX_VALUE等于1.7976931348623157e+308,最大安全数字Number.MAX_SAFE_INTEGER等于9007199254740991
  • 避免精度丢失
    • 计算小数时,先乘100或1000,变成整数再运算
    • 如果值超出了安全整数,有一个最新提案,BigInt大整数,它可以表示任意大小的整数,注意只能表示整数,而不受安全整数的限制

1.2、原型和原型链

1.2.1、理解原型设计模式以及JavaScript中的原型规则

原型设计模式

1、工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。

function createPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

let person1 = createPerson("Nicholas", 29, "Software Engineer");

let person2 = createPerson("Greg", 27, "Doctor");

这里,函数createPerson()接收3个参数,根据这几个参数构建了一个包含Person信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。

2、构造函数模式像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

function Person(name, age, job){ 
    this.name = name;
    this.age = age
    this.job = job;
    this.sayName = function() { 
        console.log(this.name);
    };
} 

let person1 = new Person("Nicholas"29"Software Engineer")
let person2 = new Person("Greg"27, "Doctor");

person1.sayName(); // Nicholas 
person2.sayName(); // Greg
复制代码

在这个例子中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象。
  • 属性和方法直接赋值给了 this。
  • 没有 return。

3、原型模式:每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型,如下所示:

function Person() {}

Person.prototype.name = "Nicholas"
Person.prototype.age = 29
Person.prototype.job = "Software Engineer"
Person.prototype.sayName = function() {
    console.log(this.name);
};

let person1 = new Person()
person1.sayName(); // "Nicholas"
let person2 = new Person()
person2.sayName(); // "Nicholas"

console.log(person1.sayName == person2.sayName); // true

这里,所有属性和 sayName()方法都直接添加到了 Person 的  prototype 属性上,构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。

1、原型规则

  • 所有的引用类型(数组、对象、函数),都具有对象特征,即可自由扩展属性;
  • 所有的引用类型,都有一个_proto_ 属性(隐式原型),属性值是一个普通对象;
  • 所有函数,都具有一个prototype(显示原型),属性值也是一个普通原型;
  • 所有的引用类型(数组、对象、函数),其隐式原型指向其构造函数的显式原型;(obj.proto === Object.prototype);
  • 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的_proto_(即它的构造函数的prototype)中去寻找;

2、原型对象:prototype 在js中,函数对象其中一个属性:原型对象prototype。普通对象没有prototype属性,但有_proto_属性。原型的作用就是给这个类的每一个对象都添加一个统一的方法,在原型中定义的方法和属性都是被所以实例对象所共享。

var person = function(name){
    this.name = name
};
person.prototype.getName=function(){//通过person.prototype设置函数对象属性
    return this.name; 
}
var crazy= new person(‘crazyLee’);
crazy.getName(); //crazyLee//crazy继承上属性
复制代码

3、 原型链  当试图得到一个对象f的某个属性时,如果这个对象本身没有这个属性,那么会去它的_proto_(即它的构造函数的prototype)obj._proto_中去寻找;当obj._proto也没有时,便会在obj._proto.proto(即obj的构造函数的prototype的构造函数的prototype)中寻找;

1.2.2、instanceof的底层实现原理,手动实现一个instanceof

instanceof 主要作用就是判断一个实例是否属于某种类型

let person = function(){
  
}
let no = new person()
no instanceof person//true

instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

手写instanceof

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

}


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

// 原型继承
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();
// 又增加了一个新问题:通过注释我们可以看到 Parent3 执行了两次,第一次是改变Child3 的 prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,那么 Parent3 多构造一次就多进行了一次性能开销,这是我们不愿看到的。 
// 这里还有一个比较细节的问题是第二次调用的Parent3,出现了属性在不同层级重复,Parent3的age也会在实例第一层对象上面,拥有这个“多余的”属性也按照原型链的规则,没什么问题。但在某些情况下会造成错误,例如删除实例上的age属性后,实际上还能访问到,此时获取到的是原型上的属性。



// 原型式继承
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()

1.2.4、至少说出一种开源项目(如Node)中应用原型继承的案例

Vue.extend( options )')

  • 参数
    • {Object} options
  • 用法

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

data  选项是特例,需要注意 - 在  Vue.extend()  中它必须是函数

<div id="mount-point"></div>

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function() {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

结果如下:

<p>Walter White aka Heisenberg</p>

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

定义:new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

详细过程:

  • 创建一个空的简单JavaScript对象(即{});
  • 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  • 将步骤1新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回this。

手动实现



/** 
    * 模仿new关键词实现 
    * @param {Function} constructor 构造函数 
    * @param {...any} argument 任意参数 
*/ 
const _new = (constructor,...argument) => { 
    const obj = {} //创建一个空的简单对象 
    
    obj.__proto__ = constructor.prototype //设置原型 
    
    const res = constructor.apply(obj, argument) //新创建的对象作为this的上下文传递给构造函数 
    
    return (typeof res === 'object') ? res : obj  //如果该函数没有返回对象,则返回this(这个this指constructor执行时内部的this,即res))。 
}

function Person(name,sex){ 
    this.name = name this.sex = sex 
}

const people = new Person('Ben','man'); 

const peopleOther = _new(Person,'Alice','woman'); 

console.info('people',people);// people Person { name: 'Ben', sex: 'man' } 

console.info('peopleOther',peopleOther);// peopleOther Person { name: 'Alice', sex: 'woman' } 

console.info('people.__proto__',people.__proto__);//people.__proto__ Person {}

console.info('peopleOther.__proto__',peopleOther.__proto__);//peopleOther.__proto__ Person {}


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

  1. Class 可以通过extends关键字实现继承,继承父类的所有属性和方法不管是公有私有还是静态
  2. 子类必须在constructor方法中调用super方法,否则新建实例时会报错
  3. ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this)
  4. ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
  5. 在子类的构造函数中,只有调用super之后,才可以使用this关键字
  6. Object.getPrototypeOf(),用来从子类上获取父类
  7. super 关键字既可以当作函数使用,也可以当作对象使用
  8. super作为函数调用,代表父类的构造函数、super内部的this指的是子类的实例
  9. super作为对象时:

1、在普通方法中,指向父类的原型对象(可以获取公有属性): 2、在静态方法之中,指向父类(1)在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例

1.3、 作用域和闭包

1.3.1、理解词法作用域和动态作用域

词法作用域,函数的作用域在函数定义的时候就决定了(取决于函数定义的位置)

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

1.3.2、理解JavaScript的作用域和作用域链

1、作用域:任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。

  • 执行上下文分全局上下文、函数上下文和块级上下文。
  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
  • 变量的执行上下文用于确定什么时候释放内存。

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

1.3.3、理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题

栈,是一种数据构,具有先进后出的原则。JS中的执行栈就具有这样的结构,当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入执行栈,每遇到一个函数调用,就会往栈中压入一个新的上下文。引擎执行栈顶的函数,执行完毕,弹出当前执行上下文。

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

另一个特殊的对象是this,它在标准函数和箭头函数中有不同的行为。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。来看下面的例子:

window.color = 'red'
let o = {
    color: 'blue'
};

function sayColor() { 
    console.log(this.color);
}

sayColor(); // 'red'


o.sayColor = sayColor
o.sayColor(); // 'blue'

在箭头函数中,this 引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor()的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:

window.color = 'red'
let o = {
    color: 'blue'
};

let sayColor = () => console.log(this.color)
sayColor(); // 'red'

o.sayColor = sayColor; o.sayColor(); // 'red'

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

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

avaScript 高级程序设计 第4版:闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

由此可以知道:

JavaScript 闭包的本质源自两点,词法作用域和函数当作值传递。

词法作用域,就是,按照代码书写时的样子,内部函数可以访问函数外面的变量。引擎通过数据结构和算法表示一个函数,使得在代码解释执行时按照词法作用域的规则,可以访问外围的变量,这些变量就登记在相应的数据结构中。

函数当作值传递,即所谓的first class对象。就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。

闭包的作用

  1. 闭包的第一个作用是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  2. 闭包的另一个作用是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

闭包的实际应用

// 例二(手写函数防抖与节流)

// 防抖:指在事件触发n秒后再执行[回调]。如果在n秒内再次被触发,则重新计算时间。合并执行
 function debounce(fn,delay) {
     let timer = null //创建一个标记用来存放定时器的返回值
     return function () {
         let _this = this 
         let args = arguments
         if(timer) {
           clearTimeout(timer) // 在delay内,清除上一个setTimeout
           timer = null
         }
         timer = setTimeout(function(){ // 创建一个新的setTimeout
             fn.apply(_this,args) // 绑定函数
         },delay)
     }
 }
 
// 节流:指定时间间隔内只会执行一次任务。

function throttle(fn,delay) {
    let isRun = false // 定义一个状态
    let timer 
    return function(){
        if(!isRun) {
            clearTimeout(timer) // 间隔时间内清除setTimeout
            return
        }
        isRun = false
        
        timer = setTimeout(function(){ // 创建一个新的setTimeout
             fn.apply(_this,args) // 绑定函数
             isRun = true
         },delay)
    }
}


// 单例模式
//它保证了一个类只有一个实例。
//单例模式的好处就是避免了重复实例化带来的内存开销:
function Singleton(){
  this.data = 'singleton';
}

Singleton.getInstance = (function () {
  var instance;
    
  return function(){
    if (instance) {
      return instance;
    } else {
      instance = new Singleton();
      return instance;
    }
  }
})();

var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'

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

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

2、堆栈溢出:是指内存空间已经被申请完,没有足够的内存提供了

常见的手段是将一个变量置为null,该变量就会被下一轮垃圾回收机制回收。 常见的内存泄露的原因:1、全局变量引起的内存泄露,2、闭包,3、没有被清除的计时器

解决方法:

  • 减少不必要的全局变量
  • 严格使用闭包(因为闭包会导致内存泄露)
  • 避免死循环的发生

1.3.7、如何处理循环的异步操作

1.3.8、理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理

1.CommonJS
Javascript模块化编程(一):模块的写法
2.AMD
Javascript模块化编程(二):AMD规范
Javascript模块化编程(三):require.js的用法
3.CMD
4.ES6 import

1.4、 执行机制

1.4.1、 为何try里面放returnfinally还会执行,理解其内部机制

无论是否出现异常,又或者前面的 try/catch 里面有 return,finally 里面的语句始终会执行

try {
  return "hello";
} finally {
  console.log("finally");
}
/*输出
finally
*/

若 try/catch/finally 里面提前出现了 return ,则该代码块里后面的部分都不会执行

const f = () => {
  try {
    return "hello";
    console.log("try");
  } finally {
    return "hello";
    console.log("finally");
  }
}
f();
//无输出

若把 return 写入到了函数的 finally 里面,则最终函数(整个try/catch/finally)的返回值(或者抛出的异常)将是 finally 里面返回的值,即使前面 try/catch 出现了 retrun

const func = () => {
  try {
    try {
      throw new Error("ERROR!");
    } catch(err) {
      console.log(err.message);
      throw new Error("error from inner")
    } finally {
      return "finally";
    } 
  } catch(err) {
   console.log(err.message); // 未捕获到异常,此处不输出
  }
};
func();
/* output
ERROR!
*/
复制代码

若把上面的 return "finally" 注释掉,则将会输出 error from inner。这告诫我们 不要轻易在 finally 里面写 return ,否则会覆盖前面返回的函数值甚至是抛出的错误

执行:

  • try 语句定义所执行的进行错误测试的代码。如果 try 里面没有抛出异常,catch 将被跳过。
  • catch 语句定义当 try 语句发生错误时,捕获该错误并对错误进行处理。只有当 try 抛出了错误,才会执行。
  • finally 语句无论前面是否有异常都会执行。|

当使用的时候,try 语句是必须的catch(err) 里面的参数是必须的; catchfinally 都是可选的。 也就是以下三种形式

  • try...catch
  • try...finally
  • try...catch..finally

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

异步编程:异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必 要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。

  • 异步返回值(callback)
  • 嵌套异步回调
  • 期约(Promise)
  • 异步函数(async/await)

EventLoop机制

事件图(网络下载)

image.png

  1. 一开始整个脚本 script 作为一个宏任务执行
  2. 执行过程中,同步代码 直接执行,在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,宏任务 进入宏任务队列,微任务 进入微任务队列。
  3. 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完毕(当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的)。
  4. 执行浏览器 UI 线程的渲染工作。
  5. 检查是否有 Web Worker 任务,有则执行。
  6. 执行完本轮的宏任务,回到步骤 2,依次循环,直到宏任务和微任务队列为空。

本来准备放上大量面试题,但我想了想,想要完全了解,这里我强烈推荐这篇文章!!!

Tasks, microtasks, queues and schedules(宏任务、微任务、队列)

1.4.3、 宏任务和微任务分别有哪些

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

宏任务包括:setTimeout, setInterval, setImmediate, I/O;

1.4.4、 可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法

1.4.5、 使用Promise实现串行

一个封装的延迟函数,然后一个装有3,4,5的数组,需求就是在开始执行时依次等待3, 4, 5秒,并在之后打印对应输出

function delay(time) {
  return new Promise((resolve, reject) => {
    console.log(`wait ${time}s`)
    setTimeout(() => {
      console.log('execute');
      resolve()
    }, time * 1000)
  })
}



方式1. reduce

arr.reduce((s, v) => {
  return s.then(() => delay(v))
}, Promise.resolve())

方式2. async + 循环 + await

(
  async function () {
    for (const v of arr) {
      await delay(v)
    }
  }
)()

1.4.6、 Node与浏览器EventLoop的差异

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

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

方案一:懒加载+前端分页 方案二:使用js缓冲器来分片处理 方案三:web worker来将需要在前端进行大量计算的逻辑移入进去

1.5、 语法和API

1.5.1、 理解ECMAScriptJavaScript的关系

完整的 JavaScript 实现应该由三部分组成:

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器模型(BOM)

EMACScript 只是定义了基础的语法和语义的标准,跟具体的浏览器环境没有关系。 也就是说 EMACScript 来源于 JavaScript,又反向作为 JavaScript 的标准

1.5.2、 熟练运用es5es6提供的语法规范,

1.5.3、 熟练掌握JavaScript提供的全局对象(例如DateMath)、全局函数(例如decodeURIisNaN)、全局属性(例如Infinityundefined

1.5.4、 熟练应用mapreducefilter 等高阶函数解决问题

  • filter 过滤,filter()使用指定的函数测试所有元素,并创建一个包含所有通过测试的元素的新数组。
let arr=[2,4,6,8];
let arr1=arr.filter(function(item){
    return item>5
})
console.log(arr1) //[6,8]
  • map 映射,map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理的后值。
let arr = ['bob', 'grex', 'tom'];
let arr1 = arr.map(function(item) {
    return `<li>${item}</li>`;
});
console.log(arr1); //[ '<li>bob</li>', '<li>grex</li>', '<li>tom</li>' ]

reduce用法和原理

  • reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
var arr=[2,4,6,8];
let result=arr.reduce(function (val,item,index,origin) {
    return val+item
},0);
console.log(result) //20

1.5.5、 setInterval需要注意的点,使用settimeout实现setInterval

使用setTimeOut实现setInterval

serInterval 有两个问题:

  • 可能多个定时器会连续执行(会导致后续的间隔误差)
  • 某些间隔会被跳过(这么设计也可能是为了尽量避免第一个问题)
let timer = null;
function interval(fn, wait) {
//内部定义一个递归函数
  let interfun = function () {
      //每次执行将fn放在全局作用域中执行
    fn.call(null);
    //递归调用自己
    timer = setTimeout(interfun,wait);
  }
  //调用递归函数
  timer = setTimeout(interfun,wait);
}
//使用方式
interval(()=>{console.log('aa')},1000)
//清除定时器
window.clearTimeout(timer);

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

1.5.7、 JavaScript异常处理的方式,统一的异常处理方案、

1、浏览器抛出异常
2、throw主动抛出异常

2、HTML和CSS

2.1、 HTML

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

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

2.1.3、 元信息类标签(headtitlemeta)的使用目的和配置方法

head标签:head标签规定了自身必须是html标签中的第一个标签,它的内容必须包含一个title,并且最多只能包含一个base。

title标签:表示文档的标题,从字面上就非常容易理解。但有一点要特别注意,语义类标签中也有一组表示标题的标签:h1-h6。

meta标签:是一组键值对,它是一种通用的元信息表示标签。

从HTML5开始,为了简化写法,meta标签新增了charset属性。添加了charset属性的meta标签无需再有name和content。

<meta charset="UTF-8" >

具有http-equiv属性的meta

具有http-equiv属性的meta标签,表示执行一个命令,这样的meta标签可以不需要name属性了。

除了content-type,还有以下几种命令:

  1. content-language 指定内容的语言;
  2. default-style 指定默认样式表;
  3. refresh 刷新;
  4. set-cookie 模拟http头set-cookie,设置cookie;
  5. x-ua-compatible 模拟http头x-ua-compatible,声明ua兼容性;
  6. content-security-policy 模拟http头content-security-policy,声明内容安全策略。

2.1.4、 HTML5离线缓存原理

写法:

<html manifest="haorooms.appcache">

扩展名".appcache"为后缀,不需要我们再进行服务器端的配置了。就可以纯前端的进行离线缓存的操作。

2.1.5、 可以使用Canvas APISVG等绘制高性能的动画

2.2、 CSS

2.2.1、 CSS盒模型,在不同浏览器的差异

盒子模型有两种,分别是IE盒子模型和标准w3c盒子模型

标准w3c盒子:模型的范围包括 margin、border、padding、content,并且content部分不包括其它部分。

IE盒子:模型的范围也包括 margin、border、padding、content,和标准w3c盒子模型不同的是:IE盒子模型的content部分包括了border和padding

在CSS的盒子模型中,有两个重要的选项,box-sizing:content-box和box-sizing:border-box

content-box被称为正常盒子模型,border-box被称为怪异盒子模型 盒子元素模型,会随着padding和border元素的加入,而增加实际占用空间,

border-box定义的盒子,不会随着padding和boder的加入而增大盒子的占用空间

border-box限定了盒子模型的总面积

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

选择器

  • id选择器(#myid)
  • 类选择器(.myclass)
  • 属性选择器(a[rel="external"])
  • 伪类选择器(a:hover, li:nth-child)
  • 标签选择器(div, h1,p)
  • 相邻选择器(h1 + p)
  • 子选择器(ul > li)
  • 后代选择器(li a)
  • 通配符选择器(*)

优先级:

  • !important
  • 内联样式(1000)
  • ID选择器(0100)
  • 类选择器/属性选择器/伪类选择器(0010)
  • 元素选择器/伪元素选择器(0001)
  • 关系选择器/通配符选择器(0000)

带!important 标记的样式属性优先级最高; 样式表的来源相同时:!important > 行内样式>ID选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性

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

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

2.2.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>

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

BFC的概念

BFCBlock Formatting Context 的缩写,即块级格式化上下文。BFC是CSS布局的一个概念,是一个独立的渲染区域,规定了内部box如何布局, 并且这个区域的子元素不会影响到外面的元素,其中比较重要的布局规则有内部 box 垂直放置,计算 BFC 的高度的时候,浮动元素也参与计算。

BFC的原理布局规则

  • 内部的Box会在垂直方向,一个接一个地放置
  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠
  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反
  • BFC的区域不会与float box重叠
  • BFC是一个独立容器,容器里面的子元素不会影响到外面的元素
  • 计算BFC的高度时,浮动元素也参与计算高度
  • 元素的类型和display属性,决定了这个Box的类型。不同类型的Box会参与不同的Formatting Context

如何创建BFC?

  • 根元素,即HTML元素
  • float的值不为none
  • position为absolute或fixed
  • display的值为inline-block、table-cell、table-caption
  • overflow的值不为visible

BFC的使用场景

  • 去除边距重叠现象
  • 清除浮动(让父元素的高度包含子浮动元素)
  • 避免某元素被浮动元素覆盖
  • 避免多列布局由于宽度计算四舍五入而自动换行

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

2.2.8、 PostCSSSassLess的异同,以及使用配置,至少掌握一种

Sass的安装需要Ruby环境,是在服务端处理的,而Less是需要引入less.js来处理Less代码输出css到浏览器,也可以在开发环节使用Less,然后编译成css文件,直接放到项目中,也有 Less.app、SimpleLess、CodeKit.app这样的工具,也有在线编译地址。Stylus需要安装node,然后安装最新的stylus包即可使用

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

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

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

2.2.12、 掌握一套完整的响应式布局方案

2.3、 手写

2.3.1、 手写图片瀑布流效果

2.3.2、 用CSS绘制几何图形(圆形、三角形、扇形、菱形等)

2.3.3、 使用纯CSS实现曲线运动(贝塞尔曲线)

2.3.4、 实现常用布局(三栏、圣杯、双飞翼、吸顶),可是说出多种方式并理解其优缺点

3、计算机基础

关于编译原理,不需要理解非常深入,但是最基本的原理和概念一定要懂,这对于学习一门编程语言非常重要

3.1、 编译原理

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

3.1.2、 正则表达式的匹配原理和性能优化

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

# 高级前端基础-JavaScript抽象语法树AST

3.1.4、 base64的编码原理

Base64编码是将字符串以每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节(6比特有效字节,最左边两个永远为0,其实也是8比特的字节)子序列,再将得到的子序列查找Base64的编码索引表,得到对应的字符拼接成新的字符串的一种编码方式。

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

3.2、 网络协议

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

一篇文章带你熟悉 TCP/IP 协议

3.2.2、 三次握手和四次挥手详细原理,为什么要使用这种机制

TCP三次握手

  1. 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
  2. 第二次握手:服务器收到syn包并确认客户的SYN(ack=j+1),同时也发送一个自己的SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。

TCP 四次挥手

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最 后的数据)。

4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态

6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些

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

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

当用户访问一个页面时,浏览器先检查 DNS 缓存中有没有被解析过的对应 IP 地址

如果有,则解析结束,建立 TCP 连接

如果没有,浏览器会检查操作系统的 hosts 文件,从中寻找对应的 IP 地址

如果有,则解析结束,建立 TCP 连接

如果没有,操作系统向本地域名服务器请求解析(这台服务器一般在所在城市的某个角落,距离比较近且性能相对较好)

如果有,则解析结束,建立 TCP 连接

DNS优化方案

  • HTTP页面自动解析

    在页面加载的过程当中,浏览器会自动将超链接 href 属性中的域名解析为 IP 地址

    注意:为了确保安全性,HTTPS 页面中不允许自动解析

  • HTTPS页面自动解析

    通过HTML标签方式

    <meta http-equiv="x-dns-prefetch-control" content="on">
    
    

    通过设置响应头的方式

    ctx.set('X-DNS-Prefetch-Control', 'on')
    
    

    on 表示开启,off 表示关闭

  • 手动解析(推荐)

    <link rel="dns-prefetch" href="//file.cdn.com">
    
    

    开启指定域名的预解析功能,多用于优化 CDN 资源,推荐在项目中使用。最佳使用位置如下

    <meta charset="utf-8">
    <link rel="dns-prefetch" href="//file.cdn.com">
    

3.2.5、 CDN的作用和原理

内容分发网络(Content delivery network或Content distribution network,缩写:CDN)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

CDN的原理

CDN做了两件事,一是让用户访问最近的节点,二是从缓存或者源站获取资源

CDN有个源站的概念,源站就是提供内容的站点(网站的真实服务器), 从源站取内容的过程叫做回源。

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

发送 HTTP 请求的过程就是构建 HTTP 请求报文,并通过 TCP 协议发送到服务器指定端口(HTTP 协议默认端口 80/8080,HTTPS 协议默认端口 443)。
HTTP 请求报文由 3 部分组成:请求行请求报文请求正文

  • 请求行:常用方法有:GET、POST、PUT、DELETE、OPTIONS、HEAD。
  • 请求报文:允许客户端向服务器传递请求的附加信息和客户端自身的信息。
  • 请求正文:通过 POST、PUT 等方法时,通常需要客户端向服务器传递数据,这些数据就储存在请求正文中。

GET VS POST

总体来说,两种请求方式有如下区别:

- 传递参数方式: GET 是将参数写在 URL 中 ? 的后面,并用 & 分隔不同参数;而 POST 是将信息存放在 Message Body 中传送。

- 传输资料量限制: HTTP 协定本身没有限制 URL 及正文长度,多半是浏览器为了避免过长的 URL 消耗过多的资源而限制长度;而以 POST 请求通常都没有内容长度限制的问题。

- 安全性问题: GET 请求方式从浏览器的 URL 地址就可以看到参数;但无论是 GET 还是 POST 其实都是不安全的,因为 HTTP 协定是明文传输,只要拦截封包便能轻易获取重要资讯。想要安全传输资料,必须使用 SSL/TLS来加密封包,也就是 HTTPS。

响应报文

(1) 响应行包含:协议版本,状态码,状态码描述

状态码规则如下:

  • 1xx:指示信息--表示请求已接收,继续处理。
  • 2xx:成功--表示请求已被成功接收、理解、接受。
  • 3xx:重定向--要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误--请求有语法错误或请求无法实现。
  • 5xx:服务器端错误--服务器未能实现合法的请求。

(2) 响应头部包含响应报文的附加信息,由 名/值 对组成

(3) 响应主体包含回车符、换行符和响应返回数据,并不是所有响应报文都有响应数据

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

如3.2.6响应报文

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

  • 从HTTP/1.0到HTTP/2,都是利用TCP作为底层协议进行通信的。

  • HTTP/1.1,引进了长连接(keep-alive),减少了建立和关闭连接的消耗和延迟。

  • HTTP/2,引入了多路复用:连接共享,提高了连接的利用率,降低延迟。

3.2.9、 HTTPS的加密原理,如何开启HTTPS,如何劫持HTTPS请求

HTTPS 协议的主要功能基本都依赖于 TLS/SSL 协议,TLS/SSL 的功能实现主要依赖于三类基本算法

  • 散列函数 散列函数验证信息的完整性
  • 对称加密 对称加密算法采用协商的密钥对数据加密
  • 非对称加密 非对称加密实现身份认证和密钥协商

image.png

详细了解见 https是如何做到安全加密的?

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

WebSocket即时通讯

区别:支持双向通信,实时性更强;

3.3、 设计模式

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

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

发布订阅模式实现的是一种多对多的关系,在发布者与订阅者之间需要有一个中介者,发布者发布事件名和参数到中间者,中间者向事件集中的订阅者发送参数。
而观察者是一种一对多的关系,所有的在同一被观察者身上绑定的观察者只能接受同一个被观察者的消息。\

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

前端工程师必知的javascript设计模式

4、数据结构和算法

据我了解的大部分前端对这部分知识有些欠缺,甚至抵触,但是,如果突破更高的天花板,这部分知识是必不可少的,而且我亲身经历——非常有用!

4.1、 JavaScript编码能力

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

数组去重

// 1. 双重循环比较

function remove(arr) {
  var newArr = []
  for (var i = 0; i < arr.length; i++) {
    for (var j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) {
        i++
        j = i
      }
    }
    newArr.push(arr[i])
  }
  return newArr;
}


// 2.遍历数组+indexOf

var arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
function remove(arr) {
  var newArr = []
  for (var i = 0; i < arr.length; i++) {
  // indexOf()方法如果查询到则返回查询到的第一个结果在数组中的索引,如果查询不到则返回-1
  if (newArr.indexOf(arr[i]) === -1) {
      newArr.push(arr[i])
    }
  }
  return newArr;
}
console.log(remove(arr)) // 结果:[2, 8, 5, 0, 6, 7]


//  3.数组下标判断法 
// 如果在`arr`数组里面找当前的值,返回的索引等于当前的循环里面的i的话,
// 那么证明这个值是**第一次出现**,所以推入到新数组里面,如果后面又遍历到了
//一个出现过的值,那也不会返回它的索引。
function remove(arr) {
  var newArr = []
  for (var i = 0; i < arr.length; i++) {
  if (arr.indexOf(arr[i]) === i) {
      newArr.push(arr[i])
    }
  }
  return newArr;
}


// 4. 排序后相邻比较去除法
function remove(arr) {
  arr.sort();               // 先排序,遍历按顺序比较
  var newArr = [arr[0]]     // 初始化arr[0]
  for (var i = 1; i < arr.length; i++) {
    if (arr[i] !== newArr[newArr.length - 1]) {
      newArr.push(arr[i])
    }
  }
  return newArr;
}

数组扁平化方法

数组扁平化概念:数组扁平化是指将一个多维数组降维成一维数组:

[1, [2, 3, [4, 5]]]  ------>    [1, 2, 3, 4, 5]


1.遍历数组+递归

// 基本思路:遍历数组,如果数组中还有数组的话,递归调用`flatten`扁平函数,用`concat`连接,返回结果。

let arr = [1, [2, 3, [4, 5]]];
const flatten = (arr) => {
  let res = [];
  for(const item of arr) {
    if(Array.isArray(item)) {
      res = res.concat(flatten(item));  // 递归调用flatten扁平函数
    } else {
      res.push(item);
    }
  }
  return res;
}
console.log(flatten(arr));    // [ 1, 2, 3, 4, 5 ]


// 2.迭代方法
function flatten2(arr) {
  const queue = [...arr];   // 用队列实现
  const res = [];
  while (queue.length) {
    const next = queue.shift();   // 从队列里取出
    if (Array.isArray(next)) {
      queue.push(...next);    // 把next扁平化,然后放入queue中
    } else {
      res.push(next);
    }
  }
  return res;
}


function flatten2(arr) {
  const stack = [...arr];
  const res = [];
  while (stack.length) {
    const next = stack.pop();       // 从栈里取出
    if (Array.isArray(next)) {
      stack.push(...next);         // 把next扁平化,然后放入stack中
    } else {
      res.push(next);
    }
  }
  return res.reverse();     // 翻转一下顺序 
}


// 3.reduce方法
// 实现思路:使用`reduce`进行归并,如果数组中还有数组的话,递归调用`flatten`扁平函数。用`concat`连接,返回结果。

const flatten = (arr) => {
  return arr.reduce((res, item) => {
    return res.concat(Array.isArray(item) ? flatten(item) : item);  
  },[]);
}
console.log(flatten(arr));  //[ 1, 2, 3, 4, 5 ]

reduce方法会迭代数组的所有项,并在此基础上构建一个最终返回值。该方法接收两个参数:对每一项都会运行的归并函数(回调函数) ,以及可选的并以之为归并起点的初始值

// 例:求数组的各项值相加的和
let sum = arr.reduce((prev, cur)=> {  
  return prev + cur;
},0);

4.toString()+split()

调用数组的toString()方法,将数组变成字符串然后再用split()分割还原为数组。split()方法会根据传入的分隔符将字符串拆分成数组。

const flatten = (arr) => {
  return arr.toString().split(',').map(item => {
    return parseInt(item);
  })
}
console.log(flatten(arr));  // [ 1, 2, 3, 4, 5 ]
复制代码

因为split分割后形成的数组(["1", "2", "3", "4", "5"])的每一项值为字符串,所以用map方法遍历数组将其每一项转换为数值类型。

5.join()+split()

和上面的toString()一样,join()也可以将数组转换为字符串。

const flatten = (arr) => {
  return arr.join(',').split(',').map(item => {
    return parseInt(item);
  })
}
console.log(flatten(arr));

6.展开运算符

ES6中对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。

let array = [1,2,3,4];
let copy = [...array];  //可以用展开运算符赋值数组
console.log(copy);    // [ 1, 2, 3, 4 ]

var arr = [1,[2,3],[4,5]];
let res = [].concat(...arr); //利用扩展运算符将二维数组变为一维
console.log(res);   // [ 1, 2, 3, 4, 5 ]

根据以上结果的启发,如果arr中含有数组则使用一次展开运算符,逐层击破,用concat连接,返回最终结果。

function flatten(arr){
  while(arr.some(item => Array.isArray(item))){
      arr = [].concat(...arr);
      // [ 1, [ 2, 3, [ 4, 5 ] ] ]
      // [ 1, 2, 3, [ 4, 5 ] ]
      // [ 1, 2, 3, 4, 5 ]
  }
  return arr;
}
console.log(flatten(arr));    // [ 1, 2, 3, 4, 5 ]

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

复制值

在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。请看下面的例子:

let num1 = 5
let num2 = num1;
复制代码

原始值赋值.png

在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:

let obj1 = new Object();

let obj2 = obj1;

obj1.name = "Nicholas";

console.log(obj2.name); // "Nicholas"
复制代码

引用值赋值.png

赋值、深拷贝与浅拷贝

赋值 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

let  obj1 = {name:’张三’,age:12}
let  obj2 = obj1
Obj2.name = ‘李四’
Console.log(obj1 ) // {name:’李四’,age:12}
var a = [1,2,3,4];
var b = a; a[0] = 0;
console.log(a,b); // [0,2,3,4][0,2,3,4]
复制代码

浅拷贝  重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

  • Object.assign() *

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "张三", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "李四";
obj2.sports = 'football'
console.log(obj1); // { person: { name: "李四", age: 41 }, sports: 'basketball' }
复制代码
  • 展开运算符... *

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 张三, address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = '李四'
console.log('obj2',obj2) // obj2 { name: '张三', address: { x: 200, y: 100 } }

复制代码
  • Array.prototype.concat() *
let arr = [1, 3, {username: '张三'}];
let arr2 = arr.concat();    
arr2[2].username = '李四';
console.log(arr);

//[ 1, 3, { username: '李四' } ]

复制代码
  • Array.prototype.slice() *
let arr = [1, 3, {username: ' 张三' }];

let arr3 = arr.slice();

arr3[2].username = '李四'

console.log(arr); // [ 1, 3, { username: '李四' } ]
复制代码

深拷贝

从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

  • JSON.parse(JSON.stringify()) * 这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
let arr = [1, 3, {username: ' 张三'}];

let arr4 = JSON.parse(JSON.stringify(arr));

arr4[2].username = 'duncan';
复制代码

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则 因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

  • 递归 递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
function deepClone(obj, hash = new WeakMap()) {

    // 如果是null或者undefined我就不进行拷贝操作

    if (obj === null) return obj;  

    if (obj instanceof Date) return new Date(obj);

    // 可能是对象或者普通的值 如果是函数的话是不需要深拷贝

    if (obj instanceof RegExp) return new RegExp(obj);

    // 是对象的话就要进行深拷贝

    if (typeof obj !== "object") return obj;

    if (hash.get(obj)) return hash.get(obj);

    //找到的是所属类原型上的constructor,而原型上的constructor指向的是当前类本身

    let cloneObj = new obj.constructor();

    hash.set(obj, cloneObj);

    for (let key in obj) {

        if (obj.hasOwnProperty(key)) {

         // 实现一个递归拷贝

        cloneObj[key] = deepClone(obj[key], hash);

        }

    }

    return cloneObj;

}

let obj = { name: 1, address: { x: 100 } };

obj.o = obj; // 对象存在循环引用的情况

let d = deepClone(obj);

obj.address.x = 200; 
console.log(d); // { name: 1, address: { x: 200 } }

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

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名的。

比如:


// 这是一个接受3个参数的函数
const add = function(x, y, z) {
  return x + y + z
}

// 接收一个单一参数
const curryingAdd = function(x) {
  // 并且返回接受余下的参数的函数
  return function(y, z) {
    return x + y + z
  }
}

// 调用add
add(1, 2, 3)
​
// 调用curryingAdd
curryingAdd(1)(2, 3)
// 看得更清楚一点,等价于下面
const fn = curryingAdd(1)
fn(2, 3)

我们可以封装一个通用柯里化工具函数(面试手写代码)

/**
 * @description: 将函数柯里化的工具函数
 * @param {Function} fn 待柯里化的函数
 * @param {array} args 已经接收的参数列表
 * @return {Function}
 */
const currying = function(fn, ...args) {
    // fn需要的参数个数
    const len = fn.length
    // 返回一个函数接收剩余参数
    return function (...params) {
        // 拼接已经接收和新接收的参数列表
        let _args = [...args, ...params]
        // 如果已经接收的参数个数还不够,继续返回一个新函数接收剩余参数
        if (_args.length < len) {
            return currying.call(this, fn, ..._args)
        }
        // 参数全部接收完调用原函数
        return fn.apply(this, _args)
    }
}

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

// 例二(手写函数防抖与节流)

// 防抖:指在事件触发n秒后再执行[回调]。如果在n秒内再次被触发,则重新计算时间。合并执行
 function debounce(fn,delay) {
     let timer = null //创建一个标记用来存放定时器的返回值
     return function () {
         let _this = this 
         let args = arguments
         if(timer) {
           clearTimeout(timer) // 在delay内,清除上一个setTimeout
           timer = null
         }
         timer = setTimeout(function(){ // 创建一个新的setTimeout
             fn.apply(_this,args) // 绑定函数
         },delay)
     }
 }
 
// 节流:指定时间间隔内只会执行一次任务。

function throttle(fn,delay) {
    let isRun = false // 定义一个状态
    let timer 
    return function(){
        if(!isRun) {
            clearTimeout(timer) // 间隔时间内清除setTimeout
            return
        }
        isRun = false
        
        timer = setTimeout(function(){ // 创建一个新的setTimeout
             fn.apply(_this,args) // 绑定函数
             isRun = true
         },delay)
    }
}

4.1.5、 实现一个sleep函数

我们都是 JavaScript 是一个单线程语言,单线程有它的好处也有它的坏处。在我们熟知的如 JavaC++等语言中,都提供了一个叫做 Sleep 的内置函数。这个函数的作用就和它的名字一样:睡眠。

Java 这类语言中,可以直接使用 Sleep 这个内置函数实现这个需求,Sleep 函数会让出或者停下当前线程,让其它程序先执行

// 实现: `async/await`
<script>
  function fnA() {
    console.log('A');
  }
  function fnB() {
    console.log('B');
  }
  function fnC() {
    console.log('C');
  }
  // sleep 函数--Promise 版本
  function sleep(time) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, time);
    });
  }
  async function sleepTest() {
    fnA();                // 输出 A
    await sleep(1000);    // 睡眠 1 秒
    console.log('E');     // 输出 E
    fnB();                // 输出 B
    await sleep(1000);    // 睡眠 1 秒
    fnC();                // 输出 C
    await sleep(1000);    // 睡眠 1 秒
    console.log('G');     // 输出 G
  }
  sleepTest();
</script>

4.2、 手动实现前端轮子

4.2.1、 手动实现call、apply、bind

三者不同点:

fn.bind: 不会立即调用,而是返回一个绑定后的新函数。

fn.call:立即调用,返回函数执行结果,this指向第一个参数,后面可有多个参数,并且这些都是fn函数的参数。

fn.apply:立即调用,返回函数的执行结果,this指向第一个参数,第二个参数是个数组,这个数组里内容是fn函数的参数。

三者应用场景

  • 需要立即调用使用call/apply
  • 要传递的参数不多,则可以使用fn.call(thisObj, arg1, arg2 ...)
  • 要传递的参数很多,则可以用数组将参数整理好调用fn.apply(thisObj, [arg1, arg2 ...])
  • 不需要立即执行,而是想生成一个新的函数长期绑定某个函数给某个对象使用,使用const newFn = fn.bind(thisObj); newFn(arg1, arg2...)
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;
}

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

4.2.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.2.4、 可以说出两种实现双向绑定的方案、可以手动实现

4.2.5、 手写JSON.stringifyJSON.parse

JSON.parse(string) :接受一个 JSON 字符串并将其转换成一个 JavaScript 对象。

JSON.stringify(obj) :接受一个 JavaScript 对象并将其转换为一个 JSON 字符串。

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

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

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

4.3、 数据结构

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

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

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

4.3.4、了解的基本结构和使用场景

4.4、 算法

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

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

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

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

4.4.5、前端处理海量数据的算法方案

5、运行环境

我们需要理清语言和环境的关系:

ECMAScript描述了JavaScript语言的语法和基本对象规范

浏览器作为JavaScript的一种运行环境,为它提供了:文档对象模型(DOM),描述处理网页内容的方法和接口、浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口

Node也是JavaScript的一种运行环境,为它提供了操作I/O、网络等API

5.1、浏览器API

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

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

5.1.3、大量DOM操作、海量数据的性能优化(合并操作、DiffrequestAnimationFrame等)

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

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

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

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

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

5.1.9、浏览器跨标签通信

5.2、浏览器原理

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

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

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

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

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

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

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

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

5.2.9、浏览器的垃圾回收机制,如何避免内存泄漏

5.2.10、浏览器采用的缓存方案,如何选择和控制合适的缓存方案

5.3、Node

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

5.3.2、掌握一种Node开发框架,如ExpressExpressKoa的区别

5.3.3、熟练使用Node提供的APIPathHttpChild Process等并理解其实现原理

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

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

6、框架和类库

轮子层出不穷,从原理上理解才是正道

6.1、 TypeScript

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

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

6.1.3.TypeScript的规则检测原理

6.1.4.可以在ReactVue等框架中使用TypeScript进行开发

6.2、 React

6.2.1.Reactvue 选型和优缺点、核心架构的区别

区别:vue是双向绑定的,采用template;react是单向的,采用jsx。Vue的优缺点:简单、快速、强大、对模块友好,但不支持IE8。React的优缺点:速度快、跨浏览器兼容、模块化;但学习曲线陡峭,需要深入的知识来构建应用程序。

监听数据变化的实现原理不同

Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。

React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。

数据绑定

vue:

vue是双向绑定, Vue.js 最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统。所谓双向绑定,指的是vue实例中的data与其渲染的DOM元素的内容保持一致,无论谁被改变,另一方会相应的更新为相同的数据。这是通过设置属性访问器实现的。

Vue 的依赖追踪是【原理上不支持双向绑定,v-model 只是通过监听 DOM 事件实现的语法糖】

vue的依赖追踪是通过 Object.defineProperty 把data对象的属性全部转为 getter/setter来实现的;当改变数据的某个属性值时,会触发set函数,获取该属性值的时候会触发get函数,通过这个特性来实现改变数据时改变视图;也就是说只有当数据改变时才会触发视图的改变,反过来在操作视图时,只能通过DOM事件来改变数据,再由此来改变视图,以此来实现双向绑定

react:

react是单向数据流;react中通过将state(Model层)与View层数据进行双向绑定达数据的实时更新变化,具体来说就是在View层直接写JS代码Model层中的数据拿过来渲染,一旦像表单操作、触发事件、ajax请求等触发数据变化,则进行双同步。推崇结合immutable来实现数据不可变。

组件通信的不同

1.png

Vue中有三种方式可以实现组件通信:

  • 父组件通过props向子组件传递数据或者回调,虽然可以传递回调,但是我们一般只传数据;
  • 子组件通过事件向父组件发送消息;
  • 通过V2.2.0中新增的provide/inject来实现父组件向子组件注入数据,可以跨越多个层级。

React中也有对应的三种方式:

  • 父组件通过props可以向子组件传递数据或者回调;
  • 可以通过 context 进行跨层级的通信,这其实和 provide/inject 起到的作用差不多。

框架本质不同

Vue本质是MVVM框架,由MVC发展而来;

React是前端组件化框架,由后端组件化发展而来。

Vue.js的优缺点

优点:

  1. 简单:官方文档很清晰,比 Angular 简单易学。

  2. 快速:异步批处理方式更新 DOM。

  3. 组合:用解耦的、可复用的组件组合你的应用程序。

  4. 紧凑:~18kb min+gzip,且无依赖。

  5. 强大:表达式 & 无需声明依赖的可推导属性 (computed properties)。

  6. 对模块友好:可以通过 NPM、Bower 或 Duo 安装,不强迫你所有的代码都遵循 Angular 的各种规定,使用场景更加灵活。

React的优缺点

优点:

  1. 速度快:在UI渲染过程中,React通过在虚拟DOM中的微操作来实现对实际DOM的局部更新。

  2. 跨浏览器兼容:虚拟DOM帮助我们解决了跨浏览器问题,它为我们提供了标准化的API,甚至在IE8中都是没问题的。

  3. 模块化:为你程序编写独立的模块化UI组件,这样当某个或某些组件出现问题是,可以方便地进行隔离。

  4. 单向数据流:Flux是一个用于在JavaScript应用中创建单向数据层的架构,它随着React视图库的开发而被Facebook概念化。

  5. 同构、纯粹的javascript:因为搜索引擎的爬虫程序依赖的是服务端响应而不是JavaScript的执行,预渲染你的应用有助于搜索引擎优化。

  6. 兼容性好:比如使用RequireJS来加载和打包,而Browserify和Webpack适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。

6.2.2.ReactsetState的执行机制,如何有效的管理状态

React深入】setState的执行机制

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

React事件机制

深入React合成事件机制原理

6.2.4.React的虚拟DOMDiff算法的内部实现

React组件的渲染流程

  • 使用React.createElementJSX编写React组件,实际上所有的JSX代码最后都会转换成React.createElement(...)Babel帮助我们完成了这个转换的过程。
  • createElement函数对keyref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个ReactElement对象(所谓的虚拟DOM)。
  • ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM

虚拟DOM的组成

ReactElementelement对象,我们的组件最终会被渲染成下面的结构:

  • type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class
  • key:组件的唯一标识,用于Diff算法,下面会详细介绍
  • ref:用于访问原生dom节点
  • props:传入组件的propschidrenprops中的一个属性,它存储了当前组件的孩子节点,可以是数组(多个孩子节点)或对象(只有一个孩子节点)
  • owner:当前正在构建的Component所属的Component
  • self:(非生产环境)指定当前位于哪个组件实例
  • _source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)

【React深入】深入分析虚拟DOM的渲染原理和特性

6.2.5.ReactFiber工作原理,解决了什么问题

为什么 React 的 Diff 算法不采用 Vue 的双端对比算法?

6.2.6.React RouterVue Router的底层实现原理、动态加载实现原理

6.2.7.可熟练应用React API、生命周期等,可应用HOCrender propsHooks等高阶用法解决问题

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

6.2.9、 React 系列总结

React 系列总结

6.4、Vue

6.4.1、熟练使用VueAPI、生命周期、钩子函数

6.4.2、MVVM框架设计理念

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

6.4.4、Vue的事件机制

6.4.5、从template转换成真实DOM的实现机制

6.5、多端开发

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

6.5.2、理解Viewportemrem的原理和用法,分辨率、pxppidpidp的区别和实际应用

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

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

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

6.5.6、掌握一种小程序开发框架或原生小程序开发

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

6.6、 数据流管理

6.6.1、掌握ReactVue传统的跨组件通信方案,对比采用数据流管理框架的异同

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

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

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

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

6.7、 实用库

6.7.1、至少掌握一种UI组件框架,如antd design,理解其设计理念、底层实现

6.7.2、掌握一种图表绘制框架,如Echart,理解其设计理念、底层实现,可以自己实现图表

6.7.3、掌握一种GIS开发框架,如百度地图API

6.7.4、掌握一种可视化开发框架,如Three.jsD3

6.7.5、工具函数库,如lodashunderscoremoment等,理解使用的工具类或工具函数的具体实现原理

6.8、 开发和调试

6.8.1、熟练使用各浏览器提供的调试工具

6.8.2、熟练使用一种代理工具实现请求代理、抓包,如charls

6.8.3、可以使用AndroidIOS模拟器进行调试,并掌握一种真机调试方案

6.8.4、了解VueReact等框架调试工具的使用

7、前端工程

前端工程化:以工程化方法和工具提高开发生产效率、降低维护难度

7.1、 项目构建

7.1.1、理解npmyarn依赖包管理的原理,两者的区别

7.1.2、可以使用npm运行自定义脚本

7.1.3、理解BabelESLintwebpack等工具在项目中承担的作用

7.1.4、ESLint规则检测原理,常用的ESLint配置

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

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

7.1.7、Webpack的编译原理、构建流程、热更新原理,chunkbundlemodule的区别和应用

7.1.8、可熟练配置已有的loadersplugins解决问题,可以自己编写loadersplugins

7.2、 nginx

7.2.1 、正向代理与反向代理的特点和实例

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

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

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

7.3、 开发提速

7.3.1 、熟练掌握一种接口管理、接口mock工具的使用,如yapi

7.3.2 、掌握一种高效的日志埋点方案,可快速使用日志查询工具定位线上问题

7.3.3 、理解TDDBDD模式,至少会使用一种前端单元测试框架

7.4、 版本控制

7.4.1 、理解Git的核心原理、工作流程、和SVN的区别

7.4.2 、熟练使用常规的Git命令、git rebasegit stash等进阶命令

7.4.3 、可以快速解决线上分支回滚线上分支错误合并等复杂问题

7.5、 持续集成

7.5.1 、理解CI/CD技术的意义,至少熟练掌握一种CI/CD工具的使用,如Jenkins

7.5.2 、可以独自完成架构设计、技术选型、环境搭建、全流程开发、部署上线等一套完整的开发流程(包括Web应用、移动客户端应用、PC客户端应用、小程序、H5等等)

8、项目和业务

8.1、 后端技能

8.1.1、了解后端的开发方式,在应用程序中的作用,至少会使用一种后端语言

8.1.2、掌握数据最终在数据库中是如何落地存储的,能看懂表结构设计、表之间的关联,至少会使用一种数据库

8.2、 性能优化

面试官:讲讲前端性能优化?

要说起前端性能优化,其实我们可以从 “输入 URL 到页面呈现” 这个知识点着手讲起。

在用户输入 URL,按下回车之后,走过的步骤:

  1. DNS 解析
  2. TCP 连接
  3. 发送 HTTP 请求
  4. 服务器响应
  5. 浏览器解析渲染页面

前端性能优化

这里先推荐一篇文章,先了解前端优化的大体框架再细学习下面的模块

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

8.2.2、了解常见的WebApp性能优化方案

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

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

8.2.5、Webpack的性能优化方案

8.2.6、Canvas性能优化方案

Canvas性能优化

8.2.7、ReactVue等框架使用性能优化方案

8.3、 前端安全