1.闭包

1.1闭包的官方定义

闭包 = 函数 + 函数能够记住的周围变量

更直白地说:一个函数,即使在外层函数执行完毕后,仍然能访问外层函数的变量。

1.2闭包的三个条件

条件说明
1. 函数嵌套一个函数里面定义了另一个函数
2. 内部函数使用了外部函数的变量比如 inner 用了 name
3. 内部函数被返回(或传递到外部)这样外部才能调用它

1.3js计数器

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()count 都会增加。 count 被“关”在闭包里,外面无法直接访问,只能通过返回的函数操作它。

1.4闭包陷阱

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i)  // 打印 3, 3, 3,不是 0,1,2
  }, 100)
}

JavaScript 是单线程的,等循环结束才开始定时器。 而且闭包让它记住了外层的 i = 两者共同导致了打印 3,3,3。

1.5闭包缺点:内存泄漏

通俗解释:用完了的内存没有被释放,程序占着不放,导致可用内存越来越少。

原因:闭包的特性是函数会一直持有它用到的外部变量,即使外层函数已经执行完了。

1.6如何避免闭包内存泄漏(口诀)

规则说明
用完就置空不需要的大对象手动设为 null
及时解绑不需要的事件、定时器要清理
注意循环引用DOM 和 JS 对象互相引用
用 let/const 而不是 var块级作用域更容易被回收
避免在闭包里存 DOM存了记得清

2.事件循环(Event Loop)

JavaScript 是单线程的,一次只能做一件事。遇到等一会儿的事(比如定时器、网络请求),就先放一边,等同步代码执行完,再回来做这些事。

Event Loop 就是 “干完手头的活,再去队列里拿下一个活” 这个循环过程。

两个关键词(先记这两个)

意思例子
同步任务立刻做的事console.logfor 循环、函数调用
异步任务等一会儿做的事setTimeout、点击事件、网络请求

规则:永远先清空同步任务,再处理异步任务

2.1事件循环的异步微任务-----promise

事件循环(Event Loop)
├── 同步任务
└── 异步任务
    ├── 宏任务(setTimeout、事件回调)
    └── 微任务(Promise.then)  ← Promise 的 .then 属于这里

Promise 是什么(最简版)

Promise 是一个容器,里面装着一个“将来才会结束”的事情。

三种状态(记住名字就行)

  1. pending:进行中

  2. fulfilled:成功了

  3. rejected:失败了

状态只能变一次,一旦改变就不会再变。

2.2promise基础语法

    // 1. 创建 Promise
    const myPromise = new Promise((resolve, reject) => {
      // 做某件事
      let success = true;
      
      if (success) {
        resolve('成功了');  // 成功
      } else {
        reject('失败了');   // 失败
      }
    });
    
    // 2. 使用 Promise
    myPromise
      .then((result) => {
        console.log(result);
      })
      .catch((error) => {
        console.log(error);
      });

    执行流程(按顺序)

    1. new Promise 立即执行里面的函数

    2. 判断 success 是 true

    3. 调用 resolve('成功了')

    4. 跳过 reject

    5. 走到 .then,把 '成功了' 传给 result,打印

    6. .catch 不执行(因为没失败)

     success = false

    走 reject('失败了')

    .then 不执行

    .catch 执行,打印 '失败了'

    小练习

    console.log('A');  // 同步
    
    new Promise((resolve) => {
      console.log('B');  // 同步!Promise 构造函数里的代码是立刻执行的
      resolve('C');
    }).then((res) => {
      console.log(res);  // 微任务,稍后执行
    });
    
    console.log('D');  // 同步

    输出:A → B → D → C

    为什么?

    1. A 同步,打印

    2. new Promise 里面的 console.log('B') 立即执行

    3. resolve('C') 只是标记成功,.then 里的代码先不执行

    4. D 同步,打印

    5. 同步代码结束,执行微任务:打印 C

    新手最常错的点

    ❌ 错误理解:Promise 里的所有代码都是异步的

    javascript

    // 错!这里面的代码是同步的
    new Promise(() => {
      console.log('我会立刻执行');
    });

    ✅ 正确理解:只有 .then() .catch() .finally() 里面的才是异步(微任务)

    结合事件循环的终极例子(面试常考)

    console.log('1');
    
    setTimeout(() => {
      console.log('2');
    }, 0);
    
    Promise.resolve()
      .then(() => {
        console.log('3');
      })
      .then(() => {
        console.log('4');
      });
    
    console.log('5');

    输出:1 → 5 → 3 → 4 → 2

    执行过程:

    1. 同步:1、5

    2. 清空微任务队列:3、4(按顺序)

    3. 执行一个宏任务:2

    3.柯里化

    // 正常写法:一次传两个参数
    function add(a, b) {
      return a + b;
    }
    add(3, 5);  // 8
    
    // 柯里化写法:一次传一个参数
    function curriedAdd(a) {
      return function(b) {
        return a + b;
      };
    }
    curriedAdd(3)(5);  // 8

    执行过程拆解

    function curriedAdd(a) {
      return function(b) {
        return a + b;
      };
    }
    
    const add5 = curriedAdd(5);  // 先固定第一个参数是5
    console.log(add5(3));  // 8
    console.log(add5(10)); // 15

    为什么要用柯里化?(实际用处)

    用处1:固定某个参数,生成新函数

    curriedAdd(3)(5)
    
    // 第一步:curriedAdd(3) 执行
    function curriedAdd(3) {
      return function(b) {
        return 3 + b;
      };
    }
    // 返回的是:function(b) { return 3 + b; }
    
    // 第二步:返回的函数再被调用,参数是 5
    function(5) {
      return 3 + 5;  // 8
    }

    用处2:延迟执行

    function multiply(a) {
      return function(b) {
        return a * b;
      };
    }
    
    const double = multiply(2);  // 还没算,只是存起来
    const triple = multiply(3);
    
    console.log(double(5));  // 10,现在才算
    console.log(triple(5));  // 15

    4.foreach和map的区别

    const numbers = [1, 2, 3];
    
    // forEach:不返回数组
    const result1 = numbers.forEach((num) => {
      return num * 2;
    });
    console.log(result1);  // undefined
    console.log(numbers);  // [1, 2, 3](原数组没变)
    
    // map:返回新数组
    const result2 = numbers.map((num) => {
      return num * 2;
    });
    console.log(result2);  // [2, 4, 6]
    console.log(numbers);  // [1, 2, 3](原数组还是没变)

    map 返回新数组,forEach 不返回东西(undefined)

    5.原型链

    JavaScript 里,几乎所有东西都是对象。对象之间通过"原型"连接起来,形成一条链。

    5.1构造函数,实例对象,原型对象

    5.11构造函数

    就是一个普通函数,用来"生产对象"的。习惯首字母大写。

    // 这是一个构造函数
    function Person(name) {
      this.name = name;
    }

    它和普通函数没区别,只是用来创建对象

    5.12实例对象

    用 new 关键字调用构造函数,生产出来的对象。

    const xiaoming = new Person('小明');
    const xiaohong = new Person('小红');

    5.13原型对象

    一个普通的对象,专门用来存放"所有实例共享的方法/属性"。

    // 这个 {} 就是原型对象
    Person.prototype = {
      sayHi: function() {
        console.log('你好');
      }
    };

    5.2简单使用

    const 公共方法 = {
      sayHi: function() { console.log('你好'); }
    };
    
    const 小明 = { name: '小明' };
    const 小红 = { name: '小红' };
    
    // 把公共方法设为原型
    Object.setPrototypeOf(小明, 公共方法);
    Object.setPrototypeOf(小红, 公共方法);
    
    小明.sayHi();  // 你好(小明自己没有 sayHi,去原型上找)
    小红.sayHi();  // 你好

    这里记住一些基础的概念:

    Prototype:

    1.Prototype他是{函数的}一个属性

    2.Prototype是个对象

    3.创建函数时默认添加Prototype属性

    __proto__:

    1.{对象}的属性

    2.指向构造函数的prototype

    3.obj.__proto__ ===test.prototype    true

    三个连接关系(背下来)

    连接怎么写方向
    实例对象 → 原型对象实例.__proto__实例找到自己的原型
    构造函数 → 原型对象构造函数.prototype构造函数找到自己的原型
    原型对象 → 构造函数原型对象.constructor原型找到自己的构造函数

    5.3什么是链

    function Person(name) {
      this.name = name;
    }
    
    const p1 = new Person('小明');
    
    // p1 的原型是 Person.prototype
    console.log(p1.__proto__ === Person.prototype);  // true
    
    // Person.prototype 的原型是 Object.prototype
    console.log(Person.prototype.__proto__ === Object.prototype);  // true
    
    // Object.prototype 的原型是 null(终点)
    console.log(Object.prototype.__proto__);  // null

    这里贴一张黑马的图帮助理解

    可以说这张图是最清晰好理解的了

    5.4原型链的查找规则(核心)

    当访问对象的一个属性/方法时,如果对象本身没有,就顺着原型链往上找,直到找到为止。找不到就返回 undefined。

    6.深拷贝与浅拷贝

    6.1简单说明

    浅拷贝:只复制第一层,里面的东西还是共享的
    深拷贝:全部复制,彻底分开,互不影响

    const original = {
      name: '小明',
      age: 18,
      address: {
        city: '北京',
        street: '长安街'
      }
    };
    
    // 浅拷贝
    const shallow = { ...original };
    
    // 深拷贝(先记住这个简单方法)
    const deep = JSON.parse(JSON.stringify(original));
    
    // 修改原对象的第一层
    original.name = '小红';
    console.log(shallow.name);  // '小明'(没变,第一层分开了)
    console.log(deep.name);     // '小明'(没变)
    
    // 修改原对象的第二层(嵌套对象)
    original.address.city = '上海';
    console.log(shallow.address.city);  // '上海'(变了!浅拷贝没分开)
    console.log(deep.address.city);     // '北京'(没变,深拷贝分开了)

    关键点:

    • 改第一层 name:浅拷贝不受影响

    • 改第二层 address.city:浅拷贝跟着变了(因为是同一个对象)

    6.2浅拷贝常见方法

    const obj = { a: 1, b: { c: 2 } };
    
    // 方法1:展开运算符
    const copy1 = { ...obj };
    
    // 方法2:Object.assign
    const copy2 = Object.assign({}, obj);
    
    // 方法3:数组的 slice(数组专用)
    const arr = [1, 2, { a: 3 }];
    const copy3 = arr.slice();

    6.3深拷贝方法

    6.3.1方法1:JSON.parse(JSON.stringify())(最常用,但有坑)

    const obj = { a: 1, b: { c: 2 } };
    const deep = JSON.parse(JSON.stringify(obj));

    缺点(重要!):

    const obj = {
      fn: function() {},     // ❌ 函数会丢失
      date: new Date(),      // ❌ 日期会变成字符串
      undef: undefined,      // ❌ undefined 会丢失
      nan: NaN,              // ❌ NaN 变成 null
      inf: Infinity,         // ❌ Infinity 变成 null
      reg: /abc/,            // ❌ 正则变成空对象 {}
    };

    6.3.2structuredClone()(现代浏览器,推荐)

    const obj = { a: 1, b: { c: 2 }, date: new Date() };
    const deep = structuredClone(obj);
    // 支持 Date、RegExp、Map、Set,比 JSON 方法强

    6.3.3手写递归(面试常考)

    function deepClone(obj) {
      // 处理 null 或不是对象
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
      
      // 处理数组
      if (Array.isArray(obj)) {
        const arrCopy = [];
        for (let i = 0; i < obj.length; i++) {
          arrCopy[i] = deepClone(obj[i]);
        }
        return arrCopy;
      }
      
      // 处理普通对象
      const objCopy = {};
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          objCopy[key] = deepClone(obj[key]);
        }
      }
      return objCopy;
    }

        
        返回 { a: 1, b: { c: 2 } }(新对象)

    6.3.3手写递归(面试常考)6.4使用场景区分

    什么时候用浅拷贝?什么时候用深拷贝?

    场景用什么原因
    对象只有一层(没有嵌套)浅拷贝就够了没有深层共享的问题
    对象有嵌套,且需要完全独立深拷贝改深层数据不影响原对象
    React state 更新浅拷贝(配合展开)React 只比较引用,但嵌套对象要小心

    7.Json.stringfy的弊端

    1.无法处理循环引用
    2.不支持特定的数据类型

    3.丢失对象的原型链和方法
    4.性能问题:在处理大量对象或者深层嵌套对象的时候,可能会消耗大量的时间和内存

    8.数据类型

    8.1总览

    JavaScript 有 8 种数据类型

    分类类型英文例子
    基本类型(7种)数字number13.14NaN
    字符串string'hello'"world"
    布尔booleantruefalse
    未定义undefinedundefined
    nullnull
    符号symbolSymbol('id')
    大整数bigint9007199254740991n
    引用类型(1种)对象object{}[]function(){}

    记忆口诀: 数、字、布、未、空、符、大 + 对象

    8.2基本类型vs引用类型

    区别1:存储方式不同

    // 基本类型:值存在变量里
    let a = 10;
    let b = a;
    b = 20;
    console.log(a);  // 10(没变)
    console.log(b);  // 20
    
    // 引用类型:地址存在变量里,值存在别处
    let obj1 = { name: '小明' };
    let obj2 = obj1;
    obj2.name = '小红';
    console.log(obj1.name);  // '小红'(变了!)
    console.log(obj2.name);  // '小红'

    区别2:比较方式不同

    // 基本类型:比较值
    console.log(10 === 10);      // true
    console.log('a' === 'a');    // true
    
    // 引用类型:比较地址(是不是同一个对象)
    const objA = { name: '小明' };
    const objB = { name: '小明' };
    console.log(objA === objB);  // false(两个不同的对象)
    console.log(objA === objA);  // true(同一个对象)

    8.3堆和栈的区别

    什么是栈(Stack)和堆(Heap)?

    对比项栈(Stack)堆(Heap)
    存储内容基本类型值、引用类型的地址引用类型的实际数据
    空间大小小,固定大,不固定
    分配方式自动分配,顺序存储动态分配,随机存储
    访问速度
    内存管理自动释放(函数结束就清空)垃圾回收(GC)自动回收,但时间不确定

    快速理解

    let name = '小明';     // 基本类型
    let age = 18;         // 基本类型
    let person = {        // 引用类型
      city: '北京',
      job: '前端'
    };

    • name 和 age 的值直接存在栈里

    • person 的栈里存的是一个地址(0x01),指向堆里的真实数据

    8.4类型转换

    1. 显式转换(手动)

    // 转字符串
    String(123);        // '123'
    (123).toString();   // '123'
    
    // 转数字
    Number('123');      // 123
    Number('abc');      // NaN
    parseInt('123px');  // 123
    parseFloat('3.14'); // 3.14
    
    // 转布尔
    Boolean(0);         // false
    Boolean(1);         // true
    Boolean('');        // false
    Boolean('hi');      // true

    2. 隐式转换(自动,容易坑)

    // 字符串 + 任何东西 → 字符串
    console.log('1' + 2);    // '12'
    console.log('1' + true); // '1true'
    
    // 数字 - * / → 转数字
    console.log('6' - 2);    // 4
    console.log('6' * '2');  // 12
    console.log('abc' - 1);  // NaN
    
    // 比较运算
    console.log('5' > 3);    // true('5' 转成 5)
    console.log('5' > '10'); // true(字符串比较,比第一位 '5' > '1')
    
    // == 会做类型转换
    console.log(1 == '1');   // true(== 会转类型)
    console.log(1 === '1');  // false(=== 不转类型,推荐用这个)
    
    // 逻辑运算
    console.log(1 && 2);     // 2(返回最后一个真值)
    console.log(0 || 'hi');  // 'hi'(返回第一个真值)

    9.数据类型判断

    判断数据类型:

    1. typeof
       - 优点:简单,判断基本类型
       - 缺点:null 变 'object',无法区分对象/数组

    2. instanceof
       - 优点:判断引用类型的具体类型
       - 缺点:不能判断基本类型

    3. Array.isArray()
       - 优点:准确判断数组
       - 缺点:只能判断数组

    4. Object.prototype.toString.call()(万能)
       - 优点:能判断所有类型,包括 null
       - 缺点:写法麻烦

    实际开发推荐:
    - 基本类型 → typeof
    - 数组 → Array.isArray()
    - 其他引用类型 → instanceof
    - 需要完全准确 → Object.prototype.toString.call()

    10.深拷贝如何解决循环引用

    循环引用是什么?

    对象里面的某个属性,又指回了自己(或间接指回自己),形成一个闭环。

    const obj = { name: '小明' };
    obj.self = obj;  // 自己的 self 属性指向自己
    
    // 现在 obj 变成了:
    // obj {
    //   name: '小明',
    //   self: { name: '小明', self: { name: '小明', self: ... } }  // 无限套娃
    // }

    问题1:JSON.stringify 会报错

    因为 JSON 格式不支持循环引用(JSON 是树形结构,不能有环)。

    问题2:普通递归深拷贝会死循环

    怎么解决循环引用?

    核心思想:记录已经拷贝过的对象,遇到重复的,直接返回之前拷贝的结果,不再递归。

    Map做缓存(新手了解即可)

    function deepClone(obj, cache = new Map()) {
      // 处理基本类型
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
      
      // 检查是否已经拷贝过这个对象
      if (cache.has(obj)) {
        return cache.get(obj);  // 直接返回之前拷贝的结果
      }
      
      // 处理数组
      if (Array.isArray(obj)) {
        const arrCopy = [];
        cache.set(obj, arrCopy);  // 先存起来
        for (let i = 0; i < obj.length; i++) {
          arrCopy[i] = deepClone(obj[i], cache);
        }
        return arrCopy;
      }
      
      // 处理普通对象
      const objCopy = {};
      cache.set(obj, objCopy);  // 先存起来
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          objCopy[key] = deepClone(obj[key], cache);
        }
      }
      return objCopy;
    }

    现在只需要记住:在深拷贝过程中,每次遇到一个对象需要拷贝时,首先检查映射表中是否已经存在该对象的拷贝。如果存在,则直接返回拷贝副本,避免重复拷贝。如果不存在,则创建一个新的拷贝对象,并将原对象和拷贝对象记录到映射表中。

    11.this指向和作用域

    11.1常见

    总结表(背下来)

    调用方式this 指向
    fn()window(严格模式下是 undefined
    obj.fn()obj
    new fn()新创建的对象
    箭头函数定义时外面的 this

    这里特殊说明一下箭头函数:由于箭头函数呢没有this,那么基于闭包,就会从外层寻找this,而闭包属于词法作用域,词法作用域在编译时态下确定(js预编译),所以箭头函数this指向在编译时就确定,这也就是箭头函数this取决于定义位置而不是运行位置的原因

    普通函数的 this 看怎么调用(运行位置决定)。
    箭头函数的 this 看在哪里定义(定义位置决定)。

    11.2易错例子(面试常考)

    例子1:函数被拿出来单独调用

    const obj = {
      name: '小明',
      sayHi: function() {
        console.log(this.name);
      }
    };
    
    const fn = obj.sayHi;  // 把函数拿出来
    fn();  // undefined(this 指向 window,window.name 不存在)

    原因: fn() 前面没有对象,是普通函数调用。

    例子2:定时器里的 this

    const obj = {
      name: '小明',
      sayHi: function() {
        setTimeout(function() {
          console.log(this.name);
        }, 1000);
      }
    };
    
    obj.sayHi();  // undefined(定时器里的函数是普通调用,this 指向 window)

    解决方法: 用箭头函数

    setTimeout(() => {
      console.log(this.name);  // this 继承外面的 this,指向 obj
    }, 1000);

    例子3:事件监听里的 th

    button.addEventListener('click', function() {
      console.log(this);  // 指向 button 元素
    });
    
    button.addEventListener('click', () => {
      console.log(this);  // 指向 window(箭头函数继承外面的 this)
    });

    11.3手动改变this(apply,call,apply)

    三个的区别(一句话)

    方法是否立即调用参数怎么传
    call✅ 立即调用一个一个传 fn.call(obj, 1, 2, 3)
    apply✅ 立即调用数组传 fn.apply(obj, [1, 2, 3])
    bind❌ 返回新函数,不立即调用一个一个传 fn.bind(obj, 1, 2, 3)

    11.3.1call(立即调用,参数一个一个传)

    function sayHi(age, city) {
      console.log(this.name + ',' + age + ',' + city);
    }
    
    const obj = { name: '小明' };
    
    // 正常调用:this 指向 window
    sayHi(18, '北京');  // undefined,18,北京
    
    // 用 call:手动把 this 指向 obj
    sayHi.call(obj, 18, '北京');  // 小明,18,北京

    语法: 函数.call(想要this指向谁, 参数1, 参数2, 参数3...)

    11.3.2apply和call一样,只有传参方式不同

    语法: 函数.apply(想要this指向谁, [参数1, 参数2, ...])
    这里不过多赘述

    11.3.3bind(不立即调用,返回新函数)

    function sayHi(age, city) {
      console.log(this.name + ',' + age + ',' + city);
    }
    
    const obj = { name: '小明' };
    
    // 用 bind:返回一个新函数,不立即执行
    const boundSayHi = sayHi.bind(obj, 18, '北京');
    
    // 以后想调用的时候再调用
    boundSayHi();  // 小明,18,北京

    语法: const 新函数 = 原函数.bind(想要this指向谁, 参数1, 参数2, ...)

    什么时候用 bind 不着急调用,比如定时器、事件监听。

    const obj = {
      name: '小明',
      sayHi: function() {
        console.log(this.name);
      }
    };
    
    // 定时器里 this 会丢失
    setTimeout(obj.sayHi, 1000);  // undefined(this 指向 window)
    
    // 用 bind 绑定
    setTimeout(obj.sayHi.bind(obj), 1000);  // '小明'

    这里this指向可能考最难的点就是bind,我们可以看一下bind伪代码来帮助理解

    (来源:前端渡一大师)

    这张图在说什么?

    它说:bind 返回的新函数,在被调用时要做一个判断

    如果这个新函数是被 new 调用的,那 this 就不再是绑定的那个 ctx 了,而是新创建的对象。否则,就用绑定的 ctx

    为什么要做这个判断?

    因为 bind 有一个特殊规则:绑定的函数,被 new调用时,this 绑定失效

    Logo

    欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

    更多推荐