关于 this 的错误认识
this 指向函数自身
存在这个误解可能是因为在 JavaScript 中既然函数是一个对象,那么就可以在这个函数对象上通过 key/value 的形式存储某些值,比如下面这个例子:
function addCount() { this.count++; console.log('count result:', this.count) console.log('addCount.count:', addCount.count) } addCount.count = 0; for (let i = 0; i < 3; i++) { console.log(`=============== ${i+1} ===============`) addCount(i) } // 以下是输出结果: =============== 1 =============== count result: NaN addCount.count: 0 =============== 2 =============== count result: NaN addCount.count: 0 =============== 3 =============== count result: NaN addCount.count: 0 复制代码
当执行 addCount.count = 0
时,确实向函数对象 addCount
添加了一个 count
属性,然而函数内部代码 this.count
中的 this
并不是指向 addCount
函数对象自身,这一点可以通过输出结果来验证。
this 指向 函数的作用域
关于作用域的问题,在某种情况下它是正确的,但是在其他情况下它却是错误的,因此不能说 this
就是指向函数的作用域,需要明确的是,this
在任何情况下都不指向函数的 词法作用域.
下面是个比较典型的例子:
function foo() { var a = 2; this.bar(); } function bar() { console.log( this.a ); } foo(); // ReferenceError: a is not defined 复制代码
这段代码要实现的内容:
首先,函数 foo 中想要通过 this.bar 调用 bar 函数;
- 正常的做法是直接通过 bar() 调用,而不是使用 this.bar()
其次,函数 bar 中想要通过 this.a 访问函数 foo 中的变量 a;
- 不能通过 this 来引用一个词法作用域内部的东西
this 是什么
当一个函数被调用时,会创建一个活动记录(也称为执行上下文),这个记录会包含函数调用的位置(调用栈)、函数的调用方法、传入的参数等信息,而 this
就是记录的其中一个属性,会在函数执行的过程中用到.
简单总结
- 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 的调用位置 复制代码
this 绑定规则
如果想要确定 this
的绑定对象,首先得确定函数的调用位置,然后判断需要符合下面四条规则的哪一条即可.
默认绑定
函数被独立调用 时,this
指向 全局对象 或 undefined,当无法应用其他规则时可将此规则作为默认规则.
正常情况下:this —> 全局对象
function foo() { console.log( this.a ); } var a = 2; foo(); // 2 复制代码
严格模式下:this —> undefined
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // TypeError: this is undefined 复制代码
注意:this
的绑定规则完全取决于 调用位置,但要根据模式而定
- 只有
foo()
运行在非strict mode
下时,默认绑定才能绑定到全局对象 - 严格模式下 与
foo()
的 调用位置 无关.
function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); // 2 })(); 复制代码
隐式绑定
调用位置是否有 上下文对象 ,或者说是否被某个对象拥有或者包含.
即当函数引用有 上下文对象 时,隐式绑定规则会把函数调用中的 this
绑定到这个上下文对象.
下面的例子中,foo 中的 this 就被隐式绑定到 obj 上:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2 复制代码
对象属性引用链中只有 最顶层 或 最后一层 会影响调用位置,例如:
function foo() { console.log(this.a); } var obj2 = { a: 2, foo: foo }; var obj1 = { a: 1, obj2: obj2 }; obj1.obj2.foo(); // 2 复制代码
隐式绑定丢失
一个最常见的 this
绑定问题就是被隐式绑定的函数会丢失绑定对象,即此时会应用 默认绑定,从而把 this
绑定到 全局对象 或 undefined 上,取决于是否是严格模式.
函数引用传递
function foo() { console.log(this.a); } var obj = { a: 'a in obj', foo: foo }; var bar = obj.foo; // 将函数引用赋值给 bar 变量 var a = "a in global"; bar(); // "a in global" 复制代码
虽然 bar
是 obj.foo
的一个引用,但是实际上它引用的是 foo
函数本身,因此 bar()
等价于 foo()
,而不等于 obj.foo()
的调用.
JS 内置函数的回调
把定义的函数作为参数传入 JS
内置函数(如 setTimeout
)或自己声明的函数,本质上其实也相当于函数引用的传递,即便函数最终调用位置不同,但是都是相当于直接调用函数本身,即应用默认绑定规则.
function foo() { console.log(this.a); } var obj = { a: 'a in obj', foo: foo }; var a = "a in global"; setTimeout(obj.foo, 0);// a in global 复制代码
显式绑定
如果需要显式绑定 this
,可以使用函数原型上的 bind()
、call()
、apply()
三个方法来实现.
如下的例子,分别通过三种显式绑定的方式进行演示:
function foo() { console.log(this.a); } var obj1 = { a: 1 }; var obj2 = { a: 2 }; var obj3 = { a: 3 }; foo.bind(obj1)();// 1 foo.call(obj2);// 2 foo.apply(obj3);// 3 复制代码
注意:
bind、call、apply
这三个方法,第一个参数是需要传入需要绑定的对象,如果传入的是一个原始值,那么这些原始值会默认被 " 装箱 ",即被转化为(new String(..)
、new Boolean(..)
或 new Number(..)
).
new 绑定
在 JavaScript
中,构造函数只是一些使用 new
操作符时被调用的函数,实际上并不存在所谓的 "构造函数",只有对于函数的 "构造调用".
使用 new 来调用函数(或 函数的构造调用),会自动执行下面的操作:
- 创建一个 全新的对象
- 新对象会被执行 [[ 原型 ]] 连接
- 新对象会绑定到函数调用的 this
- 若函数没有返回其他对象,则 new 表达式中的函数调用会自动返回这个新对象
function foo(a) { this.a = a; } var bar = new foo(1); console.log(bar.a); // 1 复制代码
this 绑定规则的优先级
上面介绍了 this 的 4 种绑定规则,如果一个调用位置符合多种规则,那么只能通过优先级来区分它们,优先级顺序如下:
- new 绑定
>
显式绑定>
隐式绑定>
默认绑定
下面这个例子中,证明了 显式绑定>
隐式绑定>
默认绑定 的优先级.
function foo() { console.log(this.a); } var a = 1; var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; // 默认绑定 foo(); // 1 // 隐式绑定 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 = { a: 1, foo: foo }; var obj2 = { a: 2 }; // 隐式绑定 obj1.foo(2); // obj1.a = 2 console.log(obj1.a); // 2 // 显式绑定 obj1.foo.call( obj2, 3 ); // obj2.a =3 console.log(obj2.a); // 3 // new 绑定 var bar = new obj1.foo(4); // bar.a = 4 console.log(obj1.a); // 2 console.log(bar.a); // 4 复制代码
this 绑定的例外
this 绑定被忽略
当使用显示绑定(bind、call、apply
)进行 this
绑定时,如果给第一个参数传递了 null
或 undefined
,那么它们在调用时会被忽略,实际应用的是 默认绑定规则.
function foo() { console.log(this.a); } var a = 1; foo.call(null); // 1 复制代码
函数被间接引用
值得注意的是,有时可能有意无意的会创建一个函数的 "间接引用",在这种情况下,调用这个函数时会应用 默认绑定规则,具体绑定值还要看是否处于 严格模式.
function foo() { console.log(this.a); } var a = 0; var obj1 = { a: 1, foo: foo }; var obj2 = { a: 2 }; obj1.foo(); // 1 (obj2.foo = obj1.foo)(); // 0 复制代码
赋值表达式 obj2.foo = obj1.foo
的返回值是 目标函数的引用,因此调用位置是 foo()
,而不是 obj2.foo()
或者 obj1.foo()
.
ES6 中的箭头函数
箭头函数并不是使用 function 关键字定义的,而是使用被称为 "胖箭头" (=>)的操作符进行定义的.
- 它不使用 this 的 4 种绑定规则,而是根据外层(函数或全局)作用域来决定 this 的指向.
- 它不能通过 new 操作,因为箭头函数没有自己的
this
. - 它没有普通函数的 arguments 实参列表.
function foo() { // 返回一个箭头函数 return (a) => { // this 继承自 foo() console.log(this.a); }; } var obj1 = { a: 1 }; var obj2 = { a: 2 }; var bar = foo.call(obj1); bar.call(obj2); // 1