前言
如果大家学习过Java,那么相信大家对this这个关键字应该不会陌生,它就像小小怪和大大怪,华生和夏洛克,格鲁特和火箭浣熊,阿布和漂泊者一样,与创建实例对象的函数形影不离。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
在这里,this.name 是成员变量,name 是构造方法的参数。
那么其在JavaScript中的表现会不会有所不同呢?接下来我将用一篇文章带你通透♂通透!
What the hell is this?
this是个啥玩意儿锕?this指的是这个?这个是哪个?
其实在JavaScript中this拥有一个明确的定义:this 是 JavaScript 中的一个特殊关键字,它拥有一个值,它的值取决于函数的调用方式,而不是定义方式。
- 在全局作用域,
this指向window(浏览器)或global(Node.js)。- 在对象方法里,
this指向调用该方法的对象。- 在构造函数里,
this指向新创建的实例。- 在箭头函数里,
this继承外层作用域的值(不会自己绑定this)。
四种绑定规则
判断this的值,我们必须找到它的调用位置,然后判断是下面4条规则的哪一条。
默认绑定
默认规则就是我们最常用的独立函数调用,其会将this的值绑定到window/global
比如下面的例子:
function example() {
console.log(this.a);
}
var a = 3;
example();
输出结果为3,因为这里是独立函数调用,直接对函数进行调用,此时的this,在浏览器中绑定到了window,在Node.js中绑定到了global,引擎会在this所指的对象中寻找相关变量或方法,比如这一个,我们输出了this.a,其就会在this所指的Window/global对象中寻找定义的a变量的值。
this所绑定的Window:
Window对象包含了浏览器可以实现的各种方法,以及我们刚刚在全局定义的变量(var)以及函数。
⚠⚠⚠Caution: 如果使用严格模式,默认绑定将会绑定到undefined
隐式绑定
隐式绑定简单来说就是看是不是通过了哪个对象调用了函数,this指向最后一层调用它的那个对象。
举个栗子:
function example() {
console.log(this);
}
var obj2 = {
name: 'obj2',
a: 42,
example: example
}
var obj1 = {
name: 'obj1',
a: 21,
obj2: obj2
}
obj1.obj2.example();
这里的this指向了谁呢? 当然是指向了最后选择了离她最近的那个人,也就是obj2(无论前面套用了多少个对象,this永远指向最靠近它的那个):
调用过程解析:
obj1.obj2访问obj1的obj2属性,得到obj2对象obj2.example()通过obj2调用example方法- 此时
this指向调用者obj2
隐式丢失
隐式丢失(Implicit Binding Loss) 是指:
函数在调用时,this 不再指向预期的对象,而是退回到默认绑定(如 window 或 global)。
以下是常发生隐式丢失的场景:
(1) 函数赋值给变量
const obj = {
name: "Bob",
greet() {
console.log(this.name);
},
};
const greetFunc = obj.greet;
greetFunc(); // undefined(this 指向全局对象)
原因:
this是动态绑定的,其是在执行过程中确认的const greetFunc = obj.greet就相当于const greetFunc = function greet(){ console.log(this.name) },实际的调用还是进行了普通的函数调用,并没有在对象中进行调用。
(2) 回调函数
回调函数(Callback Function) 是指 将一个函数作为参数传递给另一个函数,并在某个特定条件或事件发生后执行这个函数。
function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a:2,
foo:foo
};
var a = "oops,global!";
doFoo(obj.foo);
原因:
- 在这里传入的
obj.foo就只代表了函数本身,此时函数没有执行,this没有绑定 - 在
doFoo函数中,执行了foo函数,属于普通调用,this直接绑定到了Window/global
同样的隐式丢失也会发生在语言内置的回调函数中:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var a = "oops,global!";
setTimeout(obj.foo,1000);
输出结果为:oops,global!
原因与上面相同,setTimeout的函数实现大约是以下:
function setTimeout(fn,delay){
// 等待delay毫秒
fn(); // 直接调用fn
}
(3) 嵌套函数
在嵌套函数中某些情况也会发生this的隐式丢失:
const obj = {
name: "Alice",
greet() {
function innerFunc() {
console.log(this.name);
}
innerFunc();
},
};
obj.greet();
原因:
- 在调用
obj.greet()后,直接执行了innerFunc(),属于普通的函数调用,因此this绑定到了Window/global
const obj = {
name: "Bob",
logName() {
setTimeout(function() {
console.log(this.name); // this → window/undefined(丢失)
}, 1000);
},
};
obj.logName(); // 输出 undefined
这个例子也是一样,都是发生了函数的直接调用,而非通过对象调用,发生this绑定与我们的预期不符,我们本来期待它绑定到obj,但是它绑定到了Window/global.
相信各位朋友们看到这里也都感觉出来了,隐式丢失不像一个bug,而更像是符合默认绑定规则的结果
这时候可能就有小伙伴要问了:“主播主播!既然它与我们期待的绑定不符,有没有什么办法强迫它绑定到某个地方上?”
Of Course!Man!What can i say!Mamba out!
接下来我们介绍一下强人♂锁男的方法——————显式绑定
显式绑定
显式绑定的方法很简单,就是三个词:call、apply、bind
利用函数的这三个方法可以强制性将函数的this绑定到对象上,用法如下:
function.call()
call() 是 JavaScript 中的一种 函数调用方式,它允许你:
- 显式指定
this的值(改变函数执行时的上下文)。 - 以参数列表形式传递参数
基本语法:
func.call(thisArg, arg1, arg2, ...)
thisArg:函数运行时this指向的对象(如果传null/undefined,非严格模式下默认this指向全局对象)。arg1, arg2, ...:函数需要的参数(逐个传递,而非数组)。
例子:
我们可以看到在这里利用了talk.call(me,'it',true),其将talk函数的this绑定到了me上,并向talk函数传递了两个参数,分别为'it'和true。
也就是说,第一个参数为this绑定的对象,其他参数均为传入函数的参数
函数的call方法,绑定了this指向,传递参数后会立刻执行函数。
function.apply()
函数的apply方法与call方法是一模一样的,唯一的区别就是其可以将所有的参数作为数组传入:
没错,这就是唯一的区别,其他方面和call方法一模一样,只不过,执行起来call要更快一丢丢。
function.bind()
bind() 是 JavaScript 中用于 永久绑定 this 并返回新函数 的方法,与 call() 和 apply() 不同的是,bind() 不会立即执行函数,而是返回一个绑定了 this 的新函数,稍后可以调用。
基本语法:
const boundFunc = func.bind(thisArg, arg1, arg2, ...)
thisArg:函数运行时this的指向(绑定后的上下文)。arg1, arg2, ...(可选):提前传入函数的参数(部分应用)。- 返回值:返回一个新的函数,
this被固定为thisArg。
其中,它的参数和call一样,是一个一个传递的,不像apply一样可以数组传递
并且bind的有一个返回值,会返回一个新的函数,与原函数的区别就是this绑定到了thisArg
所以我们需要单独再创建一个变量来接受它的返回值,以方便我们以后调用。
例子:
function talk(lang, isPolite) {
if (isPolite) {
if (lang === 'en') {
return `Hello,I am ${this.name}`
}
else if (lang === 'it') {
return `Ciao,io sono ${this.name}`
}
}
if (!isPolite) {
if (lang === 'en') {
return `${this.name},what you want`
}
else if (lang === 'it') {
return `Sono ${this.name},🤬`
}
}
}
const me = {
name: 'Sina'
}
const res = talk.bind(me, 'it', false);
console.log(res()); // Sono Sina,🤬
注意⚠⚠⚠
-
bind后的函数无法通过call/apply修改this:function greet() { console.log(this.name); } const boundGreet = greet.bind({ name: "Alice" }); boundGreet.call({ name: "Bob" }); // 仍然输出 "Alice" -
bind可以多次调用,但只有第一次生效:const bound1 = greet.bind({ name: "Alice" }); const bound2 = bound1.bind({ name: "Bob" }); // 仍然绑定 Alice bound2(); // "Alice"
New 绑定
new 绑定是一种 通过构造函数(Constructor Function)创建对象时绑定 this 的机制。当使用 new 关键字调用函数时,this 会被自动绑定到新创建的对象上。
例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person('Trump', 79);
console.log(`My name is ${person.name}, I am ${person.age} years old`);
当利用new新创建一个对象时,引擎会调用构造函数,构造函数中的 this 会被绑定到新创建的实例对象,也就是说,在person这个变量中,它的this就指向了后面new Person('Trump', 79)这个实例。
绑定规则优先级排序
new绑定 (最高优先级)- 显式绑定 (
call/apply/bind) - 隐式绑定 (通过对象调用)
- 默认绑定 (最低优先级,通常是全局对象或
undefined)
显式绑定 vs 隐式绑定
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };
function showName() {
console.log(this.name);
}
obj1.showName = showName;
obj2.showName = showName;
obj1.showName.call(obj2); // 'Bob' (显式绑定优先)
new 绑定 vs 显式绑定
function Person(name) {
this.name = name;
}
const obj = { name: 'Pre-bound' };
const BoundPerson = Person.bind(obj); // Person的this指向了obj
// new BoundPerson是因为BoundPerson的this已经被修改,我们看看用new能不能覆盖其原来的修改
const p = new BoundPerson('New instance'); // BoundPerson的this指向了'New instance'
// 创建了新实例
console.log(p.name); // 'New instance' (`new` 绑定优先)
console.log(obj.name); // 'Pre-bound' (未被覆盖)
当然,new绑定和显式绑定一起出现的概率极其小,一般不会对构造函数使用。
关于箭头函数
this在箭头函数中的绑定规则是独立的,箭头函数本身不会创建新的this,而会利用外层作用域的this,来当作自己的this.
例如:
const obj = {
name: 'Alice',
regularFunc: function () {
console.log('Regular function:', this.name); // this 指向 obj
},
arrowFunc: () => {
console.log('Arrow function:', this.name); // this 指向外层(这里可能是 window/global)
}
};
var name = 'Skye'
obj.regularFunc(); // 输出: "Regular function: Alice"
obj.arrowFunc(); // 输出: "Arrow function: Skye"
注意:!!!!!!
这是在浏览器中运行得到的结果,在node中运行结果就会不一样,因为 箭头函数的外层作用域是它的定义位置所在的词法作用域,而不是调用位置。由于对象本身不会形成作用域,所以在这里箭头函数会跳过对象,直接继承外层的作用域中的this,其中在node.js中指向的是顶层模块,在浏览器中指向的是window.如果想让arrowFunc()输出Skye,则在全局内声明this.name='Skye'就可以了。
模块顶层作用域的 this 是一个空对象 {}
结语
OK啦,这一次关于this的讲解就是这样了,如有不对的地方,期待各位大佬多多指正,我们下一期再见啦!