阿瑞斯的BLOG

闭包中使用this存在的问题

关键词
闭包 函数声明时 函数运行时 词法作用域 特殊变量 箭头函数

前言

也是为什么老是有var that = this;的写法以及箭头函数产生的原因。

闭包中使用this的问题

在闭包中使用this是想和利用闭包的特性像引用其他其他变量一样引用保存外层函数的this,但是期望与现实是相反的。

因为this是一个运行时基于函数的调用环境绑定的特殊变量。特别是,匿名函数的执行环境具有全局性。

1
2
3
4
5
6
7
8
9
10
11
12
13
//在闭包中使用this是想和利用闭包的特性像引用其他其他变量一样引用保存外层函数的this,但是期望与现实是相反的。
// 因为this是一个运行时基于函数的调用环境绑定的特殊变量。特别是,匿名函数的执行环境具有全局性。
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function() {
return function() { //闭包
return this.name; //在闭包中使用this
}
}
};
console.log(object.getNameFunc()()); //在严格模式中 undefined
//在非严格模式下浏览器中 Window

为什么this会指向undefined呢?

这是因为每个函数在调用的时候都会自动取得两个特殊的变量,thisarguments,内部函数在搜索这2个变量的时候,只会搜索到其活动对象为止,因此永远不可能访问到外部函数中的这2个变量。

那么如何访问到这两个变量呢?

把外部作用域中的this保存到一个闭包能访问到的变量里,并在闭包中引用那个变量,而不是引用this
常见的

1
2
var this = that;
var args = Array.prototype.slice.call(arguments);

上例的修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function() {
var that = this; //修改:保存外部作用域的this到一个闭包能访问到变量中
return function() { //闭包
//return this.name; //在闭包中使用this
return that.name; //修改:在闭包中通过引用that来引用外部作用域的特殊变量this
}
}
};
console.log(object.getNameFunc()()); //在严格模式中 undefined
//在非严格模式下浏览器中 this指向Window

ES6的箭头函数:

ES6的箭头函数最佳的使用姿势就是解决this指向问题。
因为在JavaScript中,对象之间的调用是非常繁杂的,一不小心遇到this被篡改的问题,导致后面的代码出错:

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
constructor() {
this.type = ‘animal’;
}
says(say){
setTimeout(function() {
console.log(this.type + ’says’ + say);
}, 1000);
}
}
let animal = new Animal();
animal.says(‘hi’); //undefined says hi

这是一个比较经典的this被篡改的问题,因为这个setTimeout函数,他的this指向window对象。

我们可以利用一个变量保存住这个this指针,也或者使用bind(this)方法,但有了箭头函数,等于函数本身集成了保存this指针的功能,这让我们不是处处提防this陷阱。同样的,基于这个原因,箭头函数中没有自己的this,但当你在箭头函数内部使用了this,常规的局部作用域准则就起作用了,它会指向最近一层作用域内的 this

箭头函数最常用于回调函数,如事件处理器或定时器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal {
constructor() {
this.type = ‘animal’;
}
says(say){
setTimeout(() => {
console.log(this.type + ’says’ + say);
}, 1000);
}
}
let animal = new Animal();
animal.says(‘hi’); // animal says hi
//等价于
class Animal {
constructor() {
this.type = ‘animal’;
}
says(say){
var that = this;
setTimeout(function() {
console.log(that.type + ’says’ + say);
}, 1000);
}
}

对箭头函数的误解

最大的误解:箭头函数使用的是外部函数(这里理解成父函数)的this

是否局部(Lexical)?

1
2
3
4
5
6
7
8
function foo() {
setTimeout( () => {
console.log("id:", this.id);
},100);
}
foo.call( { id: 42 } );
// id: 42

这里的 => 箭头函数看起来把它内部的this绑定为父函数 foo() 里的 this。如果这个内部函数是一个常规的函数(声明或表达式),它的 this将类似 setTimeout如何调用函数一样被控制着。

但是实际上是箭头函数内部根本没有this变量,对于this变量的访问变量就像根据词法作用域查找一般变量一样,查找到有this变量的外层作用域,而不是父级作用域。

箭头函数的注意

1)箭头函数 => 所改变的并非把 this 局部化,而是完全不把 this 绑定到里面去”, 虽然 => 箭头函数没有一个自己的 this,但当你在内部使用了this,常规的局部作用域准则就起作用了,它会指向最近一层作用域内的this

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
return () => {
return () => {
return () => {
console.log("id:", this.id);
};
};
};
}
foo.call( { id: 42 } )()()();
// id: 42

有多少次 this 的绑定执行了呢?大部分人会认为有4次——每个函数里各一次。

事实上更准确地说,只有一次才对,它发生于 foo() 函数中。

这些接连内嵌的函数们都没有声明它们自己的 this,所以 this.id 的引用会简单地顺着作用域链查找,一直查到 foo() 函数,它是第一处能找到一个确切存在的 this 的地方。

说白了跟其它局部变量的常规处理是一致的!

换句话说,正如同 Dave 说的一样,this 生来局部,而且一直都保持局部态。=>箭头函数并不会绑定一个 this 变量,它的作用域会如同寻常所做的一样一层层地去往上查找。

2)不仅仅是this

如果你贸贸然地同意了“箭头函数就是常规function的语法糖”这样的观点,那是不正确的,因为事实并非如此——箭头函数里并不按常规支持 var self = this 或者 .bind(this) 这样的糖果。

那些错误的解释都是典型的“给对了答案却讲错了原因”,就像你在高中代数课的测试上明明写对了答案,但老师仍会画圈圈告诉你用错方法了——如何解得答案才是最重要的!

另外,关于“=>箭头函数不绑定自身的 this,而允许局部作用域的方案来沿袭处理之”的正确描述,也解释了箭头函数的另一个情况——它们在函数内部不走寻常路的孩子不仅仅是 this。

事实上 =>箭头函数并不绑定 this,arguments,super(ES6),抑或 new.target(ES6)。

这是真的,对于上述的四个(未来可能有更多)地方,箭头函数不会绑定那些局部变量,所有涉及它们的引用,都会沿袭向上查找外层作用域链的方案来处理。

1
2
3
4
5
6
7
8
function foo() {
setTimeout( () => {
console.log("args:", arguments);
},100);
}
foo( 2, 4, 6, 8 );
// args: [2, 4, 6, 8]

这段代码中,=>箭头函数并没有绑定 arguments,所以它会以 foo() 的 arguments 来取而代之,而 super 和 new.target 也是一样的情况。

3)this在箭头函数中被绑定,4种绑定规则中的无论哪种都无法改变其绑定

4)箭头函数不可以当作构造函数,也就是不可以使用new命令,否则会报错

最后

虽然箭头函数可以把作用域和this机制联系起来,但是却容易混淆,使代码难以维护。应该在作用域和this机制中二选一,否则就会造成混淆。要么只使用词法作用域,要么只使用this机制,必要时使用bind()。尽量避免使用that=this和箭头函数共同使用。

参考:

ES6 箭头函数中的 this?你可能想多了(翻译)

ECMAScript 6 入门-箭头函数 阮一峰