这篇文章源自我历时8个月,整理的前端知识体系与大厂面试笔记,不知不觉,已经超过了10万字
这些笔记帮助我从一个菜鸟,一步步蜕变为高级开发、前端“砖家”,并助力我拿到一些大厂的offer
同时,我将遇到过的面试题也都整理了进去,面试的公司包含:阿里、头条、美团、京东、网易、小米、叮咚买菜、喜马拉雅、货拉拉等
每次面试前,我都会花一周的时间去复习一遍这些知识点 ( 亲测保熟 ) 💕
文章面对的群体👨‍👩‍👧‍👧 :
1) 1到3年的初中级前端工程师
2) 备战大厂的朋友

前言

编程如逆水行舟,不进则退
入行这几年,焦虑与迷茫常伴左右,总会遇到各种瓶颈,不知道如何继续深入下去
这篇文章结合自己的学习与面试经历,整理出来一些学习路线,希望对小伙伴们有所启发
前端知识体系繁杂,文章中总结的知识点难免有所纰漏,希望大家多多指正,一起交流学习😊😘
前端知识体系分为4篇 基础知识篇算法篇工程化篇前端框架和浏览器原理篇 分享给大家
后面还有几万字关于 实战项目总结与收获 的笔记,正在整理中,会陆续发出来

前端知识体系导图

图片太大,就不展示了,点击可查看大图
下面,我们一起开始吧,升职加薪,YYDS!💪💪💪

JS 基础

执行上下文和执行栈

什么是执行上下文?
Javascript 代码都是在执行上下文中运行的
执行上下文: 指当前执行环境中的变量、函数声明、作用域链、this等信息
执行上下文生命周期
1)创建阶段
生成变量对象、建立作用域链、确定this的指向
2)执行阶段
变量赋值、函数的引用、执行其他代码
前端知识体系总结(基础知识篇) - 图1
变量对象
变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明
变量对象是一个抽象的概念,在全局执行上下文中,变量对象就是全局对象。 在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象

执行栈

是一种先进后出的数据结构,用来存储代码运行的所有执行上下文
1)当 JS 引擎第一次遇到js脚本时,会创建一个全局的执行上下文并且压入当前执行栈
2)每当JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部
3)当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文
4)一旦所有代码执行完毕,JS 引擎从当前栈中移除全局执行上下文
执行栈示例

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

前端知识体系总结(基础知识篇) - 图2

全局、函数、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声明的变量只在块级作用域内有效,示例

  1. function func() {
  2. if (true) {
  3. let i = 3;
  4. }
  5. console.log(i); // 报错 "i is not defined"
  6. }
  7. func();

作用域链

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

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

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

  1. let a = 10
  2. const b = 20
  3. function foo () {
  4. console.log(this.a) // undefined
  5. console.log(this.b) // undefined
  6. }
  7. foo();
  8. console.log(window.a) // undefined

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

  1. var a = 1
  2. function foo () {
  3. var a = 2
  4. console.log(this) // window
  5. console.log(this.a) // 1
  6. }
  7. foo()

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

  1. var obj2 = {
  2. a: 2,
  3. foo1: function () {
  4. console.log(this.a) // 2
  5. },
  6. foo2: function () {
  7. setTimeout(function () {
  8. console.log(this) // window
  9. console.log(this.a) // 3
  10. }, 0)
  11. }
  12. }
  13. var a = 3
  14. obj2.foo1()
  15. obj2.foo2()

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

  1. var obj = {
  2. name: 'obj',
  3. foo1: () => {
  4. console.log(this.name) // window
  5. },
  6. foo2: function () {
  7. console.log(this.name) // obj
  8. return () => {
  9. console.log(this.name) // obj
  10. }
  11. }
  12. }
  13. var name = 'window'
  14. obj.foo1()
  15. 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

  1. // 手写call
  2. Function.prototype.Call = function(context, ...args) {
  3. // context为undefined或null时,则this默认指向全局window
  4. if (!context || context === null) {
  5. context = window;
  6. }
  7. // 利用Symbol创建一个唯一的key值,防止新增加的属性与obj中的属性名重复
  8. let fn = Symbol();
  9. // this指向调用call的函数
  10. context[fn] = this;
  11. // 隐式绑定this,如执行obj.foo(), foo内的this指向obj
  12. let res = context[fn](...args);
  13. // 执行完以后,删除新增加的属性
  14. delete context[fn];
  15. return res;
  16. };
  17. // apply与call相似,只有第二个参数是一个数组,
  18. Function.prototype.Apply = function(context, args) {
  19. if (!context || context === null) {
  20. context = window;
  21. }
  22. let fn = Symbol();
  23. context[fn] = this;
  24. let res = context[fn](...args);
  25. delete context[fn];
  26. return res;
  27. };
  28. // bind要考虑返回的函数,作为构造函数被调用的情况
  29. Function.prototype.Bind = function(context, ...args) {
  30. if (!context || context === null) {
  31. context = window;
  32. }
  33. let fn = this;
  34. let f = Symbol();
  35. const result = function(...args1) {
  36. if (this instanceof fn) {
  37. // result如果作为构造函数被调用,this指向的是new出来的对象
  38. // this instanceof fn,判断new出来的对象是否为fn的实例
  39. this[f] = fn;
  40. let res = this[f](...args1, ...args);
  41. delete this[f];
  42. return res;
  43. } else {
  44. // bind返回的函数作为普通函数被调用时
  45. context[f] = fn;
  46. let res = context[f](...args1, ...args);
  47. delete context[f];
  48. return res;
  49. }
  50. };
  51. // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  52. // 实现继承的方式: 使用Object.create
  53. result.prototype = Object.create(fn.prototype);
  54. return result;
  55. };

闭包

闭包:就是函数引用了外部作用域的变量
闭包常见的两种情况:
一是函数作为返回值; 另一个是函数作为参数传递
闭包的作用:
可以让局部变量的值始终保持在内存中;对内部变量进行保护,使外部访问不到
最常见的案例:函数节流和防抖
闭包的垃圾回收:
副作用:不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收
闭包的示例

  1. // 原始题目
  2. for (var i = 0; i < 5; i++) {
  3. setTimeout(function() {
  4. console.log(i); // 1s后打印出5个5
  5. }, 1000);
  6. }
  7. // ⬅️利用闭包,将上述题目改成1s后,打印0,1,2,3,4
  8. // 方法一:
  9. for (var i = 0; i < 5; i++) {
  10. (function(j) {
  11. setTimeout(function timer() {
  12. console.log(j);
  13. }, 1000);
  14. })(i);
  15. }
  16. // 方法二:
  17. // 利用setTimeout的第三个参数,第三个参数将作为setTimeout第一个参数的参数
  18. for (var i = 0; i < 5; i++) {
  19. setTimeout(function fn(i) {
  20. console.log(i);
  21. }, 1000, i); // 第三个参数i,将作为fn的参数
  22. }
  23. // ⬅️将上述题目改成每间隔1s后,依次打印0,1,2,3,4
  24. for (var i = 0; i < 5; i++) {
  25. setTimeout(function fn(i) {
  26. console.log(i);
  27. }, 1000 * i, i);
  28. }

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

原型/原型链

原型的作用
原型被定义为给其它对象提供共享属性的对象,函数的实例可以共享原型上的属性和方法
原型链
它的作用就是当你在访问一个对象上属性的时候,如果该对象内部不存在这个属性,那么就会去它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)特殊的Object、Function

  1. console.log(Function.prototype === Function.__proto__); // true
  2. console.log(Object.__proto__ === Function.prototype); // true
  3. console.log(Function.prototype.__proto__ === Object.prototype); // true
  4. console.log(Object.prototype.__proto__ === null); // true

instanceof

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

  1. console.log(Object instanceof Object); //true
  2. console.log(Function instanceof Function); //true
  3. console.log(Function instanceof Object); //true
  4. console.log(function() {} instanceof Function); //true

手写instanceof方法

  1. function instanceOf(obj, fn) {
  2. let proto = obj.__proto__;
  3. if (proto) {
  4. if (proto === fn.prototype) {
  5. return true;
  6. } else {
  7. return instanceOf(proto, fn);
  8. }
  9. } else {
  10. return false;
  11. }
  12. }
  13. // 测试
  14. function Dog() {}
  15. let dog = new Dog();
  16. console.log(instanceOf(dog, Dog), instanceOf(dog, Object)); // true true

new 关键字

new一个对象,到底发生什么?
1)创建一个对象,该对象的原型指向构造函数的原型
2)调用该构造函数,构造函数的this指向新生成的对象
3)判断构造函数是否有返回值,如果有返回值且返回值是一个对象或一个方法,则返回该值;否则返回新生成的对象
构造函数有返回值的案例

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

手写new

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

继承

多种继承方式
1)原型链继承,缺点:引用类型的属性被所有实例共享
2)借用构造函数(经典继承)
3)原型式继承
4)寄生式继承
5)组合继承
6)寄生组合式继承
寄生组合式继承的优势
优势:借用父类的构造函数,在不需要生成父类实例的情况下,继承了父类原型上的属性和方法
手写寄生组合式继承

  1. // 精简版
  2. class Child {
  3. constructor() {
  4. // 调用父类的构造函数
  5. Parent.call(this);
  6. // 利用Object.create生成一个对象,新生成对象的原型是父类的原型,并将该对象作为子类构造函数的原型,继承了父类原型上的属性和方法
  7. Child.prototype = Object.create(Parent.prototype);
  8. // 原型对象的constructor指向子类的构造函数
  9. Child.prototype.constructor = Child;
  10. }
  11. }
  12. // 通用版
  13. function Parent(name) {
  14. this.name = name;
  15. }
  16. Parent.prototype.getName = function() {
  17. console.log(this.name);
  18. };
  19. function Child(name, age) {
  20. // 调用父类的构造函数
  21. Parent.call(this, name);
  22. this.age = age;
  23. }
  24. function createObj(o) {
  25. // 目的是为了继承父类原型上的属性和方法,在不需要实例化父类构造函数的情况下,避免生成父类的实例,如new Parent()
  26. function F() {}
  27. F.prototype = o;
  28. // 创建一个空对象,该对象原型指向父类的原型对象
  29. return new F();
  30. }
  31. // 等同于 Child.prototype = Object.create(Parent.prototype)
  32. Child.prototype = createObj(Parent.prototype);
  33. Child.prototype.constructor = Child;
  34. let child = new Child("tom", 12);
  35. child.getName(); // tom

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

Class 类

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

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

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

  1. class Point {
  2. constructor() {}
  3. toString() {}
  4. }
  5. // 等同于
  6. Point.prototype = { constructor() {}, toString() {} };

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

  1. class Foo {
  2. static classMethod() {
  3. return "hello";
  4. }
  5. }
  6. Foo.classMethod(); // 'hello'

4)实例属性的简写写法

  1. class Foo {
  2. bar = "hello";
  3. baz = "world";
  4. }
  5. // 等同于
  6. class Foo {
  7. constructor() {
  8. this.bar = "hello";
  9. this.baz = "world";
  10. }
  11. }

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

  1. class Parent {
  2. constructor(age) {
  3. this.age = age;
  4. }
  5. getName() {
  6. console.log(this.name);
  7. }
  8. }
  9. class Child extends Parent {
  10. constructor(name, age) {
  11. super(age);
  12. this.name = name;
  13. }
  14. }
  15. let child = new Child("li", 16);
  16. child.getName(); // li

手写Class类

ES6的 Class 内部是基于寄生组合式继承,它是目前最理想的继承方式
ES6的 Class 允许子类继承父类的静态方法和静态属性

  1. // Child 为子类的构造函数, Parent为父类的构造函数
  2. function selfClass(Child, Parent) {
  3. // Object.create 第二个参数,给生成的对象定义属性和属性描述符/访问器描述符
  4. Child.prototype = Object.create(Parent.prototype, {
  5. // 子类继承父类原型上的属性和方法
  6. constructor: {
  7. enumerable: false,
  8. configurable: false,
  9. writable: true,
  10. value: Child
  11. }
  12. });
  13. // 继承父类的静态属性和静态方法
  14. Object.setPrototypeOf(Child, Parent);
  15. }
  16. // 测试
  17. function Child() {
  18. this.name = 123;
  19. }
  20. function Parent() {}
  21. // 设置父类的静态方法getInfo
  22. Parent.getInfo = function() {
  23. console.log("info");
  24. };
  25. Parent.prototype.getName = function() {
  26. console.log(this.name);
  27. };
  28. selfClass(Child, Parent);
  29. Child.getInfo(); // info
  30. let tom = new Child();
  31. tom.getName(); // 123

Class 的基本语法

Promise

Promise的底层原理:callback回调函数 + 发布订阅模式

链式调用

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

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

手写promise

  1. class Promise {
  2. constructor(fn) {
  3. // resolve时的回调函数列表
  4. this.resolveTask = [];
  5. // reject时的回调函数列表
  6. this.rejectTask = [];
  7. // state记录当前状态,共有pending、fulfilled、rejected 3种状态
  8. this.state = "pending";
  9. let resolve = value => {
  10. // state状态只能改变一次,resolve和reject只会触发一种
  11. if (this.state !== "pending") return;
  12. this.state = "fulfilled";
  13. this.data = value;
  14. // 模拟异步,保证resolveTask事件先注册成功,要考虑在Promise里面写同步代码的情况
  15. setTimeout(() => {
  16. this.resolveTask.forEach(cb => cb(value));
  17. });
  18. };
  19. let reject = err => {
  20. if (this.state !== "pending") return;
  21. this.state = "rejected";
  22. this.error = err;
  23. // 保证rejectTask事件注册成功
  24. setTimeout(() => {
  25. this.rejectTask.forEach(cb => cb(err));
  26. });
  27. };
  28. // 关键代码,执行fn函数
  29. try {
  30. fn(resolve, reject);
  31. } catch (error) {
  32. reject(error);
  33. }
  34. }
  35. then(resolveCallback, rejectCallback) {
  36. // 解决链式调用的情况,继续返回Promise
  37. return new Promise((resolve, reject) => {
  38. // 将then传入的回调函数,注册到resolveTask中
  39. this.resolveTask.push(() => {
  40. // 重点:判断resolveCallback事件的返回值
  41. // 假如用户注册的resolveCallback事件又返回一个Promise,将resolve和reject传进去,这样就实现控制了链式调用的顺序
  42. const res = resolveCallback(this.data);
  43. if (res instanceof Promise) {
  44. res.then(resolve, reject);
  45. } else {
  46. // 假如返回值为普通值,resolve传递出去
  47. resolve(res);
  48. }
  49. });
  50. this.rejectTask.push(() => {
  51. // 同理:判断rejectCallback事件的返回值
  52. // 假如返回值为普通值,reject传递出去
  53. const res = rejectCallback(this.error);
  54. if (res instanceof Promise) {
  55. res.then(resolve, reject);
  56. } else {
  57. reject(res);
  58. }
  59. });
  60. });
  61. }
  62. }
  63. // 测试
  64. // 打印结果:依次打印1、2
  65. new Promise((resolve, reject) => {
  66. setTimeout(() => {
  67. resolve(1);
  68. }, 500);
  69. }).then(
  70. res => {
  71. console.log(res);
  72. return new Promise(resolve => {
  73. setTimeout(() => {
  74. resolve(2);
  75. }, 1000);
  76. });
  77. }
  78. ).then(data => {
  79. console.log(data);
  80. });

手写race、all

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

  1. class Promise {
  2. // race静态方法,返回promises列表中第一个执行完的结果
  3. static race(promises) {
  4. return new Promise((resolve, reject) => {
  5. for (let i = 0; i < promises.length; i++) {
  6. // Promise.resolve包一下,防止promises[i]不是Promise类型
  7. Promise.resolve(promises[i])
  8. .then(res => {
  9. resolve(res);
  10. })
  11. .catch(err => {
  12. reject(err);
  13. });
  14. }
  15. });
  16. }
  17. // all静态方法, 返回promises列表中全部执行完的结果
  18. static all(promises) {
  19. let result = [];
  20. let index = 0;
  21. return new Promise((resolve, reject) => {
  22. for (let i = 0; i < promises.length; i++) {
  23. Promise.resolve(promises[i])
  24. .then(res => {
  25. // 输出结果的顺序和promises的顺序一致
  26. result[i] = res;
  27. index++;
  28. if (index === promises.length) {
  29. resolve(result);
  30. }
  31. })
  32. .catch(err => {
  33. reject(err);
  34. });
  35. }
  36. });
  37. }
  38. }

史上最最最详细的手写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示例

  1. const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000));
  2. async function test() {
  3. const data = await getData();
  4. console.log("data: ", data);
  5. const data2 = await getData();
  6. console.log("data2: ", data2);
  7. return "success";
  8. }
  9. test().then(res => console.log(res))

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

深拷贝

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

手写深拷贝

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

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

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

  1. let obj = {};
  2. let target = {a: obj, b: obj};

手写深拷贝代码

  1. // 使用hash 存储已拷贝过的对象,避免循环拷贝和重复拷贝
  2. function deepClone(target, hash = new WeakMap()) {
  3. if (!isObject(target)) return target;
  4. if (hash.get(target)) return hash.get(target);
  5. // 兼容数组和对象
  6. let newObj = Array.isArray(target) ? [] : {};
  7. // 关键代码,解决对象的属性循环引用 和 多个属性引用同一个对象的问题,避免重复拷贝
  8. hash.set(target, newObj);
  9. for (let key in target) {
  10. if (target.hasOwnProperty(key)) {
  11. if (isObject(target[key])) {
  12. newObj[key] = deepClone(target[key], hash); // 递归拷贝
  13. } else {
  14. newObj[key] = target[key];
  15. }
  16. }
  17. }
  18. return newObj;
  19. }
  20. function isObject(target) {
  21. return typeof target === "object" && target !== null;
  22. }
  23. // 示例
  24. let info = { item: 1 };
  25. let obj = {
  26. key1: info,
  27. key2: info,
  28. list: [1, 2]
  29. };
  30. // 循环引用深拷贝示例
  31. obj.key3 = obj;
  32. let val = deepClone(obj);
  33. 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 与 浏览器更新渲染时机

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执行顺序早于微任务

定时器

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

  1. // 使用闭包实现
  2. function mySetInterval(fn, t) {
  3. let timer = null;
  4. function interval() {
  5. fn();
  6. timer = setTimeout(interval, t);
  7. }
  8. interval();
  9. return {
  10. // cancel用来清除定时器
  11. cancel() {
  12. clearTimeout(timer);
  13. }
  14. };
  15. }

setInterval模拟实现setTimeout

  1. function mySetTimeout(fn, time) {
  2. let timer = setInterval(() => {
  3. clearInterval(timer);
  4. fn();
  5. }, time);
  6. }
  7. // 使用
  8. mySetTimeout(() => {
  9. console.log(1);
  10. }, 2000);

作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。