Skip to content

八股文

静态作用域和动态作用域

  • 因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的

执行上下文-变量对象

  • 当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 对于每个执行上下文,都有三个重要属性:
  • 变量对象(Variable object,VO);
  • 作用域链(Scope chain);
  • this;

函数上下文

  • 在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象

  • 活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

  • 活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

    1. 全局上下文的变量对象初始化是全局对象;
    1. 函数上下文的变量对象初始化只包括 Arguments 对象;
    1. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
    1. 在代码执行阶段,会再次修改变量对象的属性值;
js
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);
// 在进入执行上下文后,这时候的 AO 是
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
// 当代码执行完后,这时候的 AO 是:
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

第一段会报错:Uncaught ReferenceError: a is not defined。 第二段会打印:1。 这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。

js
function foo() {
  console.log(a);
  a = 1;
}
// 第一段执行 console 的时候, AO 的值是:
AO = {
  arguments: {
    length: 0,
  },
};

foo(); // Uncaught ReferenceError: a is not defined。

function bar() {
  a = 1;
  console.log(a); // AO中没有会去全局上下文中查找
}
bar(); // 1

作用域链

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

function foo() {
    function bar() {
        ...
    }
}


foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

执行分析

js
var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f();
}
checkscope();
    1. 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
js
ECStack = [globalContext];
    1. 全局上下文初始化
js
globalContext = {
  VO: [global],
  Scope: [globalContext.VO],
  this: globalContext.VO,
};
    1. 初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
js
  checkscope.[[scope]] = [
    globalContext.VO
  ];
  1. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
js
ECStack = [checkscopeContext, globalContext];
    1. checkscope 函数执行上下文初始化:
js
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope: undefined,
        f: reference to function f(){}
    },
    Scope: [AO, globalContext.VO],
    this: undefined
}
    1. f 函数执行,沿着作用域链查找 scope 值,返回 scope 值;
    1. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出;
js
ECStack = [checkscopeContext, globalContext];
    1. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
js
ECStack = [globalContext];

ECMAScript 中所有函数的参数都是按值传递的。

  • 把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
  • 函数外部的值复制给函数内部的参数,如果是基本类型的值,在函数内部修改参数值,将不会影响到函数外的值。
  • 如果把引用类型的值复制给函数内部的参数,在函数内部修改参引用(属性)值,将影响到函数外的值。如果直接给内部参数直接赋值不会影响外部的值
js
var obj = {
  value: 1,
};
function foo(o) {
  o = 2;
  console.log(o); //2
}
// 共享传递
foo(obj);
console.log(obj.value); // 1
js
Function.prototype.call2 = function (context, ...args) {
  if (typeof context === "undefined" || context === null) {
    context = window;
  }
  context.fn = this;
  let data = context.fn(...args);
  delete context.fn;
  return data;
};

function fn(a, b) {
  console.log(this.name, a, b);
}

let obj = {
  name: "obj",
};

fn.call2(obj, 1, 2); // obj 1 2
js
Function.prototype.myBind = function (context) {
  // 判断是否是undefined 和 null
  if (typeof context === "undefined" || context === null) {
    context = window;
  }
  self = this;
  return function (...args) {
    return self.apply(context, args);
  };
};
js
function myNew(Fn){
 let o = {};
 o.__proto = Fn.prototype
 let ret = Fn.call(o);
 return typeof ret === 'object' ? ret : obj;
}

ajax vs fetch

fetch()的功能与 XMLHttpRequest 基本相同,但有三个差异:

  1. fetch使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁;
  2. fetch采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码;
  3. fetch通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来;

let 的底层实现过程

  1. 编译阶段 在代码编译阶段,编译器会扫描整个函数体(或全局作用域),查找所有使用 let 定义的变量,为这些变量生成一个初始值为 undefined 的词法环境(LexicalEnvironment)并将其保存在作用域链中。

  2. 进入执行上下文 当进入执行块级作用域(包括 for、if、while 和 switch 等语句块)后,会创建一个新的词法环境。如果执行块级作用域中包含 let 变量声明语句,这些变量将被添加到这个词法环境的环境记录中。

  3. 绑定变量值 运行到 let 定义的变量时,JavaScript 引擎会在当前词法环境中搜索该变量。首先在当前环境记录中找到这个变量,如果不存在变量,则向包含当前环境的外部环境记录搜索变量,直到全局作用域为止。如果变量值没有被绑定,JavaScript 引擎会将其绑定为 undefined,否则继续执行其他操作。

  4. 实现块级作用域 使用 let 定义变量时,在运行时不会在当前作用域之外创建单独的执行上下文,而是会创建子遮蔽(shadowing)新环境。在子遮蔽的词法环境中,变量的值只在最接近的块级作用域内有效。

垃圾回收机制(GC)

  • JavaScript在创建变量的时候,会自动分配内存,内存在不使用的时候就被垃圾回收器自动回收释放内存

引用计数

  • 当一个变量每次被其他变量引用时,计数器就会加一,每当被释放时计数器就减一,如果计数器为零就被自动回收释放内存
  • 优点:引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
  • 缺点:首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的

标记清除

    1. 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
    1. 然后从各个根对象开始遍历,把不是垃圾的节点改成1
    1. 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
    1. 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
  • 优点: 标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
  • 缺点: 标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片

标记整理

  • 整理清除出现的内存碎片

新生代、老生代

  • 新生代:新创建的对象所在的代。由于许多对象在创建后不久就会变成垃圾,因此这个代的回收频率较高,回收速度也较快。
  • 老生代:这包含了经过多次新生代回收仍然存活的对象。长时间存活的对象会进入这个代。老生代的回收频率较低,但可能会消耗更多时间。
  • 分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查,新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率

什么是函数式编程

  • 在JavaScript中,函数式编程是一种编程范式,它强调使用纯函数不可变性函数作为一等公民

纯函数:纯函数在JavaScript中的概念与其他编程语言中相似。它们没有副作用,对于相同的输入始终产生相同的输出。这有助于代码的可测试性可维护性

js
// 纯函数的例子
function add(a, b) {
    return a + b;
}

不可变性: 函数式编程强调不可变数据,即一旦创建就不能被修改。在JavaScript中,可以使用const声明常量,以确保变量不被重新赋值。也可以使用一些库(如Immutable.js)来实现不可变性。

js
// 不可变性的例子
const numbers = [1, 2, 3, 4];

// 使用不可变性的方式添加一个新元素
const newNumbers = [...numbers, 5];

高阶函数: JavaScript中的函数是一等公民,可以被传递给其他函数,也可以从其他函数中返回。高阶函数是接受一个或多个函数作为参数,并返回一个新函数的函数

js
// 高阶函数的例子
function multiplyBy(factor) {
    return function (number) {
        return number * factor;
    };
}

const double = multiplyBy(2);
console.log(double(5)); // 输出 10

Map、Filter和Reduce: 这些是常见的函数式编程操作,用于对列表进行转换、过滤和折叠。

js
const numbers = [1, 2, 3, 4, 5];

// 使用map和filter进行转换和过滤
const squaredEvenNumbers = numbers
    .filter(num => num % 2 === 0)
    .map(num => num ** 2);

// 使用reduce进行折叠
const sum = squaredEvenNumbers.reduce((acc, curr) => acc + curr, 0);

console.log(sum); // 输出 20

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

  • 发布-订阅模式(Pub/Sub)和观察者模式(Observer)都属于软件设计模式,它们用于实现对象之间的通信和协作,但有一些关键区别。

  • 耦合度:

    • 在发布-订阅模式中,发布者和订阅者之间是解耦的。发布者不需要知道订阅者的存在,反之亦然。消息的发布者只负责发布消息,而订阅者只负责订阅感兴趣的消息。
    • 在观察者模式中,被观察者和观察者之间是相互耦合的。观察者需要直接注册到被观察者上,被观察者需要知道其观察者的存在。
  • 通信方式:

    • 发布-订阅模式中,发布者和订阅者通过消息通道进行通信。发布者发布消息到通道,订阅者从通道中接收消息。
    • 观察者模式中,被观察者通过直接调用观察者的方法来通知它们状态的变化。
  • 灵活性:

    • 发布-订阅模式更为灵活,允许动态地添加或移除订阅者,而不影响系统的其他部分。
    • 观察者模式的灵活性较差,因为被观察者观察者之间存在直接的关系,添加或移除观察者可能需要修改被观察者的代码。
  • 扩展性:

    • 发布-订阅模式更容易扩展,可以支持多个发布者和多个订阅者,而且它们之间的关系是松散的。
    • 观察者模式相对较难扩展,因为主题和观察者之间的关系是紧密耦合的。

JavaScript中函数参数到底是值传递还是引用传递?

  • 函数的参数是值传递
js
let obj ={a:1}
function test(a) {
  // 这里a是值传递,修改a的值不会影响obj的值
    a=null
}
test(obj)
console.log(obj) // {a: 1}

产生精度丢失问题?

  • 1、小数运算
js
let result = 0.1 + 0.2; // 结果不是 0.3,而是一个近似值
  • 2、整数运算溢出
js
let largeNumber = 9007199254740991;
let result = largeNumber + 1; // 结果不是 9007199254740992,而是近似的值
  • 3、数值过大或过小
js
let largeNumber = 1e17;
let result = largeNumber + 1; // 结果近似于 largeNumber,失去了精度

那为什么0.1+0.1=0.2、0.1+0.2!=0.3?

  • 0.1 的二进制表示并不是一个有限的小数,因此在计算机中以二进制浮点数表示时,存在近似值。当你执行 0.1 + 0.1 时,这个近似值与另一个 0.1 相加,导致了舍入误差。这个误差通常很小,可能在小数点后很多位之后

  • 0.1 + 0.2 != 0.3,是因为 0.2 在二进制中也是一个无法精确表示的值,所以这两个近似值的误差可能更加明显

为什么JavaScript里面typeof(null)的值是"object"

  • 在JavaScript最初的实现中,值是由一个表示类型的标签和实际数据值表示的对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。
js
 typeof null === 'object';

WeakMap和Map的区别

  • WeakMap是弱引用类型,当存储的对象被释放后,存储的值也会被垃圾回收器回收。
  • Map是强引用类型,当存储的对象被释放后,存储的值不会被垃圾回收器回收。
js
let w = new WeakMap();
let m = new Map();
function fn() {
  let obj = { name: 10 };
  w.set(obj, obj);
  m.set(obj, obj);
}
fn();
console.log(w); // WeakMap { <items unknown> }
console.log(m); // Map(1) { { name: 10 } => { name: 10 } }

小程序特殊的双线程架构

  • 传统Web的架构模型是单线程架构,其渲染线程和脚本线程是互斥的,这也就是说为什么长时间的脚本运行可能会使页面失去响。
  • 小程序能够具备更卓越用户体验的关键在于起架构模型有别于传统 Web,小程序为双线程架构,其渲染线程脚本线程分开运行的。渲染层的界面使用了 WebView进行渲染,逻辑层采用 JsCore 线程运行 JS 脚本。

对前端工程化的理解

  • 前端工程化是一种系统化、规范化的前端开发方式,旨在提高开发效率、代码质量和项目可维扩性。其核心理念是将前端开发看作一个工程化过程,通过对前端开发流程、工具、规范等方面的优化,使得开发人员能够更加专注于业务逻辑的实现,而不是被繁琐的工作流程和技术细节所床扰。
  • 1.模块化: 将前端代码按照功能划分为不同的模块,每个模块只关注自身的功能,可以独立开 发、测试、调试和部署。常见的模块化方案不全文CommonJS、AMD、ES6 模块等
  • 2.规范化开发:制定前端代码的编写规范,包括代码风格、注释、命名规范、目录结构等,从而保证团队成员之间的代码风格统一,方便代码维护和交流。

什么是重绘与回流?

  • 重绘:简单来说就是重新绘画,当给一个元素更换颜色、更换背景,虽然不会影响页面布局,但是颜色或背景变了,就会重新渲染页面,这就是重绘。
  • 回流:当增加或删除dom节点,或者给元素修改宽高时,会改变页面布局,那么就会重新构造dom树然后再次进行渲染,这就是回流。

setTimeout 和 requestAnimationFrame 的区别

  • setTimeout 属于计时器,会在指定的时间后执行回调函数,但是其中的回调函数会在主线程中执行会阻塞主线程的执行,所以会造成页面的卡顿
  • requestAnimationFrame 属于动画帧,会在下一帧中执行回调函数,但是其中的回调函数会在浏览器的空闲时间执行,不会阻塞主。另外当切换到后台时requestAnimationFrame会暂停不会触发执行。

script 引入方式

  • html 静态 script 引入
  • js 动态插入 script

script defer

  • 1.浏览器开始解析 HTML 页面
  • 2.遇到有 defer 属性的 script 标签,浏览器继续往下面解析页面,且会并行下载 script 标签的外部 js 文件
  • 3.解析完 HTML 页面,再执行刚下载的 js 脚本(在 DOMContentLoaded 事件触发前执行,即刚刚解析完 html,且可保证执行顺序就是他们在页面上的先后顺序)

script sync

  • 1.浏览器开始解析页面
  • 2.遇到有 sync 属性的 script 标签,会继续往下解析,并且同时另开进程下载脚本
  • 3.脚本下载完毕,浏览器停止解析,开始执行脚本,执行完毕后继续往下解析

使用场景区分

  • 1.脚本之间没有依赖关系的,使用 sync
  • 2.脚本之间有依赖关系的,使用 defer
  • 3.若同时使用 sync 和 defer,defer 不起作用,sync 生效

内存泄露

  • 前端内存泄漏是指在网页或应用程序中,由于代码错误或者其他原因,导致分配给页面或应用程序的内存无法被垃圾回收器回收。这会导致内存使用量不断增加,最后可能导致应用程序崩溃或者变得超级缓慢。内存泄漏问题一旦发生,不仅会对网页或应用程序的性能造成负面影响,还会影响用户的体验。

内存泄露场景

  • 意外的全局变量: 无法被回收
  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)
  • 对象循环引用: 如果两个对象相互引用,且不存在其他对象对它们的引用,就会导致这两个对象无法被正常释放,从而导致内存泄漏。
  • 比如在vue中,绑定这个定时器或者全局的滚动事件,组件在销毁的时候没有被释放掉,当多次重复使用该组件会导致多次的定时器绑定,会造成内存泄漏
  • 闭包一般情况不会造成内存泄漏,但是闭包内的引用是无法被垃圾回收的。

如何分析内存泄露

  • 1、可以通过Performance 面板进行录制,观察js heap是不是成阶梯式的增长,且无法下降。
  • 2、可以通过JS堆快照进行分析,分别看对比快照内存的占用情况

Vite为何启动快

  • 在开发环境直接使用的ES6 Module,代码不需要编译
  • 生产环境使用的rollup,并不会快很多

Composition API 和 React Hooks 对比

  • Vue3中的setup 只会被调用一次,而React Hook函数会被多次调用
  • Vue3中的setup无需顾虑调用顺序,而React Hook需要保证 hooks 的顺序一致

Vue vs React

  • Vue使用模版拥抱html

  • Vue 是真正意义上的做到了组件级更新,每一个组件就对应一个渲染的 effect,让响应式数据去收集 effect

  • Vue 采用的是递归的方式来渲染页面不可中断

  • React使用的JSX拥抱JS

  • React 更新都是从跟节点开始调度,会讲一个大的任务拆分成多个小的任务单元

  • React 更新策略是循环的方式,有任务优先级的概念,可中断执行

Map和Objecj区别

  • API不同,Map可以任意类型为key
  • Map是有序结构

Set和数组的区别

  • API不同
  • Set元素不能重复
  • Set是无序的结构

Map、WeakMap

  • 1、Map 是字典的数据结构
  • 2、Map 的 key 可以是任意的值
  • 3、WeakMap 的 key 只能是对象(弱引用)
js
let map = new WeakMap();
let a = {};
map.set(a, 1);
a = null; // map会被自动释放掉

module chunk bundle 的区别

  • module -各个源码文件,webpack 中一切皆模块
  • chunk -多模块合并成的,如entry importo splitChunk
  • bundlle-最终的输出文件

ES6 Module 和 Commonjs 区别

  • ES6 Module 静态引入,编译时引入
  • commonjs 动态引入,执行时引入
  • Module是导入值的引用,Commonjs是导入值的拷贝
  • 只有 ES6 Module 才能静态分析,实现 Tree-Shaking

前端为何要进行打包和构建?

  • 体积更小 (Tree-Shaking、压缩、合并),加载更快
  • 编译高级语言或语法(TS,ES6+,模块化,sCSs)
  • 兼容性和错误检查 ( Polyfill、postcss、 eslint)

正式面试时,要这样问

  • 部门所做的产品和业务,产品的用户量、规模
  • 部门有多少人,都有什么角色?
  • 项目的技术栈,以及对接的其他技术团队

项目构建

  • 代码仓库,发布到哪个npm 仓库(如有需要)
  • 框架 vue React
  • 代码目录规范
  • 打包构建 webpack 等,做打包优化
  • eslint prettier commit-lint
  • pre-commit
  • 单元测试
  • CI/CD 流程
  • 开发环境,预发布环境
  • 开发文档

开发环境

  • DEV development 开发
  • SIT System Integrate Test 系统整合测试(内测)
  • UAT User Acceptance Test 用户验收测试
  • PET Performance Evaluation Test 性能评估测试(压测)
  • IM simulation 仿真
  • RD/PROD production 产品/正式/生产

forEach for in、 for of 的差异

  • forEach:break不能中断循环执行,如果想终止循环,可以把数组的长度设置为0,或者抛出异常
  • for...in 遍历 key , for...of 遍历 value
  • for...in 可以遍历对象,for...of 不可以
  • for...of 可以遍历 Map/Set ,for...in 不可以
  • for...of 可遍历 generator ,for...in 不可以
js
let arr = [1,2,3,4,5]

arr.filter(_=> {
  console.log(_)
  break // 不能中断循环执行
  return // return不会中断下面代码的执行
})

for (const key in arr) {
  console.log(key)
  break // 可以中断循环执行
  return // 可以中断循环执行,return后面的代码无法执行
}


for (const iterator of arr) {
  console.log(iterator)
  break // 可以中断循环执行
  return // 可以中断循环执行,return后面的代码无法执行
}

数据类型undefinednull

  • undefined:变量声明为初始化具体的值
  • null:表示一个对象的空指针

递减操作符合

递减在前,先减在计算

js
let num1 = 2; 
let num2 = 20; 
let num3 = --num1 + num2; 
let num4 = num1 + num2; 
console.log(num3); // 21 
console.log(num4); // 21

递减在后,先计算在减

js
let num1 = 2; 
let num2 = 20; 
let num3 = num1-- + num2; 
let num4 = num1 + num2; 
console.log(num3); // 22 
console.log(num4); // 21