本文基于你不知道的 javascript 上卷和自己的理解
当一个函数被调用时,会创建一个活动记录(有时候也成为执行上下文,见 浅析 javascript 中执行环境,变量对象及作用域链)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this
就是这个记录的一个属性。
首先清楚一个概念
调用位置
:调用位置就是函数在代码中被调用的位置(而不是声明位置)。
例如
function foo(){ // 声明位置
console.log("foo");
}
foo(); // 调用位置
某些编程模式会隐藏真正的调用位置。最重要的是要分析调用栈(就是为了达到当前执行位置所调用的所有函数。也称为环境栈)。
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()
,所以那个位置就是 baz()
的调用位置,将baz()
添加到调用栈里。然后 baz()
里面又调用了 bar()
,所以 bar()
的调用位置是在这,同样将 bar()
加入到调用栈里面。接着,bar()
里面又调用了 foo()
,所以 foo()
的调用位置是在 bar()
里面,将 foo()
加入到调用栈中。
明确调用位置对我们分析 this
的指向有很大的帮助。
默认绑定即独立的函数调用,当其他规则无法应用时的默认规则,如:
function foo(){
console.log(this.a);
}
var a = 2 ;
foo(); // 2
调用 foo()
的时候其实相当于 window.foo()
,所以 this.a
其实指向的是 window.a
思考如下代码
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
obj.foo(); // 2
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文的对象。因为调用 foo()
时 this
被绑定到 obj
,因此 this.a
和 obj.a
是一样的
对象属性引用链中只有上一层或者说最后一层在调用位置起作用(这里是由于作用域链对于 this 的寻找只会到当前的活动对象或变量对象中,不会到上一层)。如
function foo(){
console.log( this.a );
}
var obj1 = {
a: 2,
obj2: obj2
};
var obj2 = {
a: 42,
foo: foo
};
obj1.obj2.foo(); // 42
即使用 apply()
和 call()
方法。它们的第一个参数是一个对象,在调用函数时将其绑定到 this
。他们的主要区别就是第二个参数。
看个例子
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
。这种绑定是一种强制绑定,也成为硬绑定。
由于硬绑定是一种非常常用的模式,所以 ES5
提供了 bind()
函数。用法如下
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
使用 new 调用函数只是对函数的 " 构造调用 ",所有的函数都可以使用 new 来调用。
使用 new 来调用函数时,会自动执行如下操作
[[Prototype]]
连接this
用代码表示就是如下步骤
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
// 其中 new foo(2) 进行的是类似如下的操作
{
var obj = new Object();
obj.__proto__ = foo.prototype;
var result = foo.call(obj,"2");
return result === 'object' ? result : obj
}
使用 new 来调用 foo( … ) 时,我们会构造一个新对象并把它绑定到 foo( … ) 调用中的 this
毫无疑问,默认绑定优先级最低
接下来,我们看看其他绑定的优先级
function foo(a){
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
// 显然,显式绑定优先级更高
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 绑定比隐式绑定优先级高
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
var bar = new foo()
var bar = foo.call()
var bar = obj1.foo()
var bar = foo()
箭头函数不适用 this
的四种标准规则,而是在 this 定义的时候保存当前的作用域链,然后顺着当前的作用域链寻找 this,并且只会在作用域链最前端的活动对象或变量对象中寻找(有不理解的可以参考 浅析 javascript 中执行环境,变量对象及作用域链)。简单来说就是箭头函数的 this 是在定义的时候就与对象绑定了,而不是在调用的时候根据调用位置决定。
来看下面这个例子
function foo(){
// 返回一个箭头函数
return (a)=>{
// this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );// 调用位置
bar.call( obj2 ); // 2,不是 3!
foo()
内部创建的箭头函数会捕获调用 foo()
时的 this
。由于 foo()
的 this
绑定到 obj1
,bar
的 this
也会绑定到 obj1
,箭头函数的绑定无法修改。(new 绑定也不行!)
ES6 标准入门里面对箭头函数 this 的指向有如下说法:
函数体内的 this 对象就是定义时所在的对象,而不是调用时所在的对象。
箭头函数的 this 绑定看的是 this 所在的函数定义在哪个对象下,绑定到哪个对象则 this 就指向哪个对象
一般情况下 this 的绑定是默认绑定,如果有 new 绑定则 new 绑定优先级最高,其次是显式绑定,然后再是隐式绑定。如果有对象嵌套的情况,则 this 绑定到最近的一层对象上