一:什么是this
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包
含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的
其中一个属性,会在函数执行的过程中用到
二:this指向的2种误区
1:指向自身
例如:我们想要记录一下函数 foo 被调用的次数,思考以下代码
console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然 是 0。显然从字面意思来理解 this 是错误的。
执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码
this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相
同。
解决:强制this指向foo对象
2:它的作用域
第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它 是正确的,但是在其他情况下它却是错误的。
需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用 域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。
这段代码中的错误不止一个。虽然这段代码看起来好像是我们故意写出来的例子,但是实 际上它出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感) 地展示了 this 多么容易误导人。
首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之
后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。
此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让
bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一
个词法作用域内部的东西.
每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。
三:判断this指向的几种绑定规则
首先要明确this的调用位置
最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的
调用位置就在当前正在执行的函数的前一个调用中。
下面我们来看看到底什么是调用栈和调用位置:
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 的调用位置
1:默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用 其他规则时的默认规则。
思考一下下面的代码:
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用
默认绑定,无法应用其他规则,因此 this 指向全局对象。
注:如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined
2:隐式绑定
隐式绑定主要是考虑 调用位置是否有上下文对象,或者说是否被某个对象拥有或者包 含,不过这种说法可能会造成一些误导。
思考下面的代码:
function foo() {
console.log( this.a );
}
var obj = { a: 2, foo: foo };
obj.foo(); // 2
当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引
用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调
用 foo() 时 this 被绑定到 obj,因此 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; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
虽然 bar 是 obj.foo 的一个引用,,但是实际上,它引用的是 foo 函数本身,因此此时的
bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置! }
var obj = { a: 2, foo: foo };
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。
如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一 样的,没有区别:
function foo() {
console.log( this.a );
}
var obj = { a: 2, foo: foo };
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}
3:显式绑定
使用call() apply() bind()强行改变this的指向
4:new绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作
1. 创建(或者说构造)一个全新的对象。
2. 这个新对象会被执行 [[ 原型 ]] 连接。
3. 这个新对象会绑定到函数调用的 this。
4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
思考下面的代码:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
总结:
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。
可以按照下面的 顺序来进行判断:
1. 函数是否在 new 中调用(new 绑定)?
如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?
如果是的话,this 绑定的是
指定的对象。
var bar = foo.call(obj2)
3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
var bar = obj1.foo()
4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。
不过……凡事总有例外
例外:
1:被忽略的this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
2:间接引用
另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则。 间接引用最容易在赋值时发生:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是
p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。
3:箭头函数
我们之前介绍的四条规则已经可以包含所有正常的函数。
但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决
定 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 也不
行!)
摘自 : 你不知道的JavaScript(上卷)