阅读视图

发现新文章,点击刷新页面。

爬楼梯?不,你在攀登算法的珠穆朗玛峰!

爬楼梯?不,你在攀登算法的珠穆朗玛峰!

一道看似“幼儿园难度”的面试题:
“每次能爬1阶或2阶,问爬到第n阶有几种方法?”
却暗藏递归、动态规划、记忆化、空间优化四大内功心法——
它不是考你会不会算数,而是看你有没有系统性思维


🧗‍♂️ 初见:天真递归 —— “我能行!”(然后爆栈了)

最直觉的解法?当然是递归!

function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return climbStairs(n - 1) + climbStairs(n - 2);
}

逻辑完美

  • 要到第 n 阶,要么从 n-1 上来,要么从 n-2 跳上来
  • 所以 f(n) = f(n-1) + f(n-2) —— 这不就是斐波那契?

但问题来了:
当你调用 climbStairs(45),电脑会疯狂重复计算:

  • f(43) 被算两次
  • f(42) 被算三次
  • ……
    时间复杂度 O(2ⁿ) —— 指数爆炸!

就像你让一个人背完整本字典来查一个词——可行,但荒谬。


🧠 进阶:记忆化递归 —— “我记住了!”

既然重复计算是罪魁祸首,那就把算过的答案存起来

const memo = {};
function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  if (memo[n]) return memo[n]; // ← 关键:查缓存!
  memo[n] = climbStairs(n - 1) + climbStairs(n - 2);
  return memo[n];
}

效果:每个 f(k) 只算一次 → 时间复杂度 O(n)
思想空间换时间,典型的自顶向下动态规划(Top-down DP)

但有个小瑕疵:memo 是全局变量,容易被污染。


🔒 优雅封装:闭包 + 记忆化 —— “我的缓存,外人别碰!”

闭包memo 私有化,打造一个“智能函数”:

const climbStairs = (function() {
  const memo = {}; // ← 外部无法访问!
  return function climb(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    if (memo[n]) return memo[n];
    memo[n] = climb(n - 1) + climb(n - 2);
    return memo[n];
  };
})();

优势

  • 多次调用共享缓存(越用越快)
  • 状态私有,安全可靠
  • 接口干净:用户只需 climbStairs(n)

这不是函数,这是一个会学习、有记忆、懂封装的智能体


🚀 终极优化:自底向上 + 滚动变量 —— “我不需要递归!”

其实,我们根本不需要递归,也不需要存所有中间值!

观察规律:

f(1) = 1
f(2) = 2
f(3) = f(2) + f(1) = 3
f(4) = f(3) + f(2) = 5
...

只需要两个变量,就能滚动计算出结果:

function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  
  let prevPrev = 1; // f(i-2)
  let prev = 2;     // f(i-1)
  
  for (let i = 3; i <= n; i++) {
    const current = prev + prevPrev; // f(i)
    prevPrev = prev;   // 滚动窗口
    prev = current;
  }
  
  return prev;
}

时间复杂度:O(n)
空间复杂度:O(1) —— 极致优化!
无递归:避免调用栈溢出(n 很大时更安全)

这就是自底向上的动态规划(Bottom-up DP) —— 从已知出发,一步步推导未知。


📊 四种解法对比

方法 时间复杂度 空间复杂度 是否递归 适用场景
暴力递归 O(2ⁿ) O(n) 教学演示
记忆化递归 O(n) O(n) 中等规模,逻辑清晰
闭包记忆化 O(n) O(n) 需要缓存复用
滚动变量 O(n) O(1) 生产环境首选

💡 面试加分回答

当面试官问这道题,你可以这样说:

“我会根据场景选择方案:

  • 如果是教学或快速原型,用记忆化递归,逻辑直观;
  • 如果是高性能生产环境,用滚动变量的迭代法,O(1) 空间且无栈溢出风险。
    此外,我还会考虑边界情况(如 n ≤ 0)、类型校验,以及是否需要支持‘每次可爬1~k阶’的扩展。”

——瞬间从“会写代码”升级到“有工程思维”。


🌟 结语:小题大智慧

“爬楼梯”从来不是一道数学题,而是一面镜子:

  • 它照出你是否理解递归的本质
  • 它检验你是否掌握动态规划的思想
  • 它考验你能否在简洁、性能、可维护性之间做权衡

下次再有人说“这题太简单”,你可以微笑回应:

“是啊,简单到能写出四种境界。”

而这,正是优秀工程师和普通 coder 的分水岭。

🚀别再卷 Redux 了!Zustand 才是 React 状态管理的躺平神器

Zustand VS Redux

在文章开始前咱们先唠嗑一下,各位平时用哪个更多点呢?大数据不会骗人:

首先GitHub上的 Star 数量比较: image.png

image.png

其次每周的下载数量比较:

image.png

image.png

显然,想必用Zustand的可能大概也许应该会居多(单纯看数据来讲)。那么明明Redux才是大哥,为啥被Zustand这个小弟后来居上了?

给大家一个表:

对比项 Redux(老牌流程派) Zustand(新晋清爽党)
上手门槛 高:得记 action type、reducer、Provider 等一堆概念 低:会用 React Hook 就能写,几行代码起手
代码量 多:改个 count 得写 action、reducer 一堆模板代码 少:创建 store + 组件调用,加起来不到 20 行
组件里怎么用 得用 useSelector 取数据 + useDispatch 发动作 直接 useStore( state => state.xxx ) 一步到位
要不要包 Provider 必须包:得用 <Provider store={store}> 裹整个 App 不用包:组件直接调用 store,省一层嵌套
适合场景 大型复杂项目(多人协作、状态逻辑多) 中小型项目 / 快速开发(想少写代码、快速落地)

相信看完表大家已经很明了了,那么如果还想深入了解的可以自行去搜搜,我们唠嗑就到这,开始今天的学习。

具体资料大家去官网看:

www.npmjs.com/package/zus…

www.npmjs.com/package/rea…

前言

想象一下:你正在开发一个 React 项目,Home 组件要改个数字,About 组件得同步显示,List 组件还要从接口拉数据 —— 要是每个组件都自己存状态,代码早乱成一锅粥了!今天咱们就用 Zustand 这个躺平神器,把这些组件串成丝滑的整体,顺便解锁 React 全局状态的 “极简玩法”

一、先搭个 “状态仓库”:Zustand 初体验

Zustand 是啥?你可以把它理解成一个 “共享储物柜”:组件们不用再互相传 props,直接从这个柜子里拿数据、调方法就行。

首先你需要下载Zustand(在开篇的资料里也可以找到~):

image.png

先看我们的第一个 “储物格”——count.js(负责管理计数状态):

// src/store/count.js
import { create } from "zustand";

// 用 create 造一个“状态仓库”
const useCountStore = create((set) => ({
    // 存数据:初始计数是0,还有个默认年龄19
    count: 0,
    age: 19,
    // 存方法:点一下计数+1(set会自动更新视图)
    increase: () => set((state) => ({ count: state.count + 1 })),
    // 传个参数,计数直接减val
    decrease: (val) => set((state) => ({ count: state.count - val }))
}))

export default useCountStore;

就这么几行,一个能 “存数据 + 改数据” 的全局状态就搞定了 —— 比 Redux 轻量到没朋友!

二、组件 “抢着用”:状态共享原来这么丝滑

有了仓库,组件们就能 “按需取货” 了。先看 Home 组件(负责操作计数):

// src/components/Home.jsx
import useCountStore from '../store/count.js'

export default function Home() {
    // 从仓库里“拿”count数据
    let count = useCountStore((state) => state.count);
    // 从仓库里“拿”increase、decrease方法
    const increase = useCountStore((state) => state.increase);
    const decrease = useCountStore((state) => state.decrease);
    return (
        <div>
            {/* 点按钮直接调仓库里的方法,不用传参! */}
            <button onClick={increase}>发送-{count}</button>
            <button onClick={() => decrease(10)}>减少-{count}</button>
        </div>
    )
}

再看 About 组件(负责显示计数):

// src/components/About.jsx
import useCountStore from "../store/count"

export default function About() {
    // 同样从仓库拿count,Home改了这里自动更!
    let count = useCountStore((state) => state.count);
    return (
        <div>
            <h2>title-{count}</h2>
        </div>
    )
}

点击前:

image.png

点击10次发送后:

image.png

刷新然后点击10次减少后:

image.png

你看你看你看看看,Home 点按钮改了 count,About 里的标题直接同步更新 —— 连 props 都不用传,这丝滑感谁用谁知道!

三、进阶玩法:状态里塞接口请求

光存数字才哪到哪,还不够炫!咱们给仓库加个 “拉接口” 的功能。先写 list.js(负责管理列表数据):

// src/store/list.js
import { create } from "zustand";

// 先写个请求接口的函数
const fetchApi = async () => {
    const response = await fetch('https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer');
    const res = await response.json();
    return res.data; // async函数的return会变成Promise的resolve值
}

// 造个存列表的仓库
const useListStore = create((set) => ({
    list: [], // 初始列表是空数组
    // 存个“拉列表”的方法,里面调用接口
    fetchList: async () => {
        const res = await fetchApi();
        set({ list: res }) // 拿到数据后更新list
    }
}))

export default useListStore;

然后让 List 组件 用这个仓库:

// src/components/List.jsx
import { useEffect } from "react";
import useListStore from "../store/list"

export default function List() {
    // 从仓库拿list数据和fetchList方法
    const list = useListStore((state) => state.list);
    const fetchList = useListStore((state) => state.fetchList);

    // 组件一加载就调用接口拉数据
    useEffect(() => {
        fetchList()
    }, [])

    return (
        <div>
            {/* 拿到数据直接map渲染 */}
            {list.map((item) => {
                return <div key={item.name}>{item.name}</div>
            })}
        </div>
    )
}

接口数据就出现在浏览器上啦:

image.png

打开页面,List 组件会自动拉接口、存数据、渲染列表 —— 状态管理 + 接口请求,一套流程直接在仓库里包圆了!

四、最后一步:把组件都塞进 App

最后在 App.jsx 里把这些组件拼起来:

import Home from "./components/Home"
import About from "./components/About"
import List from "./components/List"

export default function App() {
    return (
        <div>
            <Home></Home>
            <About></About>
            <List></List>
        </div>
    )
}

image.png

启动项目,你会看到:About 显示着计数,List 自动渲染接口数据 —— 这就是 Zustand 给 React 带来的 “状态自由”

总结

Zustand 堪称 React 状态管理的 “轻骑兵”:无需写冗余的 reducer、不用嵌套 Provider 包裹组件树,几行代码就能搭建全局状态仓库。它剥离了传统状态管理的繁琐仪式感,让我们彻底摆脱模板代码的束缚,聚焦业务本身。

结语

相比 Redux 的 “厚重” 和 Context API 在高频更新下的性能短板,Zustand 就像一把恰到好处的 “瑞士军刀”,轻巧却锋利,用最简单的方式解决了 React 组件间的状态共享难题,让开发者能把更多精力放在业务逻辑本身,而不是状态管理的 “套路” 里。

好的工具从来不是炫技的枷锁,而是让开发者回归创造本身的桥梁。

从零到一:彻底搞定面试高频算法——“列表转树”与“爬楼梯”全解析

在前端面试中,算法往往是决定能否拿高薪的关键。很多同学一听到“算法”就头大,觉得那是天才玩的游戏。其实,大多数面试算法题考察的不是你的数学造诣,而是你对递归(Recursion)和逻辑处理的理解。

今天,我们就通过两个非常经典的面试真题—— “列表转树(List to Tree)”和“爬楼梯(Climbing Stairs)” ,带你从小白视角拆解算法的奥秘。

第一部分:列表转树 —— 业务中的“常青树”

1. 为什么要学这个?

在实际开发中,后端返回给我们的数据往往是“扁平化”的。比如一个省市区选择器,或者一个后台管理系统的左侧菜单导航。为了存储方便,数据库通常会存储为如下结构:

id parentId name
1 0 中国
2 1 北京
3 1 上海
4 2 东城区

但前端 UI 组件(如 Element UI 的 Tree 组件)需要的是一个嵌套的树形对象。如何把上面的表格数据转换成包含 children 的树?这就是面试官考察你的数据结构处理能力。

2. 解法一:暴力递归(最符合人类直觉)

核心逻辑:

  1. 遍历列表,找到根节点(parentId === 0)。
  2. 对于每一个节点,再去列表里找谁的 parentId 等于我的 id
  3. 递归下去,直到找不到子节点。
// 代码参考
function list2tree(list, parentId = 0) {
  const result = []; 
  list.forEach(item => {
    if (item.parentId === parentId) {
      // 这里的递归就像是在问:谁是我的孩子?
      const children = list2tree(list, item.id);
      if (children.length) {
        item.children = children;
      }
      result.push(item);
    }
  });
  return result;
}

小白避坑指南:

这种方法的复杂度是 O(n2)O(n^2)。如果列表有 1000 条数据,最坏情况下要跑 100 万次循环。面试官此时会问:“有没有更优的方法?”

3. 解法二:优雅的 ES6 函数式写法

如果你想让代码看起来更“高级”,可以利用 filtermap

function list2tree(list, parentId = 0) {
  return list
    .filter(item => item.parentId === parentId) // 过滤出当前的子节点
    .map(item => ({
      ...item, // 展开原有属性
      children: list2tree(list, item.id) // 递归寻找后代
    }));
}

4. 解法三:空间换时间(面试官最爱)

为了把时间复杂度降到 O(n)O(n),我们可以利用 Map 对象。Map 的查询速度极快,像是一个“瞬移器”。

思路:

  1. 先遍历一遍列表,把所有节点存入 Map 中,以 id 为 Key。
  2. 再遍历一遍,根据 parentId 直接从 Map 里把父节点“揪”出来,把当前节点塞进父节点的 children 里。
// 代码参考
function listToTree(list) {
    const nodeMap = new Map();
    const tree = [];

    // 第一遍:建立映射表
    list.forEach(item => {
        nodeMap.set(item.id, { ...item, children: [] });
    });

    // 第二遍:建立父子关系
    list.forEach(item => {
        const node = nodeMap.get(item.id);
        if (item.parentId === 0) {
            tree.push(node); // 根节点入队
        } else {
            // 直接通过 parentId 找到父亲,把儿子塞进去
            nodeMap.get(item.parentId)?.children.push(node);
        }
    });
    return tree;
}

优点: 只遍历了两遍列表。无论数据有多少,速度依然飞快。

第二部分:爬楼梯 —— 掌握算法的“分水岭”

如果说“列表转树”考察的是业务能力,那“爬楼梯”考察的就是编程思维

题目描述: 假设你正在爬楼梯。需要 nn 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

1. 自顶向下:递归的艺术

我们站在第 nn 阶往回看:

  • 要到达第 10 阶,你只能从第 9 阶跨 1 步上来,或者从第 8 阶跨 2 步上来。
  • 所以:f(10)=f(9)+f(8)f(10) = f(9) + f(8)

这就是著名的斐波那契数列公式

// 基础版
function climbStairs(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    return climbStairs(n - 1) + climbStairs(n - 2);
}

致命缺陷: 这个代码会跑死电脑。算 f(10)f(10) 时要算 f(9)f(9)f(8)f(8);算 f(9)f(9) 时又要算一遍 f(8)f(8)。大量的重复计算导致“爆栈”。

2. 优化:带备忘录的递归(记忆化)

我们可以准备一个“笔记本”(memo),算过的值就记下来,下次直接拿。

const memo = {};
function climbStairs(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    if (memo[n]) return memo[n]; // 翻翻笔记,有就直接给
    memo[n] = climbStairs(n - 1) + climbStairs(n - 2);
    return memo[n];
}

3. 自底向上:动态规划(DP)

动态规划(Dynamic Programming)听起来很高大上,其实就是倒过来想。

我们不从 f(n)f(n) 往回找,而是从 f(1)f(1) 开始往后推:

  • f(1)=1f(1) = 1
  • f(2)=2f(2) = 2
  • f(3)=1+2=3f(3) = 1 + 2 = 3
  • f(4)=2+3=5f(4) = 2 + 3 = 5
function climbStairs(n) {
  if (n <= 2) return n;
  const dp = new Array(n + 1);
  dp[1] = 1;
  dp[2] = 2;
  for (let i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]; // 每一个结果都是前两个的和
  }
  return dp[n];
}

4. 极致优化:滚动变量

既然 f(n)f(n) 只依赖前两个数,那我们连数组都不需要了,只需要三个变量在手里“滚”起来。

function climbStairs(n) {
    if(n <= 2) return n;
    let prePrev = 1; // f(n-2)
    let prev = 2;    // f(n-1)
    let current;
    for(let i = 3; i <= n; i++){
        current = prev + prePrev;
        prePrev = prev;
        prev = current;
    }
    return current;
}

这时的空间复杂度降到了 O(1)O(1),几乎不占用额外内存。

总结:小白如何精进算法?

通过这两道题,你应该能发现算法学习的规律:

  1. 先画图,后写码: 不管是树状结构还是楼梯台阶,画出逻辑图比直接写代码重要得多。
  2. 寻找重复子问题: 递归和 DP 的核心都在于把大问题拆解成一样的小问题。
  3. 从暴力到优化: 别指望一步写出最优解。先用最笨的方法写出来,再去思考如何减少重复计算。

一个定时器,理清 JavaScript 里的 this

本文将从最基础的对象方法中this的指向说起,深入剖析定时器中this“不听话” 的原因,再逐一讲解几种常见的 “救回this” 的方法,包括经典的var that = this、灵活的call/apply、实用的bind,以及 ES6 中更优雅的箭头函数。通过清晰的案例对比和原理分析,帮你彻底理清this的绑定规律,从此不再被this的指向问题困扰。

一、从最普通的对象方法说起:this 指向当前对象

先从最正常的场景看起:一个对象,里面有个方法,方法里打印 

this 和 this.name

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this);
    console.log(this.name);
  }
};
obj.func1();

image.png

在这种通过“对象.方法()”调用的场景下:

  • this 指向的是 obj 本身
  • this.name 就是 "Cherry"

也就是说,只要是“谁点出来的函数,

this 一般就指向谁”。

这一点很多人都懂,真正乱的是下面这种情况。

二、一进定时器,this 就不听话了

把上面的对象稍微改一下:再加一个方法,里面开个定时器。

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    console.log(this);   // 这里的 this 还是 obj
    setTimeout(function () {
      console.log(this); // 这里的 this 是谁?
      this.func1();      // 这里很多人第一反应是“调用不到”
    }, 3000);
  }
};
obj.func2();

运行之后你会发现:

image.png

  • func2 里面第一行 console.log(this) 打印的是 obj
  • 但是定时器回调里的 console.log(this),却不再是 obj,而是全局对象(浏览器里是 window,严格模式下甚至可能是 undefined

原因是:谁调用这个函数,

this 就指向谁

  • obj.func2() 是“对象.方法调用”,所以 this === obj
  • setTimeout 回调是“普通函数调用”,真正执行时类似 window.callback(),所以 this 又回到了全局

于是 

this.func1() 就出现了典型错误:
你以为是调用 obj.func1,实际上是在全局环境下找 func1。

三、三种常见的“救回 this”姿势

为了在定时器里还能拿到“外层的那个对象”,常见有三种写法。

1. 老派写法:var that = this

最早接触到的方案一般是这个:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    var that = this;   // 先把外层的 this 存起来
    setTimeout(function () {
      console.log(this);      // 这里还是全局对象
      that.func1();           // 用 that 调用
    }, 3000);
  }
};
obj.func2();

image.png

思路很直白:

  • 外层 this 是我们想要的对象
  • 回调内部再用一个变量 that 把它“闭包”住
  • 不再依赖回调里的 this,而是用 that 去调用

优点:

  • 所有环境都支持,ES5 就可以用
    缺点:
  • 可读性一般,多层回调时会出现 that = this / self = this 满天飞

2. call / apply:立即执行并指定 this

第二种方案是利用 Function.prototype.call / apply,它们有两个关键点:

  • 都是立即调用函数
  • 第一个参数是要绑定的 this

例如:

function show() {
  console.log(this.name);
}
var obj = { name: 'Cherry' };
show();             // this => window / undefined
show.call(obj);     // this => obj
show.apply(obj);    // this => obj

call 和 apply 的区别只在于传参方式

  • call(fnThis, arg1, arg2, ...)
  • apply(fnThis, [arg1, arg2, ...])

在和定时器结合时,有一种稍微“绕”一点的写法,会先用 call 执行一次,然后把返回的函数交给定时器:

setTimeout(function () {
  console.log(this);  // 这里的 this 被 call 成 obj
  this.func1();
  
  return function () {
    console.log(this);  // 这个函数真正被 setTimeout 调用时,this 又回到全局
  };
}.call(obj), 2000);

分析一下这个写法的流程:

  • .call(obj) 先立刻执行这段函数,里面的 this 是 obj
  • 这个函数里 this.func1() 能正常调用到 obj.func1
  • 它 return 的那个内部函数才是真正交给 setTimeout 的
  • 这个内部函数在将来执行时,又是一次“普通函数调用”,于是 this 再次回到全局

这种写法属于“利用 call 硬拉一次 

this 过来”,但在实际项目里,更常见的做法不是这样用 call,而是第三种:bind

3. bind:先订婚,后结婚

bind 和 call/apply 很容易混:

  • call/apply马上执行,并临时指定一次 this
  • bind不执行,而是返回一个“this 永远被绑死”的新函数

用一个简单对比看差异:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  }
};
console.log(obj.func1.bind(obj)); // 打印的是一个新函数
console.log(obj.func1.call(obj)); // 打印的是 func1 的返回值(这里是 undefined)
const f = obj.func1.bind(obj);
f(); // 始终以 obj 作为 this 调用

image.png

套用一个比较形象的说法:

  • call/apply闪婚,当场拍板,函数当场执行完事
  • bind先订婚,先约定好将来的 this,真正结婚(执行)是以后

因此在定时器这种“将来才会执行”的场景,bind 非常自然:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    setTimeout(this.func1.bind(this), 3000);
  }
};
obj.func2();
  • this.func1.bind(this) 立即返回了一个新函数
  • 这个新函数里 this 被固定成当前对象
  • setTimeout 三秒后再执行它时,this 依然是那个对象

相较于 that = this 和“花里胡哨的 call 写法”,bind 在这种场景下是最容易读懂的一种。

四、箭头函数:不再创建自己的 this

还有一种办法,是直接 **放弃回调自己的 **

this,而是用外层的。

这就是箭头函数的做法:箭头函数不会创建自己的执行上下文,它的 this 完全继承自外层作用域

箭头函数的核心是没有自己的 this,它的 this 是词法绑定(定义时继承外层作用域的 this),而非动态绑定。

把前面的定时器改成箭头函数版本:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this);       // obj
    console.log(this.name);  // Cherry
  },
  func2: function () {
    console.log(this);       // obj
    setTimeout(() => {
      console.log(this);     // 依然是 obj
      this.func1();          // 也能正常调用
    }, 3000);
  }
};
obj.func2();
obj.func1();

image.png

这里的关键点是:

  • func2 里的 this 是对象本身
  • 箭头函数的 this 直接沿用 func2 的 this
  • 所以在箭头函数里,this 没有发生“跳变”,始终是那个对象

再结合一个简单的对比例子,看得更清楚。

1. 普通函数和箭头函数的 this 对比

// 普通函数
function normal() {
  console.log(this);
}
// 箭头函数
const arrow = () => {
  console.log(this);
};
normal(); // 非严格模式下 this => window
arrow();  // this 继承自定义它时所在的作用域(全局里一般也是 window / undefined)

image.png

再注意一个常被问到的问题:

  • 箭头函数不能作为构造函数使用,也就是说不能 new 一个箭头函数
    在实际代码里,如果你尝试 new func()(func 是箭头函数),会直接报错

2. 顶层箭头函数的 this

在普通脚本里,如果写一个顶层箭头函数:

const func = () => {
  console.log(this);
};
func();

image.png

这里的 

this 继承自顶层作用域:

  • 浏览器非模块脚本中,一般是 window
  • 严格模式 / ES 模块中,顶层 this 往往是 undefined

这也再次说明:箭头函数的 

this 完全取决于它被定义时所处的环境,而不是被谁调用。

3. 继承外层作用域的 this

const obj = {
  name: 'Cherry',
  func: function () {          // 普通函数,this === obj
    console.log('外层 this:', this);
    setTimeout(() => {         // 箭头函数,this 继承外层
      console.log('箭头函数 this:', this);
      console.log('name:', this.name);
    }, 1000);
  }
};
obj.func();

image.png

把 setTimeout 里的箭头函数换成普通函数,this 会丢失

setTimeout 中的回调函数是被 JavaScript 引擎 “独立调用” 的,而非作为某个对象的方法调用

五、小结 & 使用建议

把上面的内容串一下,可以得到这样一份“速记表”:


  • this 的基础规律

    • 谁调用,指向谁obj.method() 里 this === obj
    • 普通函数直接调用:fn() 里 this 是全局对象 / undefined(严格模式)
  • setTimeout / 回调里的

    this

    • 回调是普通函数调用,this 默认指向全局
    • 所以在对象方法里直接写 setTimeout(function () { this.xxx }),往往拿不到我们想要的对象
  • 三种修复方式的对比

    • var that = this

      • 利用闭包保存外层 this
      • 兼容最好,但代码略显“老派”
    • call/apply

      • 立即执行函数
      • 第一个参数用来指定 this
      • 适合“当场就要执行一次”的场景
    • bind

      • 返回“this 被锁死”的新函数,而不是立即执行
      • 非常适合“定时器、事件监听、回调”这些“稍后再执行”的情形
  • 箭头函数

    • 不创建自己的 this,只继承外层
    • 在对象方法中配合回调使用,能有效避免 this 跳来跳去
    • 不适合作为构造函数(不能 new
❌