如果你是 JavaScript 开发者,那你一定对 this 这个小家伙又爱又恨 —— 它就像个调皮的小精灵,在不同场景下总爱扮演不同角色,时而指向全局对象,时而黏上某个实例,偶尔还会玩失踪。今天咱们就来给这个小精灵画个 "行为手册",让它乖乖听你的话!
一、为什么需要 this?它到底解决了啥问题?
先问个扎心的问题:如果没有 this,咱们的代码会变成啥样?
想象一下,你要写个打招呼的功能,得这样显式传参:
function 喊名字(人) {
return 人.名字.toUpperCase();
}
function 打招呼(人) {
var 问候语 = '哈喽,我是' + 喊名字(人);
console.log(问候语);
}
var 我 = { 名字: '汤姆' };
打招呼(我); // 输出 "哈喽,我是汤姆"
这就像每次说话都得把对象举在手里 —— 多累啊!有了 this 之后,相当于给对象装了个 "隐形追踪器":
-
用
call强制让speek的this指向myname; -
speek内部调用identify.call(this)时,this已绑定myname,因此identify也能访问 `myname.name
看到没?this 就像个智能代词,自动帮你找到 "说话的人",代码瞬间清爽了不少~
二、this 的五大种绑定规则:看懂这些就不会晕了
this 最让人头疼的点在于:它的值不是定义时 "写死" 的,而是调用时 "动态确定" 的。就像相亲时介绍人说 "这是我朋友"—— 至于是哪个朋友,得看介绍人是谁。
1. 默认绑定:单身函数的自由生活
当函数 "单身" 时(没有被任何对象调用),this 就会赖在全局对象家里
(浏览器里是
window,Node 里是global)。
浏览器里是 window:
Node 里是 global:
这里再给大家扔一个经典的 "坑王" 例子,保证新手看了直呼上当:
var a = 1
function foo() {
console.log(this.a);
}
function bar () {
var a = 2
foo()
}
bar()
// 猜猜输出啥?答案是 1!
是不是有人觉得应该输出 2?毕竟
foo() 是在 bar() 里面调用的,而 bar() 里明明定义了 a=2。但咱别忘了 this 的核心规则:它只看函数自己是怎么被调用的,不看它在哪个函数里被调用!
在这个例子里,foo() 依然是 "单身狗" 式的独立调用,没有任何对象给它当靠山,所以它的 this 还是指向全局对象。全局对象里的 a 是 1,那自然就输出 1 了。至于 bar() 里的 a=2,那只是 bar 函数的局部变量,跟 foo 里的 this.a 半毛钱关系都没有 —— 就像你邻居家的钱,再多也不是你的一样。
看看 这个例子:
function foo() {
var a = 1;
bar(this.a); // 这里的 this 正躺在全局对象沙发上
}
function bar(x) {
console.log(x); // 输出 undefined(全局对象表示:我家没这玩意儿)
}
foo(); // 单身函数独自出门
如果全局对象家里有 a 这个成员(比如 var a = 2),那它就会乖乖输出 2—— 毕竟寄人篱下,总得听主人的嘛~
2. 隐式绑定:有对象罩着就是不一样
当函数成为某个对象的 "小弟"(作为方法被调用)时,this 就会抱紧这个对象的大腿。
function foo() {
console.log(this.a); // 看看老大是谁
}
var obj = {
a: 1,
foo: foo // 收 foo 当小弟
};
obj.foo(); // 输出 1,this 死心塌地跟着 obj
但这里有个坑:多层对象调用时,this 只认 "直接上级"。就像公司里,你只听直属领导的,董事长再大也管不到你日常工作:
var obj2 = {
a: 2,
foo: obj // obj 带着小弟 foo 来投靠 obj2
};
obj2.foo.foo(); // 输出 1,this 依然认 obj 这个直接领导
(这种情况也叫 "隐式丢失",本质是函数最终还是被直接上级调用的)
3. 显式绑定:强行指定 "主人" 的操作
有时候我们想手动给函数找个 "主人",这时 call、apply、bind 这三兄弟就派上用场了 —— 它们就像 "介绍信",直接指定 this 该跟着谁。
-
call:带参数列表的介绍信,参数一个个传(适合人少的情况) -
apply:带参数数组的介绍信,参数打包成数组传给函数(适合人多的情况) -
bind:永久生效的介绍信,先绑定this和部分参数,返回一个 “半成品” 函数,想啥时候调用都行(一旦绑定,终身不变)
var 老板 = { 工资: 1000 };
function 算总工资(奖金, 补贴) {
console.log(this.工资, 奖金 + 补贴);
}
// call 介绍:这是老板,给100奖金+200补贴
算总工资.call(老板, 100, 200); // 输出 "1000 300"
// apply 介绍:这是老板,奖金补贴都在数组里
var 福利 = [100, 200];
算总工资.apply(老板, 福利); // 输出 "1000 300"
// bind 绑定:以后这函数就跟老板混了,先预支200奖金+400补贴
const 长期工 = 算总工资.bind(老板, 200, 400);
长期工(300); // 输出 "1000 900"(200+400+300=900)
注意 bind 是 "死忠粉"—— 就算你后来想给它换主人,它也坚决不从!
4. new 绑定:当 "爹" 的感觉
用 new 关键字调用函数时,JavaScript 会自动创建一个新对象,而 this 就成了这个新对象的 "爹",负责给它添置家产(属性和方法)。
// 先给孩子准备点祖传家产(原型链上的属性)
Person.prototype.say = 'hello'
function Person() {
// var obj = {name: '宝宝'} //1
// Person.call(obj) //2
this.name = '宝宝' // 3
// obj.__proto__ = Person.prototype //4
// return obj // 5
}
let p = new Person()
let p2 = new Person()
但有个特殊情况:如果构造函数 "偏心",非要返回一个外部对象,那 new 生出来的孩子就会被抛弃:
function Person() {
this.名字 = '宝宝';
return { 财产: 10000 }; // 带个富二代回来
}
let 孩子 = new Person();
console.log(孩子); // 输出 { 财产: 10000 }(亲生的被丢了)
(只有返回引用类型时才会这样,返回基本类型比如 return 123 是无效的)
5. 箭头函数:天生 "认死理"
箭头函数是个特例 —— 它没有自己的 this,永远继承外层作用域的 this,而且一旦确定就改不了,堪称 JavaScript 界的 "认死理" 代表。
function foo() {
// 箭头函数 bar 说:我就跟 foo 的 this 混了!
var bar = () => {
this.a = 2; // 这里的 this 就是 foo 的 this
};
bar();
}
var obj = {
a: 1,
baz: foo
};
obj.baz(); // 调用后,obj.a 变成 2
console.log(obj); // 输出 { a: 2, baz: \[Function: foo] }
就像孩子跟着家长姓 ——foo 的 this 指向 obj(隐式绑定),箭头函数 bar 作为 "孩子",自然也跟着 obj 姓,所以修改 this.a 其实就是改 obj.a。
三、全局作用域中的 this:根正苗红的 "大地主"
在全局作用域里,this 就是那个最大的 "地主"—— 浏览器里叫 window,Node 里叫 global。this 一句话道破天机:
console.log(this); // 浏览器里输出 window,Node 里输出 global
所有没找到 "主人" 的变量和函数,最终都会成为这个大地主的财产(比如全局变量 var a = 1 其实等价于 this.a = 1)。
四、优先级排序:当多种规则冲突时听谁的?
当多种绑定规则同时出现时,总得有个 "老大" 吧?它们的优先级是这样的:
-
new绑定(毕竟 "生娃" 是头等大事) -
显式绑定(
call/apply/bind,手动指定的权力很大) -
隐式绑定(跟着直接上级混)
-
默认绑定(实在没人要,才回全局老家)
而箭头函数根本不参与这场 "权力游戏"—— 它只认外层作用域的 this,管你什么 new 啊 call 啊,全都不好使!
五、实战避坑指南:这些场景要注意
- 对象方法赋值后调用:方法被单独拿出来调用时,会变成默认绑定
var obj = {
a: 1,
foo: function() { console.log(this.a); }
};
var bar = obj.foo;
bar(); // undefined(变成默认绑定了)
- 箭头函数的正确使用场景:适合回调函数,不适合作为对象方法
var obj = {
a: 1,
foo: () => { console.log(this.a); } // 这里的 this 继承全局
};
obj.foo(); // undefined(坑!别这么用)
总结:this 其实很简单
记住一句话:this 就是 "函数调用时的上下文对象"。判断步骤可以分三步走:
- 函数是用
new调用的吗?是 →this指向新实例
- 函数是用
call/apply/bind调用的吗?是 →this指向指定对象
- 函数是被某个对象调用的吗?是 →
this指向那个对象
- 都不是?→ 全局对象(严格模式下是
undefined)
- 是箭头函数?→ 继承外层作用域的
this