js
中的this
是很容易让人觉得困惑的地方,这篇文章打算说一下this
绑定的几种情况,相信可以解决大部分关于this
的疑惑。
this
是在运行的时候绑定的,不是在编写的时候绑定的,函数调用的方式不同,就可能使this
所绑定的对象不同。
1.几种绑定规则
函数调用的位置对this
的指向有着很大的影响,但却不是完全取决于它。下面是几种this
的绑定规则:
1.1.默认绑定
默认规则的意思就是在一般情况下,如果没有别的规则出现,就将this
绑定到全局对象上,我们看如下代码:
function foo() {
var a = 3;
console.log( this.a );
}
var a = 2;
foo(); //2
这段代码中,this
是被默认绑定到了全局对象上,所以this.a
得到的是2
。我们如何判断这里应用了默认绑定呢?foo
在调用的时候直接使用不带任何修饰的函数引用,只能使用默认绑定。有人会误认为结果是3
,this
常有的几种错误理解之一就是认为this
指向当前函数的词法作用域,this
与词法作用域以及作用域对象是完全不同的东西,作用域对象是在引擎内部的,js
代码是无法访问的。还有本文我们不讨论严格模式下的情况,严格模式这里的this
会绑定到undefined
。
1.2.隐式绑定
如果在调用位置有上下文对象,说简单点就是这个函数调用时是用一个对象.
出来的。就像下边这样,它就遵循隐式绑定:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var a = "opps, gloabl"; //全局对象的属性
obj.foo(); //2
var bar = obj.foo;
bar(); //"opps, gloabl"
第9
行代码,就是函数在调用的时候,是用前边的对象加上.
操作符调用出来的,此时就用到了隐式绑定规则,隐式绑定规则会将函数调用中的this
绑定到这个上下文对象,此时的this.a
和obj.a
是一样的。
而隐式绑定会出现一个问题,就是隐式丢失,上边的第10
行代码,是新建一个foo
函数的引用,即bar
,在最后一行调用的时候,这个函数不具有上下文对象,此时采用默认绑定规则,得到的结果自然是opps, gloabl
;
绑定丢失也会发生在函数作为参数传递的情况下,即传入回调函数时,因为参数传递就是一种隐式赋值,看如下代码:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn(); //在此处调用,参数传递是隐式赋值,丢失this绑定
}
var obj = {
a: 2,
foo: foo
};
var a = "opps, global";
doFoo( obj.foo ); //看似是隐式绑定,输出opps, global
javascript
环境中内置的函数,在具有接受一个函数作为参数的功能的时候,也会发生像上边这种状况。例如setTimeout
函数的实现就类似于下边的伪代码:
function setTimeout(fn, delay) {
//等待delay毫秒
fn();//在此处调用
}
所以回调函数丢失this
绑定是非常常见的,后边我们再看如何通过固定this
来修复这个问题。
1.3.显式绑定
在此之前,相信你已经用过很多次apply
和call
函数了,使用这两个函数可以直接为你要执行的函数指定this
,所以这种方式称为显式绑定。
function foo() { //显式绑定this,这种方式依然无法解决绑定丢失的问题
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); //2
通过像上边这样调用,我们可以将foo
的this
强制绑定到obj
上。如果给call
传入的是一个基本类型数据,这个基本类型数据将会被转换成对应的基本包装类型。不过这种方式依然无法解决上边的丢失绑定问题。
1.3.1.硬绑定
为了解决这个问题,我们可以写像下边这样的代码:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() { //创建一个包裹函数,以确保obj的绑定
foo.call( obj );
};
bar(); //2
setTimeout( bar, 100 ); //2
bar.call( window ); //2
上边这样的函数确实解决了绑定丢失的问题,每次调用bar
就可以确保obj
的绑定,不过还不能为函数传参,而且这种方法复用率低,所以又出现了这样的辅助绑定函数:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
function bind(fn, obj) { //辅助绑定函数
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a: 2
};
var bar = bind( foo, obj );
var b = bar(3);
console.log(b);
因为这种模式很常用,所以ES5
内置了这个方法,就是bind
,bind(...)
返回一个硬编码的新函数,将你指定的对象绑定到调用它的函数的this
上。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj ); //bind返回一个绑定到obj上的新函数
var b = bar(3);
console.log(b);
var a = "window's a";
foo("!"); //对原来的函数不产生影响
1.3.2.API调用参数指定this
许多第三方库里的函数,以及许多语言内置的函数,都提供了一个可选的参数用来指定函数执行的this
:
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
[1, 2, 3].forEach( foo, obj ); //forEach的第二个参数就是用来设置this
1.4.new绑定
js
中的所谓的构造函数,其实和一般的普通函数没有什么区别,并不具有特殊性,它们只是被new
操作符调用的普通函数而已。实际上并不存在什么构造函数,只存在对于函数的构造调用
发生构造函数的调用时,会自动执行下边的操作:
创建一个全新的对象。
这个对象会被执行[[Prototype]]连接。
这个新对象会绑定到函数调用的
this
。执行这个函数里的代码。
如果函数没有返回其他对象,则自动返回这个新对象。
这个在执行new
操作的时候对this
的绑定就叫做new
绑定。
function fun() {
this.a = 1;
this.b = 2;
}
var instance = new fun();
console.log(instance.a);
1.5.箭头函数的this
ES6
中的箭头函数是无法使用以上几种规则的,它是根据外层的作用域来决定this
,即取决于外层的函数作用域或全局作用域,而且箭头函数的绑定无法修改,即使是new
绑定也不可以。
function foo() {
return (a) => {
console.log( this.a );
}
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.apply(obj1);
bar.apply(obj2); //2
2.绑定规则的优先级
前边我们已经说了this
的几种绑定规则,当函数调用的位置可以使用多条绑定规则的时候,我们就需要确定这几种规则的优先级。
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
从上边的代码可以看出来,显式绑定的优先级要高于隐式绑定,下边再看看显式绑定和new
绑定的优先级:
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar(2);
console.log( obj1.a ); //2
var baz = new bar(3);
console.log( obj1.a ); //2
console.log( baz.a ); //3
仔细看这段代码,bar
是foo
绑定到obj1
上返回的一个函数,对这个函数进行new
操作,并传入新的a
值,发现改变的是新对象baz
的属性,和obj1
已经脱离关系。说明new
绑定的优先级高于硬绑定。
综上所述,我们在遇到this
时,如果不是箭头函数,就可以以这种顺序判断它的指向:
如果函数在
new
中调用,绑定到新建的对象。函数通过
call
或apply
或者硬绑定调用,this
绑定到指定的对象上。函数在某个上下文对象中调用,绑定到这个上下文对象上。
采用默认绑定规则。
以上就是这篇博客的所有内容了,如果有什么理解的不对的地方欢迎讨论,这里是我的博客以及github,欢迎来访。
参考书籍:《你不知道的JavaScript(上卷)》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。