「历时8个月」10万字前端知识体系总结(基础知识篇)🔥

40,967 阅读36分钟

这篇文章源自我历时8个月,整理的前端知识体系与大厂面试笔记,不知不觉,已经超过了10万字

这些笔记帮助我从一个菜鸟,一步步蜕变为高级开发、前端“砖家”,并助力我拿到一些大厂的offer

同时,我将遇到过的面试题也都整理了进去,面试的公司包含:阿里、头条、美团、京东、网易、小米、叮咚买菜、喜马拉雅、货拉拉等

每次面试前,我都会花一周的时间去复习一遍这些知识点 ( 亲测保熟 ) 💕

文章面对的群体👨‍👩‍👧‍👧 :

1) 1到3年的初中级前端工程师
2) 备战大厂的朋友

前言

编程如逆水行舟,不进则退

入行这几年,焦虑与迷茫常伴左右,总会遇到各种瓶颈,不知道如何继续深入下去

这篇文章结合自己的学习与面试经历,整理出来一些学习路线,希望对小伙伴们有所启发

前端知识体系繁杂,文章中总结的知识点难免有所纰漏,希望大家多多指正,一起交流学习😊😘

前端知识体系分为4篇 基础知识篇算法篇工程化篇前端框架和浏览器原理篇 分享给大家

前端知识体系导图

图片太大,就不展示了,点击可查看大图

下面,我们一起开始吧,升职加薪,YYDS!💪💪💪

JS 基础

执行上下文和执行栈

什么是执行上下文?
Javascript 代码都是在执行上下文中运行的

执行上下文: 指当前执行环境中的变量、函数声明、作用域链、this等信息

执行上下文生命周期

1)创建阶段
生成变量对象、建立作用域链、确定this的指向

2)执行阶段
变量赋值、函数的引用、执行其他代码

执行上下文.jpg

变量对象

变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明

变量对象是一个抽象的概念,在全局执行上下文中,变量对象就是全局对象。 在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象

执行栈

是一种先进后出的数据结构,用来存储代码运行的所有执行上下文

1)当 JS 引擎第一次遇到js脚本时,会创建一个全局的执行上下文并且压入当前执行栈

2)每当JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部

3)当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文

4)一旦所有代码执行完毕,JS 引擎从当前栈中移除全局执行上下文

执行栈示例

var a = 1; // 1. 全局上下文环境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文环境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文环境

执行栈图解 执行上下文.png

全局、函数、Eval执行上下文

执行上下文分为全局、函数、Eval执行上下文

1)全局执行上下文(浏览器环境下,为全局的 window 对象)

2)函数执行上下文,每当一个函数被调用时, 都会为该函数创建一个新的上下文

3)Eval 函数执行上下文,如eval("1 + 2")

对于每个执行上下文,都有三个重要属性:变量对象、作用域链(Scope chain)、this

执行上下文的特点:

1)单线程,只在主线程上运行;

2)同步执行,从上向下按顺序执行;

3)全局上下文只有一个,也就是window对象;

4)函数每调用一次就会产生一个新的执行上下文环境。

理解 JavaScript 中的执行上下文和执行栈
理解JavaScript的执行上下文
JavaScript进阶-执行上下文

作用域

作用域:可访问变量的集合

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

作用域类型

全局作用域函数作用域、ES6中新增了块级作用域

函数作用域
是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了

块作用域
1)块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
2)在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问

var、let、const的区别

1)var定义的变量,没有块的概念,可以跨块访问, 可以变量提升

2)let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明

3)const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明

let和const声明的变量只在块级作用域内有效,示例

function func() {
  if (true) {
    let i = 3;
  }
  console.log(i); // 报错 "i is not defined"
}
func();

var与let的经典案例

1) 用var定义i变量,循环后打印i的值

// 案例1
// i是var声明的,在全局范围内都有效,全局只有一个变量i,输出的是最后一轮的i值,也就是 10

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function() {
    console.log(i);
  };
}
a[0]();  // 10

2) 用let定义i变量,循环后打印i的值

// 案例2
// 用let声明i,for循环体内部是一个单独的块级作用域,相互独立,不会相互覆盖
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function() {
    console.log(i);
  };
}
a[0](); // 0

let 实现原理

借助闭包和函数作用域来实现块级作用域的效果

// 用var实现案例2的效果
var a = [];

var _loop = function _loop(i) {
  a[i] = function() {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[0](); // 0

作用域链

当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

JavaScript深入之作用域链
js块级作用域和let,const,var区别

this

this的5种绑定方式

1)默认绑定(非严格模式下this指向全局对象,严格模式下函数内的this指向undefined)

2)隐式绑定(当函数引用有上下文对象时, 如 obj.foo()的调用方式, foo内的this指向obj)

3)显示绑定(通过call或者apply方法直接指定this的绑定对象, 如foo.call(obj))

4)new构造函数绑定,this指向新生成的对象

5)箭头函数,this指向的是定义该函数时,外层环境中的this,箭头函数的this在定义时就决定了,不能改变

this 题目1

"use strict";
var a = 10; // var定义的a变量挂载到window对象上
function foo () {
  console.log('this1', this)  // undefined
  console.log(window.a)  // 10
  console.log(this.a)  //  报错,Uncaught TypeError: Cannot read properties of undefined (reading 'a')
}
console.log('this2', this)  // window
foo();

注意:开启了严格模式,只是使得函数内的this指向undefined,它并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。

this 题目2

let a = 10
const b = 20
function foo () {
  console.log(this.a)  // undefined
  console.log(this.b)  // undefined
}
foo();
console.log(window.a) // undefined  

如果把 var 改成了 let 或 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined

this 题目3

var a = 1
function foo () {
  var a = 2
  console.log(this)  // window
  console.log(this.a) // 1
}
foo()

foo()函数内的this指向的是window,因为是window调用的foo,打印出的this.a是window下的a

this 题目4

var obj2 = {
    a: 2,
    foo1: function () {
      console.log(this.a) // 2
    },
    foo2: function () {
      setTimeout(function () {
        console.log(this) // window
        console.log(this.a) // 3
      }, 0)
    }
  }
  var a = 3
  
  obj2.foo1()
  obj2.foo2() 

对于setTimeout中的函数,这里存在隐式绑定的this丢失,也就是当我们将函数作为参数传递时,会被隐式赋值,回调函数丢失this绑定,因此这时候setTimeout中函数内的this是指向window

this 题目5

var obj = {
 name: 'obj',
 foo1: () => {
   console.log(this.name) // window
 },
 foo2: function () {
   console.log(this.name) // obj
   return () => {
     console.log(this.name) // obj
   }
 }
}
var name = 'window'
obj.foo1()
obj.foo2()()

这道题非常经典,它证明了箭头函数内的this是由外层作用域决定的

题目5解析:
1)对于obj.foo1()函数的调用,它的外层作用域是window,对象obj当然不属于作用域了(作用域只有全局作用域、函数作用域、块级作用域),所以会打印出window

2)obj.foo2()(),首先会执行obj.foo2(),这不是个箭头函数,所以它里面的this是调用它的obj对象,因此第二个打印为obj,而返回的匿名函数是一个箭头函数,它的this由外层作用域决定,那也就是它的this会和foo2函数里的this一样,第三个打印也是obj

再来40道this面试题酸爽继续(1.2w字用手整理)

call apply bind

三者的区别

1)三者都可以显式绑定函数的this指向

2)三者第一个参数都是this要指向的对象,若该参数为undefined或null,this则默认指向全局window

3)传参不同:apply是数组、call是参数列表,而bind可以分为多次传入,实现参数的合并

4)call、apply是立即执行,bind是返回绑定this之后的函数,如果这个新的函数作为构造函数被调用,那么this不再指向传入给bind的第一个参数,而是指向新生成的对象

手写call apply bind

// 手写call
Function.prototype.Call = function(context, ...args) {
  // context为undefined或null时,则this默认指向全局window
  if (context === undefined || context === null) {
    context = window;
  }
  // 利用Symbol创建一个唯一的key值,防止新增加的属性与obj中的属性名重复
  let fn = Symbol();
  // this指向调用call的函数
  context[fn] = this; 
  // 隐式绑定this,如执行obj.foo(), foo内的this指向obj
  let res = context[fn](...args);
  // 执行完以后,删除新增加的属性
  delete context[fn]; 
  return res;
};

// apply与call相似,只有第二个参数是一个数组,
Function.prototype.Apply = function(context, args) {
  if (context === undefined || context === null) {
    context = window;
  }
  let fn = Symbol();
  context[fn] = this;
  let res = context[fn](...args);
  delete context[fn];
  return res;
};

// bind要考虑返回的函数,作为构造函数被调用的情况
Function.prototype.Bind = function(context, ...args) {
  if (context === undefined || context === null) {
    context = window;
  }
  let fn = this;
  let f = Symbol();
  const result = function(...args1) {
    if (this instanceof fn) {
      // result如果作为构造函数被调用,this指向的是new出来的对象
      // this instanceof fn,判断new出来的对象是否为fn的实例
      this[f] = fn;
      let res = this[f](...args, ...args1);
      delete this[f];
      return res;
    } else {
      // bind返回的函数作为普通函数被调用时
      context[f] = fn;
      let res = context[f](...args, ...args1);
      delete context[f];
      return res;
    }
  };
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式: 使用Object.create
  result.prototype = Object.create(fn.prototype);
  return result;
};

闭包

闭包:就是函数引用了外部作用域的变量

闭包常见的两种情况:
一是函数作为返回值; 另一个是函数作为参数传递

闭包的作用:
可以让局部变量的值始终保持在内存中;对内部变量进行保护,使外部访问不到
最常见的案例:函数节流和防抖

闭包的垃圾回收:
副作用:不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收

闭包的示例

// 原始题目
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 1s后打印出5个5
  }, 1000);
}

// ⬅️利用闭包,将上述题目改成1s后,打印0,1,2,3,4

// 方法一:
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, 1000);
  })(i);
}

// 方法二:
// 利用setTimeout的第三个参数,第三个参数将作为setTimeout第一个参数的参数
for (var i = 0; i < 5; i++) {
  setTimeout(function fn(i) {
    console.log(i);
  }, 1000, i); // 第三个参数i,将作为fn的参数
}

// ⬅️将上述题目改成每间隔1s后,依次打印0,1,2,3,4
for (var i = 0; i < 5; i++) {
  setTimeout(function fn(i) {
    console.log(i);
  }, 1000 * i, i);
}

发现 JavaScript 中闭包的强大威力
破解前端面试(80% 应聘者不及格系列):从闭包说起

箭头函数

箭头函数与普通函数的区别

1)this 绑定:箭头函数没有自己的 this 绑定,它会捕获所在上下文的 this 值。而普通函数的 this 值根据函数的调用方式动态绑定。

在箭头函数中,this 的值是词法上继承自外部作用域,也就是定义箭头函数时的所在上下文。

const obj = {
  name: "John",
  regularFunction: function () {
    console.log(this.name); // 正常输出 "John"
  },
  arrowFunction: () => {
    console.log(this.name); // 输出 undefined,因为箭头函数没有自己的 this 绑定
  },
};

obj.regularFunction();
obj.arrowFunction();

2)arguments 对象:箭头函数没有自己的 arguments 对象,而普通函数可以使用 arguments 对象获取所有传入的参数。

function regularFunction() {
  console.log(arguments); // 输出传入的参数
}

regularFunction(1, 2, 3); // 输出 [1, 2, 3]

const arrowFunction = () => {
  console.log(arguments); // 抛出 ReferenceError,因为箭头函数没有 arguments 对象
};

arrowFunction(1, 2, 3);

3)构造函数:箭头函数不能用作构造函数,不能使用 new 关键字调用,并且没有自己的原型对象(prototype)。

普通函数可以通过 new 关键字创建对象实例,并且有自己的原型对象。

function RegularConstructor() {
  this.name = "John";
}

const regularInstance = new RegularConstructor();
console.log(regularInstance.name); // 输出 "John"

const arrowConstructor = () => {
  this.name = "John";
};

const arrowInstance = new arrowConstructor(); // 抛出 TypeError,箭头函数不能用作构造函数

原型/原型链

原型的作用

原型被定义为给其它对象提供共享属性的对象,函数的实例可以共享原型上的属性和方法

原型链

它的作用就是当你在访问一个对象上属性的时候,如果该对象内部不存在这个属性,那么就会去它__proto__属性所指向的对象(原型对象)上查找。如果原型对象依旧不存在这个属性,那么就会去其原型的__proto__属性所指向的原型对象上去查找。以此类推,直到找到nul,而这个查找的线路,也就构成了我们常说的原型链

原型链和作用域的区别: 原型链是查找对象上的属性,作用域链是查找当前上下文中的变量

proto、prototype、constructor属性介绍

1)js中对象分为两种,普通对象和函数对象

2)__proto__constructor是对象独有的。prototype属性是函数独有的,它的作用是包含可以给特定类型的所有实例提供共享的属性和方法;但是在 JS 中,函数也是对象,所以函数也拥有__proto__constructor属性

3)constructor属性是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数
构造函数.prototype.constructor === 该构造函数本身

4)一个对象的__proto__指向其构造函数的prototype
函数创建的对象.__proto__ === 该函数.prototype

5)特殊的ObjectFunction

console.log(Function.prototype === Function.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

instanceof

instanceof 的基本用法,它可以判断一个对象的原型链上是否包含该构造函数的原型,经常用来判断对象是否为该构造函数的实例

特殊示例

console.log(Object instanceof Object); //true
console.log(Function instanceof Function); //true
console.log(Function instanceof Object); //true
console.log(function() {} instanceof Function); //true

手写instanceof方法

function instanceOf(obj, fn) {
  let proto = obj.__proto__;
  if (proto) {
    if (proto === fn.prototype) {
      return true;
    } else {
      return instanceOf(proto, fn);
    }
  } else {
    return false;
  }
}

// 测试
function Dog() {}
let dog = new Dog();
console.log(instanceOf(dog, Dog), instanceOf(dog, Object)); // true true

instanceof与typeof的区别

1)typeof一般被用于来判断一个变量的类型
typeof可以用来判断number、undefined、symbol、string、function、boolean、object 这七种数据类型,特殊情况:typeof null === 'object'

2)instanceof判断一个对象的原型链上是否包含该构造函数的原型

一文吃透所有JS原型相关知识点

new 关键字

new一个对象,到底发生什么?

1)创建一个对象,该对象的原型指向构造函数的原型

2)调用该构造函数,构造函数的this指向新生成的对象

3)判断构造函数是否有返回值,如果有返回值且返回值是一个对象或一个方法,则返回该值;否则返回新生成的对象

构造函数有返回值的案例

function Dog(name) {
  this.name = name;
  return { test: 1 };
}
let obj = new Dog("ming");
console.log(obj); // {test:1} 

手写new

function selfNew(fn, ...args) {
  // 创建一个instance对象,该对象的原型是fn.prototype
  let instance = Object.create(fn.prototype);
  // 调用构造函数,使用apply,将this指向新生成的对象
  let res = fn.apply(instance, args);
  // 如果fn函数有返回值,并且返回值是一个对象或方法,则返回该对象,否则返回新生成的instance对象
  return typeof res === "object" || typeof res === "function" ? res : instance;
}

继承

多种继承方式

1)原型链继承,缺点:引用类型的属性被所有实例共享
2)借用构造函数(经典继承)
3)原型式继承
4)寄生式继承
5)组合继承
6)寄生组合式继承

寄生组合式继承的优势

优势:借用父类的构造函数,在不需要生成父类实例的情况下,继承了父类原型上的属性和方法

手写寄生组合式继承

// 精简版
class Child {
  constructor() {
    // 调用父类的构造函数
    Parent.call(this);
    // 利用Object.create生成一个对象,新生成对象的原型是父类的原型,并将该对象作为子类构造函数的原型,继承了父类原型上的属性和方法
    Child.prototype = Object.create(Parent.prototype);
    // 原型对象的constructor指向子类的构造函数
    Child.prototype.constructor = Child;
  }
}

// 通用版
function Parent(name) {
  this.name = name;
}
Parent.prototype.getName = function() {
  console.log(this.name);
};
function Child(name, age) {
  // 调用父类的构造函数
  Parent.call(this, name); 
  this.age = age;
}
function createObj(o) {
  // 目的是为了继承父类原型上的属性和方法,在不需要实例化父类构造函数的情况下,避免生成父类的实例,如new Parent()
  function F() {}
  F.prototype = o;
  // 创建一个空对象,该对象原型指向父类的原型对象
  return new F(); 
}

// 等同于 Child.prototype = Object.create(Parent.prototype)
Child.prototype = createObj(Parent.prototype); 
Child.prototype.constructor = Child;

let child = new Child("tom", 12);
child.getName(); // tom

一文吃透所有JS原型相关知识点
最详尽的 JS 原型与原型链终极详解

Class 类

1) Class 类可以看作是构造函数的语法糖

class Point {}
console.log(typeof Point); // "function"
console.log(Point === Point.prototype.constructor); // true

2) Class 类中定义的方法,都是定义在该构造函数的原型上

class Point {
  constructor() {}
  toString() {}
}
// 等同于
Point.prototype = { constructor() {}, toString() {} };

3)使用static关键字,作为静态方法(静态方法,只能通过类调用,实例不能调用)

class Foo {
  static classMethod() {
    return "hello";
  }
}
Foo.classMethod(); // 'hello'

4)实例属性的简写写法

class Foo {
  bar = "hello";
  baz = "world";
}
// 等同于
class Foo {
  constructor() {
    this.bar = "hello";
    this.baz = "world";
  }
}

5)extends 关键字,底层也是利用的寄生组合式继承

class Parent {
  constructor(age) {
    this.age = age;
  }
  getName() {
    console.log(this.name);
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(age);
    this.name = name;
  }
}
let child = new Child("li", 16);
child.getName(); // li

手写Class类

ES6的 Class 内部是基于寄生组合式继承,它是目前最理想的继承方式

ES6的 Class 允许子类继承父类的静态方法和静态属性

// Child 为子类的构造函数, Parent为父类的构造函数
function selfClass(Child, Parent) {
  // Object.create 第二个参数,给生成的对象定义属性和属性描述符/访问器描述符
  Child.prototype = Object.create(Parent.prototype, {
    // 子类继承父类原型上的属性和方法
    constructor: {
      enumerable: false,
      configurable: false,
      writable: true,
      value: Child
    }
  });
  // 继承父类的静态属性和静态方法
  // **Object.setPrototypeOf()**方法设置一个指定的对象的原型(即,内部 `[[Prototype]]` 属性)到另一个对象或 [`null`]
  Object.setPrototypeOf(Child, Parent);
}

// 测试
function Child() {
  this.name = 123;
}
function Parent() {}
// 设置父类的静态方法getInfo
Parent.getInfo = function() {
  console.log("info");
};
Parent.prototype.getName = function() {
  console.log(this.name);
};
selfClass(Child, Parent);
Child.getInfo(); // info
let tom = new Child();
tom.getName(); // 123

Class 的基本语法

set get

在JavaScript中,你可以使用getset关键字定义类的访问器属性。访问器属性允许你在获取或设置属性值时执行自定义的操作

class MyClass {
  constructor() {
    this._myProperty = 0; // 带有下划线的属性表示它是私有的
  }

  get myProperty() {
    return this._myProperty;
  }

  set myProperty(value) {
    // 在设置属性值时,你可以执行一些自定义的操作
    if (value >= 0) {
      this._myProperty = value;
    } else {
      console.log("属性值必须大于等于 0");
    }
  }
}

const myObject = new MyClass();
console.log(myObject.myProperty); // 输出: 0

myObject.myProperty = 10;
console.log(myObject.myProperty); // 输出: 10

myObject.myProperty = -5; // 输出: 属性值必须大于等于 0
console.log(myObject.myProperty); // 输出: 10

在上面的示例中,MyClass类定义了一个名为myProperty的访问器属性。get myProperty()方法用于获取属性的值,而set myProperty(value)方法用于设置属性的值。在设置属性值时,我们可以添加一些自定义的逻辑。在这个示例中,我们确保属性值大于等于0,如果小于0,则输出一条错误消息

注意,为了避免与访问器属性冲突,在构造函数中使用了一个带有下划线前缀的私有属性_myProperty。这是一种常见的命名约定,用于表示该属性应该被视为私有的,以防止直接访问。通过访问器属性myProperty,我们可以对该私有属性进行间接访问和操作

Promise

链式调用

1)promise的回调只能被捕获一次
2)在then函数加上return,后面的then函数才能继续捕获到

链式调用示例

// 只有第一个then函数能捕获到结果,第二个then打印undefined
let pro = new Promise((resolve, reject) => resolve(1));
pro.then(res => {
    console.log(res);
  })
  .then(res => {
    console.log(res);
  });

手写promise

class Promise {
  constructor(fn) {
    // resolve时的回调函数列表
    this.resolveTask = [];
    // reject时的回调函数列表
    this.rejectTask = [];
    // state记录当前状态,共有pending、fulfilled、rejected 3种状态
    this.state = "pending";
    let resolve = value => {
      // state状态只能改变一次,resolve和reject只会触发一种
      if (this.state !== "pending") return;
      this.state = "fulfilled";
      this.data = value;
      // 模拟异步,保证resolveTask事件先注册成功,要考虑在Promise里面写同步代码的情况
      setTimeout(() => {
        this.resolveTask.forEach(cb => cb(value));
      });
    };
    let reject = err => {
      if (this.state !== "pending") return;
      this.state = "rejected";
      this.error = err;
      // 保证rejectTask事件注册成功
      setTimeout(() => {
        this.rejectTask.forEach(cb => cb(err));
      });
    };

    // 关键代码,执行fn函数
    try {
      fn(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(resolveCallback, rejectCallback) {
    // 解决链式调用的情况,继续返回Promise
    return new Promise((resolve, reject) => {
      // 将then传入的回调函数,注册到resolveTask中
      this.resolveTask.push(() => {
        // 重点:判断resolveCallback事件的返回值
        // 假如用户注册的resolveCallback事件又返回一个Promise,将resolve和reject传进去,这样就实现控制了链式调用的顺序
        const res = resolveCallback(this.data);
        if (res instanceof Promise) {
          res.then(resolve, reject);
        } else {
          // 假如返回值为普通值,resolve传递出去
          resolve(res);
        }
      });

      this.rejectTask.push(() => {
        // 同理:判断rejectCallback事件的返回值
        // 假如返回值为普通值,reject传递出去
        const res = rejectCallback(this.error);
        if (res instanceof Promise) {
          res.then(resolve, reject);
        } else {
          reject(res);
        }
      });
    });
  }
}

// 测试
// 打印结果:依次打印1、2
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 500);
}).then(
    res => {
      console.log(res);
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(2);
        }, 1000);
      });
    }
  ).then(data => {
      console.log(data);
    });

手写race、all

race:返回promises列表中第一个执行完的结果
all:返回promises列表中全部执行完的结果

class Promise {
  // race静态方法,返回promises列表中第一个执行完的结果
  static race(promises) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        // Promise.resolve包一下,防止promises[i]不是Promise类型
        Promise.resolve(promises[i])
          .then(res => {
            resolve(res);
          })
          .catch(err => {
            reject(err);
          });
      }
    });
  }

  // all静态方法, 返回promises列表中全部执行完的结果
  static all(promises) {
    let result = [];
    let index = 0;
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i])
          .then(res => {
            // 输出结果的顺序和promises的顺序一致
            result[i] = res; 
            index++;
            if (index === promises.length) {
              resolve(result);
            }
          })
          .catch(err => {
            reject(err);
          });
      }
    });
  }
}

手写retry

retry的作用,当接口请求失败后,每间隔几秒,再重发几次

/* 
* @param {function} fn - 方法名
* @param {number} delay - 延迟的时间
* @param {number} times - 重发的次数
*/
function retry(fn, delay, times) {
  return new Promise((resolve, reject) => {
    function func() {
      Promise.resolve(fn()).then(res => {
          resolve(res);
        })
        .catch(err => {
          // 接口失败后,判断剩余次数不为0时,继续重发
          if (times !== 0) {
            setTimeout(func, delay);
            times--;
          } else {
            reject(err);
          }
        });
    }
    func();
  });
}

史上最最最详细的手写Promise教程

async、await

作用:用同步方式,执行异步操作

总结

1)async函数是generator(生成器函数)的语法糖

2)async函数返回的是一个Promise对象,有无值看有无return值

3)await关键字只能放在async函数内部,await关键字的作用 就是获取Promise中返回的resolve或者reject的值

4)async、await要结合try/catch使用,防止意外的错误

generator

1)generator函数跟普通函数在写法上的区别就是,多了一个星号*

2)只有在generator函数中才能使用yield,相当于generator函数执行的中途暂停点

3)generator函数是不会自动执行的,每一次调用它的next方法,会停留在下一个yield的位置

async、await示例

const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000));
async function test() {
  const data = await getData();
  console.log("data: ", data);
  const data2 = await getData();
  console.log("data2: ", data2);
  return "success";
}
test().then(res => console.log(res))

将上面示例转化为generator函数

function* testG() {
  // await被编译成了yield
  const data = yield getData();
  console.log("data: ", data);
  const data2 = yield getData();
  console.log("data2: ", data2);
  return "success";
}

手动执行generator函数

// 执行结果与`async、await`示例一致
const getData = () =>
  new Promise(resolve => setTimeout(() => resolve("data"), 1000));

function* testG() {
  // await被编译成了yield
  const data = yield getData();
  console.log("data: ", data);
  const data2 = yield getData();
  console.log("data2: ", data2);
  return "success";
}
var gen = testG();
var dataPromise = gen.next();
dataPromise.value.then(value1 => {
  // data1的value被拿到了,继续调用next
  var data2Promise = gen.next(value1);
  data2Promise.value.then(value2 => {
    // data2的value拿到了 继续调用next并且传递value2
    gen.next(value2);
  });
});

手写async、await

function generatorToAsync(generatorFn) {
  // 返回的是一个新的函数
  return function() {
    // 先调用generator函数
    // 对应 var gen = testG()
    const gen = generatorFn.apply(this, arguments);

    // 返回一个Promise, 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值
    return new Promise((resolve, reject) => {
      // 内部定义一个step函数 用来一步步next
      function step(key, arg) {
        let res;

        // 这个方法需要包裹在try catch中
        // 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误
        try {
          res = gen[key](arg); // 这里有可能会执行返回reject状态的Promise
        } catch (error) {
          return reject(error); // 报错的话会走catch,直接reject
        }

        // gen.next() 得到的结果是一个 { value, done } 的结构
        const { value, done } = res;
        if (done) {
          // 如果done为true,说明走完了,进行resolve(value)
          return resolve(value);
        } else {
          // 如果done为false,说明没走完,还得继续走

          // value有可能是:常量\Promise;
          // Promise有可能是成功或者失败
          return Promise.resolve(value).then(
            val => step("next", val),
            err => step("throw", err)
          );
        }
      }

      step("next"); // 第一次执行
    });
  };
}

// 测试generatorToAsync

// 1秒后打印data1 再过一秒打印data2 最后打印success
const getData = () =>
  new Promise(resolve => setTimeout(() => resolve("data"), 1000));
var test = generatorToAsync(function* testG() {
  // await被编译成了yield
  const data = yield getData();
  console.log("data1: ", data);
  const data2 = yield getData();
  console.log("data2: ", data2);
  return "success";
});

test().then(res => console.log(res));

20分钟就能搞定的async/await原理
手写async await的最简实现
async/await 一定要加 try/catch吗?

深拷贝

深拷贝的方式

1)JSON.parse(JSON.stringify())
缺点: 无法拷贝 函数、正则、时间格式、原型上的属性和方法等

2)递归实现深拷贝

手写深拷贝

解决 循环引用多个属性引用同一个对象(重复拷贝)的情况

1)循环拷贝:对象的属性引用自己

let target = {name: 'target'}; 
target.target = target

2)重复拷贝:对象的属性引用同一个对象

let obj = {}; 
let target = {a: obj, b: obj};

手写深拷贝代码

// 使用hash 存储已拷贝过的对象,避免循环拷贝和重复拷贝
function deepClone(target, hash = new WeakMap()) {
  if (!isObject(target)) return target;
  if (hash.get(target)) return hash.get(target);
  // 兼容数组和对象
  let newObj = Array.isArray(target) ? [] : {};
  // 关键代码,解决对象的属性循环引用 和 多个属性引用同一个对象的问题,避免重复拷贝
  hash.set(target, newObj);
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      if (isObject(target[key])) {
        newObj[key] = deepClone(target[key], hash); // 递归拷贝
      } else {
        newObj[key] = target[key];
      }
    }
  }
  return newObj;
}
function isObject(target) {
  return typeof target === "object" && target !== null;
}

// 示例
let info = { item: 1 };
let obj = {
  key1: info,
  key2: info,
  list: [1, 2]
};

// 循环引用深拷贝示例
obj.key3 = obj;
let val = deepClone(obj);
console.log(val);

使用WeakMap的好处是,WeakMap存储的key必须是对象,并且key都是弱引用,便于垃圾回收

JSON.parse(JSON.stringify()) 实现对对象的深拷贝
如何实现一个深拷贝

事件轮询机制 Event Loop

JS 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着

所有任务可以分成两种,一种是宏任务,另一种是微任务

宏任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务
微任务指的是,不进入主线程、而进入"微任务列表"的任务

当前宏任务执行完后,会判断微任务列表中是否有任务。如果有,会把该微任务放到主线程中并执行,如果没有,就继续执行下一个宏任务

宏任务 微任务

1)宏任务(Macrotasks)
script全部代码(注意同步代码也属于宏任务)、setTimeout、setInterval、setImmediate等

2)微任务(Microtasks)
Promise、MutationObserver

事件轮询机制执行过程

1)代码执行过程中,宏任务和微任务放在不同的任务队列中

2)当某个宏任务执行完后,会查看微任务队列是否有任务。如果有,执行微任务队列中的所有微任务(注意这里是执行所有的微任务)

3)微任务执行完成后,会读取宏任务队列中排在最前的第一个宏任务(注意宏任务是一个个取),执行该宏任务,如果执行过程中,遇到微任务,依次加入微任务队列

4)宏任务执行完成后,再次读取微任务队列里的任务,依次类推。

Event Loop经典题目

Promise.resolve()
  .then(function() {
    console.log("promise0");
  })
  .then(function() {
    console.log("promise5");
  });
setTimeout(() => {
  console.log("timer1");
  Promise.resolve().then(function() {
    console.log("promise2");
  });
  Promise.resolve().then(function() {
    console.log("promise4");
  });
}, 0);
setTimeout(() => {
  console.log("timer2");
  Promise.resolve().then(function() {
    console.log("promise3");
  });
}, 0);
Promise.resolve().then(function() {
  console.log("promise1");
});
console.log("start");

// 打印结果: start promise0 promise1 promise5 timer1 promise2 promise4 timer2 promise3

案例的解释

宏任务是一个个执行,执行一个宏任务,然后就把在任务队列中的所有微任务都执行完,再执行下一个宏任务,再执行所有微任务,依次类推

async、await事件轮询执行时机

async隐式返回Promise,会产生一个微任务
await后面的代码是在微任务时执行

console.log("script start");
async function async1() {
  await async2(); // await 隐式返回promise
  console.log("async1 end"); // 这里的执行时机:在执行微任务时执行
}
async function async2() {
  console.log("async2 end"); // 这里是同步代码
}
async1();
setTimeout(function() {
  console.log("setTimeout");
}, 0);
new Promise(resolve => {
  console.log("Promise"); // 这里是同步代码
  resolve();
})
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  }); 
console.log("script end");

// 打印结果:  script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout

event loop 与 浏览器更新渲染时机

1) 浏览器更新渲染会在event loop中的 宏任务 和 微任务 完成后进行,即宏任务 → 微任务 → 渲染更新(先宏任务 再微任务,然后再渲染更新)

2)宏任务队列中,如果有大量任务等待执行时,将dom的变动作为微任务,能更快的将变化呈现给用户,这样就可以在这一次的事件轮询中更新dom

event loop与 vue nextTick

vue nextTick为什么要优先使用微任务实现?

1) vue nextTick的源码实现,优先级判断,总结就是Promise > MutationObserver > setImmediate > setTimeout

2)这里优先使用Promise,因为根据event loop与浏览器更新渲染时机,使用微任务,本次event loop轮询就可以获取到更新的dom

3)如果使用宏任务,要到下一次event loop中,才能获取到更新的dom

Node中的process.nextTick

有很多文章把Node的process.nextTick和微任务混为一谈,但其实并不是同一个东西

process.nextTick 是 Node.js 自身定义实现的一种机制,有自己的 nextTickQueue

process.nextTick执行顺序早于微任务

示例

console.log("start");
setTimeout(() => {
  console.log("timeout");
}, 0);
Promise.resolve().then(() => {
  console.log("promise");
});
process.nextTick(() => {
  console.log("nextTick");
  Promise.resolve().then(() => {
    console.log("promise1");
  });
});
console.log("end");
// 执行结果 start end nextTick  promise promise1 timeout 

这一次,彻底弄懂 JavaScript 执行机制
从event loop规范探究javaScript异步及浏览器更新渲染时机
Vue异步更新 - nextTick为什么要microtask优先
浏览器与Node的事件循环(Event Loop)有何区别?

定时器

JS提供了一些原生方法来实现延时去执行某一段代码

setTimeout/setInterval

setTimeout固定时长后执行
setInterval间隔固定时间重复执行
setTimeout、setInterval最短时长为4ms

定时器不准的原因

setTimeout/setInterval的执行时间并不是确定的

setTimeout/setInterval是宏任务,根据事件轮询机制,其他任务会阻塞或延迟js任务的执行

考虑极端情况,假如定时器里面的代码需要进行大量的计算,或者是DOM操作,代码执行时间超过定时器的时间,会出现定时器不准的情况

setTimeout/setInterval 动画卡顿

不同设备的屏幕刷新频率可能不同, setTimeout/setInterval只能设置固定的时间间隔,这个时间和屏幕刷新间隔可能不同

setTimeout/setInterval通过设置一个间隔时间,来不断改变图像实现动画效果,在不同设备上可能会出现卡顿、抖动等现象

requestAnimationFrame

requestAnimationFrame 是浏览器专门为动画提供的API

requestAnimationFrame刷新频率与显示器的刷新频率保持一致,使用该api可以避免使用setTimeout/setInterval造成动画卡顿的情况

requestAnimationFrame:告诉浏览器在下次重绘之前执行传入的回调函数(通常是操纵dom,更新动画的函数)

setTimeout、setInterval、requestAnimationFrame 三者的区别

1)引擎层面

setTimeout属于 JS引擎 ,存在事件轮询
requestAnimationFrame 属于 GUI引擎
JS引擎与GUI引擎是互斥的,也就是说 GUI引擎在渲染时会阻塞JS引擎的计算

这样设计的原因,如果在GUI渲染的时候,JS同时又改变了dom,那么就会造成页面渲染不同步

2)性能层面

当页面被隐藏或最小化时,定时器 setTimeout仍会在后台执行动画任务

当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,requestAnimationFrame也会停止

setTimeout模拟实现setInterval

// 使用闭包实现
function mySetInterval(fn, t) {
  let timer = null;
  function interval() {
    fn();
    timer = setTimeout(interval, t);
  }
  interval();
  return {
    // cancel用来清除定时器
    cancel() {
      clearTimeout(timer);
    }
  };
}

setInterval模拟实现setTimeout

function mySetTimeout(fn, time) {
  let timer = setInterval(() => {
    clearInterval(timer);
    fn();
  }, time);
}

// 使用
mySetTimeout(() => {
  console.log(1);
}, 2000);

setTimeout/setInterval与requestAnimationFrame的区别?

设计模式

设计模式是从许多优秀的软件系统中,总结出的成功的、能够实现可维护性、复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作

单例模式

一个类只能构造出唯一实例

应用案例:弹框

单例模式示例

class Single {
  constructor(name) {
    this.name = name;
  }
  static getInstance(name) {
    // 静态方法
    if (!this.instance) {
      // 关键代码 this指向的是Single这个构造函数
      this.instance = new Single(name);
    }
    return this.instance;
  }
}

let single1 = Single.getInstance("name1");
let single2 = Single.getInstance("name2");
console.log(single1 === single2);  // true

策略模式

根据不同参数命中不同的策略

应用案例:表单验证

策略模式的表单验证示例

// 策略对象
const strategies = {
  // 验证是否为空
  isNoEmpty: function(value, errorMsg) {
    if (value.trim() === "") {
      return errorMsg;
    }
  },
  // 验证最小长度
  minLength: function(value, length, errorMsg) {
    if (value.trim().length < length) {
      return errorMsg;
    }
  },
  // 验证最大长度
  maxLength: function(value, length, errorMsg) {
    if (value.length > length) {
      return errorMsg;
    }
  },
  // 验证手机号
  isMobile: function(value, errorMsg) {
    if (
      !/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(
        value
      )
    ) {
      return errorMsg;
    }
  }
};

// 验证类
class Validator {
  constructor() {
    this.cache = []; // 存储要验证的方法
    this.errList = []; // 存储最终的验证结果
  }
  add(value, rules) {
    for (let i = 0, rule; (rule = rules[i++]); ) {
      let strategyAry = rule.strategy.split(":");
      let errorMsg = rule.errorMsg;
      this.cache.push(() => {
        let strategy = strategyAry.shift();
        strategyAry.unshift(value);
        strategyAry.push(errorMsg);
        // 执行策略对象中的不同验证规则
        let error = strategies[strategy](...strategyAry);
        if (error) {
          this.errList.push(error);
        }
      });
    }
  }
  start() {
    for (let i = 0, validatorFunc; (validatorFunc = this.cache[i++]); ) {
      validatorFunc();
    }
    return this.errList;
  }
}

let validataFunc = function(info) {
  let validator = new Validator();
  validator.add(info.userName, [
    {
      strategy: "isNoEmpty",
      errorMsg: "用户名不可为空"
    },
    {
      strategy: "minLength:2",
      errorMsg: "用户名长度不能小于2位"
    }
  ]);
  validator.add(info.password, [
    {
      strategy: "minLength:6",
      errorMsg: "密码长度不能小于6位"
    }
  ]);
  validator.add(info.phoneNumber, [
    {
      strategy: "isMobile",
      errorMsg: "请输入正确的手机号码格式"
    }
  ]);
  return validator.start();
};

// 需要验证表单的对象
let userInfo = {
  userName: "王",
  password: "1234",
  phoneNumber: "666"
};
let errorMsg = validataFunc(userInfo);
console.log(errorMsg); // ['用户名长度不能小于2位', '密码长度不能小于6位', '请输入正确的手机号码格式']

代理模式

代理对象和本体对象具有一致的接口

应用案例:图片预加载

图片代理模式示例

// 代理模式
let relImage = (function() {
  let imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {
    setSrc(src) {
      imgNode.src = src;
    }
  };
})();
let proxyImage = (function() {
  let img = new Image();
  // 实际要加载的图片 加载成功后 替换调占位图
  img.onload = function() {
    relImage.setSrc(img.src);
  };
  return {
    setSrc(src) {
      img.src = src;
      // 设置占位图
      relImage.setSrc(
        "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
      );
    }
  };
})();

// 设置实际要加载的图片
proxyImage.setSrc(
  "https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg"
);

装饰者模式

在不改变对象自身的基础上,动态地给某个对象添加一些额外的职责

应用案例:在函数执行前后添加新的方法

装饰者模式示例

function fuc() {
  console.log(2);
}
Function.prototype.before = function(beFn) {
  let self = this;
  return function() {
    beFn.apply(this, arguments); // 先执行插入到前面的方法,类似于二叉树的前序遍历
    return self.apply(this, arguments); // 后执行当前的方法
  };
};
Function.prototype.after = function(afFn) {
  let self = this;
  return function() {
    self.apply(this, arguments); // 先执行当前的方法
    return afFn.apply(this, arguments); // 后执行插入到后面的方法
  };
};

function fuc1() {
  console.log(1);
}
function fuc3() {
  console.log(3);
}
function fuc4() {
  console.log(4);
}

fuc = fuc.before(fuc1).before(fuc4).after(fuc3);
fuc();

// 最终打印结果:4 1 2 3

组合模式

组合模式在对象间形成树形结构
组合模式中基本对象和组合对象被一致对待
无须关心对象有多少层, 调用时只需在根部进行调用

应用案例: 打印文件目录

函数组合模式示例

class Combine {
  constructor() {
    this.list = [];
  }
  add(fn) {
    this.list.push(fn);
    return this; // 链式调用
  }
  excute() {
    for (let i = 0; i < this.list.length; i++) {
      this.list[i].excute();
    }
  }
}
let comb1 = new Combine();
comb1
  .add({
    excute() {
      console.log(1);
    }
  })
  .add({
    excute() {
      console.log(2);
    }
  });

let comb2 = new Combine();
comb2
  .add({
    excute() {
      console.log(3);
    }
  })
  .add({
    excute() {
      console.log(4);
    }
  });

let comb3 = new Combine();
comb3
  .add({
    excute() {
      console.log(5);
    }
  })
  .add({
    excute() {
      console.log(6);
    }
  });
comb2.add(comb3);

let comb4 = new Combine();
comb4.add(comb1).add(comb2);
comb4.excute();

// 最终打印结果:1 2 3 4 5 6

工厂模式

工厂模式是用来创建对象的一种最常用的设计模式

不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,这个函数就可以被视为一个工厂

应用案例: jquery中的window.$

工厂模式示例

class Car {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }
}
class Factory {
  static create(type) {
    switch (type) {
      case "car":
        return new Car("汽车", "白色");
        break;
      case "bicycle":
        return new Car("自行车", "黑色");
        break;
      default:
        console.log("没有该类型");
    }
  }
}
let p1 = Factory.create("car");
let p2 = Factory.create("bicycle");
console.log(p1, p1 instanceof Car); // {name: '汽车', color: '白色'} true
console.log(p2, p2 instanceof Car); // {name: '自行车', color: '黑色'} true

访问者模式

在不改变该对象的前提下访问其结构中元素的新方法

应用案例:babel插件

访问者模式示例

// 元素类
class Student {
  constructor(name, chinese, math, english) {
    this.name = name;
    this.chinese = chinese;
    this.math = math;
    this.english = english;
  }

  accept(visitor) {
    visitor.visit(this);
  }
}

// 访问者类
class ChineseTeacher {
  visit(student) {
    console.log(`语文${student.chinese}`);
  }
}

class MathTeacher {
  visit(student) {
    console.log(`数学${student.math}`);
  }
}

class EnglishTeacher {
  visit(student) {
    console.log(`英语${student.english}`);
  }
}

// 实例化元素类
const student = new Student("张三", 90, 80, 60);
// 实例化访问者类
const chineseTeacher = new ChineseTeacher();
const mathTeacher = new MathTeacher();
const englishTeacher = new EnglishTeacher();
// 接受访问
student.accept(chineseTeacher); // 语文90
student.accept(mathTeacher); // 数学80
student.accept(englishTeacher); // 英语60

发布订阅模式

订阅者订阅相关主题,发布者通过发布主题事件的方式,通知订阅该主题的对象

应用案例:EventBus

手写发布订阅模式示例

// 发布订阅模式
class EventBus {
  constructor() {
    this.task = {};
  }
  on(type, fn) {
    // on 注册事件
    if (!this.task[type]) this.task[type] = [];
    this.task[type].push(fn);
  }
  emit(type, ...args) {
    // emit 发送事件
    if (this.task[type]) {
      this.task[type].forEach(fn => {
        fn.apply(this, args); // 注意this指向
      });
    }
  }
  off(type, fn) {
    // 删除事件
    if (this.task[type]) {
      this.task[type] = this.task[type].filter(item => item !== fn);
    }
  }
  once(type, fn) {
    // 只执行一次
    function f(...args) {
      fn(...args);
      this.off(type, f);
    }
    this.on(type, f);
  }
}

// 测试
let event = new EventBus();
event.on("change", (...args) => {
  console.log(args);
});
// 只执行一次
event.once("change", (...args) => {
  console.log(args);
});
event.emit("change", 1, 2);
event.emit("change", 2, 3);

观察者模式

一个对象有一系列依赖于它的观察者(watcher),当对象发生变化时,会通知观察者进行更新

应用案例: vue 双向绑定

观察者模式示例

let data = {
  name: "ming",
  age: 18
};
Object.keys(data).forEach(key => {
  let value = data[key];
  Object.defineProperty(data, key, {
    get() {
      console.log("get", value);
      return value;
    },
    set(newValue) {
      console.log("更新");
      value = newValue;
    }
  });
});
data.name = "佩奇";
console.log(data.name);

// 依次打印: 更新 → get 佩奇 → 佩奇

观察者与发布订阅模式的区别

观察者模式:一个对象有一系列依赖于它的观察者(watcher),当对象发生变化时,会通知观察者进行更新

发布订阅模式:订阅者订阅相关主题,发布者通过发布主题事件的方式通知订阅该主题的对象,发布订阅模式中可以基于不同的主题去执行不同的自定义事件

javaScript设计模式统计
JavaScript 中常见设计模式整理

Web Worker

让前端拥有后端的计算能力

在HTML5的新规范中,实现了 Web Worker 来引入 js 的 多线程 技术, 可以让我们在页面主运行的js线程中,加载运行另外单独的一个或者多个 js线程

Web Worker专门处理复杂计算的,从此让前端拥有后端的计算能力

页面大量计算,造成假死

浏览器有GUI渲染线程与JS引擎线程,这两个线程是互斥的关系

当js有大量计算时,会造成UI 阻塞,出现界面卡顿、掉帧等情况,严重时会出现页面卡死的情况,俗称假死

Web Worker使用案例

计算十万条数据,计算时长从35s变成6s,并且全程无卡顿

在Vue中 使用 Web Worker

web worker提高Canvas运行速度

web worker除了单纯进行计算外,还可以结合离屏canvas进行绘图,提升绘图的渲染性能和使用体验

web worker 提高Canvas运行速度

计算时长超过多久适合用Web Worker

原则:

运算时间超过50ms会造成页面卡顿,属于Long task,这种情况就可以考虑使用Web Worker

但还要先考虑通信时长的问题,假如一个运算执行时长为100ms, 但是通信时长为300ms, 用了Web Worker可能会更慢

最终标准:
计算的运算时长 - 通信时长 > 50ms,推荐使用Web Worker

如何让前端拥有后端的计算能力?一文彻底了解Web Worker

沙箱(Sandbox)

沙箱(Sandbox),就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响

Chrome浏览器打开的每个页面就是一个沙箱,保证彼此独立互不影响

JS中沙箱的使用场景

1)执行 JSONP 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码

2)Vue模板表达式的计算是运行在一个沙盒之中的,在模板字符串中的表达式只能获取部分全局对象,这一点官方文档有提到,详情可参阅源码

如何实现一个 JS 沙箱?

要实现一个沙箱,需要去制定一套程序执行机制,在这套机制的作用下沙箱内部程序的运行不会影响到外部程序的运行

with

with的作用:在于改变作用域;with语句将某个对象添加到作用域链的顶部

沙箱要求
要实现这样一个沙箱,要求程序中访问的所有变量均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值

非常简陋的沙箱示例

// 定义全局变量foo
var foo = "foo1";

// 执行上下文对象
const ctx = {
  func: variable => {
    console.log(variable);
  },
  foo: "f1"
};

// 非常简陋的沙箱
function veryPoorSandbox(code, ctx) {
  // 使用with,将eval函数执行时的执行上下文指定为ctx
  with (ctx) {
    // eval可以将字符串按js代码执行,如eval('1+2')
    eval(code);
  }
}

// 待执行程序
const code = `func(foo)`;

veryPoorSandbox(code, ctx); 
// 打印结果:"f1",不是最外层的全局变量"foo1"

这个沙箱有一个明显的问题,若提供的上下文对象中没有找到某个变量时,代码仍会沿着作用域链一层一层向上查找,这样的一个沙箱仍然无法控制内部代码的执行

假如上文示例中的ctx对象没有设置foo属性,打印的结果还是外层作用域的foo1

With + Proxy

希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量则提示对应的错误

Proxy中的get和set方法,只能拦截已存在于代理对象中的属性,对于代理对象中不存在的属性这两个钩子是无感知的。因此这里我们使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问,并设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量,会继续判断是否存在沙箱自行维护的上下文对象中,存在则正常访问,不存在则直接报错

这里使用new Function替代eval,new Function与eval的区别

使用 new Function() 运行代码比eval更为好一些:函数的参数提供了清晰的接口来运行代码,而没有必要使用较为笨拙的语法来间接的调用eval()

重写上面的示例

var foo = "foo1";

// 执行上下文对象
const ctx = {
  func: variable => {
    console.log(variable);
  }
};

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
  code = "with(shadow) {" + code + "}";
  return new Function("shadow", code);
}

// 可访问全局作用域的白名单列表
const access_white_list = ["func"];

// 待执行程序
const code = `func(foo)`;

// 执行上下文对象的代理对象
const ctxProxy = new Proxy(ctx, {
  has: (target, prop) => {
    // has 可以拦截 with 代码块中任意属性的访问
    if (access_white_list.includes(prop)) {
      // 在可访问的白名单内,可继续向上查找
      return target.hasOwnProperty(prop);
    }
    if (!target.hasOwnProperty(prop)) {
      throw new Error(`Not found - ${prop}!`);
    }
    return true;
  }
});

// 没那么简陋的沙箱
function littlePoorSandbox(code, ctx) {
  // 将 this 指向手动构造的全局代理对象
  withedYourCode(code).call(ctx, ctx); 
}
littlePoorSandbox(code, ctxProxy);

// 执行func(foo),报错: Uncaught Error: Not found - foo!

执行结果

执行func(foo)函数时,会报错Uncaught Error: Not found - foo!

达到预期效果:如果上下文对象中不存在该变量则提示对应的错误 error.jpg

天然的优质沙箱(iframe)

iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离

利用iframe来实现一个沙箱是目前最方便、简单、安全的方法

可以把iframe.contentWindow作为当前沙箱执行的全局对象

利用iframe实现沙箱的示例

// 沙箱全局代理对象类
class SandboxGlobalProxy {
  constructor(sharedState) {
    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
    const iframe = document.createElement("iframe", { url: "about:blank" });
    iframe.style.display = "none";
    document.body.appendChild(iframe);
    const sandboxGlobal = iframe.contentWindow; // 沙箱运行时的全局对象

    return new Proxy(sandboxGlobal, {
      has: (target, prop) => {
        // has 可以拦截 with 代码块中任意属性的访问
        if (sharedState.includes(prop)) {
          // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
          return false;
        }
        if (!target.hasOwnProperty(prop)) {
          throw new Error(`Not find - ${prop}!`);
        }
        return true;
      }
    });
  }
}

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
  code = "with(sandbox) {" + code + "}";
  return new Function("sandbox", code);
}
function maybeAvailableSandbox(code, ctx) {
  withedYourCode(code).call(ctx, ctx);
}

const code_1 = `
  console.log(history == window.history) // false
  window.abc = 'sandbox'
  Object.prototype.toString = () => {
      console.log('Traped!')
  }
  console.log(window.abc) // sandbox
`;

const sharedGlobal_1 = ["history"]; // 希望与外部执行环境共享的全局对象

const globalProxy_1 = new SandboxGlobalProxy(sharedGlobal_1);

maybeAvailableSandbox(code_1, globalProxy_1);

// 对外层的window对象没有影响
console.log(window.abc); // undefined
Object.prototype.toString(); // 并没有打印 Traped

思考题:设计一个环境,要求不能操作dom、不能调接口,该如何设计呢?

说说JS中的沙箱
浅析 JavaScript 沙箱机制
动手写 js 沙箱

JSBridge

随着移动端盛行,不管是混合开发(Hybrid)应用,还是 React-Native 都离不开 JSBridge,当然也包括在国内举足轻重的微信小程序

JSBridge的作用

通过JSBridge可以实现 H5 和 原生之间的双向通信,主要是给 H5 提供调用 原生(Native)功能的接口,让混合开发中的 H5 可以方便地使用地址位置、摄像头甚至支付等原生功能

jsbridge.png

JSBridge 的通信原理

主要有两种:注入 API 和 拦截 URL SCHEME

注入API

注入 API 方式是最常用的方式,主要原理是通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

拦截 URL SCHEME

先解释一下 URL SCHEME:URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的

例如打开微信扫码的SCHEME:weixin://scanqrcode
protocol 是 weixin,host 则是 scanqrcode

拦截 URL SCHEME 的主要流程

Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求,并根据 URL SCHEME(包括所带的参数)进行相关操作(类似JSONP的方式)

URL SCHEME的缺陷

1)使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患
2)创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长

注入API时,H5端的代码

1)初始化 WebViewJavascriptBridge

// 根据navigator.userAgent来判断当前是 Android 还是 ios
const u = navigator.userAgent;
// Android终端
const isAndroid = u.indexOf("Android") > -1 || u.indexOf("Adr") > -1;
// IOS 终端
const isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);

/**
 * 配合 IOS 使用时的初始化方法
 */
const iosFunction = callback => {
  if (window.WebViewJavascriptBridge) {
    return callback(window.WebViewJavascriptBridge);
  }
  if (window.WVJBCallbacks) {
    return window.WVJBCallbacks.push(callback);
  }
  window.WVJBCallbacks = [callback];
  var WVJBIframe = document.createElement("iframe");
  WVJBIframe.style.display = "none";
  WVJBIframe.src = "demo://__BRIDGE_LOADED__";
  document.documentElement.appendChild(WVJBIframe);
  setTimeout(function() {
    document.documentElement.removeChild(WVJBIframe);
  }, 0);
};

/**
 * 配合 Android 使用时的初始化方法
 */
const androidFunction = callback => {
  if (window.WebViewJavascriptBridge) {
    callback(window.WebViewJavascriptBridge);
  } else {
    document.addEventListener(
      "WebViewJavascriptBridgeReady",
      function() {
        callback(window.WebViewJavascriptBridge);
      },
      false
    );
  }
};

window.setupWebViewJavascriptBridge = isAndroid ? androidFunction : iosFunction;

isAndroid &&
  window.setupWebViewJavascriptBridge(function(bridge) {
    // 注册 H5 界面的默认接收函数
    bridge.init(function(msg, responseCallback) {
      responseCallback("JS 返回给原生的消息内容");
    });
  });

2)注册与原生交互的事件函数

// bridge.registerHandler('事件函数名',fun 执行函数);
window.setupWebViewJavascriptBridge(bridge => {
  // data:原生传过来的数据; 
  // callback: 原生传过来的回调函数
  bridge.registerHandler("H5Function", (data, callback) => {
    callback && callback();
  });
});

3)调用原生注册的事件函数

// bridge.callHandler('安卓端函数名', "传给原生端的数据", callback 回调函数);
window.setupWebViewJavascriptBridge(bridge => {
  bridge.callHandler("changeData", data, result => {
    console.log(result);
  });
});

使用 JSBridge 与原生 IOS、Android 进行交互
JSBridge的原理

手写JS面试题

除了上文JS基础中提到的一些手写题外,另外补充以下题目,这些是面试中经常会遇到,也是一个优秀前端工程师的必备技巧

reduce函数

reduce的参数说明,reduce(callbackFn, initialValue)

1)callbackFn接收4个参数,reduce((pre,cur, index, array) => {})
pre累加器、cur当前值、 index当前下标、array用于遍历的数组

2)initialValue作为reduce方法的初始值
reduce函数内部判断initialValue是否存在,不存在,需要找到数组中第一个存在的值作为初始值

手写reduce函数

// 如果提供了initialValue时,则作为pre的初始值,index从0开始; 
// 如果没有提供initialValue,找到数组中的第一个存在的值作为pre,下一个元素的下标作为index

Array.prototype.myReduce = function(fn, initialValue) {
  let pre, index;
  let arr = this.slice();
  if (initialValue === undefined) {
    // 没有设置初始值
    for (let i = 0; i < arr.length; i++) {
      // 找到数组中第一个存在的元素,跳过稀疏数组中的空值
      if (!arr.hasOwnProperty(i)) continue;
      pre = arr[i]; // pre 为数组中第一个存在的元素
      index = i + 1; // index 下一个元素
      break; // 易错点:找到后跳出循环
    }
  } else {
    index = 0;
    pre = initialValue;
  }
  for (let i = index; i < arr.length; i++) {
    // 跳过稀疏数组中的空值
    if (!arr.hasOwnProperty(i)) continue;
    // 注意:fn函数接收四个参数,pre之前累计值、cur 当前值、 当前下标、 arr 原数组
    pre = fn.call(null, pre, arr[i], i, this);
  }
  return pre;
};
console.log([, , , 1, 2, 3, 4].myReduce((pre, cur) => pre + cur)); // 10

compose

函数式编程当中有一个很重要的概念就是函数组合,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果

在多个框架源码中都有用到,比如reduxkoa 中多次遇到这个方法

效果: 将一系列函数,通过compose函数组合起来,像管道一样连接起来,比如函数结合[f, g, h ],通过compose最终达到这样的效果: f(g(h()))

compose函数要求:可执行同步方法,也可执行异步方法,两者都可以兼容

手写compose函数

function compose(list) {
  // 取出第一个函数,当做reduce函数的初始值
  const init = list.shift();
  return function(...arg) {
    // 执行compose函数,返回一个函数
    return list.reduce(
      (pre, cur) => {
        // 返回list.reduce的结果,为一个promise实例,外部就可以通过then获取
        return pre.then(result => {
          // pre始终为一个promise实例,result为结果的累加值
          // 在前一个函数的then中,执行当前的函数,并返回一个promise实例,实现累加传递的效果
          return cur.call(null, result); 
        });
      },
      // Promise.resolve可以将非promise实例转为promise实例(一种兼容处理)
      Promise.resolve(init.apply(null, arg))
    );
  };
}

// 同步方法案例
let sync1 = data => {
  console.log("sync1");
  return data;
};
let sync2 = data => {
  console.log("sync2");
  return data + 1;
};
let sync3 = data => {
  console.log("sync3");
  return data + 2;
};
let syncFn = compose([sync1, sync2, sync3]);
syncFn(0).then(res => {
  console.log(res);
});
// 依次打印 sync1 → sync2 → sync3 → 3

// 异步方法案例
let async1 = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("async1");
      resolve(data);
    }, 1000);
  });
};
let async2 = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("async2");
      resolve(data + 1);
    }, 1000);
  });
};
let async3 = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("async3");
      resolve(data + 2);
    }, 1000);
  });
};
let composeFn = compose([async1, async2, async3]);
composeFn(0).then(res => {
  console.log(res);
});
// 依次打印 async1 → async1 → async1 → 3

数组扁平化

deep用来控制扁平的层数,默认为1

手写数组扁平化

// deep初始值为1
Array.prototype.myFlat = function(deep = 1) {
  let arr = this;
  // deep为0则返回,递归结束
  if (deep == 0) return arr;
  // 使用reduce作为累加器
  return arr.reduce((pre, cur) => {
    // cur为数组,继续递归,deep-1
    if (Array.isArray(cur)) {
      return [...pre, ...cur.myFlat(deep - 1)];
    } else {
      return [...pre, cur];
    }
  }, []);
};
console.log([1, 2, 3, [4, [5, [6]]]].myFlat(2)); // [1, 2, 3, 4, 5, [6]]

map 函数实现

map中的第二个参数作为第一个参数的this

注意:需要判断稀疏数组,跳过稀疏数组中的空值

手写map 函数

// fn 接受3个参数,element 当前正在处理的元素、index 正在处理的元素在数组中的索引、array 调用了 map() 的数组本身
// map中的第二个参数作为fn函数的this
Array.prototype.selfMap = function(fn, content) {
  if (Object.prototype.toString.call(fn) != '[object Function]') {
    throw new TypeError(`${fn} is not a function `);
  }
  // Array.prototype.map() 函数的第二个参数只有为 null 或 undefined 时,回调函数中的 this 值才会指向全局对象
  if (content === null || content === undefined) {
    content = window;
  }
    let arr = this.slice();
  let list = new Array(arr.length);
  for (let i = 0; i < arr.length; i++) {
    // 跳过稀疏数组
    if (i in arr) {
      // 依次传入this, 当前项,当前索引,整个数组
      list[i] = fn.call(content, arr[i], i, arr);
    }
  }
  return mappedArr;
};
let arr = [1, 2, 3];
console.log(arr.selfMap(item => item * 2)); // [2, 4, 6]

some 函数实现

some()方法用于检测数组中的元素是否满足指定条件(函数提供)

如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测;如果没有满足条件的元素,则返回false

手写some函数

Array.prototype.mySome = function(fn) {
  let result = false;
  for (let i = 0; i < this.length; i++) {
    // 判断条件是否满足,满足跳出循环
    if (fn(this[i])) {
      result = true;
      break;
    }
  }
  return result;
};
console.log([1, 2, 3, 4].mySome(item => item > 6)); // false

判断所有数据类型的方法

通过Object.prototype.toString.call实现

示例

function getDataType(target) {
  return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
// 判断所有的数据类型
console.log(getDataType(null)); // null
console.log(getDataType(undefined)); // undefined
console.log(getDataType(Symbol())); // symbol
console.log(getDataType(new Date())); // date
console.log(getDataType(new Set())); // set

实现es6模板字符串

replace函数,第二个参数是函数的情况说明:每个匹配都调用该函数,它返回的字符串将替换文本使用

示例

let name = "小明";
let age = 20;
let str1 = "我叫${name},我的年龄 ${ age}";
function tempalteStr(str) {
  return str.replace(/\$\{(.*?)\}/g, function(str, k) {
    // eval(name) 替换成 小明
    // // eval(age) 替换成 20
    return eval(k);
  });
}
console.log(tempalteStr(str1)); // 我叫小明,我的年龄20

函数柯里化

函数柯里化: 将使用多个参数的一个函数,转换成一系列使用一个参数的函数

函数柯里化的原理: 用闭包把参数保存起来,当参数的长度等于原函数时,就开始执行原函数

示例

function mycurry(fn) {
  // fn.length 表示函数中参数的长度
  // 函数的length属性,表示形参的个数,不包含剩余参数,仅包括第一个有默认值之前的参数个数(不包含有默认值的参数)
  if (fn.length <= 1) return fn;
  // 自定义generator迭代器
  const generator = (...args) => {
    // 判断已传的参数与函数定义的参数个数是否相等
    if (fn.length === args.length) {
      return fn(...args);
    } else {
      // 不相等,继续迭代
      return (...args1) => {
        return generator(...args, ...args1);
      };
    }
  };
  return generator;
}
function fn(a, b, c, d) {
  return a + b + c + d;
}
let fn1 = mycurry(fn);
console.log(fn1(1)(2)(3)(4)); // 10

函数防抖

应用场景:搜索框输入文字后调用对应搜索接口

利用闭包,不管触发频率多高,在停止触发n秒后才会执行,如果重复触发,会清空之前的定时器,重新计时,直到最后一次n秒后执行

示例

/*
 * @param {function} fn - 需要防抖的函数
 * @param {number} time - 多长时间执行一次
 * @param {boolean} flag - 第一次是否执行
 */
function debounce(fn, time, flag) {
  let timer;
  return function(...args) {
    // 在time时间段内重复执行,会清空之前的定时器,然后重新计时
    timer && clearTimeout(timer);
    if (flag && !timer) {
      // flag为true 第一次默认执行
      fn.apply(this, args);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, time);
  };
}

function fn(a) {
  console.log("执行:", a);
}
let debounceFn = debounce(fn, 3000, true);
debounceFn(1);
debounceFn(2);
debounceFn(3);

// 先打印:执行: 1  
// 3s后打印: 执行: 3

函数节流

应用场景: 下拉滚动加载

利用闭包,不管触发频率多高,每隔一段时间内执行一次

示例

/*
 * @param {function} fn - 需要防抖的函数
 * @param {number} time - 多长时间执行一次
 * @param {boolean} flag - 第一次是否执行
 */
function throttle(fn, time, flag) {
  let timer;
  return function(...args) {
    // flag控制第一次是否立即执行
    if (flag) {
      fn.apply(this, args);
      // 第一次执行完后,flag变为false;否则以后每次都会执行
      flag = false;
    }
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        clearTimeout(timer);
        // 每次执行完重置timer
        timer = null;
      }, time);
    }
  };
}

// 测试
function fn() {
  console.log("fn");
}
let throttleFn = throttle(fn, 3000, true);
setInterval(throttleFn, 500);

// 测试结果,一开始就打印"fn", 以后每隔3s打印一次"fn"

render函数

虚拟dom转化为真实dom

示例

// 虚拟dom转化为真实dom
function render(node) {
  if (typeof node === "string") {
    // 创建文本节点
    return document.createTextNode(node);
  }
  // 创建对应的dom节点
  let dom = document.createElement(node.tag);
  if (node.attrs) {
    // 设置dom属性
    Object.keys(node.attrs).forEach(key => {
      dom.setAttribute(key, node.attrs[key]);
    });
  }
  // 递归生成子节点
  if (node.children) {
    node.children.forEach(item => {
      dom.appendChild(render(item));
    });
  }
  return dom;
}

dom To JSON

将真实dom转化为虚拟dom

示例

 // 将真实dom转化为虚拟dom
function domToJson(node) {
  let obj = {};

  // 存储节点名称和节点类型
  obj.nodeName = node.nodeName;
  obj.nodeType = node.nodeType;

  // 存储节点的属性
  if (node.attributes && node.attributes.length > 0) {
    obj.attributes = {};

    for (let i = 0; i < node.attributes.length; i++) {
      let attr = node.attributes[i];
      obj.attributes[attr.nodeName] = attr.nodeValue;
    }
  }

  // 存储节点的子节点
  if (node.childNodes && node.childNodes.length > 0) {
    obj.childNodes = [];

    for (let i = 0; i < node.childNodes.length; i++) {
      let child = node.childNodes[i];
      // nodeType 为1表示元素节点,3为文本节点
      if (child.nodeType === 1) {
        obj.childNodes.push(domToJson(child));
      } else if (child.nodeType === 3) {
        obj.childNodes.push(child.nodeValue);
      }
    }
  }

  return obj;
}

图片懒加载

图片的懒加载原理: 当图片元素出现在屏幕中时,才给图片的src赋值对应的链接,去加载对应的图片

使用IntersectionObserver监听元素来判断是否出现在视口,当图片出现在视口时,给img.src赋值

IntersectionObserver替代监听scroll事件来判断元素是否在视口中,性能更高

图片懒加载示例

// html内容
// <img src="./loading.jpg" data-src="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg">
// <img src="./loading.jpg" data-src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg">

function observerImg() {
  // 获取所有的图片元素
  let imgList = document.getElementsByTagName("img"); 
  let observer = new IntersectionObserver(list => {
    // 回调的数据是一个数组
    list.forEach(item => {
      // 判断元素是否出现在视口
      if (item.intersectionRatio > 0) {
        // 设置img的src属性
        item.target.src = item.target.getAttribute("data-src");
        // 设置src属性后,停止监听
        observer.unobserve(item.target);
      }
    });
  });
  for (let i = 0; i < imgList.length; i++) {
    // 监听每个img元素
    observer.observe(imgList[i]);
  }
}

IntersectionObserver API 使用教程

最大并发数

控制请求最大并发数,前面的请求成功后,再发起新的请求

示例

/*
 * 控制并发数
 * @param {array} list - 请求列表
 * @param {number} num - 最大并发数
 */
function control(list, num) {
  function fn() {
    if (!list.length) return;
    // 从任务数 和 num 中 取最小值,兼容并发数num > list.length的情况
    let max = Math.min(list.length, num);
    for (let i = 0; i < max; i++) {
      let f = list.shift();
      num--;
      // 请求完成后,num++
      f().finally(() => {
        num++;
        fn();
      });
    }
  }
  fn();
}

LazyMan

考察:事件轮询机制、链式调用、队列

示例

class LazyMan {
  constructor(name) {
    this.name = name;
    this.task = []; // 任务列表
    function fn() {
      console.log("hi" + this.name);
      this.next();
    }
    this.task.push(fn);
    // 重点:使用setTimeout宏任务,确保所有的任务都注册到task列表中
    setTimeout(() => {
      this.next();
    });
  }
  next() {
    // 取出第一个任务并执行
    let fn = this.task.shift();
    fn && fn.call(this);
  }
  sleepFirst(time) {
    function fn() {
      console.log("sleepFirst" + time);
      setTimeout(() => {
        this.next();
      }, time);
    }
    // 插入到第一个
    this.task.unshift(fn);
    // 返回this 可以链式调用
    return this;
  }
  sleep(time) {
    function fn() {
      console.log("sleep" + time);
      setTimeout(() => {
        this.next();
      }, time);
    }
    this.task.push(fn);
    return this;
  }
  eat(something) {
    function fn() {
      console.log("eat" + something);
      this.next();
    }
    this.task.push(fn);
    return this;
  }
}

new LazyMan("王")
  .sleepFirst(3000)
  .eat("breakfast")
  .sleep(3000)
  .eat("dinner");

sleep函数的多种实现

JS没有语言内置的休眠(sleep or wait)函数,所谓的sleep只是实现一种延迟执行的效果

等待指定时间后再执行对应方法

示例

// 方法一:
// 这种实现方式是利用一个伪死循环阻塞主线程。
// 因为JS是单线程的,所以通过这种方式可以实现真正意义上的sleep
function sleep1(fn, time) {
  let start = new Date().getTime();
  while (new Date().getTime() - start < time) {
    continue;
  }
  fn();
}

// 方式二: 定时器
function sleep2(fn, time) {
  setTimeout(fn, time);
}

// 方式三: promise
function sleep3(fn, time) {
  new Promise(resolve => {
    setTimeout(resolve, time);
  }).then(() => {
    fn();
  });
}

// 方式四: async await
async function sleep4(fn, time) {
  await new Promise(resolve => {
    setTimeout(resolve, time);
  });
  fn();
}
function fn() { console.log("fn")}

sleep1(fn, 2000);
sleep2(fn, 2000);
sleep3(fn, 2000);
sleep4(fn, 2000);

Ts 基础

TS 编程的好处

1、静态类型检查:TS引入了静态类型检查,可以在编译时捕获潜在的类型错误,从而减少运行时错误。通过类型检查,可以提高代码的可靠性和可维护性,并减少调试时间

2、更好的可读性和可维护性:TS通过类型注解使代码更加清晰易读,提供了更好的文档化和自我描述性。类型注解可以作为代码的文档,帮助开发人员理解和使用代码,尤其在大型项目中尤为重要。

3、IDE支持:TS有很好的集成开发环境(IDE)支持,例如Visual Studio Code等。IDE可以根据类型信息提供代码自动补全、导航和重构等功能,极大地提高开发效率。

4、提高团队协作:TS的类型系统可以明确定义接口、参数和返回值的类型约束,从而减少了团队成员之间的沟通成本,使得团队协作更加高效和准确。

5、渐进增强:TS是JavaScript的超集,这意味着可以将现有的JavaScript代码逐步迁移到TS中,而不需要一次性重写整个代码库。可以选择性地为现有代码添加类型注解,逐渐引入TS的好处,同时保留对现有代码的兼容性。

6、强大的生态系统和社区支持:TS拥有庞大的生态系统和活跃的社区支持,许多流行的JavaScript库和框架都提供了TS类型定义。这意味着可以在TS中无缝地使用这些库,并获得类型安全和更好的开发体验。此外,社区提供了大量的教程、文档和工具,方便开发者学习和解决问题。

总而言之,通过使用TS替换JS编程,可以获得静态类型检查、可读性、可维护性、IDE支持、团队协作、渐进增强以及强大的生态系统和社区支持等多个好处。这些好处使得TS成为许多开发者选择的首选语言,尤其是在大型项目和团队协作环境中。

interface 与 type 异同点

1、在对象扩展情况下,interface 使用 extends 关键字,而 type 使用交叉类型(&)。

// 继承接口
interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
}

const myDog: Dog = {
  name: "Buddy",
  age: 3,
  breed: "Labrador",
};

// 继承类型
type Animal = {
  name: string;
  age: number;
};

type Dog = Animal & {
  breed: string;
};

const myDog: Dog = {
  name: "Buddy",
  age: 3,
  breed: "Labrador",
};

2、同名的 interface 会自动合并,并且在合并时会要求兼容原接口的结构。

3、interface 与 type 都可以描述对象类型、函数类型、Class 类型,但 interface 无法像 type 那样表达元组、一组联合类型等等。

4、interface 无法使用映射类型等类型工具,也就意味着在类型编程场景中我们还是应该使用 type 。

5、type 语法更适合描述复杂类型的别名或联合类型,比如类型体操,interface 的语法可能更具可读性,特别是在描述对象的形状时

ts的代码打包成js代码后,还有类型校验的功能吗

当将 TypeScript 代码打包成 JavaScript 后,JavaScript 本身是没有类型系统的,因此类型校验的功能在打包后的 JavaScript 代码中是不存在的。

TypeScript 的类型系统是在编译时进行类型检查的,编译器会对 TypeScript 代码进行静态类型分析,并在编译过程中发现类型错误或不一致时给出相应的错误提示。这有助于开发人员在编写代码时尽早地发现潜在的类型问题,提高代码的可靠性和可维护性。

一旦 TypeScript 代码被编译成 JavaScript,类型信息将会被擦除,生成的 JavaScript 代码不再包含任何类型相关的信息。因此,在运行时无法进行类型校验。

如果你需要在 JavaScript 运行时进行类型校验,可以考虑使用其他运行时类型检查库,如PropTypes(用于React应用),Joi或Yup(用于Node.js应用),这些库提供了在运行时进行类型验证的能力。

ts 实现类型推断的原理

TypeScript的类型推断底层原理是基于编译器对代码的静态分析和类型推导。

在编译过程中,TypeScript编译器会遍历整个代码,并根据一系列的规则和算法进行类型推断。这些规则和算法包括:

1、上下文类型:当遇到表达式需要确定类型时,编译器会考虑该表达式所在的上下文环境,例如函数参数、赋值语句、返回语句等。通过分析上下文环境中的已知类型信息,编译器可以推断出表达式的类型。

2、类型注解:TypeScript中可以使用类型注解来明确地指定变量的类型。当变量具有类型注解时,编译器会直接采用注解中指定的类型,而不进行类型推断。

3、初始化赋值:编译器会根据变量的初始化赋值来推断其类型。当变量在声明时就被赋予了一个初始值,编译器会根据该初始值的类型来推断变量的类型。

4、控制流分析:编译器会进行控制流分析,分析变量在不同代码路径上的赋值情况,进而推断出变量的类型。例如,如果在某个代码路径上变量被赋值为数字类型,而在另一个代码路径上被赋值为字符串类型,编译器会将该变量推断为数字类型与字符串类型的联合类型。

5、类型兼容性:TypeScript的类型推断也会考虑类型兼容性规则。例如,当传递参数给一个函数时,编译器会根据函数的参数类型来推断实参的类型,并检查是否符合函数参数的期望类型。

Css 基础

BEM规范

BEM规范我觉得放到css这个模块讲比较合适

因为有了BEM,可以让css的编码变得有规范可循,使得css也变得整洁起来,拥有了很强的可维护性

这里以elementUI的BEM规范为例

BEM代表 块(block)、元素(element)、修饰符(modifier),三个部分结合使用,生成一套具有唯一性的class命名规范,起到样式隔离,避免css样式污染的作用

el-input , el-input__inner, el-input--mini

定义block

作用:给组件添加统一的el-前缀,通过@contentinclude{}中传递过来的内容导入到指定位置

@mixin b($block) {
  $B: $namespace+'-'+$block !global;  // 使用el-拼接组件名
  .#{$B} {
    @content;
  }
}

block示例

// 编译前
@include b(button) {
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
}

// 编译后
.el-button {
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
}

定义element

作用:
1)通过__连接符将父级选择器和传入的子元素拼接起来

2)通过hitAllSpecialNestRule函数判断父级选择器($selector: &),是否包含-- .is- 这三种字符

3)如果父级选择器包含这几种字符,输出父级选择器包含子元素的嵌套关系

@mixin e($element) {
  $E: $element !global;
  $selector: &;
  $currentSelector: "";
  @each $unit in $element { // $element传入的值可以单个,也可以是列表
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }

  @if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}

element示例

// 编译前
@include b(message-box) {
    color: blue;
    @include m(center) {
       padding-bottom: 30px;
    @include e(header) {
       padding-top: 30px;
    }
  }
}
// 编译后
.el-message-box {
    color: blue;
}
.el-message-box--center {
    padding-bottom: 30px; 
}
.el-message-box--center .el-message-box__header {
    padding-top: 30px;
}

定义modifier(修饰符)

通过--连接符将父级选择器和传入的修饰符拼接起来

@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

modifier示例

// 编译前
@include b(button) {
  display: inline-block;
  @include m(primary) {
    color:blue;
  }
}
// 编译后
.el-button {
  display: inline-block;
}
.el-button--primary {
  color:blue;
}

通过学习elementUI这套BEM规范,可以应用到自己的项目中,使得css编码也规范起来

var() 实现换肤

1)通过cssvar() 函数,定义颜色变量
2)css中引入var变量
3)需要换肤时,通过js修改body的颜色变量

换肤代码示例

  let style = {
    '--color-white': '#ffffff',
    '--color-black': '#000000'
  };
  for (let i in style) {
    document.body.style.setProperty(i, styleVar[i]);
  }

缺点是兼容性差一些

var.png

换肤方案有哪些?

rem+vw 布局

rem+vw布局是手机端常见的布局方案

rem布局

rem布局的原理:

本质是等比缩放,rem作用于根元素字体大小

1)假设屏幕宽度为750px,将屏幕平分为10份,1rem=75px,根元素的fontSize大小为75px

html {font-size: 75px}
div {width: 1rem} // div {width: 75px}

2)利用js动态的设置html的font-size

// 设置html的font-size 
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10'px'

rem布局的缺点

字体并不合适使用rem, 字体的大小和字体宽度,并不成线性关系,会出现随着屏幕的变大,字体变的越来越大,所以需要结合媒体查询来调整字体大小

rem+vw布局

优势

1)使用纯css的方式来实现,避免使用js动态计算html根元素font-size大小
2)结合使用媒体查询,解决宽屏下(如ipad)字体过大的问题

rem+vw布局的原理

1)设计稿为750px时,rootValue设置为75,则屏幕宽为10rem,1rem=75px,根元素的fontSize大小为75px

2)屏幕总共有100vw,所以1vw为7.5px ,10vw为75px, 得出1rem为10vw, 故得到根元素的fontSize为10vw

在项目入口文件中引入flexible.less中,flexible.less代码如下

@base_fontSize: 10vw;

html{
    font-size: @base_fontSize;
}
// 使用媒体查询,解决ipad屏幕下(宽屏)字体过大的问题
@media screen and (min-width: 560px) {  
    html{
        font-size: @base_fontSize * 0.7 
    }
}

link style @import及三者的区别

加载顺序的差别

1)当一个页面被加载的时候,link引用的CSS会同时被加载

2)而@import引用的CSS会等到页面全部被下载完再被加载

有时候浏览用@import加载CSS的页面时,可能会出现闪烁的情况

加载内容的区别

1)@import只能导入样式文件
2)link不仅可以引入样式,还可以引入js文件
3)style标签,它是定义在当前页面的样式

CSS3 硬件加速

CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案,可以提升网页的性能

开启GPU硬件加速的属性有:

1)transform不为none
2)opacity
3)filter
4)will-change

硬件加速的弊端

GPU处理过多的内容会导致内存问题;
不在动画结束的时候关闭硬件加速,会出现字体模糊

CSS动画开启硬件加速
这一次,彻底搞懂 GPU 和 css 硬件加速

css方面如何减少回流、重绘

1)可以使用GPU硬件加速

2)动画可以使用绝对定位或fixed,让其脱离文档流,修改动画不造成主界面的影响

3)使用 visibility 替换 display: none(前者只会引起重绘,后者则会引发回流)

4)避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

移动端实现1px

需要兼容不同的设备像素比

1)device-pixel-ratio 设备像素比 和resolution 分辨率 来区分的不同设备像素比

2)伪类 + scale缩放 来实现1px效果(包括圆角功能)

如果设备像素比为1,伪类不缩放
如果设备像素比为2,伪类缩放为0.5
如果设备像素比为3,伪类缩放为0.33

示例

// 使用scss语法实现
@mixin side-parse($color, $border:1px, $side:all, $radius:0, $style: solid) {
  @if ($side == all) {
    border:$border $style $color;
  } @else {
    border-#{$side}:$border $style $color;
  }
}
@mixin border-s1px($color, $border:1px, $side:all, $radius:0, $style: solid, $radius: 0){
  position: relative;
  &::after{
    content: '';
    position: absolute;
    pointer-events: none;
    top: 0; left: 0;
    border-radius: $radius;
    @include side-parse($color, $border, $side, $radius, $style);
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    -webkit-transform-origin: 0 0;
    transform-origin: 0 0;  // 默认值为50% 50%
    @media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx){
      width: 100%;
      height: 100%;
      border-radius: $radius;
    }
    @media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49),(min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49),(min-resolution: 144dpi) and (max-resolution: 239dpi),(min-resolution: 1.5dppx) and (max-resolution: 2.49dppx){
      width: 200%;
      height: 200%;
      transform: scale(.5);
      -webkit-transform: scale(.5);
      border-radius: $radius * 2;
    }
    @media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5),(min-device-pixel-ratio: 2.5), (min-resolution: 240dpi),(min-resolution: 2.5dppx){
      width: 300%;
      height: 300%;
      transform: scale(0.333);
      -webkit-transform: scale(0.333);
      border-radius: $radius * 3;
    }
  }
}

BFC 块级格式化上下文

BFC解决哪些问题

1)清除浮动,解决父元素高度塌陷
2)外边距重叠

创建BFC的4种方式

1)float属性不为none
2)position为absolute或fixed
3)display为inline-block、table-cell、table-caption、flex、inline-flex
4)overflow不为visible

最常用是overflow为hidden,这种方式的副作用最小,其他三种方式的副作用较大

什么是BFC

sticky 粘性布局

当元素在屏幕内,表现为relative,当就要滚出屏幕的时候,表现为fixed

随着页面的滚动,将元素固定在设置的位置(固定效果如同fixed),position:sticky可以看作是position:relative和position:fixed的结合体

sticky.gif

以下情况粘性布局会失效

1)父元素设置overflow:hidden
2)父元素高度不够或者高度为内部元素高度之和(总之没有剩余的高度,不会产生滚动)

杀了个回马枪,还是说说position:sticky吧

animation 动画

animation:动画名称 + 动画时间 + 速度曲线 + 是否延迟 + 动画次数 + 是否逆向播放

// linear 线性的  infinite 无穷的   alternate 逆向的
animation: mymove 2s linear infinite alternate;   

实现不间断播报

animation.gif

利用translate,修改内容在父元素中 y 轴的位置,来实现不间断播报效果

为了保证广播滚动效果的连贯性,防止滚动到最后一帧时没有内容,需要多添加一条重复数据进行填充

translate设置的高度为列表的总高度(不包含最后一条插入的数据)

示例

<html>
  <div class="container">
    <div class="ul">
      <div class="li">小王同学加入了凹凸实验室</div>
      <div class="li">小李同学加入了凹凸实验室</div>
      <div class="li">小赵同学加入了凹凸实验室</div>
      <div class="li">小马同学加入了凹凸实验室</div>
      <!-- 重复插入第一条数据 -->
      <div class="li">小王同学加入了凹凸实验室</div>
    </div>
  </div>
  <style>
    .container {
      height: 30px;
      overflow: hidden;
      background-color: #256def;
      color: #ffffff;
      width: 300px;
      border-radius: 30px;
      text-align: center;
    }
    .ul{
      animation: scroll 5s linear infinite;
    }
    .li{
      line-height: 30px;
      height: 30px;
    } 
    @keyframes scroll {
    0% {
       transform: translate(0,0)
    }
    100% {
      /* 120 = 4*30 不包含最后一条数据的总高度*/
      transform: translate(0,-120px)
    }
  }
  </style>
</html>

你可能不知道的Animation动画技巧与细节

文中所有的代码,都已放到github

总结

这些基础知识点,是我从这几年的学习经历中总结出来的,其中也包含了很多我遇到的面试题,常常需要温故而知新

越往后走,越发现基础知识的重要性,能让我们在工作中更快的解决问题,提出更好的方案

10w字总结的其他篇章

「历时8个月」10万字前端知识体系总结(算法篇)
「历时8个月」10万字前端知识体系总结(工程化篇)
「历时8个月」10万字前端知识体系总结(前端框架+浏览器原理篇)

程序猿.gif

文章系列

文章系列地址:github.com/xy-sea/blog

文中如有错误或不严谨的地方,请给予指正,十分感谢。如果喜欢或有所启发,欢迎 star