第一周01天:this 指向与 call / apply / bind

两小时学习讲义:this 指向与 call / apply / bind

配套《学习计划.md》使用,本讲义把每个知识点打碎到颗粒级 ,并逐点给出类比 + 例子 + 真实开发场景 。 学习方法建议:先猜后跑。看到任何一段代码,先在心里或纸上写下"this 指向谁",再运行验证。 全程约 120 分钟,与学习计划的时间轴一一对应。


⏱️ 时间轴总览

时间段 环节 本讲义位置
20:00--20:10 🔁 回顾与预习 第 1 段
20:10--20:25 📚 知识块①:四种绑定与优先级 第 2 段
20:25--20:40 📚 知识块②:箭头函数与 this 第 3 段
20:40--20:50 📚 知识块③:call / apply / bind 第 4 段
20:50--21:00 📝 费曼复述 第 5 段
21:00--21:20 🛠️ 练习①:手写 myBind 第 6 段
21:20--21:40 🛠️ 练习②③:myCall / myApply + 修复 this 第 7 段
21:40--21:50 💭 思考提高题 第 8 段
21:50--22:00 ✅ 测验 第 9 段

第 1 段(10min)回顾与预习:为什么必须搞懂 this

1.1 先接昨日(闭包)的线

昨天你学了闭包。闭包解决的是"函数记住它定义时所在作用域里的变量"。

js 复制代码
function createCounter() {
  let count = 0;                    // count 是"环境里的变量"
  return function () { count++; };  // 内层函数记住并访问它
}
  • 闭包回答的问题是:"这个函数能访问到哪些变量?" → 看定义位置(词法作用域)。

而今天的主角 this 回答的是一个完全不同的问题:

  • "这个函数执行时,站在谁的角度说话?" → 看调用位置(谁调用的、怎么调用的)。

一句话概括两者的分工:

变量看定义处this调用处。这条区分你会反复用到。

1.2 先凭直觉回答 5 个小问题(不用对,先写下答案)

js 复制代码
// 问题 1
function who() { console.log(this); }
who();                              // this 是?

// 问题 2
const obj = { name: 'vue', who() { console.log(this); } };
obj.who();                          // this 是?

// 问题 3
const obj2 = { name: 'vue', who() { console.log(this); } };
const fn = obj2.who;
fn();                               // this 是?

// 问题 4
const arrow = () => console.log(this);
arrow();                            // this 是?

// 问题 5
function Person(name) { this.name = name; }
const p = new Person('vue');        // new 执行时 this 是?

等学完第 2、3 段再回来对照你的答案。第 1 段不做别的,把"this 由调用方式决定"这句话先刻进脑子里。


第 2 段(15min)知识块①:this 的四种绑定与优先级

2.1 心智模型:this 到底是什么

定义this函数调用时 被绑定到的一个对象,代表"当前上下文(current context)"。它不是定义时决定的,而是调用时由调用点(call-site)决定

类比:函数是一份工作能力,this 是"当前老板是谁"。

调用方式 类比 老板是谁
foo() 自由职业者,没人雇佣 全局(window / globalThis),或严格模式下的"无雇主"(undefined)
obj.foo() 受雇于 obj,在 obj 这家公司里干活 obj
foo.call(obj) 中介明确指定雇主 obj
new Foo() 自己创业 自己(新建的对象)
箭头函数 不找工作、不领工资,跟着"写下它的地方"的那个老板混 定义处外层函数的 this

这五个就是全部答案。 所有 this 问题,最终都落到"它是哪种调用方式"上。

2.2 绑定①:默认绑定(独立调用)

知识点(细化)

  • 函数被独立调用fn()、变量名直接加括号、被其他代码当作普通函数调用)时走默认绑定。
  • 非严格模式 → 指向全局对象(浏览器 window,Node global,通用写法 globalThis)。
  • 严格模式(函数体第一行 'use strict',或整个文件是 ES Module / .mjs)→ 指向 undefined
  • 判断是否严格:有没有 'use strict',以及脚本是否以模块方式加载。

例子:直接运行看输出

js 复制代码
function who() { console.log(this); }
who();  // 浏览器控制台 → window(非严格模式)

function whoStrict() {
  'use strict';
  console.log(this);
}
whoStrict(); // undefined

最容易漏的默认绑定:哪怕在对象方法内部,独立声明的普通函数也是默认绑定

js 复制代码
const obj = {
  name: 'vue',
  method() {
    function inner() { console.log(this); } // inner 是独立调用!
    inner(); // this = window / undefined,绝不是 obj
  }
};
obj.method();

这是新手最懵的一题:明明在 obj 的方法里,inner() 的 this 却不是 obj。记住:this 只看它自己的调用方式,跟"外面是谁"无关。 inner() 没有点号、没有 call、没有 new,就是默认绑定。

类比inner 不是 obj 的员工,它只是"碰巧在 obj 的办公室里办公的自由职业者",所以老板是全局(或没有老板)。

真实场景 :回调函数、定时器里的普通函数、forEach 回调,都是独立调用 → 默认绑定。所以你在这些地方经常需要箭头函数(第 3 段会讲)。

2.3 绑定②:隐式绑定(对象方法调用 obj.fn()

知识点(细化)

  • obj.fn() 形式调用时,this 指向点号左侧那个对象
  • 链式写法 a.b.c.fn(),this 指向紧贴调用括号(最右侧点号)左侧 的那个对象 c,不是 a
  • 它有一个致命前提:必须是"紧贴调用" ------fn 和括号之间是 obj.,中间不能把它先"拿走"。

例子

js 复制代码
const obj = { name: 'vue', getName() { return this.name; } };
obj.getName(); // 'vue' ✅

// 链式:看最右边的点号
const a = { b: { c: { getName() { return this; } } } };
a.b.c.getName() === a.b.c; // true ------ this 是 c,不是 a 也不是 b

类比obj.getName() 就像"刷 obj 的门禁卡进 obj 的办公室"。点号左边就是门禁卡的持有者。

常见陷阱①:方法被"剥离"(本日第一大坑)

js 复制代码
const obj = { name: 'vue', getName() { return this.name; } };
const fn = obj.getName; // 把方法"拿走"了
fn(); // this 丢失 → 返回 undefined(严格模式下直接报错)

fn() 没有 obj. 前缀了,走默认绑定。"剥离"有三兄弟,本质全是同一个问题:

js 复制代码
// 剥离方式一:赋值给变量
const fn = obj.getName;
fn();

// 剥离方式二:解构
const { getName } = obj;
getName();

// 剥离方式三:把方法作为参数"递"给别的函数
setTimeout(obj.getName, 100);   // 定时器独立调用它
[1, 2].forEach(obj.sayHi);       // 回调被独立调用

理解本质 :传递出去的永远是"函数值"本身,obj. 这个上下文并不会跟着函数一起走。函数是个"没有记忆工作单位的求职者"------你只把"能力"递出去,没说"他属于 obj",那它被调用时就没有老板。

常见陷阱②:this 与调用者混淆

js 复制代码
const obj = {
  name: 'vue',
  getName() { console.log(this === obj); } // true
};
obj.getName();

这里 this === obj 是因为 obj.getName() 紧贴调用。不是"obj 里的方法 this 一定等于 obj",而是"以 obj.getName() 这种方式调用时等于 obj"。

修复三件套(先记住,后面逐个展开)

js 复制代码
fn.call(obj);              // 显式绑定,立即执行
const f = obj.getName.bind(obj); f();  // 显式绑定,返回新函数
const getName = () => obj.getName();    // 箭头函数包一层

2.4 绑定③:显式绑定(call / apply / bind)

知识点(细化)

  • call / apply / bindFunction.prototype 上的方法,用来主动指定 this。
  • fn.call(ctx, a, b):立即执行,参数逐个传。
  • fn.apply(ctx, [a, b]):立即执行,参数装进数组传。
  • fn.bind(ctx)不立即执行,返回一个"this 已固定为 ctx"的新函数。

例子

js 复制代码
const obj = { name: 'vue' };
function greet(word) { return `${word}, ${this.name}`; }

greet.call(obj, 'Hi');     // 'Hi, vue'
greet.apply(obj, ['Hi']);  // 'Hi, vue'
const bound = greet.bind(obj);
bound('Hi');               // 'Hi, vue' ------ bind 返回的函数再调用才有结果

类比:call/apply 是"现在、马上、指名道姓地让这个函数为 ctx 服务";bind 是"签一份长期协议------以后谁调用这个函数,老板都自动是 ctx"。

(call / apply / bind 的详细区别、使用场景、手写原理,在第 4 段专门展开。)

2.5 绑定④:new 绑定

知识点(细化)

  • new Fn() 调用函数时,this 指向新创建的那个对象
  • new 内部实际做了四步(以 new Foo() 为例):
    1. 创建一个全新的空对象;
    2. 把这个空对象的原型指向 Foo.prototype(即让空对象能访问 Foo 的 prototype 上的方法);
    3. this 指向这个空对象,并执行函数体;
    4. 若函数体显式返回了一个对象 ,用那个对象替换;否则返回第 3 步的 this 对象。

例子

js 复制代码
function Person(name) {
  this.name = name;   // 此时 this = 正在被创建的新对象
  // 没有 return → 返回 this
}
const p = new Person('vue');
console.log(p.name);  // 'vue' ------ p 就是那个"this"

// 显式 return 对象 → 覆盖 this(大坑,面试爱考)
function Trick() {
  this.a = 1;
  return { b: 2 };
}
console.log(new Trick()); // { b: 2 },而不是 { a: 1 }

类比new 是"自己创业"。1. 领一个空营业执照(空对象);2. 把公司章程(prototype)复印到执照上;3. 执照上的法人写你的名字(this = 新对象),开始营业;4. 除非你中途"变卖"给别的公司(return 对象),否则法人就是你。

2.6 优先级:new > 显式 > 隐式 > 默认

知识点(细化)

  • 四种绑定可能"同时出现",此时按优先级取最高者: new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
  • 隐含的推论:只要命中更高级的绑定,低级绑定就完全失效

逐个验证(先猜后跑)

js 复制代码
function foo() { console.log(this); }
const obj = { foo };

// 显式 vs 隐式 → 显式赢(优先级高)
obj.foo.call({ a: 1 });      // { a: 1 }

// new vs 显式(bind) → new 赢
const bound = foo.bind({ b: 2 });
new bound();                  // 一个全新的对象,不是 { b: 2 }

// 显式 vs 默认 → 显式赢
foo.call({ c: 3 });           // { c: 3 }

// 隐式 vs 默认 → 隐式赢
obj.foo();                    // obj

关于 new bound():原生 bind 返回的新函数被 new 调用时,bind 指定的 this 会被忽略,this 是新建对象。这个细节正是思考题①,第 8 段会引导你自己想清楚。

把优先级变成一棵判定树(背下来,所有题都用它)

kotlin 复制代码
看调用形式:
① 是箭头函数吗?        → 是:this = 定义处外层作用域的 this(见第 3 段)🔚
② 是不是 new 调用?      → 是:this = 新建对象 🔚
③ 是不是 call/apply/bind 调用? → 是:this = 第一个参数 🔚
④ 是不是 obj.fn() 紧贴调用?    → 是:this = 点号左侧最后的对象 🔚
⑤ 以上都不是(独立调用) → 默认绑定:非严格=全局 / 严格=undefined 🔚

综合实战:用判定树一口气做 6 题(先写答案再跑)

js 复制代码
const obj = {
  name: 'obj',
  who() { console.log(this.name); }
};
const fn = obj.who;
const name = 'global';

fn();                                  // ? (默认绑定,this=global/undefined)
obj.who();                             // ? (隐式,obj)
fn.call({ name: 'ctx' });              // ? (显式)
const b = fn.bind({ name: 'bound' });
b();                                   // ? (bind)
new obj.who();                         // ? (new 绑定,新对象没有 name)
obj.who.call({ name: 'again' });       // ? (显式 vs 隐式 → 显式)

答案undefined(非严格下是 window.name → 也可能是 'global',取决于全局有没有 name 变量;严格模式报错)→ 'obj''ctx''bound'undefined'again'

真实开发场景(贯穿四种绑定)

  1. React class 组件方法绑 this<button onClick={this.handleClick}> 里的 this.handleClick 被 React 独立调用 → 默认绑定 → this 丢失。老代码里 constructorthis.handleClick = this.handleClick.bind(this) 就是在补一个显式绑定。
  2. Vue 2 的 methods :模板里 @click="obj.method" 同理,Vue 内部会帮你绑定到组件实例,但你若在 JS 里把方法剥出来传给别人,一样会丢。
  3. 函数式工具库 :lodash 的 _.bind_.debounce 之所以要接收 context 参数,就是为了转发 this。

第 3 段(15min)知识块②:箭头函数与 this

3.1 核心知识点:箭头函数"没有自己的 this"

知识点(细化)

  • 普通函数:每次调用,都会根据调用点重新决定自己的 this。
  • 箭头函数:根本没有自己的 this 。它内部的 this 是"借"来的------在定义时,沿外层作用域(词法作用域)一层层往外找,找到最近的那个普通函数的 this。
  • 因为 this 是借的,所以:
    • call / apply / bind 改不动它(第一个参数被忽略);
    • 不能用 new(没有自己的 this 可绑定,也就不能做构造函数);
    • 没有自己的 arguments(同样沿外层找)。

和昨天闭包的衔接(重点!) :闭包的规则是"函数能访问定义时所在作用域的变量"。箭头函数的 this 遵行的正是这条词法规则 ------它在定义时就把外层 this 捕获进自己的"环境"里了。所以箭头函数的 this 是词法绑定(lexical this) ,跟闭包是一家人;普通函数的 this 是动态绑定,跟闭包不是一家人。

类比:箭头函数是"跟班"------自己不领工资、不签合同,老板永远跟着"写下它的那位(外层普通函数)"走。定义它的时候,老板是谁就定格了,之后谁都改不了。

3.2 怎么判定:沿定义处往外找

例子①:箭头函数捕获外层函数的 this

js 复制代码
function outer() {
  const arrow = () => console.log(this);
  arrow();
}
outer.call({ tag: 'A' });  // { tag: 'A' } ------ 箭头函数借了 outer 的 this(被 call 设为 A)
outer();                    // window/undefined ------ outer 自己是默认绑定,箭头也借这个

例子②:多层嵌套,找"最近的普通函数"

js 复制代码
const obj = {
  name: 'vue',
  fn() {
    const level2 = () => {
      const level3 = () => console.log(this);
      level3(); // this = obj(一路借 fn 的 this)
    };
    level2();
  }
};
obj.fn(); // obj

例子③:定义在顶层/模块层的箭头函数,this 就是那层的 this

js 复制代码
// 脚本文件顶部(非严格,浏览器)
const top = () => console.log(this);
top(); // window ------ 顶层本身是默认绑定的全局,箭头借它

// ES Module(.mjs / <script type="module">)里
const m = () => console.log(this);
m(); // undefined ------ 模块顶层 this 就是 undefined

3.3 三大"不可"逐个验证

js 复制代码
const obj = { name: 'vue' };
const arrow = () => this;   // 假设定义在浏览器全局

// ① call 改不动它
arrow.call(obj);  // 仍是全局 this,不是 obj ------ 第一个参数被忽略

// ② 不能 new
new (() => {})(); // TypeError: (intermediate value) is not a constructor

// ③ 没有自己的 arguments
function outer() {
  const a = () => console.log(arguments); // 找到的是 outer 的 arguments
  a();
}
outer(1, 2, 3); // Arguments[1, 2, 3]

⚠️ 第 ① 点要非常注意表述:"箭头函数不能 call" 是错的------它可以用 call,只是 call 的第一个参数(this 值)被忽略。参数照常传。

3.4 使用场景:什么时候用箭头函数(重点讲"为什么省心")

场景①:定时器 / 事件回调里要"记住"外层 this

js 复制代码
const obj = {
  name: 'vue',
  logName() {
    // 普通函数:独立调用 → this 丢失
    setTimeout(function () { console.log(this.name); }, 100);   // undefined
    // 箭头函数:借 logName 的 this(= obj)
    setTimeout(() => console.log(this.name), 100);              // 'vue'
  }
};
obj.logName();

场景②:数组遍历回调

js 复制代码
const cart = { items: ['a', 'b'], total() { this.items.forEach(() => console.log(this)); } };
cart.total(); // 每次都是 cart;若换成 function() 则变成 window/undefined

场景③:React/Vue 里的事件处理器

js 复制代码
// React 函数组件(无 this,箭头函数很自然)
const handleClick = () => { /* ... */ };

// Vue 3 setup / 组合式函数里,回调几乎都用箭头函数,因为不依赖动态 this

场景④:防抖/节流里转发 this(高级但极常见)

js 复制代码
function debounce(fn, wait) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);  // 箭头借外层函数(this=调用者),再用 apply 转发给 fn
  };
}

这里一行代码同时用了箭头函数 (借外层 this)和 apply(转发 this)------如果你能读懂它,第 3、4 段就算过关了。

3.5 反模式:什么时候别用箭头函数

反模式①:做对象方法(this 借不到对象的 this)

js 复制代码
const obj = {
  name: 'vue',
  getName: () => this.name  // this = 定义处外层,不是 obj!
};
obj.getName(); // undefined / 报错

用普通方法 getName() { return this.name; } 才是对的。口诀:对象方法用普通函数,回调用箭头函数。

反模式②:需要"动态 this"的场合(比如事件委托里要拿 currentTarget)

js 复制代码
// 原生 DOM:普通函数回调里 this = 当前元素,用箭头函数就丢了
el.addEventListener('click', function () { console.log(this.id); });  // 能拿到元素 id
el.addEventListener('click', () => console.log(this.id));             // this 是外层,拿不到元素

反模式③:构造函数、原型方法

js 复制代码
const Foo = () => { this.x = 1; }; // 不能 new,报错
// 原型方法要挂到实例上,依赖动态 this,也不能用箭头

真实开发场景小结 :Vue 2 的 data() 里如果用了箭头函数,this 就不是组件实例(这是常见的低级错误);Vue 3 的 <script setup>ref/回调随便用箭头函数,因为根本没有动态 this 的负担。React 的类字段箭头函数属性handleClick = () => {...})是把箭头函数用到方法上的经典正解------它利用"类字段初始化时 this 已是实例"这一点,让回调永远绑定实例。

3.6 速查表:箭头函数 vs 普通函数

维度 普通函数 箭头函数
this 来源 调用点动态决定 定义处词法继承,改不动
能否被 call/apply/bind 改变 this 不能(参数仍传,this 忽略)
能否 new 不能(也不是构造函数)
arguments 有自己的 没有,沿外层找
适用 对象方法、需要动态 this、构造函数 回调、定时器、遍历、需要"记住外层 this"

第 4 段(10min)知识块③:call / apply / bind 区别与手写思路

4.1 三兄弟对比表(背下来)

call apply bind
是否立即执行 ✅ 立即 ✅ 立即 ❌ 返回新函数,调用才执行
传参方式 逐个 (ctx, a, b, c) 数组 (ctx, [a, b, c]) 逐个,且可以分两次传(柯里化)
返回值 函数执行结果 函数执行结果 一个"this 已固定"的新函数

类比(快递):call 是"单件发货,逐个报参数";apply 是"打包一箱,参数装一个数组里";bind 是"先签一份长期合同(固定老板),以后每次发货都按合同执行,参数可以下次再补"。

4.2 真实开发场景:它们分别解决什么问题

apply 场景①:把数组展开成参数(历史解法,今天多被展开运算符替代)

js 复制代码
Math.max.apply(null, [3, 1, 4, 2]); // 4 ------ apply 把数组铺开成 4 个参数
Math.max(...[3, 1, 4, 2]);           // 4 ------ 现代写法

apply 场景②:arguments(类数组)借用数组方法

js 复制代码
function sum() {
  // arguments 没有 .slice,借 Array.prototype 的来用
  const args = Array.prototype.slice.call(arguments);
  return args.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6(现代写法:const sum = (...args) => args.reduce(...))

这个"借方法"模式在 lodash / 老代码里非常常见,是 call 的核心用途之一:A 没有这个方法,把 A 传给 B 的方法当 this

call 场景:类型精确检测

js 复制代码
Object.prototype.toString.call([]);       // '[object Array]'
Object.prototype.toString.call(null);     // '[object Null]'
// 比 typeof 可靠:typeof null === 'object' 是历史 bug

bind 场景①:修 this(配合事件/定时器)

js 复制代码
const obj = { name: 'vue', log() { console.log(this.name); } };
const fn = obj.log.bind(obj); // 以后怎么调用都是 obj
fn();
[1, 2].forEach(obj.log.bind(obj)); // 两个 'vue'

bind 场景②:柯里化 / 偏函数(预设参数)

js 复制代码
const mul = (a, b) => a * b;
const double = mul.bind(null, 2); // 预先把 a 定为 2
double(5); // 10
double(8); // 16

// 更贴近真实的:格式化器
function fmt(open, close, text) { return open + text + close; }
const bold = fmt.bind(null, '<b>', '</b>');
bold('hello');       // '<b>hello</b>'
bold('world');       // '<b>world</b>'

这就是"柯里化"的雏形:把一个多参数函数"分步喂参数"。bind 预置一部分,剩下的调用时再给。

bind 场景③:借 console

js 复制代码
const log = console.log.bind(console); // 某些环境里 console.log 依赖 this 是 console

4.3 手写思路(讲解版,先看懂再动手写)

手写 myCall:核心一句话"把函数挂到目标对象上再调用"

原理:ctx[key] = this; ctx[key](...) 这是隐式绑定obj.fn() 形式),所以 this 自动变成 ctx

js 复制代码
Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx ?? globalThis;          // ① ctx 为 null/undefined 时回退到全局
  const key = Symbol('fn');          // ② 用 Symbol 当临时键,避免覆盖 ctx 已有的同名属性
  ctx[key] = this;                   // ③ 把当前函数挂到 ctx 上
  const result = ctx[key](...args);  // ④ 以 ctx.method() 形式调用 → 隐式绑定,this = ctx
  delete ctx[key];                   // ⑤ 用完删掉,不给 ctx 留下垃圾
  return result;                     // ⑥ 返回原函数的返回值
};

逐点为什么要这么做(面试常问):

  • ctx ?? globalThiscall(null, ...) 在原生里表示"this 设为全局",我们必须兼容。
  • Symbol('fn'):如果用字符串 'fn',万一 ctx 本来就有 fn 属性就被覆盖了,Symbol 保证唯一。
  • delete ctx[key]:不删的话,ctx 上会残留一个函数,污染对象、还可能被枚举出来。
  • 为什么这样就能改变 this? 因为 ctx[key](...args) 的形式等价于 ctx.方法(),命中隐式绑定。

手写 myApply:只差参数处理

applycall 唯一区别:参数以数组接收。注意处理 args 没传(undefined)的情况:

js 复制代码
Function.prototype.myApply = function (ctx, args) {
  ctx = ctx ?? globalThis;
  const key = Symbol('fn');
  ctx[key] = this;
  const result = ctx[key](...(args ?? [])); // args 可能是 undefined,兜底为 []
  delete ctx[key];
  return result;
};

手写 myBind:核心是"闭包保存 this + 预设参数"

bind返回一个新函数 ,新函数被调用时,把"预设参数 + 本次参数"拼接起来,再用 apply 调用原函数。保存 this 和预设参数,靠的就是昨天学的闭包

js 复制代码
Function.prototype.myBind = function (ctx, ...presetArgs) {
  const fn = this;                    // 闭包捕获:原函数
  return function (...laterArgs) {    // 返回新函数,闭包捕获:fn、ctx、presetArgs
    return fn.apply(ctx, [...presetArgs, ...laterArgs]);
  };
};

// 验证
const greet = function (w, p) { return `${w}, ${this.name}${p}`; };
greet.myBind({ name: 'vue' }, 'Hi')('!'); // 'Hi, vue!' ✅

⚠️ 这个版本没有处理"新函数被 new 调用"的情况------那是第 8 段思考题①的作业。真实的 bind 还要做两件事:① 被 new 时忽略绑定的 this、改用新对象;② 让返回函数的原型链继承原函数。第 6 段练习①里会再升级一次。


第 5 段(10min)费曼复述:用大白话讲给"零基础同学"听

费曼技巧:如果你不能把知识讲给一个完全不懂的人听懂,说明你自己还没懂。 这一段的目的是让你"过一遍嘴",卡壳的地方就是没学透的地方,回去重看。

5.1 复述提纲(讲一遍,最好真出声或写下来)

  1. this 是谁? ------ 它是函数执行时的"老板"。不是定义时定的,是调用时定的。
  2. 四种老板怎么认?
    • 没人雇(独立调用)→ 全局老板(严格模式没老板);
    • 对象.方法() → 这个对象是老板;
    • call/apply/bind → 指定谁就是谁;
    • new → 老板是新开的公司(新对象)。
    • 老板撞车了听谁的?→ new > 显式 > 隐式 > 默认
  3. 箭头函数为什么特殊? ------ 它自己不找老板,定义时就认定了外层函数的老板,之后改不动。
  4. call / apply / bind 怎么区分? ------ 两个立即执行、一个不立即;一个数组传参;bind 能分次传参(柯里化)。
  5. 手写 call 的关键 :把函数挂到对象上再 对象.函数() 调用(隐式绑定);手写 bind 的关键:闭包记住 this 和预设参数。

5.2 自测清单(每条都能说出来/写出例子才算过)

  • 能背出优先级,并各举一个调用例子
  • 能解释"方法被剥离后为什么 this 丢失",给出至少两种修复
  • 能解释箭头函数为什么"改不动",顺带说出它与闭包的联系
  • 能说出 call / apply / bind 三个维度(执行时机/传参/返回值)的区别
  • 能讲清楚"为什么 ctx[key](...) 能改变 this"

第 6 段(20min)练习①:手写 myBind(code\myBind.js)

目标:先自己写 ,卡住再看提示。写完用下面的验证清单测。 📄 参考实现已放好在 code\myBind.js(含 V1 基础版 + V2 处理 new 的进阶版 + 对照测试),写完自己的版本再打开对照,逐行读注释。

6.1 分步提示(不要一上来就看)

  • 第 1 步 :写出签名。Function.prototype.myBind = function (ctx, ...presetArgs) {}。这里的 this 是谁?(提示:哪个函数调用 .myBind,this 就是谁。)
  • 第 2 步 :先保存 const fn = this;,因为等下返回的函数里 this 就不是原函数了。
  • 第 3 步 :返回一个新函数 return function (...laterArgs) {}。这个函数里要用 fn.apply(ctx, [...presetArgs, ...laterArgs])
  • 第 4 步(升级) :思考------如果新函数被 new 调用怎么办?此时应该忽略 ctxthis 是新建对象,并且要 new fn(...)。(先自己想,参考下面)

如果第 4 步实在想不出来,参考这段"工程师版"(先跑通基础版,再对比这段的差异):

js 复制代码
Function.prototype.myBind = function (ctx, ...presetArgs) {
  const fn = this;
  function bound(...laterArgs) {
    if (new.target) {                       // new.target:被 new 调用时非空
      return new fn(...presetArgs, ...laterArgs);  // new 优先,忽略 ctx
    }
    return fn.apply(ctx, [...presetArgs, ...laterArgs]); // 普通调用,apply 转发
  }
  bound.prototype = fn.prototype;           // 让 new bound() 能拿到原函数的原型方法
  return bound;
};

6.2 验证清单(每一条都应通过)

js 复制代码
// ① this 绑定
const o = { name: 'vue' };
function greet(w) { return `${w}, ${this.name}`; }
greet.myBind(o)('Hi');                       // 'Hi, vue'

// ② 柯里化(分次传参)
const b = greet.myBind(o, 'Hello');
b();                                          // 'Hello, vue'

// ③ 多个参数拼接
function add(a, b, c) { return a + b + c; }
add.myBind(null, 1)(2, 3);                    // 6

// ④ new 场景(升级版)
function Person(name) { this.name = name; }
const BoundPerson = Person.myBind({ x: 1 });
new BoundPerson('vue').name;                  // 'vue'(忽略 {x:1},用新对象)

6.3 常见报错排查

报错/现象 原因 修法
fn is not a function 忘了 const fn = this,在返回的函数里用了 this 外层先把 this 存下来
参数顺序错了 拼接顺序写反 必须是 [...presetArgs, ...laterArgs](预设在前)
new bound() 拿不到方法 没接 bound.prototype = fn.prototype 加上这行
结果 undefined 忘了 return fn.apply(...) 的 return 返回调用结果

6.4 深度展开:myBind 为什么要处理 new?(面试高频)

背景 :原生 bind 返回的 bound 函数有两个身份------既能当普通函数调用,也能当构造函数用。万一有代码写了 new boundFn(),就必须按"构造"来对待,否则构造函数就废了。

分两种情况走:

php 复制代码
new boundFn(...) 被调用时(new.target 非空)
    → 忽略 bind 绑定的 ctx,this 交给 new 去创建(新对象)
    → 参数仍要合并:new fn(...presetArgs, ...laterArgs)

boundFn(...) 普通调用时(new.target 是 undefined)
    → 用 apply 转发:fn.apply(ctx, [...presetArgs, ...laterArgs])

两个关键机制的逐步理解

  1. new.target 是什么? 它是一个只在函数内部可见的"元属性":

    • 普通调用 boundFn()new.target === undefined
    • new 调用 new boundFn()new.target === boundFn(指向当前正在执行的函数)
    • 所以 if (new.target) 一行就能判断"我是不是被当构造函数用了"。
  2. 为什么必须 bound.prototype = fn.prototype

    • new boundFn() 创建实例时,实例的原型是 boundFn.prototype
    • 不赋值的话,boundFn.prototype 是默认对象,实例就访问不到 fn.prototype 上的方法
    • bound.prototype 指到 fn.prototypenew boundFn() 出来的实例才能正常继承原函数原型(比如 Person.prototype.introduce)。
    • (原生的 bind 还会同步 lengthname 等属性,手写版本做到这两点已经足够撑场面。)

对照记忆 :V1(不处理 new)→ new boundFn() 时执行的是 fn.apply(ctx, ...),this 变成 ctx、实例原型链也断了,全错;V2 用 new.target 分流后,new boundFn() 等价于 new fn(...),行为正确。参考实现 code\myBind.js 里 V1 / V2 / 原生 bind 三组测试跑一遍就能直观看到差别。


第 7 段(20min)练习②③:myCall / myApply + 修复 this(code\myCallApply.js、fix-this.js)

📄 参考实现已放好:code\myCallApply.jscode\fix-this.js。同样先自己写、跑通,再打开对照

7.1 练习②:手写 myCall / myApply

分步提示

  • ctx = ctx ?? globalThis 处理空值;
  • Symbol 做临时键(Symbol('fn')),避免覆盖 ctx 已有属性;
  • ctx[key] = this; const r = ctx[key](...args); delete ctx[key]; return r;
  • myApply 与 myCall 唯一差异:参数是数组,记得处理 args 为空:ctx[key](...(args ?? []))

验证清单

js 复制代码
function info(a, b) { return `${this.name}-${a}-${b}`; }
const o = { name: 'vue' };
info.myCall(o, 1, 2);                 // 'vue-1-2'
info.myApply(o, [1, 2]);              // 'vue-1-2'
info.myCall(null, 1, 2);              // 不报错(this 回退全局)
info.myApply(undefined, [1, 2]);      // 不报错

// 与原生对比:用几组数据跑,输出应完全一致
console.log(info.call(o, 1, 2) === info.myCall(o, 1, 2));   // true

挑战(可选) :原生 call 的 thisArg 传数字/字符串(原始值)时不会报错,你的实现会不会?提示:非严格模式下 ctx[key] = fn 对原始值赋值会静默失败,严格模式直接报错。想彻底搞懂可以查"装箱(boxing)"。

7.2 练习③:修复 this 指向错误(fix-this.js)

js 复制代码
const obj = { name: 'vue', getName() { return this.name; } };
const fn = obj.getName;
fn(); // 这里 this 指向谁?如何修复?

按三步作答(写进注释里)

① this 指向谁?

  • 浏览器非严格模式:window(独立调用 → 默认绑定);
  • 严格模式 / ES Module:undefinedthis.name 直接抛 TypeError: Cannot read properties of undefined

② 为什么?

  • const fn = obj.getName 把方法"剥离"出了对象。fn() 是独立调用,没有 obj. 前缀,不满足隐式绑定的"紧贴调用"前提,退化为默认绑定。this 看调用点,不看方法写在哪个对象里。

③ 至少给出三种修复(写出来、跑通):

js 复制代码
// 修复一:显式绑定 call(立即执行)
fn.call(obj);                      // 'vue'

// 修复二:bind 生成新函数
const bound = obj.getName.bind(obj);
bound();                           // 'vue'

// 修复三:箭头函数包一层(保留 obj)
const arrow = () => obj.getName();
arrow();                           // 'vue'

// 修复四:改造原方法为箭头函数字段(改定义处)
const obj2 = { name: 'vue', getName: () => obj2.name };
obj2.getName();                    // 'vue'(注意:这里的 this 其实没用上,直接访问 obj2)

修复四有"写死对象引用"的局限(复用、继承时有问题),对比前三种你就能体会到 bind 的优势。


第 8 段(10min)思考提高题:先独立想,再看提示

思考题①:为什么 new 能压过 bind?实现 bind 时如何处理 new 情况?

提示(先自己想再看)

  • 原生 bind 返回的 boundnew 调用时,ECMAScript 规定:以 bound 的目标函数为构造器创建一个新对象,绑定的 this 被忽略
  • 想一下 new.target:普通调用时它是 undefined,被 new 调用时它指向当前函数。这就是"怎么判断是否被 new"的钥匙。
  • 你刚才在第 6 段已经写过一个"工程师版" myBind------回去对照 new.target 分支,想清楚为什么 bound.prototype = fn.prototype 也必须有。

思考题②:箭头函数的 this 由"定义位置"决定,与闭包有什么关系?

提示

  • 闭包的本质:函数在定义时捕获外层作用域(变量),之后一直能用。
  • 箭头函数的 this 是"定义时捕获外层函数的 this"------这跟闭包捕获变量用的是同一套词法作用域机制
  • 所以可以这样记:普通函数的 this 是"每次调用现发"(动态),箭头函数和闭包一样是"定义时就锁死"(词法)。想通了这一点,很多 this 题就直接秒了。

思考题③:React/Vue 组件方法丢 this,除了 bind 还有哪些工程方案?取舍?

提示

  • 至少四种:① bind(this.handleClick.bind(this));② 类字段箭头函数(handleClick = () => {});③ 调用处包箭头函数(onClick={() => this.handleClick()});④ 彻底不用 this(函数组件 / Vue3 <script setup>)。
  • 从"心智负担"角度排个序:④ 最少(没有 this 就没有这个问题)→ ② 次之(写法内聚)→ ①③ 都要手写语法。代价上:④ 与老 class 组件风格差异大,改造成本高;③ 每次渲染都新建函数,细粒度 memo 优化会失效。

第 9 段(10min)测验与收尾

9.1 测验

  • 打开 test\测验.md,限时 15 分钟完成 10 题(巩固 5 + 提升 3 + 横向 2)。
  • 做完后再翻 答案与解析,把错题按"概念错误 / 表达不清 / 纯粗心"分类标记。
  • 错题回到本讲义对应章节重读一遍,不求快,求真正搞懂

9.2 今日能力自检(打分 1--5,低于 3 分明天补)

能力 自评
能背出四种绑定优先级,并用判定树解任意调用形态的 this
能解释箭头函数"没有自己的 this",以及它与闭包的关系
能徒手写出 myCall / myApply / myBind(含 new 处理)
能说清 call / apply / bind 的时机、传参、返回值差异
能解释"方法剥离丢 this"并给出修复

📌 今日核心速记卡(可截图/摘抄带走)

vbnet 复制代码
this = 调用时的老板(不是定义时定的)

优先级:new > 显式(call/apply/bind) > 隐式(obj.fn()) > 默认(独立调用)

箭头函数 = 没老板的跟班:定义时锁死外层 this,call/apply/bind/new 都改不动

call  立即执行  逐个传参    fn.call(ctx, a, b)
apply 立即执行  数组传参    fn.apply(ctx, [a, b])
bind  不执行    返回新函数  const f = fn.bind(ctx); f()

手写 call 关键:ctx[key]=this; ctx[key](...)  ← 隐式绑定
手写 bind 关键:闭包保存 fn + presetArgs + ctx,返回新函数,apply 拼接参数
              (升级:new.target 检测 + bound.prototype = fn.prototype)

一句口诀:方法做回调必丢 this,用 bind / 箭头函数 / call 救回来

❌ 常见误区清单(学完自查,哪个说过/想过就划掉)

  • "this 指向函数本身" ------ ❌ 错误(this 是调用时的上下文对象,不是函数)
  • "this 指向定义位置所在对象" ------ ❌ 错误(指向调用点决定的对象)
  • "箭头函数不能 call" ------ ❌ 能用,只是 this 参数被忽略
  • "bind 会立即执行" ------ ❌ bind 返回新函数,调用才执行
  • "call 传数组 / apply 逐个传" ------ ❌ 反了:call 逐个、apply 数组
  • "在对象方法里定义的普通函数 this 就是该对象" ------ ❌ 独立调用就是默认绑定
  • "obj.fn() 的 this 一定是 obj" ------ ❌ 仅当没有更高级绑定且紧贴调用
  • "箭头函数是普通函数的语法糖" ------ ❌ 两者 this 语义根本不同
相关推荐
小帅不太帅1 小时前
给大家推荐一个特别好用的专为 AI Agent 打造的最快浏览器
前端·agent·浏览器
半仙er1 小时前
第一周03天 事件循环与宏任务 / 微任务
前端
做前端的娜娜子1 小时前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划
用户921080262861 小时前
1. Ant Design X Vue 项目介绍:结构、组件和启动方式
前端
摸鱼研究员1 小时前
neverthrow,ts 中优雅的异常处理方案
前端·javascript
jingchao19981 小时前
Cannot read properties of null (reading ‘insertBefore‘)
前端·javascript·vue.js
计算机魔术师2 小时前
新文章
前端
爱跳舞的烤冷面2 小时前
自学嵌入式第N天(Linux篇——文件编程)
java·开发语言·前端
IT_陈寒2 小时前
Vite的热更新突然失效,原来我忽略了这个配置
前端·人工智能·后端