写在前面:今天学了一个让我"裂开"的知识点------JS 的
this。老师说了一句话让我记忆深刻:"this不是声明时决定的,是运行时决定的。" 也就是说,同一个函数,你站着调用、坐着调用、拿锤子敲着调用------结果可能全不一样。听完这节课我才明白,为什么老 JS 程序员看到this都要先深呼吸一下。
先来个"剧透":this 到底是谁?
老师用一句话总结了 this 的指向规则:
"
this指向函数的调用者。"
就这么简单?不,没那么简单。因为"函数的调用者"在不同的场景下,是不同的人。
先记住这张表,后面我们一个一个拆开讲:
| 调用方式 | this 指向谁 |
举个例子 |
|---|---|---|
| 普通函数调用 | 全局 window(严格模式是 undefined) |
fn() |
| 作为对象的方法 | 调用该方法的对象 | obj.say() → this 是 obj |
| 作为构造函数 | 新创建的实例对象 | new Person() → this 是实例 |
| 事件处理函数 | 触发事件的元素 | btn.click → this 是 btn |
| call / apply | 手动指定的第一个参数 | fn.call(obj) → this 是 obj |
| bind | 绑定时指定的对象 | const f = fn.bind(obj) |
| 箭头函数 | 没有自己的 this,继承外层的 |
() => {} |
一、"use strict":严格模式的"紧箍咒"
1.1 为什么要用严格模式?
看代码第一行:
javascript
"use strict";
老师说:"启用严格模式"。这是写在文件最上面的一行字符串,但它会让 JS 的解释器进入"严格模式"。
严格模式干了什么?
最重要的是这一条:普通函数里的 this 不再指向全局 window,而是变成 undefined。
javascript
"use strict";
function test() {
console.log(this); // undefined(严格模式下)
}
test(); // 普通函数调用
为什么要这样?因为在非严格模式下,如果你不小心在函数里用了 this,它偷偷指向 window,你可能会意外地创建或修改全局变量------这是灾难性的 bug。
老师说:
"普通函数被调用,
this指向全局 window,也没有必要,启用严格模式。"
非严格模式(默认):
javascript
function fn() {
console.log(this); // window(全局对象)
}
严格模式:
javascript
"use strict";
function fn() {
console.log(this); // undefined
}
这就像把宿舍楼的大门锁上了------你没法随意进出(访问全局对象),更安全。
二、表单提交与事件:一场"默认行为"的阻击战
2.1 表单为什么要用 JS 提交?
先看 HTML:
html
<form class="add-items">
<input type="text" id="item" placeholder="Add a new tapas" required>
<input type="submit" value="+ Add Item">
</form>
老师说:
"
form表单收集用户的输入,点击 submit 按钮,向 action 地址提交。一般不用这种默认提交,体验不好,它会刷新页面。fetch/ajax,由 JS 来提交。"
表单的默认提交行为:把你的输入发到服务器,然后刷新整个页面。 这在以前是网页交互的主流方式,但现在我们追求的是"无刷新体验"。
2.2 如何阻止默认行为?
javascript
function addItem(e) {
console.log(e);
// 阻止默认提交行为------刷新页面
e.preventDefault();
}
oForm.addEventListener('submit', addItem);
e.preventDefault() 就是"阻止默认行为"的方法。
- 默认行为:表单提交 → 刷新页面。
- 你做的事情:绑定了事件监听 →
e.preventDefault()→ 表单不提交。
这就像你在网页上点了一个链接,正常情况下它会跳转。但如果你 e.preventDefault() 了,它就老老实实待在原地,你可以在 JS 里用 fetch 把数据默默发到后台,用户完全感知不到。
三、this 的五大场景:同一个世界,不同的 this
3.1 场景一:普通函数调用 → this 指向 window
javascript
"use strict";
var name = "王五";
let obj = {
name: "张三",
say: function() {
console.log(`我是${this.name}`);
}
};
const fn = obj.say; // 引用式赋值
fn(); // 普通函数被调用,this指向全局
这里的关键:fn = obj.say 之后,fn 只是一个普通函数。你调用 fn(),就是普通函数调用,this 指向全局------在严格模式下是 undefined。
这就像你从图书馆借了一本书(obj.say),带回家看。书的内容没变,但环境变了------在图书馆里它是"图书馆的书",在你家里它就是"你桌上的书"。
3.2 场景二:作为对象的方法调用 → this 指向调用对象
javascript
let obj = {
name: "张三",
say: function() {
console.log(`我是${this.name}`);
}
};
obj.say(); // 我是张三 ------ this 指向 obj
obj.say()------谁调用的?obj。所以在 say 里面,this 就是 obj。
老师说:
"函数作为对象的方法调用,
this指向调用对象。"
3.3 场景三:事件处理函数 → this 指向触发事件的元素
javascript
document
.querySelector('.lnk')
.addEventListener('click', goBaidu);
function goBaidu(e) {
console.log(this); // 指向触发事件的元素------<a> 标签
e.preventDefault();
}
事件处理函数里的 this,指向触发事件的 DOM 元素。 点击了 <a> 标签,this 就是那个 <a>。
但这里有个坑------事件处理函数是需要异步执行的。 等到用户点击时,函数才被执行。
老师说:
"事件一定要有一个事件处理函数 ,
this默认指向事件触发对象。如果我想手动指定this?call、apply、bind 都可以。call、apply 立即运行,但是我们要的函数是异步运行的,所以用 bind。"
3.4 场景四:手动指定 this------call、apply、bind
call:立即执行,一个一个传参
javascript
obj.say.call(obj2); // 借用 obj 的 say 方法,this 指向 obj2
obj.speak.call(obj2, '你好', '我是李四'); // 参数一个一个传
apply:立即执行,参数传数组
javascript
obj.say.apply(obj2, ['你好', '我是李四']);
// 效果和 call 一样,但参数放在数组里
bind:不立即执行,返回一个新函数
javascript
const addItemBind = addItem.bind(obj2); // 全新函数,不立即执行
addItemBind(); // 调用时,this 指向 obj2
老师说:
"call、apply 都可以手动指定 this,区别是 call 是一个一个传参数,apply 是传数组对象。bind 手动指定 this,返回一个新的函数,但不立即执行。"
用一张图记住三者的区别:
| 方法 | 是否立即执行 | 参数传递方式 | 返回值 |
|---|---|---|---|
call |
✅ 立即执行 | 一个一个传 | 函数执行结果 |
apply |
✅ 立即执行 | 传数组 | 函数执行结果 |
bind |
❌ 不执行 | 一个一个传 | 新函数 |
3.5 场景五:箭头函数------没有 this 的"另类"
看这段有坑的代码:
javascript
var name = "梅西";
let obj = {
name: "姆巴佩",
say: function() {
console.log(this.name); // 姆巴佩(this 指向 obj)
setTimeout(function() {
console.log(this.name); // 梅西(this 指向 window!)
}, 1000);
}
};
obj.say();
这里的 setTimeout 里的回调函数是普通函数,它的 this 指向全局 window。 所以 this.name 是全局的 var name = "梅西",而不是 obj.name = "姆巴佩"。
怎么解决?用箭头函数:
javascript
let obj = {
name: "姆巴佩",
say: function() {
console.log(this.name); // 姆巴佩
setTimeout(() => {
console.log(this.name); // 姆巴佩(箭头函数没有自己的 this!)
}, 1000);
}
};
obj.say();
老师说:
"箭头函数没有
this指向,是简化的函数。"
箭头函数不创建自己的 this ,它会从外层作用域继承 this。所以箭头函数里的 this,和它外面那个函数的 this 是一样的。
四、var 和 let 的区别:为什么 var 会污染 window?
javascript
var name = "王五"; // 挂载到 window 上
let name2 = "赵六"; // 不会挂载到 window 上
console.log(window.name); // "王五"
console.log(window.name2); // undefined
老师说:
"
var声明的变量,挂载在window上,污染了window对象。let就不会污染window对象。"
这就是为什么现在都用 let 和 const------它们不会污染全局对象。
五、完整代码串讲:把一切串起来
看 common.js 里的完整代码:
javascript
"use strict";
const oForm = document.querySelector('.add-items');
let obj2 = { name: "李四" };
let obj = {
name: "张三",
say: function() {
console.log(`我是${this.name}`);
},
speak: function(a, b) {
console.log(a, b);
console.log(this);
}
};
// bind:手动指定 this,返回新函数,不立即执行
const addItemBind = addItem.bind(obj2);
oForm.addEventListener('submit', addItemBind); // 事件触发时,this 指向 obj2
function addItem(e) {
e.preventDefault(); // 阻止默认提交
}
// 事件处理函数中的 this 指向触发事件的元素
document.querySelector('.lnk')
.addEventListener('click', goBaidu);
function goBaidu(e) {
console.log(this); // <a> 标签
e.preventDefault();
}
// 作为对象的方法调用 --- this 指向 obj
obj.say(); // 我是张三
// 引用式赋值 --- this 指向全局(严格模式下 undefined)
const fn = obj.say;
fn();
// call 手动指定 this
obj.say.call(obj2); // 我是李四
// apply 传数组
obj.speak.call(obj2, '你好', '我是李四');
obj.speak.apply(obj2, ['你好', '我是李四']);
// bind 返回新函数
const fn2 = obj.speak.bind(obj2);
fn2('你好', '我是李四');
六、总结:this 的"血泪史"
| 调用方式 | this 指向 |
口诀 |
|---|---|---|
| 普通函数 | window / undefined(严格模式) |
"裸奔"调用 |
| 对象方法 | 调用该方法的对象 | "谁点我,我是谁" |
| 构造函数 | 新创建的实例 | "new 出来的崽" |
| 事件处理 | 触发事件的元素 | "谁碰我,我是谁" |
| call/apply | 第一个参数 | "你说是谁就是谁" |
| bind | 绑定时指定的对象 | "提前预约,到点生效" |
| 箭头函数 | 没有自己的 this,继承外层 |
"我是复读机" |
最核心的一句话: this 是在函数运行时 决定的,不是在声明时决定的。同样的函数,用不同的方式调用,this 指向完全不一样。
写在最后
今天终于把 JS 的 this 搞懂了。以前看到 this 就头皮发麻,现在知道了------不要管函数在哪定义的,只看它怎么被调用的。
下次面试官问你:"call、apply、bind 有什么区别?"
你可以淡定地说:
"
call和apply都是立即执行函数,手动指定this。区别是call一个一个传参数,apply传数组。bind不立即执行,它返回一个新函数,新函数的this被永久绑定到指定的对象上。bind特别适合事件处理函数这种异步场景------先用bind绑好this,等事件触发时再执行。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。