关于this关键字的魔幻现实
不得不承认 JavaScript 中存在一些"历史遗产",比如基于全局变量的编程模型。
如果写一个函数:
function test(name){
var namex = name
console.log(this.namex)
}
test('jack')
方法调用之后,this.namex
等于 undefined
,这感觉很奇怪,为什么会等于 undefined
?因为此处的 this
是全局对象 window
,而 namex
是函数内的局部变量,并非 window
的属性。
不过别着急,慢慢往下看,关于 this
还有很多魔幻的现实,我们来一一解锁。
# 一、完整的函数调用
还是上面的代码,其实它的完整调用写法是这样的:
function test(name){
var namex = name
console.log(this.namex)
}
test.call(undefined, 'jack')
test
是一个函数对象——即 Function
对象,Function.prototype
有 call
方法。call()
的第一个参数是 this
上下文对象,后续参数是函数的入参列表。
如果 call()
传入的 this
上下文是 undefined
或 null
,那么在非严格模式下,window
对象将成为默认的 this
上下文(严格模式下 this
保持为 undefined
)。这也就解释了开头例子中 this
为什么是 window
的原因了。
# 二、对象中的 this
const obj = {
name: 'Jack',
greet: function() {
console.log(this.name)
}
}
obj.greet() // 简写调用,输出 'Jack'
obj.greet.call(obj) // 完整调用,输出 'Jack'
obj.greet()
中的 this
指向 obj
对象,因为函数作为对象的方法被调用时,this
自动绑定到该对象。
# 三、对象方法中嵌套函数的 this
对第 2 节中的代码进行修改:
const obj = {
name: 'Jack',
greet: function() {
return function(){
console.log(this.name)
}
}
}
obj.greet()() // 输出 undefined
需要注意的是嵌套函数中的 this
依然是 window
,为什么呢?可以拆分来看:
var greet = obj.greet()
greet() // 等价于 greet.call(undefined)
嵌套函数被调用时,真实的调用者上下文是 undefined
,在非严格模式下会被转换为 window
。
# 四、原型与 this
function Clt() {
}
Clt.prototype.x = 10
Clt.prototype.test = function () {
console.log(this) // 输出 Clt 实例对象
this.y = this.x + 1
}
let bean = new Clt()
bean.test()
console.log(bean.y) // 输出 11
test
方法中输出的 this
是一个 Clt
实例对象。
这里需要引入一个新的概念:构造器函数。
使用 new
关键字调用函数时,它的作用是:
一旦函数被
new
来调用,就会创建一个链接到该函数的prototype
属性的新对象,同时this
会被绑定到那个新对象上
理解了构造器函数,我们就理解了原型与 this
的关系了,因为 new
会重新指定 this
上下文。
# 五、箭头函数与 this
ECMAScript 6 引入了箭头函数,关于箭头函数中的 this
,需要先记住一句话:
箭头函数没有自己的
this
,它会捕获其所在上下文的this
值,作为自己的this
值。
看代码(引自阮一峰老师的教程 (opens new window)):
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
此处的 this
指向了 foo
的 this
上下文,即箭头函数定义时所在的词法作用域。
箭头函数与普通函数的 this
区别:
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
箭头函数的 this
指向了定义时所在对象的 this
;普通函数的 this
指向了运行时的作用域,即全局域 window
。
this
的指向固定化,解决了许多历史问题,带来了很大好处,比如事件处理的封装:
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
一个嵌套的例子:
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
最里层的 this
也绑定到了定义时所在对象——即 foo
的 this
。注意,由于箭头函数没有自己的 this
,所以 call
、apply
、bind
方法对箭头函数无效。
# 六、bind、call 和 apply
JavaScript 提供了三个方法来显式设置函数的 this
值:
# 1、和 apply
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
// 使用 call
greet.call(person, 'Hello', '!'); // Hello, Alice!
// 使用 apply
greet.apply(person, ['Hi', '?']); // Hi, Alice?
call
和 apply
的区别仅在于传参方式:
call
接受参数列表apply
接受参数数组
# 2、bind
bind
创建一个新函数,永久绑定 this
:
const person = { name: 'Bob' };
function greet() {
console.log('Hello, ' + this.name);
}
const boundGreet = greet.bind(person);
boundGreet(); // Hello, Bob
// 即使作为对象方法调用,this 仍然是绑定的值
const obj = {
name: 'Charlie',
sayHello: boundGreet
};
obj.sayHello(); // Hello, Bob(不是 Charlie)
# 七、严格模式下的 this
在严格模式下,this
的行为有所不同:
'use strict';
function test() {
console.log(this); // undefined,而不是 window
}
test();
// 但作为对象方法调用时行为不变
const obj = {
method: function() {
'use strict';
console.log(this); // 仍然是 obj
}
};
obj.method();
# 八、this 绑定优先级
当多种绑定规则同时存在时,优先级从高到低为:
- new 绑定:使用
new
调用 - 显式绑定:使用
call
、apply
或bind
- 隐式绑定:作为对象方法调用
- 默认绑定:独立函数调用
function foo() {
console.log(this.a);
}
const obj1 = { a: 2, foo: foo };
const obj2 = { a: 3, foo: foo };
obj1.foo(); // 2(隐式绑定)
obj1.foo.call(obj2); // 3(显式绑定优先)
const bar = obj1.foo.bind(obj2);
bar(); // 3(bind 绑定)
const instance = new bar(); // new 绑定优先于 bind
# 九、总结
JavaScript 中的 this
是一个动态的概念,其值取决于函数的调用方式而非定义位置(箭头函数除外)。理解 this
的各种绑定规则和优先级,对于编写健壮的 JavaScript 代码至关重要。
关键要点:
- 普通函数的
this
由调用方式决定 - 箭头函数没有自己的
this
,继承外层作用域 new
、call
、apply
、bind
可以显式控制this
- 严格模式改变了默认绑定的行为
- 绑定规则有明确的优先级顺序
掌握这些规则,就能在 JavaScript 的"魔幻现实"中游刃有余。