两小时学习讲义: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,Nodeglobal,通用写法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 / bind是Function.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()为例):- 创建一个全新的空对象;
- 把这个空对象的原型指向
Foo.prototype(即让空对象能访问 Foo 的 prototype 上的方法); - 让
this指向这个空对象,并执行函数体; - 若函数体显式返回了一个对象 ,用那个对象替换;否则返回第 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'。
真实开发场景(贯穿四种绑定)
- React class 组件方法绑 this :
<button onClick={this.handleClick}>里的this.handleClick被 React 独立调用 → 默认绑定 → this 丢失。老代码里constructor中this.handleClick = this.handleClick.bind(this)就是在补一个显式绑定。 - Vue 2 的 methods :模板里
@click="obj.method"同理,Vue 内部会帮你绑定到组件实例,但你若在 JS 里把方法剥出来传给别人,一样会丢。 - 函数式工具库 :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 ?? globalThis:call(null, ...)在原生里表示"this 设为全局",我们必须兼容。Symbol('fn'):如果用字符串'fn',万一ctx本来就有fn属性就被覆盖了,Symbol 保证唯一。delete ctx[key]:不删的话,ctx 上会残留一个函数,污染对象、还可能被枚举出来。- 为什么这样就能改变 this? 因为
ctx[key](...args)的形式等价于ctx.方法(),命中隐式绑定。
手写 myApply:只差参数处理
apply 与 call 唯一区别:参数以数组接收。注意处理 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 复述提纲(讲一遍,最好真出声或写下来)
- this 是谁? ------ 它是函数执行时的"老板"。不是定义时定的,是调用时定的。
- 四种老板怎么认?
- 没人雇(独立调用)→ 全局老板(严格模式没老板);
对象.方法()→ 这个对象是老板;call/apply/bind→ 指定谁就是谁;new→ 老板是新开的公司(新对象)。- 老板撞车了听谁的?→
new > 显式 > 隐式 > 默认。
- 箭头函数为什么特殊? ------ 它自己不找老板,定义时就认定了外层函数的老板,之后改不动。
- call / apply / bind 怎么区分? ------ 两个立即执行、一个不立即;一个数组传参;bind 能分次传参(柯里化)。
- 手写 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调用怎么办?此时应该忽略ctx,this是新建对象,并且要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])
两个关键机制的逐步理解
-
new.target是什么? 它是一个只在函数内部可见的"元属性":- 普通调用
boundFn()→new.target === undefined - 被
new调用new boundFn()→new.target === boundFn(指向当前正在执行的函数) - 所以
if (new.target)一行就能判断"我是不是被当构造函数用了"。
- 普通调用
-
为什么必须
bound.prototype = fn.prototype?new boundFn()创建实例时,实例的原型是boundFn.prototype;- 不赋值的话,
boundFn.prototype是默认对象,实例就访问不到fn.prototype上的方法; - 把
bound.prototype指到fn.prototype,new boundFn()出来的实例才能正常继承原函数原型(比如Person.prototype.introduce)。 - (原生的 bind 还会同步
length、name等属性,手写版本做到这两点已经足够撑场面。)
对照记忆 :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.js、code\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:
undefined,this.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 返回的
bound被new调用时,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 语义根本不同