积累:01-js基础

130 阅读50分钟

1. js数据类型和存储差别

  • 基本类型-【Number,String,Undefined,Boolean,Null,Symbol】

    • Number
    • String
    • Undefined
      • let 和 var 声明变量后未赋值,相当于给变量赋予了 undefined 值
    let a ;
    console.log(a == undefined) // true
    // const a; 这样会报错;const 必须在声明的时候赋值且不能修改;
    
    • Boolean
    • null
    console.log(typeof null) // 'object' 
    // (这是 JavaScript 的一个已知bug,null 实际上是基本数据类型)
    
    • Symbol (每次调用symbol都返回一个全新的symbol类型数据,不可枚举,用作对向属性键避免属性名冲突)
    const sym2 = Symbol('desc'); // 可选的描述
    
  • 引用类型

    • Object
    • Array
    • Function
    • 除了上述说的三种之外,还包括 Date 、 RegExp 、 Map 、 Set 等......
  • 存储的区别

    • 基本数据类型存储在栈中
     let a = 10;
     let b =a;
     b = 20;
     console.log(a) // a= 10
     // 基本数据类型存储在不同的栈内存中,值不会互相影响
    
    • 引用类型的对象存储在堆中
    var obj = {}
    var obj1 = obj;
    obj1.name="xxx"
    console.log(obj.name) // "xxx"
    
    
    • 引用数据类型存放在堆中,每个堆内存对象都有对应的引用地址,引用地址存放在栈中。上面的例子是将堆内存对象在栈内存对象的引用地址复制了一份,两者指向同一个堆内存对象,改变属性值会影响

image.png

2. js数据结构

✅ 基本类型(存储在栈内存中)

  • Number:整数、小数、NaN
  • String:字符串(不可变)
  • Boolean:布尔值
  • Null:空对象指针
  • Undefined:未定义
  • Symbol:唯一值
  • BigInt:大整数

📌 特点:不可变,比较时是值的比较。

✅ 引用类型(存储在堆内存中,栈里存地址引用)

  • Object:最常见的复杂数据结构
  • Array:有序集合
  • Function:函数对象
  • Date:日期对象
  • RegExp:正则对象
  • Map / Set / WeakMap / WeakSet:ES6 新增的集合结构

📌 特点:存放在堆中,变量保存的是引用地址,比较时是地址比较。

3. DOM常见操作【Document Object Model】- 操作页面内容的API

✅ 节点获取

*   `document.getElementById("id")`
*   `document.getElementsByClassName("class")`
*   `document.getElementsByTagName("div")`
*   `document.querySelector(".class")`
*   `document.querySelectorAll(".class")`

✅ 节点创建 / 插入 / 删除

    let div = document.createElement("div");
    div.innerText = "Hello DOM";
    document.body.appendChild(div);     // 插入
    document.body.removeChild(div);     // 删除

✅ 修改内容 / 属性 / 样式

    let el = document.getElementById("title");
    el.innerText = "新标题";         // 修改文本
    el.setAttribute("class", "highlight"); // 设置属性
    el.style.color = "red";          // 修改样式

✅ 事件绑定

    let btn = document.getElementById("btn");
    btn.addEventListener("click", () => {
      alert("按钮被点击了!");
    });

4. 你对BOM的理解,常⻅的BOM对象你了解哪些?

  • Browser Object Model,浏览器对象模型。是浏览器提供的与窗口交互的接口,主要是操作 浏览器窗口和外层环境 的对象集合。

  • 📌 区别:

    • DOM 关注文档内容(HTML、XML)
    • BOM 关注浏览器本身
  • window

    • 浏览器的全局对象(JS 顶层对象),DOM 和 BOM 的顶级对象
        window.alert("Hi");
        console.log(window.innerWidth); // 窗口宽度
  • navigator

    • 浏览器信息(类型、版本、语言、用户代理等)

            console.log(navigator.userAgent); // 获取浏览器引擎!!!
  • location

    • 当前页面的 URL 信息,可用来跳转
      console.log(location.href);  // 当前网址
      location.href = "https://juejin.cn"; // 页面跳转
    
  • history

    • 访问历史记录(前进/后退)
       history.back();  // 后退
       history.forward(); // 前进
  • screen

    • 用户屏幕的信息
    
         console.log(screen.width, screen.height);
    

5. == 和 ===区别, 分别的使用场景

  • == 会做类型转换

  • 但在⽐较 null 的情况的时候,我们⼀般使⽤相等操作符 ==

    • 因为 null 和 undefined在宽松情况下是相等的 null==undefined
    • 在比较 null 时,null 只和 undefined 相等,不会与其他类型相等。
    • 所以 if (value == null) 这个判断,等价于: value === null || value === undefined

    👉 一条语句就能同时判断 值为 null 或 undefined 的情况。

6.typeof 与 instanceof 区别

  • typeof 返回一个字符串,注意typeof null 为 'object'(这是 JavaScript 的一个已知bug,null 实际上是基本数据类型)
  • instanceof(是否实例?) 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型上
    • object instanceof Constructor
    • 使用:【对象 instanceof 构造函数】。 obj instanceof Function
    • 构造函数可以通过new实例对象,insatnceof判断这个对象是不是这个构造函数的实例
function Foo() {}
const obj = new Foo();

console.log(obj instanceof Foo); // true
console.log(obj instanceof Object);
// true,因为 Foo.prototype.__proto__ === Object.prototype
console.log(obj instanceof Array); // false

👆关键点:

  • JS内部会做:
    • 取出Foo.prototype
    • 沿着obj的原型链(__proto__)往上找
    • 如果在某一层原型上找到Foo.prototype就返回true;否则返回false
  • 手写instanceof
function myInstanceof(left,right){
// 先判断是不是基础数据类型,如果是,返回false【是不是引用类型,且不为null】
    if(left === null || typeof left !== 'object') return false;
     // getPrototypeOf 是Object构造函数上帝的方法, 返回对象的原型(即 [[Prototype]])内部的属性
     // `Object.getPrototypeOf(obj)` 是获取【指定对象】的【原型】(即 `__proto__` 或 `[[Prototype]]` 内部槽)。
    let proto = Object.getPrototypeOf(left);//本质上就是 `obj.__proto__`
     if(proto === null) {
      return false;
    }
    while(proto) {
      if(proto === right.prototype) { // 判断right.prototype是否出现在left的原型练上
        return true;
      }
      proto = Object.getPrototypeOf(proto);
    }
    return false;
}
  • 区别
    • typeof 返回字符串;instanceof返回布尔值

    • instanceof可以准确判断复杂引用数据类型,但是不能正确判断基础数据类型

    • typeof 不能判断null,也不能判断Function

    • 各有利弊:最优解: Object.property.toString,调用该方法统一返回 “[object xxx] ”的字符串

image.png

  • 为什么要用call?

    1. 先看 Object.prototype.toString

  • 这是所有对象都继承的一个方法,作用是 返回对象的内部 [[Class]] 标记,结果类似:

    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call(123); // "[object Number]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    
  • 它比 typeof 更强大,因为 typeof 对数组、对象、null 都会返回 "object",而 toString 能精确区分。


2. 为什么要用 call

  • 如果你直接调用 .toString(),情况会不一样:
const arr = [];
console.log(arr.toString()); // ""  (空字符串)

👉 这里返回的并不是 [object Array],而是数组自己重写的 toString 方法(把元素用逗号拼接)。

再比如:

console.log((123).toString()); // "123"
console.log((true).toString()); // "true"

👉 基本类型和数组、函数等,都有自己覆盖过的 toString


  • 为了强制调用最原始的 Object.prototype.toString 方法,必须写成:

    Object.prototype.toString.call(value)
    

    这样可以绕过对象本身的 toString,保证用的就是 Object.prototype 上的那个版本。


3. call 的作用
  • call 的语法:func.call(thisArg, arg1, arg2, ...)
  • 在这里就是把 thisArg 设置为我们要检测的值,让 Object.prototype.toStringthis 指向那个值。
Object.prototype.toString.call([]); 
// 等价于:Object.prototype.toString.apply([], []);
// 内部 this → []

Object.prototype.toString.call(123);
// 内部 this → 123(会被装箱成 new Number(123))

如果不用 call,你就没法让 Object.prototype.toStringthis 指向其他对象。

function sayHi(){
    console.log(this.name)
}
const tom = {name:'tom'};
const jerry = {name:'jerry'};
// 默认调用sayHi() ; 结果是undefined或者null 
// 如果想让它代表tom去执行
sayHi.call(tom) // 让sayHi的this指向tom
  • 回到 Object.prototype.toString(value) , 是想让toString中的this指向value,让value成为toString中的this。
  • 总结:.call() 是 为了明确的告诉Object.prototype.toString() “请用我传进来的值作为this来判断类型”

7. 原型,原型链 ? 有什么特点?

  • 一句话记住:javascript所有对象都有“祖先”(原型),如果找不到东西就会一层层往祖先(原型)身上找。这就是原型链

  • 通俗类比

    • 小明要找钥匙,在自己口袋(对象本身)找
    • 找打不到,就去找爸爸(__proto__指向的原型)
    • 没有,再往爷爷身上找,爸爸__proto__指向的原型,
    • 最后实在没有,就返回undefined
  • 重点概念解释

    • 什么是原型prototype

      • 每个函数function都有一个.prototype属性,他是一个对象,用来给它创建的实例当作原型
      function Person(){}
      Person.prototype.sayHi = function (){
          console.log('Hi')
      }
      const p = new Person()
      p.sayHi(); // p 找不到sayHi就去p.__proto__上去找,也就是Person.prototype
      
      • 可以理解为,构造函数.prototype属性是所有“儿子”(实例)的共同祖先
    • 什么是__proto__

      • 每个对象都有一个隐藏属性__proto__。他指向他的原型(构造函数的.prototype)
      console.log(p.__proto__ === Person.prototype) // true
      // 也就是__proto__是指向爸爸的手指
      
    • 原型链是什么?

      • 就是一条由__proto__串起来的链条
      • p.proto ---> Person.prototype ---> Object.prototype ---> null
      p.__proto__ === Person.prototype
      Person.prototype.__proto__ === Object.prototype
      Object.prototype.__proto__ === null
      
      
    • 记忆口诀

      • 构造函数有.prototype
      • 实例对象有__proto__
      • 查属性:先自己,再爸爸,再爷爷……
      • 顶头祖宗是 Object.prototype

    核心口诀

    • 对象有 __proto__,指向构造函数的 prototype
    • 函数既是对象,也有 __proto__,而且还有 prototype 属性
    • 所有 prototype 的尽头都是 Object.prototype
    • 所有 __proto__ 的尽头都是 null

8. 说说你对【作⽤域链】的理解

  • 作用域链就是“找变量”的路线图, 优先级:inner->outer->global
    let a = 1; // 全局作用域

    function outer() {
      let b = 2; // 外层函数作用域

      function inner() {
        let c = 3; // 内层函数作用域
        console.log(a); // 往上找到了 1
        console.log(b); // 往上找到了 2
        console.log(c); // 自己作用域里就有 3
      }

      inner();
    }

    outer();

  • 全局作⽤域、函数作⽤域、块级作⽤域
    • 全局作⽤域

      • 最外层,不在任何代码块和函数内
      • 一旦创建,在整个页面/程序中都能访问
      • 过多全局变量会造成变量命名冲突
    • 函数作用域

      • 函数内部声明,函数内部访问
      • 每次调用函数都会创建一个新的作用域环境。
      • 函数作用域变量不会污染全局
      • 函数内部变量可以访问函数外部变量,通过作用域链
    • 块级作用域

      • 用花括号{}括起来的区域,比如if,else,while,代码块,使用let/const 声明的变量只在这个块中有效
      • var不受块作用域限制!
      • 类比:像一个临时会议室,讨论完就走,东西也不能带出去
      {
        let c = 300;
        console.log(c); // ✅ OK
      }
      console.log(c); // ❌ 报错:c is not defined
      
      
    • 词法作用域

      • 又叫静态作用域。就是变量被创建的时候就确定好了作用域,而不是执行阶段。javascript遵循的就是词法作用域

        var a = 2;
        function foo(){
            console.log(a) // 输出: 2 
        }
        function bar(){
            var a = 3;
            foo()
        }
        bar()
        
        

9. 谈谈this的理解

  • 一句话记住:普通函数中,this是谁在调用函数,就指向谁

  • 五种常见情况+通俗解释

    • 1、普通函数调用(谁也没喊)
    function show(){
        console.log(this)
    }
    show() // this是window
    // 类比:你一个人自言自语:“我是谁?” ——没人回应,所以默认是 **全局对象**(浏览器中是 `window`)。
    // 在 **严格模式下(`'use strict'`)** ,`this` 是 `undefined`
    
    • 2、对象调用自身的方法(谁喊听谁的)
    const person = {
      name: 'Tom',
      sayHi() {
        console.log(this.name);
      }
    };
    
    person.sayHi(); // 👉 输出 Tom
    
    
    • 3、构造函数中(new)
    function Person(name) {
      this.name = name;
    }
    const p = new Person('Alice');
    console.log(p.name); // 👉 Alice
    
    • 类比:你去开了一家“人”工厂,每次 new 出来一个人,this 就是新出生的那个对象
  • 箭头你函数(不听调用者,只认生父)

    const obj ={
        name:'Lucy',
        sayHi:()=>{
            console.log(this.name)
        }
    }
    obj.sayHi() // undefined
    
    • 类比:箭头函数里的 this 就像“遗传基因”,永远指向它“出生的地方”的 this不会变

           function outer(){
               const inner = ()=>{
                   console.log(this) // 箭头函数没有自己的this,定义时捕获它所在作用域的this
               }
               inner()
           }
           outer() // 普通函数在调用时动态绑定 `this`。this 是 window(或 undefined,严格模式下)
      

    步骤追踪:

    1. 你调用 outer() 时:

      • 非严格模式下,普通函数 outerthis 默认是 window
      • 严格模式下,outerthisundefined
    2. outer 内部定义了 inner 箭头函数:

      • 箭头函数不会生成自己的 this,它的 this 直接继承自 outerthis
    3. 因此:

      • 如果非严格模式:outerthis = windowinnerthis 也是 window

      • 如果严格模式:outerthis = undefinedinnerthis 也是 undefined

记忆小口诀

  • 普通函数 this:运行时决定
  • 箭头函数 this:定义时决定(继承外层作用域)
  • 手动指定this,call、bind、apply
    • call是立即调用,参数用逗号隔开
    • apply也是立即调用,参数是数组
    • bind 不是立即执行,返回一个新的函数,稍后执行

10. new操作符具体干了什么

  • 一句话记住:“造人”,用构造函数作为蓝图,生成一个全新的对象,并把“我是谁”(this)指定给他
  • new 做了四件事:(超级重要!一定要记住)

const obj = new Constructor()
// 上面👆实际上相当于:
function newOperator(Constructor,...args){
    // 1. 新建一个对象
    const obj ={};
    
    // 2.将新对象的原型指向构造函数的prototype
    obj.__proto__ = Constructor.prototypr;
    
    // 3.执行构造函数,把this指向新的对象
    const result = Constructor.apply(obj,args);
    
    // 4.  如果构造函数返回一个对象,就返回它;否则返回新对象
  return typeof result === 'object' && result !== null ? result : obj;
  
}
  • 一句话:“造对象、改原型、执行函数、决定返回谁”。
function Animal(type) {
  this.type = type;
}

const dog = new Animal('dog');

// 相当于:
// 1. 创建新对象
const dog = {}; 

// 2. 设置原型指向 Animal.prototype
dog.__proto__ = Animal.prototype;

// 3. 执行 Animal,把 this 指向 dog
Animal.call(dog, 'dog'); // dog.type = 'dog'

// 4. 构造函数没有手动 return 对象 => 返回 dog。返回值看构造函数有没有返回对象。

  • 小细节:

    • 如果构造函数里写了 return { xx },就会返回那个对象,this 就被覆盖了;
    • 如果 return 返回基本类型(如 return 123),new 会无视,还是返回 this

11. bind、call、apply

除了箭头函数外,普通函数的this不是在定义的时候决定的,而是在调用的时候决定的。call/apply/bind 只是显式改变调用方式(也就是改变调用时的“接收者”),从而改变 this
四种调用方式:
  1. 默认/函数调用(Default binding)

    • 写法:fn()

    • this:非严格模式下为全局对象(浏览器是 windowglobalThis),严格模式下为 undefined

    • 例:

      function f(){ console.log(this); }
      f(); // window(非严格) 或 undefined(严格)
      
  2. 隐式绑定 / 对象方法调用(Implicit binding)

    • 写法:obj.method() —— 关键是“点左边的那个值(base)”决定了 this

    • this:指向调用点的对象 obj

    • 例:

      const o = {
          x:1, 
          getX(){ 
              return this.x; 
          }};
      o.getX(); // this 指向 o;输出:1
      
      • 注意:把方法赋给变量再调用会丢掉隐式绑定(const g = o.getX; g() -> 丢失 o)。
  3. 显式绑定(Explicit binding):call / apply / bind

    • 写法:
      • fn.call(ctx, a, b)
      • fn.apply(ctx, [a,b])
      • const g = fn.bind(ctx, preArg1)(返回一个新函数 g
    • this:由第一个参数ctx决定
  4. 构造调用

    • 写法new Fn()
    • this:指向新创建的实例(由引擎创建),并且该实例继承 Fn.prototype
    • 与显式绑定的关系:构造调用(new)优先于显式绑定 —— 也就是说 new (fn.bind(obj))() 时,this 是新实例,不是 obj(但预设参数仍然生效)。

this 如何被决定 —— 引擎的心理模型(更接近规范)

JS 引擎内部在遇到一次函数调用时,大体做这件事:

  • 判断调用形式(是作属性调用 obj.fn() 还是普通函数 fn() 还是用 call/apply/bound/ new)。
  • 如果是属性调用:取那个属性访问的“base value”作为 thisobj)。
  • 如果是 call/apply:把你传的 thisArg 作为 this(非严格模式下若传 null/undefined 则替换为全局对象;非对象原始值会被装箱成对应对象)。
  • 如果是 new:新对象成为 this(即使原来函数是通过 bind 得到的,new 也会优先产生新 this)。

箭头函数的特别点

  • 箭头函数没有自己的 this,它在定义时就从外层词法环境捕获 this(类似闭包)。
  • 因此对箭头函数调用 call/apply/bind 不会改变它的 this

例:

const obj = {x: 1};
const arrow = () => console.log(this);
arrow.call(obj); // this 仍然是定义时的 this(不会被 call 改变)

call/apply 的原理(为什么能改变 this

把函数作为对象属性去调用:obj.fn(),此时 fnthis 就是 obj
call/apply 的 trick 就是临时把函数挂到目标对象上,然后以 obj.fn() 的形式调用它

  • fn.call(obj) 样函数内部的 this 就是 obj!
  • fn.call(obj) 样函数内部的 this 就是 obj!
  • fn.call(obj) 样函数内部的 this 就是 obj!

  • 实现步骤(心理模型):
  1. 接收 thisArg(目标对象)和参数;
  2. 如果 thisArgnull/undefined,在非严格模式下替换为全局对象(浏览器:window/globalThis);如果是原始值(number/string/boolean),将其包装成对象(Object(thisArg));
  3. thisArg 上创建一个临时唯一属性(通常用 Symbol),把要调用的函数赋给它;
  4. 执行 thisArg[临时属性](...args)
  5. 删除临时属性并返回结果。

这就是你在 polyfill 中看到“把函数挂到上下文对象上然后删掉”的原因 —— 它模拟了属性调用的 this 语义。

bind 的更多细节(为什么更复杂)

  • bind 返回的不是原函数本身,而是一个新的函数对象(内部记录了:要调用的原函数、绑定的 this、以及预置的参数)。
  • 当调用这个“bound 函数”时,运行时会把绑定的 this 和预置参数与调用时的参数合并再去调用目标函数。
  • 但如果用 new boundFn(),引擎需要当作构造来处理:这时候 bind 绑定的 this 被忽略this 指向新实例(这是规范要求),但预设参数仍然被传入。
  • 标准里 bind 返回的是一个 “Bound Function exotic object”,它内部有 [[BoundTargetFunction]][[BoundThis]][[BoundArguments]] 等槽。

严格模式 vs 非严格模式(与 this 相关)

  • 非严格模式

    • fn.call(null)fn.call(undefined) -> this 会被替换成全局对象(浏览器是 window)。
    • fn.call(123) -> this 被装箱为 new Number(123)(也就是一个对象)。
  • 严格模式

    • fn.call(null) -> this 就是 null(不会替换为全局对象)。
    • 原始值也不会被强制装箱(this 按原样传入)。

♻️ 注:在实际 polyfill 中无法判断目标函数是否以严格模式定义,所以 polyfill 通常采用兼容性做法(把 null/undefined 替换成全局对象),这是为什么实现上会看到 context == null ? globalThis : Object(context) 的写法。

常见面试/实战例子(加深理解)

  1. 方法丢失上下文:
        const o = {
            x: 1, 
            getX(){ 
                return this.x 
            }
        };
        const g = o.getX;// 只是复制了函数的引用,没有绑定上下文,导致this丢失
        g(); // this 丢失 -> undefined/global
        // 解决:const g = o.getX.bind(o);
  1. bindnew
        function Person(name) {
            this.name = name; 
        }
        const B = Person.bind({x:1}, 'Alice');
        const p = new B(); // p.name === 'Alice',this 指向新实例,不是 {x:1}
  1. 箭头函数不受 bind 影响:
        const arrow = () => this;
        const b = arrow.bind({x:1});
        b() === this; // true

最简单的 call 方法的完整实现(示意,不含严格模式分支)

Function.prototype._call = function(ctx, ...args) {
  // 1. 处理上下文
  // -   `null/undefined` → 全局对象
  // -   原始值(`string`, `number`等) → 包装对象 `Object(1)` → `Number(1)`
  ctx = ctx == null ? globalThis : Object(ctx);
  
  // 2. 创建唯一键避免属性冲突
  const key = Symbol();
  
  // 3. 关键步骤:将当前函数绑定到上下文对象
  ctx[key] = this;
  
  // 4. 通过对象方法调用方式触发 this 绑定
  const res = ctx[key](...args);
  
  // 5. 清理临时属性,避免内存泄漏和属性污染

  delete ctx[key];
  
  return res;
};


// 使用验证
function introduce() {
    return `我是${this.name},今年${this.age}岁`;
}

const person = {name: "小明", age: 20};

// 正常调用
introduce._call(person); // "我是小明,今年20岁"

这段代码就是把“把函数挂到对象上再调用”的思想浓缩为最小示例。

小结(你该记住的关键点)

  • this调用时决定的(箭头函数例外)。

  • 四种常见调用规则:默认、隐式(对象方法)、显式(call/apply/bind)、构造(new)。

  • call/apply 的实现思路:临时把函数挂到目标对象上并作为其属性调用。

  • bind 返回一个带内部槽的“已绑定函数”,并处理部分应用与 new 的特殊行为。

  • 严格模式会改变 null/undefined 与原始值如何成为 this

  • call apply 立即来,参数格式别弄歪;

  • call 参数用逗号,apply 括号装起来;

  • bind 返回新函数,执行时才把它拽。

    • 实现一个bind
    // 目标:
    const boundFn = originalFn.bind(obj, arg1, arg2);
    boundFn(arg3, arg4); // 会以 obj 作为 this 执行 originalFn(arg1, arg2, arg3, arg4)
    
    
    // 给 Function.prototype 添加一个 myBind 方法
    Function.prototype.myBind = function (context, ...bindArgs) {
      // 保存原始函数(也就是被绑定的函数)
      const originalFn = this;
    
      // 返回一个新函数,用户调用这个新函数时才会执行原始函数
      function boundFn(...callArgs) {
        // 判断是否通过 new 调用这个 boundFn
        // 如果是,this 应该是 boundFn 的实例,而不是 context
        const isNewCall = this instanceof boundFn;
    
        // 如果是通过 new 调用,就使用 new 出来的 this;
        // 否则使用 bind 传入的 context
        const finalThis = isNewCall ? this : context;
    
        // 调用原始函数,合并 bind 时的参数和调用时的参数
        return originalFn.apply(finalThis, [...bindArgs, ...callArgs]);
      }
    
      // 为了让通过 new 创建的对象继承原始函数的原型,需要设置 boundFn 的原型
      // Object.create 创建一个空对象,并将其原型设置为 originalFn.prototype
      boundFn.prototype = Object.create(originalFn.prototype);
    
      // 返回绑定后的函数
      return boundFn;
    };
    

12. JavaScript中【执⾏上下⽂】和【执⾏栈】是什么?

  • 一句话理解:执行上下文是js执行一段代码时的“环境信息打包箱子”,执行栈是JS 管理这些箱子的“堆叠货架”。

  • 什么是执行上下文

    • 每段js代码在执行前,js引擎都会创建一个上下文对象,里面装着:
      1. 变量环境(变量、函数声明等)
      2. 作用域链 (作用域信息)
      3. 带上记忆(this的指向)
    • 三种执行上下文类型
      1. 全局执行上下文:页面加载时最先创建,全局作用域
      2. 函数执行上下文:每次函数调用时生成新的上下文
      3. eval执行上下文:eval 执行代码也有自己的上下文(很少用)
  • 什么是执行栈

    • 后进先出
    function a() {
      b();
      console.log('a done');
    }
    
    function b() {
      console.log('b done');
    }
    
    a();
    // 执行结果:先 b done,然后 a done 
       
    
    // 执行过程:
    // -   创建全局上下文(`global`),**压栈**
    // -   调用 `a()`,创建 `a` 上下文,**压栈**
    // -   `a` 里调用 `b()`,创建 `b` 上下文,**压栈**
    // -   `b` 执行完,**弹出 b 的上下文**
    // -    `a` 执行完,**弹出 a 的上下文**
    // -   最后只剩下全局上下文
    
    [全局上下文]         // 页面开始执行
    [全局上下文]
    [a上下文]           // 调用了 a()
    [全局上下文]
    [a上下文]
    [b上下文]           // a() 里面调用了 b()
    [全局上下文]
    [a上下文]           // b() 结束,弹出 b
    [全局上下文]         // a() 结束,弹出 a
    
    

    执行栈(从底到顶):

    • 🔽
    • [ 全局执行上下文 ]
    • [ 函数a的执行上下文 ]
    • [ 函数b的执行上下文 ]

13. 说说JavaScript中的事件模型

  • 捕获 → 目标 → 冒泡 三个阶段。

  • 衍生概念:
    • 事件冒泡(默认机制)
      • 默认事件是从内往外传播的
    • 事件委托
      • 利用冒泡,在父元素上监听子元素的事件。性能好,不用给每个子元素都绑定事件
    • 阻止冒泡&默认行为
      • 阻止冒泡:event.stopPropagation();
      • 阻止默认行为(比如 a 标签跳转) :event.preventDefault();

14. 解释下什么是事件代理(事件委托)?应⽤场景?

  • 就是把子元素的事件,交给父元素来处理,利用事件冒泡来实现。
  • 事件触发后,会冒泡到父元素,你只要在父元素上监听,在事件对象中判断是谁触发的(通过 e.target ,就能知道具体哪个子元素点击了。

15. 闭包

  • 通俗说:就是让函数记住它**出生**时候的作用域,即使搬出去了也不会忘记

  • 闭包是一个函数,它可以访问定义它时的外部函数作用域的变量,即使外部函数已经执行完毕

  • 最常见的闭包例子

    function outer(){
        let count = 0 ;// 外部变量
        return function inner(){
            count++;
            console.log(count)
        }
    }
    const fn = outer() // outer 执行完毕,但是inner记住了count
    fn() // 输出 1
    fn() // 输出 2
    // inner是闭包,它访问了outer中的count。即使outer已经执行完了,但是count没有被释放
    
    

思考:

  • 按理说 outer() 执行完后,count 应该被销毁对吧?
  • inner 还能访问它!为什么?
    • 原因:
      • inner 是一个闭包
      • 它保存着一个作用域链:inner->outer->global
      • 所以即使outer执行完毕了,count这个变量仍然被inner的作用域引用着,不会被当垃圾回收掉
    • 所以可以理解为:闭包是作用域链的快照,它把当时的作用域链保存了下来
  • 三个特点

    • 函数嵌套函数:闭包必须是函数里定义另一个函数
    • 内部函数访问外部变量:内层函数可以使用外层函数的变量
    • 外部函数执行后变量不释放:变量会被记住,不会被销毁
  • 常见使用场景:

    • 数据私有化(模拟私有变量)
    function createrCounter(){
        let count = 0;
        return {
            inc(){
                count ++;
            },
            get(){
                return count;
            }
        }
    }
    const counter = createCounter();
    counter.inc();
    console.log(counter.get()); // 1
    // 外部无法直接访问count,只能通过丰富控制 -- 实现封装
    
    • 循环中创建函数(防止变量被覆盖)
    for (var i = 0; i < 3; i++) {
      setTimeout(function () {
        console.log(i); // 输出 3,3,3 ❌
      }, 100);
    }
    
    // 使用闭包修改
    for (var i = 0; i < 3; i++) {
      (function (j) {
        setTimeout(function () {
          console.log(j); // ✅ 0,1,2
        }, 100);
      })(i);
    }
    
    
    • 惰性计算/缓存结果
    function memoizedAdd() {
      const cache = {};
    
      return function (n) {
        if (cache[n]) {
          return cache[n];
        } else {
          cache[n] = n + 10;
          return cache[n];
        }
      };
    }
    
    const add = memoizedAdd();
    add(5); // 计算并缓存
    add(5); // 直接返回缓存
    
    • 生成函数工厂(自定义函数)
    function makeMultiplier(x) {
      return function (y) {
        return x * y;
      };
    }
    
    const double = makeMultiplier(2);
    console.log(double(5)); // 10
    
    
  • 闭包的优缺点

    • 优点:
      • 模拟私有变量,封装性好
      • 变量不会被提前销毁
      • 工厂函数、延迟执行等场景好
    • 缺点:
      • 容易造成内存泄露
      • 作用域链变长,调试麻烦
      • 滥用会导致性能问题
  • 结合之前说的作用域链:inner->outer->global作用域链的知识

    • 闭包=函数+它的作用域链
    • 也就是说闭包能够记住外部变量是因为保存了作用域链
  • 怎么解决闭包可能会导致内存泄露的问题?

    • 可能会导致内存泄漏的场景:
      • DOM 事件绑定里使用闭包但没移除
      function bind() {
        const bigData = new Array(1000000).fill('x'); // 模拟大内存
      
        document.getElementById('btn').addEventListener('click', function () {
          console.log(bigData[0]); // 闭包引用 bigData
        });
      }
      
      
    • 解决:
      • 1.及时移除事件监听器
      function bind() {
        const bigData = new Array(1000000).fill('x');
      
        const handler = function () {
          console.log(bigData[0]);
        };
      
        const btn = document.getElementById('btn');
        btn.addEventListener('click', handler);
      
        // ❗ 不再需要时移除
        btn.removeEventListener('click', handler);
      }
      
      
      • 避免不必要的闭包,尤其是大数据量
      // ❌ 不要这样保留没必要的数据
      function create() {
        const large = new Array(1000000).fill('x');
        return () => console.log('do something');
      }
      
      // 改写成、
      function create() {
        return () => console.log('do something');
      }
      // 不用的数据别放在闭包了
      
      
      
      • 将闭包变量置为 null(断引用)
      let closure;
      function create() {
        let temp = 'xxx';
        closure = function () {
          console.log(temp);
        };
      }
      
      // 使用完 closure 后清理
      closure = null;
      
      
      • 模块化隔离数据,不暴露不必要的闭包
      const Counter = (function () {
        let count = 0;
      
        function inc() {
          count++;
          console.log(count);
        }
      
        return {
          inc
        }; // ❗ 只暴露 inc,不暴露 count
      })();
      

16. 谈谈 JavaScript 中的类型转换机制

  • 分为自动转换、手动转换
    • 隐式转换
        1. “+” : 字符串有限;只要一边是字符串,就会把另一边也转成字符串
        1. 减法 -、乘法 *、除法 /:都转成数字
        1. 比较 == 会自动转类型
    • 显式转换
      • 数值转换规则:

        • Number('123') // 👉 123
        • Number('') // 👉 0
        • Number(null) // 👉 0
        • Number(undefined) // 👉 NaN
        • Number(true) // 👉 1
        • Number(false) // 👉 0
      • 字符串转换:

        • String(123) // 👉 '123'
        • String(null) // 👉 'null'
        • String(undefined) // 👉 'undefined'
        • String(true) // 👉 'true'
      • 布尔值转换:

        |原始值转为 Boolean 的结果
        false0NaN''nullundefined👉 false
        其他值(包括对象、数组、非零数)👉 true
        • Boolean('hello') // 👉 true
        • Boolean(0) // 👉 false
        • Boolean([]) // 👉 true(数组是对象)**

17. 深拷⻉浅拷⻉的区别?如何实现⼀个深拷⻉?

  • 一句话理解:
    • 浅拷贝:只复制第一层,内部引用类型共享地址。
    • 深拷贝:复制所有层级,彻底断开引用关系。
  • 如何实现一个深拷贝?
    • 方法 1:JSON 方式(最简单)
      • const obj2 = JSON.parse(JSON.stringify(obj1));
      • 优点:简单粗暴
        缺点 ❌:
        • 会丢失:undefined函数symbol
        • 无法处理循环引用(自己引用自己)
    • 方法 2:手写递归版深拷贝
    // obj 是要拷贝的目标对象,hash是用来记住已经拷贝过的内容
        function deepClone(obj, hash = new WeakMap()) {
        // 判断是否为基本数据类型,基本数据类型不需要深拷贝,直接返回
          if (obj === null || typeof obj !== 'object') return obj;
        // 如果 `hash` 里已经存在这个对象,说明它之前被拷贝过。
        // 直接返回之前存好的拷贝,防止 **循环引用导致的无限递归**。
          if (hash.has(obj)) return hash.get(obj); // 处理循环引用
            
          const clone = Array.isArray(obj) ? [] : {};
          hash.set(obj, clone);//**我刚开始克隆这个对象,就先登记一下,这样后续如果又遇到这个对象,就知道直接返回正在构建的克隆版本**
    
          for (let key in obj) {
            if (obj.hasOwnProperty(key)) {// `hasOwnProperty` 确保只拷贝对象自身的属性
              clone[key] = deepClone(obj[key], hash);//**继续深入克隆每个属性,并把同一个 hash 传递下去,让所有递归调用共享这个记忆**
            }
          }
    
          return clone;
        }
        ✅ 优点:能复制数组、对象、嵌套结构  
        ✅ 支持循环引用!  
        ❌ 不支持函数、DOMMap/Set 等特殊对象(可以扩展)
        
        * 扩展下WeakMap
            * WeakMap的key只能是对象,基本数据类型不能作为键
            * -   **值可以是任意类型**
            *  **弱引用**:键是对象的弱引用,不会阻止垃圾回收
            *  当没有其他引用指向这个对象时,它会被垃圾回收
            *  对应的 WeakMap 键值对也会自动被清理
            *  如果 `obj` 本身没有其他引用了,它和对应的 `clone` 可以被垃圾回收,不会造成内存泄漏。这是使用 `WeakMap` 而不是普通 `Map` 的关键优势之一。
    
    
            const obj1 = {
              a: 0,
              c: 1,
              b: {
                d: {
                  e: 9999
                }
              }
            };
    
            const result = deepClone(obj1);
            console.log(result);
        运行时 `console.log(hash)` 会在每一层递归打印 `WeakMap` 的内容:
        -   第一次:  
            `WeakMap { obj1 → {} }`
        -   第二次:  
            `WeakMap { obj1 → {}, obj1.b → {} }`
        -   第三次:  
            `WeakMap { obj1 → {}, obj1.b → {}, obj1.b.d → {} }`
    
    
  • 常见的浅拷贝
方法是否浅拷贝说明
Object.assign()✅ 是后者覆盖前者
展开运算符 {...}✅ 是推荐语法简洁
Array.slice()✅ 是拷贝数组第一层
Array.concat()✅ 是拼接也能产生浅拷贝
* 还会用 loadsh库 最全面

18. Javascript中如何实现【函数缓存】?函数缓存有哪些应用场景?

  • 一句话理解: 函数缓存就是把函数运行的结果保存起来,下次遇到一样的参数就直接返回结果,不再重新计算。
function square(n) {
  console.log('计算了!');
  return n * n;
}
// 你每次都传 `5`,它每次都“重新算一遍”。
// 我们想: **“如果5已经算过了,别再算了!”**


// 实现函数缓存:记住算过的
function cachedSquare(){
    const cache ={}; //创建一个缓存对象
     return function (n){
         if(cache[n] !== undefined){
             console.log('从缓存取结果!');
             return cache[n];
         }
        console.log('计算了!');
        const result = n * n;
        cache[n] = result; // 把结果缓存起来
        return result;
     }
}
const square = cachedSquare();

square(5); // 计算了!25
square(5); // 从缓存取结果!25
square(6); // 计算了!36
square(5); // 从缓存取结果!25

  • 应用场景
场景解释
🔄 递归函数优化(如斐波那契)减少重复计算,大幅提升性能
📊 数据处理(如 filter/sort/map)某些函数会被频繁调用
🧠 AI 计算(如路径规划、搜索)缓存中间结果避免爆炸计算
⚙️ 后端缓存 API 调用(SSR 场景)相同输入避免反复请求接口
🎮 游戏中的物理/碰撞计算相同位置避免重复运算
🖼️ 图片/组件渲染缓存Vue/React 中也常用 memoization 优化渲染性能
  • 通用的“记忆化工具函数”
function memoize(fn){
    const cache = new Map();
    
    return function(...args){
        const key = JSON.stringify(args); // 多参数支持
        if(cache.has(key)){
            return cache.get(key)
        }
        
        const result = fn.apply(this,args);
        cache.set(key, result);
        return result;
    }
}

// 使用
const slowAdd = (a, b) => {
  console.log('计算了!');
  return a + b;
};

const fastAdd = memoize(slowAdd);

fastAdd(1, 2); // 计算了!3
fastAdd(1, 2); // 缓存!3

19. JavaScript字符串的常⽤⽅法有哪些?

  • 一句话:改、查、切、拼、删、补、转
    • 方法作用例子
      replace(old, new)替换字符串内容'apple'.replace('p', 'b')'abple'
      replaceAll(old, new)替换所有匹配'aaa'.replaceAll('a', 'b')'bbb'
      toUpperCase()转大写'hi'.toUpperCase()'HI'
      toLowerCase()转小写'HI'.toLowerCase()'hi'
    • 方法作用例子
      includes(sub)是否包含子串'hello'.includes('ll')true
      indexOf(sub)第一次出现的位置'hello'.indexOf('l')2
      lastIndexOf(sub)最后一次出现的位置'hello'.lastIndexOf('l')3
      startsWith(sub)是否以子串开头'hello'.startsWith('he')true
      endsWith(sub)是否以子串结尾'hello'.endsWith('lo')true
    • 方法作用例子
      slice(start, end)截取 [start, end)'abcdef'.slice(1, 4)'bcd'
      substring(start, end)同上,但不支持负数'abcdef'.substring(1, 4)'bcd'
      substr(start, length)从 start 开始,取 length 个'abcdef'.substr(2, 3)'cde'
    • 方法作用例子
      concat(str1, str2)拼接字符串'hello'.concat(' ', 'world')'hello world'
      +拼接运算符'hi' + '!''hi!'
      join()(数组方法)把数组变字符串['a', 'b'].join('-')'a-b'
    • 方法作用例子
      trim()去掉头尾空格' hi '.trim()'hi'
      trimStart() / trimEnd()去头或尾空格' hi '.trimStart()'hi '
    • 方法作用例子
      padStart(len, fill)左边补到指定长度'5'.padStart(3, '0')'005'
      padEnd(len, fill)右边补到指定长度'5'.padEnd(3, '*')'5**'
    • 方法作用例子
      split(delimiter)字符串 → 数组'a,b,c'.split(',')['a', 'b', 'c']
      parseInt(str)字符串 → 整数parseInt('123')123
      Number(str)字符串 → 数字Number('12.3')12.3

20. 数组的常⽤⽅法有哪些?

🧩 一句话记忆法:

增删改查、遍历排序、转化过滤、判断查找

也可以记成:

增删改查 → 排序遍历 → 判断过滤 → 转换拍平


🍱 一类一类带你理解 + 举例 + 好记逻辑

✅ 一、增:往数组里添加元素

方法作用示例
push()尾部加元素[1, 2].push(3)[1, 2, 3]
unshift()头部加元素[1, 2].unshift(0)[0, 1, 2]
splice(index, 0, item)指定位置插入[1, 2].splice(1, 0, 99)[1, 99, 2]

✅ 二、删:删除数组中的元素

方法作用示例
pop()删除最后一个[1,2,3].pop()[1,2]
shift()删除第一个[1,2,3].shift()[2,3]
splice(index, n)删掉从 index 起的 n 个[1,2,3,4].splice(1, 2)[1,4]

✅ 三、改:修改数组

方法作用示例
splice()可删除 + 替换[1,2,3].splice(1, 1, 9)[1,9,3]
直接赋值指定位置修改arr[0] = 99

✅ 四、查:访问数组信息

方法作用示例
includes(val)是否包含[1,2,3].includes(2)true
indexOf(val)找第一个匹配的下标[1,2,3].indexOf(3)2
lastIndexOf(val)找最后匹配的下标[1,2,3,2].lastIndexOf(2)3

✅ 五、遍历(非常常用)

方法作用示例
forEach(fn)每项执行一次函数(无返回)arr.forEach(x => console.log(x))
map(fn)每项变换,返回新数组[1,2].map(x => x*2)[2,4]

✅ 六、过滤 & 查找

方法作用示例
filter(fn)过滤符合条件的项[1,2,3].filter(x => x > 1)[2,3]
find(fn)找到第一个符合条件的值[1,2,3].find(x => x > 1)2
findIndex(fn)找到第一个符合条件的下标[1,2,3].findIndex(x => x > 1)1

✅ 七、判断数组结构

方法作用示例
Array.isArray(x)是否为数组Array.isArray([1,2])true
every(fn)是否所有项都满足[1,2,3].every(x => x > 0)true
some(fn)是否有任意一项满足[1,2,3].some(x => x > 2)true

✅ 八、排序 & 反转

方法作用示例
sort()排序(默认按字符串)[3, 1, 2].sort()[1,2,3]
sort((a,b) => a-b)数字排序[3,1,2].sort((a,b)=>a-b)[1,2,3]
reverse()倒序[1,2,3].reverse()[3,2,1]

✅ 九、连接与拍平

方法作用示例
join(',')数组 → 字符串[1,2,3].join('-')'1-2-3'
concat(arr2)两数组拼接[1].concat([2,3])[1,2,3]
flat()拍平多维数组[1,[2,3]].flat()[1,2,3]

✅ 十、聚合:reduce

array.reduce(callback(accumulator, currentValue[, currentIndex[, array]]), initialValue)

reduce 方法接收两个参数:

  • 一个回调函数(callback),该回调函数接收四个参数:

    • accumulator:累积器,保存每次回调计算的结果。
    • currentValue:当前正在处理的元素。
    • currentIndex(可选):当前元素的索引。
    • array(可选):原始数组。
  • 一个可选的 initialValue(初始化值),如果提供,则会作为累积器的初始值。如果没有提供,则默认使用数组的第一个元素。

方法作用示例
reduce(fn, init)将数组变成单个值[1,2,3].reduce((sum, x) => sum + x, 0)6

21. 说说你对事件循环的理解

  • 事件循环是为了处理一步操作,安排代码执行顺序的机制
  • 事件循环的本质:同步做完,异步排队等候
  • 关键术语:
名称含义
调用栈(Call Stack)当前执行的代码(同步任务
任务队列(Task Queue)异步任务的回调函数排队等待执行(比如 setTimeout
事件循环(Event Loop)不断检查“调用栈是否空”,有空就执行队列任务
重点:
  1. 执行同步代码,调用栈
  2. 遇到异步任务(定时器,Promise等),把异步回调丢到“任务队列”
  3. 当前调用栈清空后,事件循环开始
  4. 任务队列的函数进入调用栈继续执行
宏任务 vs 微任务
类型举例执行优先级
微任务Promise.thenqueueMicrotask高(先执行)
宏任务setTimeoutsetIntervalDOM事件低(后执行)
  • 经典题:

    console.log('1');
    
    setTimeout(() => {
      console.log('2');
    }, 0);
    
    Promise.resolve().then(() => {
      console.log('3');
    });
    
    console.log('4');
    // 1,4,3,2
    因为:
    -   `console.log('1')` 是同步 → 立即执行
    -   `setTimeout(...)` 是宏任务 → 放入任务队列
    -   `Promise.then(...)` 是微任务 → 微任务队列
    -   `console.log('4')` 是同步 → 立即执行
    -   微任务先于宏任务 → 打印 `3`
    -   最后执行宏任务 → 打印 `2`
    
  • 思考:async/await 也是基于事件循环

    async function test() {
      console.log('A');
      await Promise.resolve();
      console.log('B');
    }
    test();
    console.log('C');
    // A C B
    // await 后面的代码会变成微任务,等待同步代码执行完毕后执行
    

22. Javascript本地存储的⽅式有哪些?区别及应⽤场景

  • 一句话: localStorage、sessionStorage、cookie、IndexedDB 是最常见的本地存储方式。
  • 对比:
名称存储大小生命周期是否自动发送给服务器适合场景
localStorage约 5MB永久,除非手动清除❌ 不会长期存储,如用户设置、token、本地缓存数据
sessionStorage约 5MB会话结束就清除❌ 不会短期存储,如页面跳转传值、登录中转状态
cookie~4KB可设置过期时间✅ 会随请求发送给服务器登录状态保持、跨页面识别用户等
IndexedDB大于 50MB(按浏览器)永久❌ 不会存储结构化数据、大文件,如离线应用、本地数据库缓存

✅ 1. localStorage

📌 特点:

  • 键值对存储(字符串格式)
  • 数据长期保留,除非主动清理
  • 页面刷新、浏览器关闭也不会丢

📦 示例代码:

localStorage.setItem('name', 'Tom');
let name = localStorage.getItem('name'); // 'Tom'
localStorage.removeItem('name');

📍 应用场景:

  • 用户设置(主题色、语言)
  • 本地 token(登录后身份校验)
  • 页面缓存、偏好选项

✅ 2. sessionStorage

📌 特点:

  • localStorage 类似,但只在当前标签页/会话中生效
  • 关闭页面就没了

📦 示例代码:

sessionStorage.setItem('step', '2');
let step = sessionStorage.getItem('step');
sessionStorage.clear();

📍 应用场景:

  • 页面跳转中的临时状态
  • 表单填写缓存(防止刷新丢失)
  • 登录页面中间步骤记录

✅ 3. cookie

📌 特点:

  • 会自动随请求发送给服务端(可选)
  • 数据量小(4KB 左右)
  • 支持设置过期时间、安全选项(如 HttpOnly)

📦 示例代码(JS 设置 cookie):

  document.cookie = 'token=abc123; expires=Fri, 01 Jan 2026 12:00:00 UTC';

📍 应用场景:

  • 跨页面登录状态识别
  • 与后端配合身份认证
  • 追踪用户行为(常见于广告系统)

✅ 4. IndexedDB

📌 特点:

  • 支持结构化数据(对象、数组等)
  • 支持事务、大容量存储
  • 异步操作

📦 示例代码:

    let request = indexedDB.open('MyDB', 1);
    request.onsuccess = function(event) {
      const db = event.target.result;
      // 使用 db 来增删改查
    };

📍 应用场景:

  • 离线网页应用(PWA)
  • 缓存大量数据(如地图、图片、视频)
  • 本地小型数据库

🧠 应用场景快速对照

需求用什么?为什么?
保存登录 tokenlocalStorage页面刷新后也保留
跨页面传参数但不持久保存sessionStorage关闭就清除
后端要识别用户身份cookie自动带到服务器
离线存储用户资料IndexedDB数据大且复杂

🎯 小技巧建议

  • 登录状态建议用:localStorage + cookie(HttpOnly)组合
  • 多页面共享数据建议用:localStorage
  • 页面临时数据:sessionStorage
  • 大数据离线缓存:IndexedDB

23. ⼤⽂件上传如何做断点续传

  • 一句话:切片-上传每一片-服务端整合-断点续传(遇到失败,只补传失败的切片)
  • 步骤:
    • 把大文件切成小块(比如每块 1MB)
    // 前端 JS 例子
    const chunkSize = 1 * 1024 * 1024; // 1MB
    const chunks = [];
    for (let i = 0; i < file.size; i += chunkSize) {
      chunks.push(file.slice(i, i + chunkSize));
    }
    
    
    • 把每一片加上编号,然后逐个上传
      • 上传时告诉服务器:第几块、第几号文件、总共有几块
    • 失败重试,断点续传
      • 上传前问服务器,你已经收到几块了?
      • 然后跳过已经上传的,只传剩下的
    • 上传完成后,告诉服务器可以整合了
      • 后端把文件合起来,恢复成原始文件。
  • 💡 通俗记忆口诀

🎬 "切块上传,失败重传,上传完后,后端合拢"

  • 切块:用 slice() 把文件分片
  • 上传:逐个 POST 到服务器
  • 失败重传:用记录(比如后端或本地)判断哪些还没传
  • 合并:后端收到所有片后再拼成完整文件

✅ 补充知识点

概念说明
slice()JS 中切文件的方法
MD5可以给整个文件生成唯一标识,便于断点记录
并发上传多个切片可以同时上传,提速!Promise.all
FormData用于前端上传文件数据

如果你用 Vue + Vite,上传流程可以这样配合:

js
复制编辑
const uploadChunk = async (chunk, index) => {
  const form = new FormData()
  form.append('file', chunk)
  form.append('index', index)
  form.append('fileId', 'xxxx') // 文件唯一标识
  await axios.post('/api/upload-chunk', form)
}
  • 拓展: 如何控制最大并发数量?下面封装一个并发函数
/**
 * 并发控制器:限制同时运行的异步任务数量
 * @param tasks 所有任务函数组成的数组,每个函数返回 Promise(如上传某个切片)
 * @param limit 最大并发数(同时运行的任务上限)
 */
async function runWithConcurrencyLimit<T>(
  tasks: (() => Promise<T>)[], // 每个任务是一个返回 Promise 的函数
  limit: number // 最大同时执行的任务数量
): Promise<T[]> {
  const results: T[] = [] // 用于收集所有任务的结果
  let index = 0 // 当前准备执行的任务索引
  let active = 0 // 当前正在执行的任务数量

  return new Promise((resolve, reject) => {
    const next = () => {
      // 所有任务完成后,且没有正在执行的任务,返回最终结果
      if (index >= tasks.length && active === 0) {
        return resolve(results)
      }

      // 当有可用并发槽位且还有未执行的任务时,继续调度
      while (active < limit && index < tasks.length) {
        const currentTask = tasks[index++] // 取出当前任务函数并递增索引
        active++ // 标记有一个任务开始执行

        currentTask()
          .then(res => {
            results.push(res) // 成功后存储结果
          })
          .catch(reject) // 任何任务失败则直接中止并抛出错误
          .finally(() => {
            active-- // 一个任务执行完成,释放一个并发槽
            next() // 尝试继续执行下一个任务
          })
      }
    }

    next() // 初始化调度
  })
}

假设我们有 5 个任务:

tasks = [task1, task2, task3, task4, task5]

并发限制是:

limit = 2

意味着“同一时间最多执行 2 个任务”。


🧠 关键变量说明

变量名含义
index下一个要启动的任务下标
active当前正在执行的任务数量
results保存已完成任务的结果
limit最大并发数

🧩 执行流程(以 limit=2 举例)

第 1 轮:开始调度

  • index = 0
  • active = 0
  • next() 被第一次调用

while (active < limit && index < tasks.length) 成立
→ 进入循环两次(因为 limit=2)

currentTask = tasks[0]
active++   // active = 1
currentTask().then(...).finally(() => { active--; next() })

currentTask = tasks[1]
active++   // active = 2
currentTask().then(...).finally(() => { active--; next() })

此时:

正在执行:task1, task2
active = 2
index = 2

当 task1 完成时(执行 .finally()):

active--   // 从 2 变成 1
next()     // 再次调度

执行 next() 内的 while:

此时 active=1, limit=2,所以还有空位
→ 再启动下一个任务 task3

active++   //1 -> 2
index++    //2 -> 3

现在:

正在执行:task2, task3
active = 2
index = 3

当 task2 完成时:

同理:

active--   // 从 2 -> 1
next()     // 调度

→ 启动 task4。

此时:

正在执行:task3, task4
active = 2
index = 4

当 task3 完成时:

active--   // 2 -> 1
next()

→ 启动 task5。

正在执行:task4, task5
active = 2
index = 5

当 task4 完成时:

active-- // 2 -> 1
next()

但此时 index = 5 === tasks.length,所以不会再启动新任务。
→ 继续等待最后一个任务 task5。


当 task5 完成时:

active-- // 1 -> 0
next()

再执行到这行判断:

if (index >= tasks.length && active === 0) {
  resolve(results)
}

→ 所有任务完成,返回结果 ✅


🔁 总结逻辑

事件active 变化含义
启动任务时active++占用一个并发槽
任务结束时active--释放一个并发槽
释放后调用 next()尝试继续分配新任务

24. ajax原理是什么?如何实现

  • 原理
// 创建XMLHttpRequest
const xhr = new XHLHttpRequest()
// 2. 配置请求:method, url, async(是否异步)
xhr.open('GET', 'https://api.example.com/data', true);

// 3. 监听状态变化(当 readyState 和 status 满足条件,说明请求完成)
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    // 4. 处理响应数据(通常是 JSON)
    const data = JSON.parse(xhr.responseText);
    console.log(data);
    // 可以更新 DOM 内容
    document.getElementById('result').innerText = data.message;
  }
};

// 5. 发送请求
xhr.send();
  • xhr.readyState 状态码
意义
0未初始化
1正在建立连接
2请求已发送
3正在接收响应
4响应已完成

🚀 AJAX 的核心优势:

  • 页面不刷新,用户体验好
  • 提高前后端交互效率
  • 可局部刷新、按需加载内容(如表格分页、搜索建议等)

25. 什么是防抖和节流?有什么区别?如何实现

  • 一句话:防抖是“等你不动了我再做”,节流是“每隔一段时间我就做”。
  • 防抖(Debounce)
    • 事件触发后等待一段时间,如果这段时间内再次触发,就再重新计时。只有最后一次触发会被执行。
    • 应用场景:

      • 搜索框输入(input)
      • resize 事件
      • 表单验证
  • 什么是节流(Throttle)
    • 限制事件在一定时间间隔内只能触发一次。
    • 无论你触发多少次,只会隔一段时间执行一次。
    • 应用场景:

      • 页面滚动监听(scroll)
      • 页面窗口缩放(resize)
      • 拖拽监听(mousemove)
特性防抖(Debounce)节流(Throttle)
执行频率停止一段时间才执行一次(最后一次)每隔一段时间执行一次
应用场景搜索框、输入校验滚动、拖动、节省资源
典型例子停止输入时搜索页面滚动优化
  • 如何实现?(代码示例)
    • 防抖函数
    // fn每次执行都会先清除定时器,导致fn延迟被执行
    function debounce(fn, delay) {
      let timer = null;
      return function (...args) {
      // 每次调用包装函数都会先清除上一次未执行的定时器,
      // 这样做的效果是:只要事件不断触发,定时器就一直被重置,`fn` 就不会执行。
        clearTimeout(timer); 
        timer = setTimeout(() => {
          fn.apply(this, args);
        }, delay);
      };
    }
    
  • fn.apply(this, args):使用 apply 把原始调用时的 this(即包装函数被调用时的 this)和参数 args 传给 fn

  • 这里非常重要:setTimeout 的回调是一个箭头函数 () => { ... },箭头函数不会改变 this,它会“词法捕获”外层函数的 this。因此 thisfn.apply(this, args) 中是包装函数被调用的上下文(通常是你期望的那个 this)。

  • 如果你把 setTimeout 回调写成普通函数 function() { fn.apply(this, args) },回调里的 this 会不一样(通常是 windowundefined),所以要小心。

  • thisapply 为什么要这样用

    • apply(this, args) 的目的就是正确地把包装函数被调用时的 this 绑定给被包装的 fn,并传入参数数组 args

    • 示例:obj.method = debounce(fn)obj.method() 希望 fn 内部的 this 指向 obj。如果不 apply、或 this 被 setTimeout 改掉了,就会出错。

    • 节流函数
    function throttle(fn, interval) {
      let lastTime = 0;
      return function (...args) {
        const now = Date.now();
        if (now - lastTime >= interval) {
          lastTime = now;
          fn.apply(this, args);
        }
      };
    }
    
    

关于防抖函数中的apply

假设我们要给一个按钮绑定防抖点击事件:

const btn = document.querySelector("button");

btn.onclick = debounce(function() {
  console.log(this); // 我希望这里 this -> btn
}, 500);

但是如果你直接写:

setTimeout(fn, delay);

那么执行的时候,fn普通函数调用this 不会再是 btn,而是变成 windowundefined


3. 用 apply(this, args) 保留 this

当我们在 debounce 返回的函数里写:

fn.apply(this, args);

这里的 this 指的就是 调用 debounce 返回函数时的 this

  • btn.onclick = debounce(...); 的场景下:
    thisbtn

于是 fn.apply(this, args) → 等价于

fn.call(btn, ...args)

这样就能保证:fn 执行时,里面的 this 还是 btn,没有丢失。


4. 改写版示例

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);

    // 保存当前 this
    const context = this;

    timer = setTimeout(() => {
      fn.apply(context, args); // 用 context 保留原来的 this
    }, delay);
  };
}

const btn = document.querySelector("button");

btn.onclick = debounce(function() {
  console.log(this); // this 仍然是 <button>
}, 500);

输出结果:点击按钮时,this 正确打印 button 元素。

26. 如何判断⼀个元素是否在可视区域中

  • 两种方法:
    • 方法一:使用getBoundingClientRect() (最常用,兼容性好,语义直观)

      • 核心原理:
          const rect = element.getBoundingClientRect();
      

      它会返回元素相对于视口的大小和位置,如:

      {
        top: 120,
        left: 0,
        bottom: 300,
        right: 500,
        width: 500,
        height: 180
      }
      
      // 判断是否在可是区域
      function isInViewport(el){
          const rect = el.getBoundingClientRect()
          return {
              rect.top >= 0 &&
              rect.left >= 0 &&
              rect.bottom <= window.innerHeight &&
              rect.right <= window.innerWidth
          }
      }
      
    • 方法二:使用IntersectionObserver(更现代)

      • 浏览器提供的api,可以自动监听元素离开还是进入视口
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            console.log('元素进入可视区域');
          } else {
            console.log('元素离开可视区域');
          }
        });
      });
      
      // 监听某个元素
      const el = document.getElementById('target');
      observer.observe(el);
      
  • 总结
方法是否推荐场景
getBoundingClientRect()✅经典通用简单判断,配合滚动事件
IntersectionObserver✅现代推荐监听进入/离开视口,懒加载

27. 什么是单点登录SSO (Single Sign-on)?如何实现

  • 登录一次,多个系统(网站)都能自动登录,不用每次都输入账号密码
  • 常用于:
    • 一个公司有多个后台系统(OA、人事系统、财务系统)
    • 一个用户只登录一次,其他系统自动登录,无需重复输入密码
  • 工作原理:

image.png

✅ SSO 的核心是:

  1. 统一认证中心(统一登录系统)
  2. 登录状态共享(跨系统识别你已登录)
  3. 跳转机制(未登录就跳转去认证中心)

🔧 常见的实现方式:

🥇 方式一:基于 Cookie + 同域名

  • 多个子系统使用相同主域(如 a.test.comb.test.com
  • 在主域下设置 cookie(如设置在 .test.com),所有子系统都能读到
  • 用户登录一次后,每个子系统访问时都能识别 cookie

适用场景:同域名的多个子系统


🥈 方式二:基于 Token(JWT)+ 跳转

适用于不同域名的系统,比如:

系统A: a.com
系统B: b.com
认证中心: sso.com

实现流程:

  1. 你访问 a.com,没有登录
  2. 被跳转到 sso.com 登录中心
  3. 登录成功后,SSO 中心生成 token,并带回 a.com
  4. a.com 保存 token,标记你已登录
  5. 你访问 b.com,b.com 检查没有登录
  6. 又跳转到 sso.com,SSO 中心检测你已登录
  7. SSO 中心再次生成 token 返回 b.com,完成登录

✅ Token 可用方式:

  • 放在 URL 参数中:b.com?token=xxxxx
  • 放在 cookie 或 localStorage 中
  • 可使用 JWT(JSON Web Token)进行加密传输

28. 如何实现上拉加载,下拉刷新

29. 说说你对正则表达式的理解?应⽤场景?

30. 说说你对函数式编程的理解?优缺点

  • 函数式编程是一种“把计算过程看成函数的组合”的编程方式,强调无副作用、不可变数据**,核心思想是:函数就是值,代码像数学公式一样干净可预测。**
  • 函数式编程就像开汉堡连锁店:
    • 你每次传入相同的原料(输入)
    • 都做出一模一样的汉堡(输出)
    • 中间不污染环境,不偷拿材料,不动刀换配方(无副作用

31. web常见的攻击⽅式有哪些?如何防御

1. 🦠 XSS(跨站脚本攻击)

Cross Site Scripting

📌 原理:

攻击者在网页中注入恶意脚本,当用户访问页面时被执行,可能盗取 Cookie、劫持登录、跳转钓鱼站等。

⚠️ 示例:

html
<input value="<script>alert('中招')</script>" />

用户打开网页后,JS 代码就会执行。

✅ 防御方法:

手段说明
✅ 对用户输入进行转义<&lt;>&gt;
✅ 使用 CSP(内容安全策略)禁止内联脚本,限制资源来源
✅ DOM 插值时用 textContent不用 innerHTML
✅ 后端过滤危险标签<script><iframe>

2. 🧬 CSRF(跨站请求伪造)

Cross Site Request Forgery

📌 原理:

用户登录了网站 A,但打开了攻击者的 B 网站,B 利用浏览器带的 Cookie 向 A 发起请求,伪造用户操作。

⚠️ 示例:

你登录了银行网站没退出,然后访问了一个恶意页面:

html
<img src="https://bank.com/transfer?to=attacker&money=10000" />

浏览器会自动带 cookie,把钱转走了。

✅ 防御方法:

手段说明
✅ 加 CSRF Token后端发一个随机 token,提交表单时必须带上
✅ 检查 Referer / Origin拒绝非本站域名的请求
✅ 使用 SameSite Cookie设置 Cookie 的 SameSite 属性,阻止跨站发送
✅ 不用 GET 做敏感操作比如改密码、转账等必须用 POST/PUT

3. 🐟 钓鱼攻击(Fake Login / Clickjacking)

📌 原理:

做一个和真实网站一模一样的登录页,引导你输入账号密码,然后窃取。

也可能用 iframe + CSS 把你引导去点击“看不见的按钮”。

✅ 防御方法:

  • 限制 iframe 加载X-Frame-Options: DENY
  • 真实登录页使用 HTTPS + 域名校验
  • 启用双因子验证(2FA)

4. 🔐 SQL 注入

📌 原理:

用户在输入框输入 SQL 代码,拼接到 SQL 语句里执行,导致查询泄露或修改数据。

⚠️ 示例:

sql
SELECT * FROM users WHERE username = 'admin' OR 1=1

✅ 防御方法:

手段说明
✅ 使用预处理语句 / 参数化查询prepareStatement 或 ORM 自动转义
✅ 后端校验字段类型防止拼接 SQL
✅ 不拼接 SQL 字符串严禁使用字符串拼接动态 SQL

5. 💣 DOS/DDOS 攻击(拒绝服务)

📌 原理:

攻击者用大量请求压垮服务器,使其无法正常服务。

✅ 防御方法:

手段说明
✅ 加入限流机制如 Nginx 的 limit_req、后端中间件限流
✅ 使用 CDN + 防火墙阿里云、Cloudflare 可做抗 DDoS
✅ 设置超时时间 / 黑名单 IP

6. 📂 文件上传漏洞

📌 原理:

用户上传恶意文件(如 .php 脚本),被服务器执行,控制系统。

✅ 防御方法:

  • 限制文件类型(MIME 类型 + 后缀双验证)
  • 文件重命名 + 隔离存储(如非公开路径)
  • 设置执行权限(上传目录不允许执行)
  • 检查文件内容魔数,不仅靠扩展名

🧩 其他常见攻击方式(了解即可):

攻击名简要说明
📊 中间人攻击(MITM)网络被监听,未加密数据被窃取
🧱 开放重定向URL 参数可被修改,诱导跳转到恶意网站
💥 SSRF(服务器请求伪造)后端服务被利用去访问内网资源(如 127.0.0.1
📤 信息泄露错误堆栈、API 接口返回敏感字段

✅ 总结口诀(面试速记):

XSS 注脚本、CSRF 伪请求、SQL 拼注入、上传防执行、DOS 要限流、敏感别暴露

✅ Web 安全攻击方式与防御速查表(完整版)

编号攻击方式原理简述常见场景防御手段
1XSS 跨站脚本注入 JS 代码,被浏览器执行评论区、搜索框、富文本等✅ 输入/输出转义 ✅ 不用 innerHTML ✅ 设置 CSP
2CSRF 跨站请求伪造利用用户 Cookie 向目标站伪造请求登录用户未退出账号的操作(如转账)✅ CSRF Token ✅ 检查 Referer/Origin ✅ SameSite Cookie
3SQL 注入拼接 SQL 时注入恶意语句登录、搜索、查询接口✅ 使用预处理语句 ✅ 参数化查询 ✅ 严格校验输入
4文件上传漏洞上传恶意脚本被执行上传头像、附件等接口✅ 限制文件类型 ✅ 不保存原始文件名 ✅ 关闭执行权限
5DOS/DDOS大量请求压垮服务器公开接口被刷、爬虫攻击✅ 限流(Nginx/中间件) ✅ CDN防护 ✅ 黑名单/验证
6钓鱼攻击 / ClickJacking伪造登录页面或诱导点击登录页、支付页等✅ X-Frame-Options ✅ 域名验证 ✅ 二次确认操作
7信息泄露返回调试信息、堆栈、敏感字段接口报错/返回值未脱敏✅ 统一错误处理 ✅ 删除敏感字段 ✅ 关闭调试模式
8中间人攻击 MITM网络被劫持,篡改请求或读取数据公共 Wi-Fi / HTTP 请求✅ 全站 HTTPS ✅ TLS 加密传输 ✅ HSTS 策略
9SSRF 服务端请求伪造后端被诱导访问内部资源图片地址、Webhook 回调✅ 限制请求 IP 域名 ✅ 禁用本地回环地址 ✅ URL 白名单
10开放重定向参数可被修改导致跳转到恶意站点登录后跳转、分享链接✅ 验证跳转 URL 是否可信 ✅ 使用 URL 白名单

🚨 前端项目中常用的防御建议

场景防御方案
表单提交添加 csrf-token,使用 POST 请求
显示用户数据所有动态内容用 textContent 而非 innerHTML
显示 HTML 内容使用 DOMPurify 等库做 HTML 清洗
登录状态管理使用 HttpOnly + SameSite Cookie
上传文件限制类型、校验魔数、文件重命名、服务器隔离存储
接口设计永远不信前端传参,后端强校验
控制台日志不打印敏感信息(如 token、手机号)

🧰 推荐工具与库

类型工具
XSS 过滤DOMPurify(前端) / 后端模板转义
CSRF 防御csrf 中间件(如 Express/Koa 插件)
限流express-rate-limit / nginx limit_req
Token 加密jsonwebtoken / crypto
安全扫描npm audit / OWASP ZAP / SonarQube
监控告警阿里云安骑士 / 腾讯云云镜 / 自建日志系统

32. 说说 JavaScript 中内存泄漏的⼏种情况

  • 内存泄漏:指的是程序中不再使用的数据依旧占用内存,无法被垃圾回收器回收,导致内存持续增加
  • 总结口诀(记忆用):

闭包没清、定时忘停、事件不解绑、DOM 被引用、缓存没清理。

✅ 如何定位内存泄漏?

  • 浏览器 DevTools → Memory → Heap Snapshot → 看未释放对象
  • Performance → Timeline → Watch memory trend
  • 使用 WeakMap/WeakSet 存储临时对象(自动释放)

33. Javascript如何实现继承

🧩 常见继承方式:

方式简述
原型链继承子类原型指向父类实例
借用构造函数在子类构造函数中调用父类构造函数
组合继承(最常用)原型链 + 构造函数的结合
ES6 class 继承语法糖,底层依然是原型链
✅ 示例:组合继承(经典)
function Parent(name){
    this.name = name;
}
Parent.prototype.say = function () {
  console.log('hi', this.name);
};

function Child(name, age) {
  Parent.call(this, name); // 借用构造函数
  this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 原型链继承
Child.prototype.constructor = Child;

const child = new Child('Tom', 18);
child.say(); // hi Tom

✅ ES6 class 实现继承(推荐写法)

js
复制编辑
class Parent {
  constructor(name) {
    this.name = name;
  }
  say() {
    console.log('hi', this.name);
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name); // 等价于 Parent.call(this, name)
    this.age = age;
  }
}

🧠 面试这样说:

JS 是基于原型的语言,常见继承方式有原型链、借用构造函数、组合继承和 ES6 的 class 继承。最推荐的是组合继承和 ES6 的 extends,它既能继承属性,也能继承方法,避免原型共享带来的副作用。


✅ 34. JavaScript 数字精度丢失问题,如何解决?

🎯 一句话理解:

JS 使用 **64位双精度浮点数(IEEE 754)**表示数字,无法精确表示小数或大整数,会出现精度丢失问题。


🔥 经典例子:

js
复制编辑
0.1 + 0.2 === 0.3 // ❌ false
console.log(0.1 + 0.2); // 0.30000000000000004

✅ 原因:

JS 把数字转成二进制时,某些小数变成无限循环二进制,舍入误差导致精度问题。


🛠️ 解决方案:

方法示例
✅ 手动放大缩小(0.1 * 10 + 0.2 * 10) / 10
✅ 使用 toFixedNumber((0.1 + 0.2).toFixed(2))
✅ 使用 BigInt处理大整数:BigInt(9007199254740991)
✅ 使用专门库decimal.jsbignumber.js

✅ decimal.js 示例:

js
复制编辑
import Decimal from 'decimal.js';

const sum = new Decimal(0.1).plus(0.2);
console.log(sum.toString()); // "0.3"

🧠 面试这样说:

JS 使用 IEEE754 标准,导致某些浮点数运算不精确,如 0.1 + 0.2 != 0.3。常用解决方式是放大缩小处理,或使用 BigInt / decimal.js 等库来处理高精度运算。


✅ 35. 举例说明你对尾递归的理解,有哪些应用场景?

🎯 一句话理解:

**尾递归(Tail Recursion)**是递归中的一种优化形式,函数调用自身时,返回值是直接返回,不做额外运算,可被引擎优化为循环,节省内存。


✅ 普通递归(非尾递归):

js
复制编辑
function sum(n) {
  if (n === 1) return 1;
  return n + sum(n - 1); // ⛔️ 返回后还有加法,不能优化
}

每次递归都要等前一个返回后再加法,容易栈溢出。


✅ 尾递归(返回的就是递归结果):

js
复制编辑
function tailSum(n, acc = 0) {
  if (n === 0) return acc;
  return tailSum(n - 1, acc + n); // ✅ 直接返回递归结果
}

尾递归优化后相当于循环:

js
复制编辑
let acc = 0;
for (let i = n; i > 0; i--) {
  acc += i;
}

⚠️ 注意:

  • JavaScript 引擎(如 V8)并未默认开启尾递归优化(即使写对也可能没优化)
  • ES6 标准支持尾调用优化,但实际浏览器不普遍支持

✅ 应用场景:

场景原因
✅ 递归计算sum、factorial、斐波那契等
✅ 大数据遍历用递归避免循环回调嵌套
✅ 减少栈溢出比普通递归节省内存,适合大规模递归调用场景

🧠 面试这样说:

尾递归是指函数调用自身时是“最后一步”,不会做额外运算,理论上可被优化为迭代,减少调用栈。虽然目前大部分 JS 引擎未开启尾调优化,但代码写成尾递归形式依然有助于理解递归的本质,并在某些语言中获得性能提升。