1 我们印象中的this
this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(我们在下面慢慢分析)
2 this到底是什么
之前我们说过 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。
3 分析this的调用位置
在理解 this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的 位置(而不是声明的位置)。
最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的 调用位置就在当前正在执行的函数的前一个调用中。
例子1:
function fn () {
console.log(this.name)//此时this指向obj
}
var obj={
name:'llj',
fn:fn
}
//我们关心的 调用位置就在当前正在执行的函数的前一个调用中。fn函数的调用前一个指的是obj
window.obj.fn()
例子2:
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log("baz");
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log("bar");
foo(); // <-- foo 的调用位置
}
function foo() {
debugger
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log("foo");
}
baz(); // <-- baz 的调用位置
- 你可以把调用栈想象成一个函数调用链,就像我们在前面代码段的注释中所 写的一样。但是这种方法非常麻烦并且容易出错
- 另一个查看调用栈的方法 是使用浏览器的调试工具。就本例来说,你可以在工具中给 foo() 函数的 第一行代码设置一个断点,或者直接在第一行代码之前插入一条 debugger; 语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数 调用列表,这就是你的调用栈。因此,如果你想要分析 this 的绑定,使用开 发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。
4 绑定规则
4.1 绑定到全局对象window(默认绑定)
例子1:
function foo() {
console.log(this.a);//this指向window
}
var a = 2;
foo(); // 2
在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。
例子2:
function foo() {
"use strict";
console.log(this.a);//this指向undefined
}
var a = 2;
foo(); // TypeError: this is undefined
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined
4.2 隐式绑定
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调 用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
4.3 显示绑定
JavaScript 提供的绝大多数函数以及你自 己创建的所有函数都可以使用 call(..) 和 apply(..) 方法来改变this指向,关于call和apply和bind的介绍可以看阮一峰老师文章
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
4.4 new绑定(构造函数)
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
例子
/* 根据上面说的第4小点,有下面几种情况 */
function foo(a) {
this.a = a;
//情况一:返回空对象时,this指向该空对象,该对象没有a属性,所以的到undefined
// return {}
//情况二:this指向实例对象bar,与默认情况一致
return null
}
var bar = new foo(2);
console.log(bar.a); // 2;默认情况绑定到bar对象
简单总结new:
- 当构造函数中没有return时, this指向新建的对象并返回新建的对象
- 当构造函数中return null时, this指向新建的对象并返回新建的对象
- 当构造函数中return 对象时, this指向新建的对象,但返回return 后的对象
4.5 箭头函数中的this
函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。箭头函数根本没有自己的this,导致内部的this就是外层代码块的this
function test(){
let x = 1;
console.log(this.x); //2
return ()=>{
console.log(this.x); //2 箭头函数将this指向当前环境上下文,即this指向test中的this,即obj
}
}
let obj = {
x:2,
fn:test,
fn1:()=>{
console.log(this.x); //undefined 箭头函数将this指向当前环境上下文,即this指向全局环境中的this,即window
}
};
var fn=obj.fn();
fn();
obj.fn1();
不适用场合
例子1.
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。
例子二.
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。
5 被忽略的this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
那么什么情况下你会传入 null 呢? 一种非常常见的做法是使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。 类似地,bind(..) 可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:
function foo(a,b) { console.log( "a:" + a + ", b:" + b ); }
// 把数组“展开”成参数 foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化 var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3
这两种方法都需要传入一个参数当作 this 的绑定对象。如果函数并不关心 this 的话,你 仍然需要传入一个占位值,这时 null 可能是一个不错的选择,就像代码所示的那样。
然而: 总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了 this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览 器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。 显而易见,这种方式可能会导致许多难以分析和追踪的 bug。
更安全的this
一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序 产生任何副作用。由于这个对象完全是一个空对象,我自己喜欢用变量名 ø(这是数学中表示空集合符号的 小写形式)来表示它。在 JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)。Object.create(null) 和 {} 很像,但是并不会创建 Object. prototype 这个委托,所以它比 {}“更空”:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象 var ø = Object.create( null );
// 把数组展开成参数 foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化 var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
使用变量名 ø 不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为 ø 表示 “我希望 this 是空”,这比 null 的含义更清楚。不过再说一遍,你可以用任何喜欢的名字 来命名 DMZ 对象。
6 总结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样
声明:以上多数内容引用来自书籍"你不知道的JavaScript".
本文使用 mdnice 排版