this 到底是什么?
《你不知道的JavaScript》上的原话:
我的理解:
this
是一个在函数执行时动态绑定 的关键字,本质上是一个 JavaScript 运行时上下文(Execution Context)中的一个特殊属性,当我们使用this
时,它会指向一个对象,帮助函数访问这个对象里的属性和方法。
在 JavaScript 中,执行一段代码时,会创建一个执行上下文(Execution Context) ,其中包含:
- 变量环境(Variable Environment)
- 词法环境(Lexical Environment)
this
绑定
当 JavaScript 执行一个函数时,它会创建一个新的执行上下文,并将一个特定的对象赋值给 this
。这个对象可能是:
- 全局对象 (如
window
或global
) - 调用该函数的对象
- 手动绑定的对象
- 新创建的对象(在
new
关键字下) - 箭头函数继承的外部
this
this
并不是变量 ,也不是作用域的一部分,而是执行上下文中的一个特殊属性。
这么复杂的this 有啥用?
还是那本熟悉的小黄书,它说:
JavaScript 设计
this
的初衷是为了提供一种灵活的方式来访问调用者(即调用函数的对象)的属性和方法,而不需要显式地将对象传递给函数。这使得代码更加简洁和通用。
举个例子:
js
const person = {
name: "Alice",
greet: function () {
console.log("Hello, my name is " + this.name);
},
};
person.greet(); // 输出: Hello, my name is Alice
在这个例子中,this
指向 person
对象。通过使用 this
,我们可以在 greet
方法中直接访问 name
属性,而不需要显式地写成 person.name
。如果将来我们将 greet
方法复制到其他对象上,this
会自动指向新的对象,而不是硬编码为某个特定对象。
this在全局环境下的指向
在大多数情况下,this都是出现在函数中的。
浏览器环境
在浏览器中,全局对象是 window
。因此,在全局作用域下直接使用 this
时:
- 非严格模式 :
this
指向全局对象window
。 - 严格模式 :
this
仍然指向全局对象window
。
js
console.log(this)
console.log(window)
console.log(this === window)
Node.js 环境
在 Node.js 中,全局对象是 global
。然而,在模块化的代码中,每个文件被当作一个独立的模块,this
在顶层作用域下指向的是当前模块的 module.exports
对象,而不是 global
。
在 Node.js 的全局作用域中:
- 非严格模式 :
this
指向module.exports
。 - 严格模式 :
this
也指向module.exports
。
js
console.log(this)
console.log(global)
console.log(this === global)
console.log(this === module.exports)
this的四种绑定规则
默认绑定
独立的函数调用时,this
会指向全局对象:
- 在 浏览器 环境下,全局对象是
window
。 - 在 Node.js 环境下,全局对象是
global
。
js
function foo1(){
console.log(this)
}
function foo2(){
console.log(this)
foo1()
}
function foo3(){
console.log(this)
foo2()
}
foo3()
这三个函数全部都是不带任何修饰的独立调用,所以全部都是指向window
。
js
var obj = {
name: 'obj',
foo1: function () {
console.log(this)
},
}
var bar = obj.foo1;
bar()
bar就是对函数foo1的一个引用,我不管你foo1是在obj里面还是在全局作用域下定义的,bar函数运行时是纯粹的独立调用,所以还是this指向window。
js
function foo(){
function bar(){
console.log(this)
}
return bar
}
const fn = foo()
fn()// window
严格模式("use strict"
)下:
js
"use strict";
function strictFunction() {
console.log(this); // undefined
}
strictFunction();
在严格模式下,函数中的 this
会变成 undefined
,而不是 window
或 global
。
隐式绑定(对象调用)
如果函数是通过某个对象调用的,this
绑定到该对象,记住这个XXX.fun()。:
js
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
}
var a = 3;
obj.foo(); // 2
在这里,this
指向 obj
,因为 sayHello()
是被 obj
调用的。
需要注意的是:对象属性链中只有最后一层会影响到调用位置。
js
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
结果是:42
因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的obj2。
注意:丢失隐式绑定
js
var name = 'Bob'
const obj = {
name: "Alice",
sayHello: function() {
console.log(this.name);
}
};
obj.sayHello(); // 输出: Alice
const say = obj.sayHello;
say(); // 输出: Bob
其实之前就说过了这个例子,这里 say()
作为普通函数调用,是一个不带任何修饰的函数调用,因此应用了默认绑定。
显式绑定(call
、apply
、bind
)
call
和 apply
call
和apply
可以手动设置this
,并立即执行函数。- 区别是
call
逐个传参,而apply
传入一个数组。
js
function greet(age) {
console.log(`My name is ${this.name}, I am ${age} years old.`);
}
const person = { name: "Bob" };
greet.call(person, 25); // My name is Bob, I am 25 years old.
greet.apply(person, [25]); // My name is Bob, I am 25 years old.
除了直接传递一个对象,也可以传入一个基本数据类型,call和apply内部会帮包装成对象。
js
function sum(num1,num2,num3){
console.log(num1+num2,this)
}
sum.call('call',1,2,3)
sum.apply('apply',[1,2,3])
sum.call(undefined,1,2,3)
sum.apply(null,[1,2,3])
传入null和undefined时会采用默认绑定规则。
但是在严格模式下:既不会帮我包装对象,也不会采用默认绑定。
bind
bind
也是手动设置this
,但不会立即执行函数,而是返回一个新的绑定了this
的函数。
js
function foo(){
console.log(this)
}
var bar = foo.bind('bind')
bar()
bar()
bar()
显示绑定的优先级比默认绑定更高,之后每次调用bar,都是:
new
绑定(构造函数)
当函数被 new
调用时:
- JavaScript 会创建一个新的对象。
this
绑定到新创建的对象。
js
function Person(name) {
this.name = name;
}
const p = new Person("Charlie");
console.log(p.name); // Charlie
API中的this
js
setTimeout(function() {
console.log(this); // 在浏览器中是 window
}, 1000);
可以把它理解为:
在 DOM 事件监听器中,this
默认指向绑定事件的 DOM 元素。
js
document.getElementById('myButton').addEventListener('click', function () {
console.log(this); // 输出:<button id="myButton">...</button>
});
数组上的forEach/map/filter能绑定this
js
var arr = [1, 2, 3]
arr.forEach(function (item) {
console.log(item,this)
})
arr.forEach(function (item) {
console.log(item,this)
},'hello')
new和bind的优先级
new
不能直接与 call
或 apply
一起使用
你不能这样写:
arduino
new obj.call(...)
new obj.apply(...)
原因:
new
需要一个构造函数,而call
/apply
返回的是函数调用的结果,不是构造函数- 语法上这是不允许的,会直接抛出语法错误
可以与 bind
一起使用
你可以这样使用:
go
new (func.bind(thisArg, arg1, arg2))
为什么可以:
bind
返回的是一个绑定函数,这个函数可以作为构造函数使用
执行顺序
bind
先执行 :bind
方法首先创建一个新的绑定函数new
后执行 :然后new
操作符作用于这个绑定函数
优先级
-
new
的优先级高于bind
的this
绑定:- 当使用
new
调用绑定的函数时,bind
设置的this
值会被完全忽略 new
会按照常规规则创建一个新对象作为this
- 当使用
js
function Person() {
console.log("this is:", this);
}
const BoundPerson = Person.bind({ foo: 1 });
// 普通调用 - 使用 bind 的 this
BoundPerson(); // 输出: this is: { foo: 1 }
// new 调用 - 忽略 bind 的 this
new BoundPerson(); // 输出: this is: Person {} 就是我们说的新创建的对象
-
bind
的参数绑定仍然有效:bind
预先绑定的参数会被保留- 调用时
new
传入的参数会追加在预先绑定的参数之后
bind
预先绑定的参数是永久固定的 ,无法通过后续调用更改。这是 bind
的核心特性之一。
js
function sum(a, b, c) {
return a + b + c;
}
const boundSum = sum.bind(null, 1, 2); // 永久绑定 a=1, b=2
console.log(boundSum(3)); // 1+2+3 = 6
// 无法改变已绑定的1和2
当与 new
一起使用时:
- 绑定的参数仍然不可更改
- 新传入的参数会追加在已绑定参数之后
js
function Person(name, age, country) {
this.name = name;
this.age = age;
this.country = country;
}
const BoundPerson = Person.bind(null, 'John', 30);
const p1 = new BoundPerson(); // Person {name: 'John', age: 30, country: undefined}
const p2 = new BoundPerson('USA'); // Person {name: 'John', age: 30, country: 'USA'}
特殊情况
如果绑定的函数本身没有使用 this
(不是构造函数风格),new
仍然会创建一个对象,但可能不是你期望的:
js
function sum(a, b) {
return a + b;
}
const BoundSum = sum.bind(null, 2);
const result = new BoundSum(3); // 奇怪但合法的用法
console.log(result);
// sum {} (一个无意义的对象,因为 sum 没有使用 this)
// 但 sum 仍然执行了,返回了 5,只是返回值被 new 忽略了
优先级从高到低:
- new 绑定 - 当使用构造函数创建对象实例时,
this
绑定到新创建的实例。 - 显式绑定 - 使用
call
、apply
或bind
方法时,this
被明确指定。 - 隐式绑定 - 函数作为对象的方法调用时,
this
绑定到该对象。 - 默认绑定 - 在非严格模式下指向全局对象(如
window
),在严格模式下为undefined
。
例外情况
间接函数引用
js
var obj1 = {
name: 'obj1',
foo: function () {
console.log(this)
}
}
var obj2 = { name: 'obj2' }
obj2.foo = obj1.foo;
obj2.foo()
隐式绑定,输出obj2,相信大家都知道,但是如果是这样呢:
javascript
var obj1 = {
name: 'obj1',
foo: function () {
console.log(this)
}
}
var obj2 = {
name: 'obj2',
};
(obj2.foo = obj1.foo)()
(obj2.foo = obj1.foo)
的结果是一个独立的函数引用,因此调用位置是foo(),而不是绑定到某个对象的方法,所以最后采用默认绑定。
箭头函数的this绑定
在箭头函数中,this
的指向是定义时 决定的,而不是调用时 决定的。这与普通函数不同,普通函数的 this
是在调用时动态绑定的。
箭头函数 this
的规则
继承外层作用域的 this
(词法作用域)
- 箭头函数不会创建自己的
this
,它会继承定义它的作用域 中的this
。 - 这个作用域通常是它的外层函数,或者是全局作用域(如果箭头函数是在全局作用域中定义的)。
不能通过 call
、apply
或 bind
改变 this
- 对于普通函数,我们可以使用
call
、apply
、bind
显式改变this
,但箭头函数不受这些方法影响。
- 不能作为构造函数
- 由于箭头函数没有自己的
this
,因此不能使用new
关键字实例化对象,否则会报错。
- 继承外层
this
js
const obj = {
name: "Alice",
sayHello: function () {
const arrowFn = () => {
console.log(this.name); // 继承 sayHello 方法的 this(即 obj)
};
arrowFn();
},
};
obj.sayHello(); // Alice
这里 arrowFn
继承了 sayHello
方法的 this
,因此 this.name
指向 obj.name
。
call
/apply
/bind
无效
js
const obj = { name: "Alice" };
const arrowFn = () => {
console.log(this); // this 继承自定义它的作用域(通常是 window 或 global)
};
arrowFn.call(obj); // 依然是 window 或 global,不是 obj
- 不能作为构造函数
js
const Arrow = () => {};
const obj = new Arrow(); // TypeError: Arrow is not a constructor
- 在
setTimeout
中的行为
js
const obj = {
name: "Alice",
sayHello: function () {
setTimeout(() => {
console.log(this.name); // this 继承自 sayHello 方法
}, 1000);
},
};
obj.sayHello(); // Alice
为什么? 因为箭头函数不会创建自己的 this
,所以 setTimeout
里的 this
依然是 sayHello
方法的 this
,即 obj
。
练习
js
<script>
var name = "window";
var person = {
name: "person",
sayName: function () {
console.log(this.name);
}
}
function sayName() {
var sss = person.sayName;
sss();
//window 独立函数调用 默认绑定
person.sayName();
//person 隐式绑定
(person.sayName)();
//其实就是person.sayName() 隐式绑定
(b = person.sayName)();
//间接函数引用 默认绑定
}
sayName();
//person
//person
//window
</script>
js
<script>
var name = 'window'
var person1 = {
name: 'peson1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: 'person2' }
person1.foo1();//person1 隐式绑定
person1.foo1.call(person2);//person2 显示绑定大于隐式绑定
person1.foo2()//window 箭头函数继承的this是全局作用域
person1.foo2.call(person2);//window 箭头函数无法被显示绑定
person1.foo3()()//window 理解为person1.foo3()返回的是一个普通函数fn,然后fn(),默认绑定
person1.foo3.call(person2)()//window foo3()函数被显示绑定,但是foo3()函数返回的是一个普通函数,所以普通函数的this指向window
person1.foo3().call(person2)//person2 最终调用返回函数式,使用的是显示绑定
person1.foo4()()//person1 箭头函数不绑定this,上层作用域this是personl
person1.foo4.call(person2)();//person2 上层作用域被显示的绑定了person2
person1.foo4().call(person2);//person1 上层作用域已经被显示的绑定了person1,call无法改变箭头this的指向
总结
最后还是用《你不知道的JavaScript》中的秘诀来结尾。
希望能帮到正在面试的你。