谈到this绑定规则要先找到函数执行过程中的调用位置。
调用位置就是函数在代码中被调用的位置。这就涉及分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中。
那什么是调用栈和调用位置呢?
//1.调用栈和调用位置
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的调用位置
调用栈是baz->bar->foo,当调用foo()时调用位置就是bar(),可以简单理解为在bar()中调用foo().
注意是如何(从调用栈中)分析出真正的调用位置的,因为它决定了this的绑定。
this有四大绑定规则,下面来仔细看看这四大规则。
- 默认规则
function foo(){
console.log(this.a); //this.a被解析成全局变量a a=2
}
var a=2;
foo(); //2
foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则
//严格模式下全局对象将无法使用默认绑定,this会绑定到undefined. 严格模式下与foo()的调用位置无关
function foo(){
"use strict";
console.log(this.a);
}
var a=2;
foo(); //undefined
- 隐式绑定
先看一段代码
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
obj.foo();//2
调用位置会使用obj上下文来引用函数。当foo()被调用时,它的落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此this.a和obj.a是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
function foo(){
console.log(this.a);
}
var obj2={
a:42,
foo:foo
};
var obj1={
a:2,
obj2:obj2
};
obj1.obj2.foo();//42
隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
//隐式丢失
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
var bar=obj.foo; //函数别名!bar引用的是foo函数本身
var a="Hello Febby"; //a是全局对象属性
bar(); //Hello Febby
虽然bar只是obj.foo的一个引用,实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
同时注意区分下面这种调用方式
function foo(){
return this.a;
}
var obj={
a:2,
foo:foo
};
var bar=obj.foo(); //绑定到obj上
var a="Hello Febby"; //a是全局对象属性
// bar; //2
console.log(bar); //2
再来思考一下下面这段代码:
function foo(){
console.log(this.a);
}
function doFoo(fn){
//fn其实引用的是foo
fn(); //<--调用位置
}
var obj={
a:2,
foo:foo
};
var a="Hello Febby"; //a是全局对象属性
doFoo(obj.foo); //Hello Febby
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果跟上面一样。
如果把函数传入语言内置的函数而不是传入自己声明的函数,结果是一样的。
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
var a="Hello Febby"; //a是全局对象属性
setTimeout(obj.foo,100); //Hello Febby
//js环境中内置的setTimeout()函数实现与下面伪代码类似
function setTimeout(fn,delay){
//等待delay毫秒
fn();//<--调用位置
}
- 显式绑定
就像我们刚才看到那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定在这个对象上。
那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
可以利用JS提供的call()和apply()方法。
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this的绑定对象,称之为显式绑定。
//显式绑定
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo:foo
};
foo.call(obj);//2
foo.apply(obj);//2
硬绑定可以解决丢失绑定问题。
function foo(){
console.log(this.a);
}
var obj={
a:2
};
var bar=function(){
foo.call(obj);
};
bar();//2
setTimeout(bar,100);//2
//硬绑定的bar不可能再修改它的this
bar.call(window)//2
我们创建了bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj上。无论之后怎么调用函数bar,它总会手动在obj上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
new绑定
在JS中,构造函数只是一些使用new操作符是被调用的函数。它们并不属于某个类,也不会实例化一个类。实际上,它们只是被new操作符调用的普通函数而已。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象。
//new绑定
function foo(a){
this.a=a;
}
var bar=new foo(2);
console.log(bar.a);//2
使用new来调用foo(…)时,会构造一个新对象并把它绑定到foo(…)调用中的this。
以上便是this四大绑定规则的总结,希望可以帮到大家。
具体用法传送门:https://blog.youkuaiyun.com/Febby_/article/details/89430391
知识整合来源:《你不知道的JavaScript》