前端面试常考问题准备

235 阅读28分钟

前端面试常考问题准备:美团三次面试考点全覆盖

本篇复习内容及资料参考了(《做了一份前端面试复习计划,保熟~》,原文链接:juejin.cn/post/706158… ,作者: vortesnail)也算是一篇在此文基础上的阅读笔记了吧~

1. Js基础

1. 数据类型

在 JS 中共有 8  种基础的数据类型,分别为: Undefined 、 Null 、 Boolean 、 Number 、 String 、 Object 、 Symbol 、 BigInt 。其中,Object对象类型,剩下的全部都是原始类型。Array, Function都属于Object

  • Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
  • BigInt 可以表示任意大小的整数。
    Js中,每个变量在内存中都需要一个独立空间来储存, 其中又分为:
  • 栈(stack)内存
    其特点是: a. 空间较小 b.可以直接操作其保存的变量,运行效率高 c. 由系统自动分配存储空间
    原始类型的值被直接存储在栈中。 由于栈中的内存空间的大小是固定的,那么注定了存储在栈中的变量就是不可变的。当我们更改原始类型的数值的时候,实际上是开辟了新的空间进行储存。这并不违背不可变性的特点。
  • 堆(heap)内存
    其特点是: a. 存储的值大小不定,可动态调整 b. 空间较大,运行效率低 c. 无法直接操作其内部存储,使用引用地址读取 d. 通过代码进行分配空间
    引用类型就不再具有不可变性, 引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值

数据类型的判断

  • typeof: 能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object。
  • instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。原理是顺着原型链去找。
  • Object.prototype.toString.call(): 所有原始数据类型都是能判断的,还有 Error 对象,Function, Array, Date, Math, Object等。
  • 判断数组? Array.isArray(arr) >>> true arr instanceof Array >>> true Object.prototype.toString.call(arr) >>> object Array arr.proto === Array.prototype >>> true

2. 赋值、深拷贝与浅拷贝

1. 赋值 Assignment


将一个变量的值赋给另一个变量,赋值操作后,两个变量指向同一块内存地址,修改其中一个变量的值会影响另一个变量。

let a = { name: 'John' };
let b = a;
b.name = 'Alice';
console.log(a); // { name: 'Alice' }
console.log(b); // { name: 'Alice' }

在上面的例子中,变量 ab 指向同一个对象,修改 b 的属性 name 也会同时修改 a 的属性 name。 对于对象同样可以赋值:

person = Person("John", 25)
person2 = person

person和person2变量都指向同一个对象。

2. 浅拷贝 Shallow Copy

只复制对象的一层属性,如果对象的属性是基本类型,就复制它的值,如果属性是引用类型,就复制它的引用。修改其中一个对象的属性会影响另一个对象的属性。 image.png 例:

let a = { name: 'John', age: 20 };
let b = Object.assign({}, a);
b.name = 'Alice';

console.log(a); // { name: 'John', age: 20 }
console.log(b); // { name: 'Alice', age: 20 }

变量 ab 是两个不同的对象,但它们的属性 age 的值相同,因为它们复制的是基本类型的值。而属性 name 的值不同,因为它们复制的是对象的引用,修改 b 的属性 name 不会影响 a 的属性 name

3. 深拷贝 Deep Copy


不仅复制对象的一层属性,而是递归复制对象的所有属性,包括属性值为引用类型的属性。深拷贝的结果是两个完全独立的对象,修改其中一个对象的属性不会影响另一个对象的属性。 image.png 最简单的例子:

let a = { name: 'John', hobbies: ['reading', 'swimming'] };
let b = JSON.parse(JSON.stringify(a));
b.name = 'Alice';
b.hobbies[0] = 'cooking';

console.log(a); // { name: 'John', hobbies: ['reading', 'swimming'] }
console.log(b); // { name: 'Alice', hobbies: ['cooking', 'swimming'] }

在上面的例子中,变量 ab 是两个完全独立的对象,它们的属性都不相同,修改 b 的属性 namehobbies[0] 不会影响。

4. 手写深拷贝

循环引用(Circular reference)指的是在对象之间互相引用,形成一个环形依赖关系。比如对象 A 引用了对象 B,而对象 B 又引用了对象 A,这样就形成了一个循环引用。
Map():set() 方法可以设置键值对,get() 方法可以获取指定键的值,has() 方法可以判断是否存在指定键,delete() 方法可以删除指定键值对等等

/**
 * 深拷贝
 * @param {Object} obj 要拷贝的对象
 * @param {Map} map 用于存储循环引用对象的地址
 */
 
// `obj = {}` 表示,如果函数 `deepClone` 调用时没有传入第一个参数,则默认为一个空对象 `{}`
// 而 `map = new Map()` 表示,如果函数 `deepClone` 调用时没有传入第二个参数,则默认为一个新的 Map 对象
// 这里默认参数的作用是为了方便函数的调用,在调用函数时,如果不传入第二个参数
// 那么函数将使用一个新的 Map 对象来存储已经拷贝过的对象,防止出现循环引用
// Map的优点:键和值都可以有弱引用的选项,可以避免内存泄漏
// 如果不传入第一个参数,则默认拷贝一个空对象,这样不会影响函数的运行
// 同时,使用默认参数也可以让代码更简洁,避免繁琐的参数检查和初始化操作

function deepClone(obj = {}, map = new Map()) {
  // 判断 obj 的类型, 如果 obj 的类型为 null,那么直接返回 obj
  if (typeof obj !== "object" obj === null) {
    return obj;
  }
  
  // 判断是否有循环引用, 避免栈溢出或者无限递归
  if (map.get(obj)) {
    return map.get(obj);
  }
  
  // 创建一个新的对象或数组
  let result = {};
  
  // 初始化返回结果
  if (
    obj instanceof Array ||
    // 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
    Object.prototype.toString(obj) === "[object Array]"
  ) {
    result = [];
  }
  
  // 防止循环引用,存储原对象和新对象的对应关系
  map.set(obj, result);
  
  // 遍历原对象的属性,并递归调用 deepClone
  for (const key in obj) {
    // 判断 key 是否是原型属性
    if (obj.hasOwnProperty(key)) {
      // 递归调用
      result[key] = deepClone(obj[key], map);
    }
  }
  // 返回结果
  return result;
}

5. 总结

赋值操作是将一个变量的值(或者说是内存地址)直接赋给另一个变量,两个变量指向同一个内存地址,它们完全共享相同的数据,因此对一个变量进行修改会对另一个变量产生影响。对象和简单数据类型均可以进行赋值。

而拷贝操作是复制一个变量的值并将其赋给另一个变量,它们指向不同的内存地址,即使其中一个变量的值发生改变,另一个变量的值也不会受到影响, 拷贝操作通常用于对象

浅拷贝和深拷贝是针对复杂数据类型,如对象和数组等的操作。浅拷贝仅复制了对象的一层属性值,而没有复制对象内部的子对象和子数组等,因此修改复制后的对象的子对象或子数组时,原对象的子对象或子数组也会被修改。深拷贝则是递归地复制对象的所有属性,包括对象内部的子对象和子数组等,因此修改复制后的对象的子对象或子数组时,原对象的子对象或子数组不会被修改。

浅拷贝会创建一个新的对象,其中包含了原始对象的所有简单数据类型属性的副本,但对于原始对象中的引用类型属性,只是复制了引用地址,并没有复制对象本身。可以简单理解为浅拷贝就是将对象里面的简单数据类型的数值赋值并存在了新的内存地址里,但是对象内部的对象(如列表、数组等)仍然与被拷贝对象指向同一内存地址。

6. 0.1+0.2 ! == 0.3?

IEEE 754 是一种二进制浮点数算术标准,它规定了浮点数的编码方式以及浮点数的算术运算规则。由于浮点数的编码方式和算术运算规则的限制,所以在计算机中进行浮点数运算时,可能会出现精度丢失的情况。

对于 JavaScript 中的浮点数,它们都是基于 IEEE 754 标准实现的。在 JavaScript 中,0.1 和 0.2 的二进制表示都是无限循环的,这意味着它们的精度可能会有所丢失,导致它们的和不等于 0.3。

为了让 0.1+0.2 的结果等于 0.3,我们可以:

  • 使用 toFixed() 方法将浮点数转换为整数进行运算
const a = 0.1, b = 0.2;
const c = 0.3;
const multiplier = 1000000;
const result = (a * multiplier + b * multiplier) / multiplier;
console.log(result.toFixed(1)); // 0.3
  • 使用第三方库
const math = require('mathjs');
const a = math.bignumber(0.1);
const b = math.bignumber(0.2);
const c = math.bignumber(0.3);
const result = math.add(a, b);
console.log(result.equals(c)); // true

3. 原型与原型链

1. 基本概念

  • 原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype 对象。
  • 原型链:由相互关联的原型组成的链状结构就是原型链。

在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]],称之为原型(prototype),它指向另一个对象,即该对象的原型对象(也可以称之为父对象、基对象等)。
原型对象也可以有自己的原型对象,这样就形成了一个原型链。如果一个属性或方法在对象本身上找不到,JavaScript 就会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端为止。
在 ES5 中,可以通过 Object.getPrototypeOf() 方法获取一个对象的原型对象,或者使用 proto 属性来访问和修改一个对象的原型对象。在 ES6 中,可以使用 Object.setPrototypeOf() 方法来修改一个对象的原型对象。

设置原型的作用不仅仅是为了创建对象更方便,更重要的是实现对象的继承和共享属性和方法。原型是 JavaScript 中的一项非常重要的特性,它能够让我们更方便地创建对象,同时还能够实现对象的继承和共享属性和方法,提高代码的复用性和可维护性。

2. 如何顺着原型链寻找属性

以下是一个顺着原型链找属性的例子:
假设我们有一个对象 person 和一个构造函数 Person,其中 person 是由 Person 构造函数创建的实例。我们可以使用 proto 或 Object.getPrototypeOf() 方法来访问原型链上的属性。

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
}

const person = new Person('John');

// 通过 __proto__ 属性访问原型链上的属性
console.log(person.__proto__); // 输出:Person { sayHello: [Function] }
console.log(person.__proto__.__proto__); // 输出:{}

// 通过 Object.getPrototypeOf() 方法访问原型链上的属性
console.log(Object.getPrototypeOf(person)); // 输出:Person { sayHello: [Function] }
console.log(Object.getPrototypeOf(Object.getPrototypeOf(person))); // 输出:{}
  • 通过 person.proto 和 Object.getPrototypeOf(person) 访问了 person 的原型对象,即 Person.prototype。我们可以在原型对象上定义属性和方法,这些属性和方法将被 person 对象继承。
  • 通过 person.proto.proto 和 Object.getPrototypeOf(Object.getPrototypeOf(person)),我们可以访问原型链上更高层次的对象,即 Object.prototype。在 JavaScript 中,所有对象都是 Object 的实例,因此原型链最终会指向 Object.prototype。

4. 作用域与作用域链 (call、apply、bind)

  • 作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
  • 作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。 js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了
  • 作用域链:JavaScript 中的作用域链条(Scope Chain)是指一个函数执行时,从当前函数的活动对象(Activation Object,简称 AO)开始向上查找变量和函数的过程。作用域链条决定了一个函数可以访问哪些变量和函数。

1. 静态作用域和动态作用域

JavaScript 中的作用域是指变量、函数和对象在代码中可访问的范围。在 JavaScript 中,作用域有两种类型:静态作用域和动态作用域

  • 静态作用域

JavaScript 中的大多数变量和函数都具有词法作用域。词法作用域是指变量和函数的作用域在代码编写时就已经确定了,不会因为程序的执行环境而改变。当在函数内部定义一个变量或函数时,它们只能在函数内部访问,而在函数外部无法访问这些变量或函数。

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar(); // 1
  • 动态作用域

动态作用域是指变量和函数的作用域在程序运行时才能确定。在动态作用域中,变量和函数的作用域是根据调用堆栈来确定的。也就是说,当一个函数被调用时,它的作用域是由调用它的函数的作用域决定的。

在 JavaScript 中,实现动态作用域的方式是使用 this 关键字。当一个函数被调用时,this 关键字指向调用该函数的对象。因此,在函数内部使用 this 关键字来访问变量和函数时,其作用域是在运行时动态确定的。

2. 全局作用域和函数作用域

全局作用域

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
  • 所有window对象的属性拥有全局作用域

函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。

  • 作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行
  • 块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中

例1:

var x = 10;

function foo() {
  console.log(this.x);
}

function bar() {
  var x = 20;
  foo.call(this);
}

bar(); // 10

在上面的例子中,函数foo被调用时,使用this.x来访问变量x,此时的作用域是在运行时动态确定的,由调用函数bar的对象决定,因此输出结果是全局变量x的值10。即使在 bar() 函数中不使用call()方法来调用foo(),而是直接调用foo(),因为foo()函数定义在全局作用域中,所以this关键字仍然会指向全局对象window,也就是 x = 10
例2:

function foo() {
  console.log(x);
}

function bar() {
  var x = 20;
  foo();
}

bar(); // 20

在这个例子中,foo() 函数中的 x 变量是通过 JavaScript 闭包来访问的,它会沿着作用域链向上查找变量 x 的值,找到了 bar() 函数中的变量 x,因此输出结果是 20

3. 作用域链

先看以下示例:

function outer() {
  var x = 10;

  function inner() {
    console.log(x);
  }

  inner();
}

outer(); // 输出 10

在这个例子中,变量x定义在 outer() 函数的内部,而函数 inner() 嵌套在 outer() 函数内部,因此在 inner() 函数中可以访问到变量 x

当执行 inner() 函数时,JavaScript 引擎首先查找当前执行环境的 AO,即 inner() 函数的 AO,如果没有找到变量 x,则沿着作用域链向上查找,找到了 outer() 函数的 AO,从而可以访问变量 x 的值。

作用域链的顶端是全局对象(例如浏览器中的 window 对象),任何一个执行环境都可以访问全局对象的属性和方法。在访问变量或函数时,JavaScript 引擎会从当前执行环境的 AO (Activation Object,简称 AO)开始沿着作用域链向上查找,直到找到目标变量或函数,或者查找到了作用域链的顶端,如果还没有找到目标,那么就会报错。

4. .this

this 是一个关键字,用于指代当前函数执行时的上下文对象,也就是函数的调用者。

this 的具体值取决于函数的调用方式

  • 当函数以普通方式调用时,this 指向的是全局对象,也就是 window(如果是在浏览器中运行)
  • 当函数作为对象的方法被调用时,this 指向的是该对象
  • 当函数使用 call、apply 或 bind 方法调用时,this 指向的是传入的第一个参数

this 的作用域和作用域链是一致的。例:

var x = 10;

function foo() {
  console.log(this.x);
}

var obj = { x: 20, foo: foo };

foo(); // 输出 10,this 指向全局对象 window
obj.foo(); // 输出 20,this 指向 obj

5. Call, Apply, Bind

call, apply, 和 bind 是 JavaScript 中用于更改函数 this 指向的三个方法。

  • call 和 apply 的作用是一样的,都是在调用函数时显式地指定函数中的 this 指向哪个对象。它们的第一个参数都是要指定的 this 值,后面的参数是函数的实参列表。区别在于 call 方法传入的参数是一个个逐个列举的,而 apply 方法传入的参数是一个数组。例:
const person = {
  name: 'Alice',
  age: 30,
  sayHi() {
    console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

我们可以使用 call 或 apply 方法来更改 sayHi 函数中的 this 指向:

const otherPerson = {
  name: 'Bob',
  age: 40
};

person.sayHi.call(otherPerson); // "Hi, my name is Bob and I am 40 years old."
person.sayHi.apply(otherPerson); // "Hi, my name is Bob and I am 40 years old."
  • bind 方法也可以更改函数中的 this 指向,但它并不会立即调用函数,而是返回一个新的函数,这个新的函数中的 this 指向是绑定的值。可以理解为是预先绑定了函数中的 this 值,之后再调用新函数时,this 指向会固定为预先绑定的值。例:
const sayHiToPerson = person.sayHi.bind(person);
sayHiToPerson(); // "Hi, my name is Alice and I am 30 years old."

在这个例子中,sayHiToPerson 函数中的 this 指向 person 对象。这里使用了 bind 方法将 sayHi 函数中的 this 预先绑定为 person 对象,然后返回了一个新的函数 sayHiToPerson,调用 sayHiToPerson 时,this 值被固定为 person 对象。

5. 执行上下文

当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain
  • this

6. 闭包

1. 闭包与作用域的关系

JavaScript闭包确实可以让函数在其创建的作用域以外的地方执行时仍能访问该作用域中的变量,从而延长了该作用域的生命周期。但是,它并没有直接改变作用域,而是通过保留该作用域的引用,使得函数仍能访问该作用域中的变量。

换句话说,闭包使得函数可以“记住”其被创建时所处的作用域,并在之后的执行过程中仍能访问该作用域中的变量,这是一种比作用域更长久的变量保存方式。

定义闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量变量。 闭包 = 函数 + 函数能够访问的自由变量。

2. 闭包原理

原理:在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。

例1 - 函数作为参数被传递:

function print(fn) {
  const a = 200;
  fn();
}
const a = 100;
function fn() {
  console.log(a);
}
print(fn); // 100

例2:

function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

每次调用 counter() 函数时,都会访问并修改它的父函数 createCounter() 作用域内的 count 变量的值。 createCounter() 函数返回了内部定义的匿名函数,这个匿名函数形成了一个闭包,其中包含了 count 变量的引用,这个闭包函数被赋值给了 counter 变量。由于 counter 变量指向的是这个闭包函数,因此每次调用 counter() 实际上是在访问这个闭包函数内部的 count 变量。而 createCounter() 函数只会在第一次调用时执行一次,返回了这个闭包函数,以后每次调用 counter() 都是在访问这个闭包函数内部的 count 变量,而不会重新执行 createCounter() 函数。

3. 闭包引起的内存泄漏

闭包可能会引起内存泄漏,因为它们可以阻止函数中定义的变量被垃圾回收。为了避免这种情况,可以尝试以下几种方法:

  • 避免循环引用:在闭包中,将函数引用作为对象属性或事件处理程序等被注册到DOM元素上,如果这个对象同时也是闭包中的变量,则会导致循环引用,从而导致内存泄漏。
  • 及时释放引用:尽可能地将不再需要的引用释放掉,可以使用 delete 关键字删除对象属性的引用,或者在不需要闭包时将其赋值为 null
  • 避免创建过多的闭包:过多的闭包会导致内存占用增加,因此可以考虑将一些通用的函数提取出来,避免创建过多的闭包。
  • 使用事件委托:在添加事件处理程序时,使用事件委托可以避免为每个元素添加事件处理程序,从而避免闭包的大量使用。
  • 使用节流和防抖:使用节流和防抖技术可以避免因过多事件处理程序的使用而导致的闭包过多,从而减少内存占用。

4. 应用

应用实例:比如缓存工具,隐藏数据,只提供 API 。

7. 异步

JavaScript 是一门单线程语言,它采用事件循环(event loop) 机制来处理异步操作。当 JavaScript 程序运行时,它会维护一个调用栈(Call Stack),所有同步任务都会被添加到调用栈中执行。而异步任务会被添加到任务队列(Task Queue) 中等待执行。

1.宏任务与微任务

宏任务通常是由事件触发或者是由 setTimeout/setInterval 等方法添加到任务队列中的,而微任务则是由 Promise.then() 或者 MutationObserver 等方法添加到任务队列中的。

当调用栈为空时,事件循环开始执行。它会从任务队列中取出一个任务,如果这个任务是宏任务,它会将这个任务放到调用栈中执行;如果这个任务是微任务,则会执行所有的微任务,直到微任务队列为空,然后再从宏任务队列中取出下一个任务。这个过程就是事件循环(Event Loop)。

当 Call Stack 调用栈空闲时,浏览器会尝试进行 DOM 渲染,然后检查微任务队列,如果队列不为空,会一次性执行所有的微任务,直到队列为空;如果队列为空,则会继续从宏任务队列中取出下一个任务进行处理。

事件循环(Event Loop)机制使得 JavaScript 可以处理异步操作,它通过任务队列(Task Queue)、宏任务(Macro Task)和微任务(Micro Task)来控制异步任务的执行顺序,让我们的程序可以更加高效地处理异步操作。

因此,整体任务的执行顺序是:1.Call Stack 调用栈空闲 -> 2.尝试 DOM 渲染 -> 触发 Event loop

  • 每次 Call Stack 清空(即每次轮询结束),即同步任务执行完。
  • 都是 DOM 重新渲染的机会,DOM 结构有改变则重新渲染。
  • 然后再去触发下一次 Event loop。

宏任务:setTimeout,setInterval,Ajax,DOM 事件。DOM 渲染后触发,如 setTimeoutsetIntervalDOM 事件script

微任务:Promise async/await。DOM 渲染前触发,如 Promise.thenMutationObserver 、Node 环境下的 process.nextTick

为何微任务执行更早?

  • 微任务是 ES6 语法规定的(被压入 micro task queue)。
  • 宏任务是由浏览器规定的(通过 Web APIs 压入 Callback queue)。
  • 宏任务执行时间一般比较长。
  • 每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的。
  • 在事件循环中,微任务是在渲染之前执行的,但微任务的执行时间是很短的,因此不会对页面渲染造成明显的延迟。而宏任务则可能会导致页面响应性能下降,所以在编写代码时需要注意控制宏任务的数量和执行时间。

需要注意的是:即使微任务比宏任务先添加到任务队列中,它们也不一定会比宏任务先执行。因为宏任务可能会在微任务执行完成之前就被添加到任务队列中,所以执行顺序取决于任务队列中任务的类型和添加顺序。

2. Promise

Promise 是一种用于处理异步操作的 JavaScript 对象。它解决了回调函数嵌套过深的问题,并提供了更加清晰和可读的代码结构,使得处理异步操作变得更加容易。

Promise 对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。当 Promise 对象处于 pending 状态时,表示异步操作尚未完成。当异步操作完成时,Promise 对象将进入 fulfilled 或 rejected 状态,表示异步操作已经成功或失败。

在 Promise 中,我们可以使用 then() 方法和 catch() 方法来处理异步操作的结果。then() 方法用于处理异步操作成功的情况,catch() 方法用于处理异步操作失败的情况。

let promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('Operation completed successfully.');
  }, 2000);
});

promise.then(function(result) {
  console.log(result); // 'Operation completed successfully.'
}).catch(function(error) {
  console.error(error);
});

我们使用 Promise 对象封装了一个异步操作。在异步操作成功时,我们调用 resolve() 方法,并将成功的结果作为参数传递给该方法。在异步操作失败时,我们调用 reject() 方法,并将错误信息作为参数传递给该方法。然后,我们使用 then() 方法来处理成功的情况,并使用 catch() 方法来处理失败的情况。

  • 链式调用Promise 的链式调用是一种在多个异步操作之间进行顺序控制的方式。链式调用是指将多个 Promise 对象串联在一起,以便在一个异步操作完成后执行另一个异步操作。我们可以使用then()方法返回一个新的 Promise 对象,并在新的Promise对象中执行下一个异步操作。
  • 处理异步操作:使用 Promise 对象封装异步操作,并使用 resolve() 方法在异步操作成功时返回结果,使用 reject() 方法在异步操作失败时返回错误信息。
  • 错误处理:错误处理主要是通过 Promise 实例的 catch() 方法来进行的。如果 Promise 对象在它的执行过程中遇到错误,它将会被拒绝,并抛出一个错误。
  • Promise.all():1. Promise.all() 方法用于在多个 Promise 对象之间进行并行处理,并在所有 Promise 对象都成功完成后返回一个结果数组。如果其中任何一个 Promise 对象失败,则 Promise.all() 方法将返回失败信息。我们使用 Promise.all() 方法并行处理两个异步操作,并在它们都成功完成后返回一个结果数组。例如:
let promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('Operation 1 completed successfully.');
  }, 2000);
});

let promise2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('Operation 2 completed successfully.');
  }, 1000);
});

Promise.all([promise1, promise2]).then(function(results) {
  console.log(results); // ['Operation 1 completed successfully.', 'Operation 2 completed successfully.']
}).catch(function(error) {
  console.error(error);
});
  • Promise.race():2. Promise.race() 方法用于在多个 Promise 对象之间进行竞争,并在第一个完成的 Promise 对象完成后返回该结果。如果其中任何一个 Promise 对象失败,则 Promise.race() 方法将返回失败信息。例如:
let promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('Operation 1 completed successfully.');
  }, 2000);
});

let promise2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('Operation 2 completed successfully.');
  }, 1000);
});

Promise.race([promise1, promise2]).then(function(result) {
  console.log(result); // 'Operation 2 completed successfully.'
}).catch(function(error) {
  console.error(error);
});

3. async/await 和 Promise 的关系

  • async/await 是一种让异步操作更像同步操作的语法糖,它通过封装 Promise 对象来提供更简洁、更易读的代码编写方式。
  • await 相当于 Promise 的 then。
  • 利用try...catch 可捕获异常,代替了 Promise 的 catch。

8. 浏览器的垃圾回收

有两种垃圾回收策略:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。

标记清除的缺点:

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
  • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。

解决以上的缺点可以使用 标记整理(Mark-Compact)算法,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图) image.png 引用计数的缺点:

  • 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
  • 解决不了循环引用导致的无法回收问题。

9. 进程和线程

  • Js是单线程的

JavaScript 运行在浏览器或 Node.js 环境中,它是单线程的,也就是说一次只能执行一个任务。这个任务可以是同步操作,也可以是异步操作。异步操作通常通过事件循环和任务队列来实现,当异步操作完成时,会将它的回调函数加入任务队列中,等待 JavaScript 空闲时再执行。

因此,JavaScript 中的同步和异步操作不直接涉及进程和线程的概念,但是它们可以利用事件循环和任务队列来实现异步操作的效果,从而更有效地利用单线程的 JavaScript 运行环境。

  • 进程和线程的区别是什么?

进程是操作系统资源分配的最小单位,它有自己的内存空间、程序代码和系统资源。线程是进程中的一个执行单元,它与同一进程中的其他线程共享内存空间和系统资源。

  • 什么是线程安全?

线程安全是指在多线程环境下,一个函数、对象或变量能够在不同线程中被安全地访问和操作,而不会引发不确定的结果。

  • 进程间通信的方式有哪些?

进程间通信(IPC)的方式包括管道、消息队列、共享内存、信号量、Socket 等。

  • 什么是死锁?

死锁是指两个或多个线程在互相请求对方占用的资源时,出现了相互等待的情况,导致程序无法继续执行的现象。

  • 什么是线程池?

线程池是一种管理和复用线程的机制,它可以减少创建和销毁线程的开销,提高线程的利用率。线程池可以控制线程的数量,并且可以为线程分配任务,从而更有效地管理线程。

  • 进程和线程的优缺点是什么?

进程的优点是相互独立,可靠性高,但开销大;线程的优点是轻量级,切换快,但可靠性低。在实际应用中,需要根据具体情况选择进程还是线程来实现程序的功能。

  • 什么是上下文切换?

上下文切换是指从一个进程或线程切换到另一个进程或线程时,需要保存当前进程或线程的状态(上下文),然后加载另一个进程或线程的状态。上下文切换的开销较大,需要消耗大量的 CPU 时间和系统资源。

10. 回调函数

JavaScript 的回调函数是一种函数式编程的概念,它是一种将函数作为参数传递给另一个函数,并在另一个函数执行完后调用该函数的方式。回调函数通常用于异步操作,例如处理用户输入、读取文件、请求网络数据等。 例:

function upperCaseArray(arr, callback) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(arr[i].toUpperCase());
  }
  callback(result);
}

function printResult(result) {
  console.log(result); // ['HELLO', 'WORLD']
}

let arr = ['hello', 'world'];
upperCaseArray(arr, printResult);

11. new

new是一个关键字,用于创建一个新对象实例。它通常用于创建一个自定义的JavaScript对象或者一个内置对象的实例。

当你使用new操作符来创建一个对象时,它会执行以下操作:

  • 创建一个新的空对象。
  • 将该对象的原型设置为构造函数的原型。
  • 调用构造函数,并将新对象绑定到this关键字上。
  • 如果构造函数没有显式地返回一个对象,则返回新创建的对象。

例:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

var person1 = new Person("John", 30);

new操作符创建了一个新的Person对象实例,并将其绑定到person1变量上。这个新的对象有两个属性,name和age,它们的值分别为"John"和30。

2. HTML与CSS

1. HTML

2. CSS盒模型

3. CSS flex布局

4. CSS优先级

5. 重排和重绘

6. BFC

3. React相关

1. 事件机制

为什么要自定义事件机制?

  • 抹平浏览器差异,实现更好的跨平台。
  • 避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。
  • 方便事件统一管理和事务机制。

2. Class Component

React组件的生命周期可以分为三个阶段:Mounting(挂载阶段)、Updating(更新阶段)和Unmounting(卸载阶段)。

  • 挂载阶段:

constructor():组件的构造函数,用于初始化state和绑定事件处理函数等。

getDerivedStateFromProps():在组件挂载之前和更新时被调用,用于根据props更新state。

render():渲染组件的UI结构,返回一个React元素。

componentDidMount():组件挂载后被调用,可以进行一些异步操作、事件监听等。

  • 更新阶段:

getDerivedStateFromProps():在组件挂载之前和更新时被调用,用于根据props更新state。

shouldComponentUpdate():用于控制组件是否重新渲染,返回一个布尔值。

render():渲染组件的UI结构,返回一个React元素。

componentDidUpdate():组件更新后被调用,可以进行一些异步操作、事件监听等。

  • 卸载阶段:

componentWillUnmount():组件卸载前被调用,用于清理定时器、取消事件监听等。

React废弃了三个生命周期方法,分别是componentWillMount()、componentWillUpdate()和componentWillReceiveProps()。这是因为这三个生命周期方法在某些情况下会导致不必要的性能问题和不一致的行为。替代这三个生命周期方法的方式是使用getDerivedStateFromProps()和shouldComponentUpdate()方法。在React 17及以后的版本中,这三个生命周期方法会被标记为“unsafe”(不安全的),并在未来的版本中完全删除。因此,在编写React组件时,应该尽量避免使用这三个生命周期方法。

3. 函数式编程的理解

函数式编程有两个核心概念。

  • 数据不可变(无副作用): 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
  • 无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。

纯函数带来的意义。

  • 便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。
  • 可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。
  • 更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。

4. react hooks

5. React dom diff算法

React的DOM Diff算法是用于优化组件渲染的重要算法,它通过比较前后两个版本的虚拟DOM树,找出需要更新的部分,并尽可能地减少DOM操作,提高组件渲染的性能。

React的DOM Diff算法分为以下三个步骤:

  • 生成虚拟DOM树:在组件挂载或更新时,React会通过render()方法生成新的虚拟DOM树。
  • 比较新旧虚拟DOM树:React会逐层比较新旧虚拟DOM树,找出需要更新的部分。比较的过程是从根节点开始,逐层遍历虚拟DOM树,对比节点类型、属性、子节点等信息。
  • 执行DOM操作:根据比较结果,React会尽可能地减少DOM操作,对需要更新的部分进行更新。对于需要删除的节点,React会执行删除操作;对于需要添加的节点,React会执行插入操作;对于需要更新的节点,React会执行属性更新或文本内容更新等操作。

React的DOM Diff算法是一个高效的算法,但也存在一些注意事项:

  • 尽量减少DOM操作:在编写组件时,应该尽量避免频繁地操作DOM,因为DOM操作是很耗性能的。
  • 使用key属性:在渲染列表时,应该为每个列表项指定一个唯一的key属性,这样可以帮助React更准确地找出需要更新的列表项。
  • 不要过度优化:虽然DOM Diff算法可以帮助我们优化组件渲染,但过度优化也会带来不必要的复杂性和维护成本。在实际开发中,应该根据具体情况进行优化。

6. React Fiber理解

React Fiber 是 React 16 中全新的协调引擎,它的设计目的是为了解决 React 在渲染大型应用时可能会出现的卡顿现象。在 React 16 之前,React 使用了一种叫做 Stack Reconciliation 的算法来进行组件协调和更新,但是这种算法有一个缺点,就是无法中断。在 Stack Reconciliation 算法中,如果开始进行了组件更新,那么更新过程就无法中断,直到整个更新完成。

React Fiber 采用了一种新的算法,叫做异步渲染,它的主要思想是将整个组件更新过程分成一个个小的单元,然后在每个小单元中插入优先级较低的任务,以便在更新过程中可以中断、暂停或者恢复渲染。在 React Fiber 中,每个小单元称为 Fiber 节点,每个 Fiber 节点包含了组件的状态、组件对应的 DOM 节点信息以及更新状态等信息,通过这些信息 React Fiber 可以对组件进行更加细粒度的控制。

React Fiber 的主要目标是实现更好的渲染性能和更好的用户体验。由于 Fiber 节点可以随时中断和恢复,所以 React Fiber 能够更加高效地利用浏览器的异步渲染能力,避免阻塞 UI 线程,提高应用的响应速度。此外,React Fiber 还提供了更加细粒度的控制能力,使得 React 能够更加灵活地响应用户操作和数据更新。

总之,React Fiber 是 React 的一项重要技术革新,它的出现使得 React 更加灵活、高效和强大,为开发者提供了更好的性能和用户体验。

7. React的性能优化

  • 使用 React.memo 来缓存组件。
  • 使用 React.useMemo 缓存大量的计算。
  • 避免使用匿名函数。
  • 利用 React.lazyReact.Suspense 延迟加载不是立即需要的组件。
  • 尽量使用 CSS 而不是强制加载和卸载组件。
  • 使用 React.Fragment 避免添加额外的 DOM。 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

8. React和Vue

9. WebPack

Webpack是一个现代化的JavaScript应用程序打包工具,可以将多个JavaScript文件、CSS、图片等资源打包成一个或多个文件,以便于在浏览器中加载。它是前端工程化中的重要工具之一,可以提高代码的可维护性、可读性和可扩展性。

Webpack 5与Webpack 4相比,有以下一些主要的改进:

  • Webpack 5引入了持久化缓存,提高了构建速度和性能。
  • Webpack 5增加了更多的内置功能,比如支持顶层await语句、支持WebAssembly模块等。
  • Webpack 5对于CSS模块和Chunk处理等方面进行了优化,提高了性能和稳定性。

4. 网络

1. Web存储

1. Cookie

  • 本身用于浏览器和 server 通讯。
  • 被“借用”到本地存储来的。
  • 可用 document.cookie = '...' 来修改。

其缺点:

  • 存储大小限制为 4KB。
  • http 请求时需要发送到服务端,增加请求数量。
  • 只能用 document.cookie = '...' 来修改,太过简陋。

2. localStorage 和 sessionStorage

  • HTML5 专门为存储来设计的,最大可存 5M。
  • API 简单易用, setItem getItem。
  • 不会随着 http 请求被发送到服务端。

它们的区别:

  • localStorage 数据会永久存储,除非代码删除或手动删除。
  • sessionStorage 数据只存在于当前会话,浏览器关闭则清空。
  • 一般用 localStorage 会多一些。

2. HTTP

1. 基本信息

HTTP是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。 HTTP是一个无状态的面向连接的协议

2. 常见状态码:

1: 服务器收到请求 2: 成功 3: 重定向 4: 客户端错误 5: 服务端错误

3. GET和POST的区别

  • 编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
  • 参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
  • 幂等性的角度,GET 是幂等的,而 POST 不是。(幂等表示执行相同的操作,结果也是相同的)
  • TCP 的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)

4. HTTP/2改进

  • 头部压缩
  • 多路复用
  • 服务器推送

5. 跨域资源共享CORS

CORS(跨来源资源共享)是一种浏览器安全机制,用于防止跨域请求。当你的网页向不同域名的服务器发出请求时,如果服务器没有开启CORS,那么浏览器就会阻止请求,并抛出一个CORS错误。

解决CORS问题的方法通常有以下几种:

  • 在服务器端设置CORS: 如果你有权限修改服务器端的代码,可以在服务器端设置CORS响应头,以允许跨域请求。

  • 使用代理:如果你没有权限修改服务器端代码,可以使用代理来解决CORS问题。例如,你可以在本地启动一个Node.js服务器,将跨域请求转发到目标服务器,然后在客户端通过代理服务器发出请求。这种方法需要额外的开销和配置,但可以解决CORS问题。

  • JSONP: JSONP(JSON with Padding)是一种利用

6. HTTP缓存

什么是缓存? 把一些不需要重新获取的内容再重新获取一次

为什么需要缓存? 网络请求相比于 CPU 的计算和页面渲染是非常非常慢的。

哪些资源可以被缓存? 静态资源,比如 js css img。

个人理解:Cookie更倾向储存用户信息,而HTTP缓存则是网站加载的静态资源。

image.png 刷新对缓存的影响:

  • 正常操作:强制缓存有效,协商缓存有效。
  • 手动刷新:强制缓存失效,协商缓存有效。
  • 强制刷新:强制缓存失效,协商缓存失效。

3. TCP

1. 特点

  • 面向连接的、可靠的字节流服务
  • 仅两方通信,广播和多播不能用于TCP
  • TCP使用校验和,确认和重传机制来保证可靠传输
  • TCP使用滑动窗口来实现流量控制,通过动态改变窗口大小来进行拥塞控制
  • 使用累积确认保证数据的顺序不变和非重复

2. 与UDP比较

  • UDP不需要建立连接,直接传输数据(不可靠)
  • UDP支持一对一,一对多,多对多的交互传输
  • UDP是尽最大努力交付,而不是可靠性交付
  • TCP有拥塞控制和流量控制机制,而UDP没有,网络拥堵不会影响发送速率
  • TCP是流式传输,UDP是一个包一个包传输,没有边界
  • TCP UDP 可以使用一个端口*

3. TCP的三次握手

三次握手:建立TCP连接,需要服务器和客户端发送三个包

  • 客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。
  • 服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。
  • 客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED状态,TCP 握手结束。
第三次握手是可以携带数据的,前两次是不可以携带数据的。 *

4. 为什么TCP不是两次握手?

防止已失效的连接请求又传送到服务器端,因而产生错误,两次握手不能保证是否建立了通信连接,因为客户端并不能保证一定能接收到服务器端发来的信息;

经过三次握手之后,客户端A 和服务器端B 都可以确认之前他们的所发送的消息,各自都能收到且报文也都成功发送给对方了,所以四次是多余的。

4. IP(DNS解析)

1. 使用浏览器访问域名的全过程:

  1. 浏览器向DNS服务器发出解析域名的请求
  2. DNS服务器将域名解析为对应的IP地址,并返回给浏览器
  3. 浏览器根据IP地址与目标服务器建立TCP连接
  4. 浏览器发出HTTP请求报文
  5. 服务器回复HTTP请求报文
  6. 浏览器解析响应报文,并显示在Web页面上
  7. 收到报文结束,释放TCP连接
    使用的协议:
    • 应用层:HTTP, DNS
    • 传输层:TCP/ UDP
    • 网络层:IP(IP数据包传输和路由选择),ICMP(提供网络传输过程中的差错检测),ARP(将目的主机的IP地址映射成MAC地址)

2. IP和MAC区别(网络层和链路层):

MAC只负责某一个区间之间的通信, 而IP负责把数据包发送给最终的地址

3. IPV4: 32位点分十进制

5. 网络协议

1. 内容

  • 应用层: HTTP, FTP, SMTP 对应程序的通信服务
  • 表示层: 主要定义数据格式以及加密,包括多个消息的控制和管理
  • 会话层: 定义如何开始结束、控制对话;包括多个双向信息的控制管理
  • 传输层: 差错恢复协议以及无差错恢复协议,以及对于顺序不对的数据包重新排序,比如TCP和UDP
  • 网络层: 定义了标识所有节点的逻辑地址,路由的实现,以及一个包分成更小包的分段方法,比如IP
  • 数据链路层: 定义了单个链路上如何传输数据,如ATM,MAC
  • 物理层: 定义了传输介质的特性

应用层, 表示层,会话层 定义应用程序的功能 

传输层,网络层,数据链路层,物理层 主要面向网络的端到端,点到点的数据流

2. 优点

  • 使人们容易探讨和理解协议的许多细节。
  • 在各层间标准化接口,允许不同的产品只提供各层功能的一部分。
  • 创建更好集成的环境。
  • 减少复杂性,允许更容易编程改变或快速评估。
  • 用各层的headers和trailers排错。
  • 较低的层为较高的层提供服务。
  • 把复杂的网络划分成为更容易管理的层。

3. 广播风暴

广播风暴(broadcast storm)简单的讲是指当广播数据充斥网络无法处理,并占用大量网络带宽,导致正常业务不能运行,甚至彻底瘫痪,这就发生了“广播风暴”。一个数据帧或包被传输到本地网段 (由广播域定义)上的每个节点就是广播;由于网络拓扑的设计和连接问题,或其他原因导致广播在网段内大量复制,传播数据帧,导致网络性能下降,甚至网络瘫痪,这就是广播风暴。