在 JavaScript 编程中,this 关键字是一个非常重要但却常常令人困惑的概念。理解 this 的工作原理对编写和调试代码至关重要。在不同的情况下,this 可能会引用不同的对象,这取决于函数的调用方式。
本文将深入探讨 JavaScript 中的 this 关键字,解释它在不同绑定规则下的行为,包括默认绑定、隐式绑定、显式绑定、硬绑定和 new 绑定。我们还将通过具体的代码示例来展示这些规则是如何影响 this 的值的。
通过这篇文章,您将更好地理解 this 的工作原理,能够更准确地预测和控制函数中的 this 绑定,为编写高质量的 JavaScript 代码打下坚实的基础。
this是什么
在 JavaScript 中,this
是一个函数在被调用时自动生成的一个关键字,它的值取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this
就是记录的其中一个属性,会在函数执行的过程中用到(this
是一个执行上下文的一个属性)。
调用位置
调用位置是指函数在代码中被调用的位置,而不是声明的位置。调用栈展示了为了到达当前执行位置所调用的所有函数的顺序。分析函数的调用位置可以确定 this
的绑定方式,因为 this
的绑定规则取决于函数的调用方式(例如,直接调用、作为方法调用、使用 new
关键字调用等)。
javascript
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置
绑定规则
默认绑定
如果一个函数独立调用(即它不是作为对象的方法或通过 new
关键字调用),那么 this
通常会引用全局对象(在浏览器中是 window
,在 Node.js 中是 global
)。在严格模式下,this
会是 undefined
。
javascript
function foo() {
console.log(this.a);
}
let a = 2;
foo(); // 输出2,在非严格模式下
// 严格模式下会报错
注意:只有函数运行在非 strict mode 下时,默认绑定才能绑定到全局对象
隐式绑定
隐式绑定是指当函数作为对象的方法被调用时,函数调用时的 this 会被隐式地绑定到调用该方法的对象。如果一个函数作为对象的方法被调用,那么 this 会引用调用这个方法的对象。
javascript
let obj = {
a: 2,
foo: function() {
console.log(this.a);
}
};
obj.foo(); // 输出2
javascript
function foo(){
console.log(this.a);
}
let obj = {
a: 2,
foo: foo
};
obj.foo(); // 输出2
需要注意的是 foo() 函数的声明方式,以及它如何被添加为 obj 对象的属性。无论是直接在 obj 中定义,还是先定义再添加为引用属性,这个函数实际上都不属于 obj 对象本身。原因如下:
函数不管是先声明再引用或者直接在对象中声明后引用,该函数都是在其声明时的作用域(通常是全局作用域)下定义的。对象本身并不创建新的作用域,作用域是由函数创建的。这意味着对象内部的方法仍然在全局作用域中执行。
javascript
function foo() {
console.log(this.a);
}
let obj = {
a: 2,
foo: foo
};
obj.foo(); // 输出2
即使 foo 函数被添加为 obj 的属性,它的作用域仍然是全局的,而不是 obj 的局部作用域。因此,this 绑定取决于函数的调用方式,而不是它的定义位置。
javascript
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
隐式丢失
如果你将这个方法赋值给一个变量,或者作为参数传递给另一个函数,那么这个函数可能会"丢失"它的this绑定。
javascript
let obj = {
a: 2,
foo: function() {
console.log(this.a);
}
};
let bar = obj.foo; // 函数别名!
let a = "Oops, global 'a'";
bar(); // "Oops, global 'a'"
javascript
let obj ={
a:'a',
foo:function(){
console.log(this.a)
}
}
obj.foo() // a
function thisFunc(fn){
this.a = 10
fn() // 10
}
thisFunc(obj.foo)
当你将一个对象的方法赋值给一个变量,然后通过这个变量调用这个方法时(例如 let bar = obj.foo; bar();),你实际上是在进行直接调用。因此,this 的值会按照默认绑定的规则来确定,而不是绑定到原来的对象。
显式绑定
使用call,apply或bind方法来显式地设置函数的this值。
javascript
function foo() {
console.log(this.a);
}
let obj = {
a: 2
};
foo.call(obj); // 输出2
注意:当你使用call,apply或bind方法显式地设置函数的this值时,这个this值是固定的,不能被修改。
硬绑定
"硬绑定"是JavaScript中的一个术语,它指的是使用 call、apply 或 bind 方法显式地设置函数的 this 值,并返回一个新的函数。这个新函数的 this 值在任何情况下都不会改变。
在 bind 方法的例子中,bind 函数创建了一个新的函数,这个新函数的 this 值被永久地设置为指定的对象(例如 obj)。无论你如何调用这个新函数,它的 this 值都会是指定的对象。这就是所谓的"硬绑定"。
javascript
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
};
}
在ES5中,Function.prototype.bind 方法提供了一个内置的方式来进行硬绑定。bind 方法会创建一个新的函数,这个新函数的this 值被永久地绑定到bind方法的第一个参数。
javascript
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
在这个例子中,foo.bind(obj)创建了一个新的函数bar ,这个新函数的this 值被永久地绑定到obj 。无论你如何调用bar 函数,它的this 值都会是obj。这就是所谓的"硬绑定"。 注意:
- call 和 apply 不会永久性地改变函数的 this 指向。它们仅仅是在当前的函数调用中改变 this。
- 用 .bind() 方法会创建一个新的函数,这个新函数的 this 指向被永久性地绑定到传入 .bind() 的第一个参数。一旦通过 .bind() 创建了新函数,这个新函数的 this 指向就不能再被改变了。
new绑定
如果一个函数通过new关键字被调用,那么this会引用新创建的对象。
javascript
function Foo() {
this.a = 2;
}
let obj = new Foo();
console.log(obj.a); // 输出2
在JavaScript中,使用new 关键字调用一个函数(通常被称为构造函数)会创建一个新的对象,并且这个新对象的this值会被绑定到这个函数中。这个过程可以分为以下四个步骤:
- 创建一个全新的对象。 当你使用new关键字调用一个函数时,JavaScript会创建一个新的空对象。
- 这个新对象会被执行[[原型]]连接。 这意味着新对象的原型(也就是它的proto 属性)会被设置为构造函数的prototype属性。这样,新对象就可以访问构造函数原型上的所有属性和方法。
- 这个新对象会绑定到函数调用的this。 在构造函数内部,this 关键字会引用新创建的对象。这意味着你可以在构造函数内部使用this关键字来设置新对象的属性和方法。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。 如果构造函数返回了一个对象,那么这个对象会被new 表达式返回。如果构造函数没有返回一个对象,或者返回了一个非对象类型的值,那么new表达式会返回新创建的对象。
箭头函数
箭头函数不会创建自己的this上下文,而是从它们的定义位置的作用域继承this。
javascript
let obj = {
a: 2,
foo: function() {
setTimeout(() => {
console.log(this.a);
}, 100);
}
};
obj.foo(); // 输出2
javascript
let obj = {
a: 2,
foo: () => {
console.log(this.a);
}
};
var a =11
obj.foo();//11
说明:对象中的作用域为全局作用域(对象不会创建作用域)
异步回调函数this
javascript
let obj={
a:10
}
function foo(){
setTimeout(function(){console.log(this.a)},1000)
}
foo.call(obj)//全局作用域的this
箭头函数在异步方法中定义的时候就继承当前作用域的this并绑定。
javascript
let objB ={
name:'objB',
foo:function(){
(()=>{
console.log(this.name) // 箭头函数继承了foo函数作用域的this
})()
}
}
let name ='glory'
objB.foo() // glory
const add = '当前的位置是在全局作用域里面'
function setTimeoutFunc(){
this.add = '当前的位置是在setTimeoutFunc里面'
setTimeout(()=>{
console.log(this.add);
},100) // 这个箭头函数在此处定义的时候就继承当前的作用域
}
setTimeoutFunc() // 当前的位置是在setTimeoutFunc里面
优先级
绑定规则的优先级排序:new 绑定和显式绑定的优先级最高,箭头函数的优先级次之,隐式绑定的优先级再次之,最后是默认绑定。如果多个规则同时适用,那么优先级高的规则会决定 this 的值。
javascript
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
//显式的优先级比隐式高
javascript
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
//new的优先级比隐式高
推荐阅读《你不知道的JavaScript》