this的绑定规则有四条,按优先级排列为:默认绑定、隐式绑定、显式绑定、new绑定
一. 调用位置
如想明白this,一定要清楚
调用位置是什么
调用位置:函数在代码中被调用的位置(而不是声明的位置)
调用栈:在当前正在执行的函数的前一个调用中
function baz() {
// 当前调用栈是 baz
// 当前调用位置是全局作用域
bar() // <-- bar的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 当前调用位置在 baz 中
foo() // <-- foo的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 当前调用位置是在 bar 中
}
baz() // <-- baz的调用位置
二、绑定规则
1. 默认绑定
在非严格模式下,默认绑定才能绑定到全局对象,严格模式下为undefined
2. 隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的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 // 函数别名(只是一个foo函数的引用地址)
var a = "oops global" // a 是全局对象的属性
bar(); // oops global,调用位置为全局
一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:
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,调用位置为全局
setTimeout(obj.foo, 100) // oops global
/* function setTimeout(fn, delay) {
fn(); // 调用位置,被设置为默认绑定 全局
} */
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
3. 显式绑定
a. 硬绑定
```javascript
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
```
```javascript
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
}
/* var bar = function() {
return foo.apply(obj, arguments);
} */
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
}
}
var bar = bind(foo, obj)
var b = bar(3)
console.log(b) // 5
```
b. API调用的"上下文"
第三方库的许多函数,以及JS语言和宿主环境中许多新的内置函数,都提供了以一个可选的参数,通常被称为"上下文"(context),其作用和bind(...)一样,确保你的回调函数使用指定的this
```javascript
function foo(el) {
console.log(el, this.id)
}
var obj = {
id: "awesome"
}
// 调用foo(...)时把this绑定到obj
[1, 2, 3].forEach(foo, obj) // 1 awesome 2 awesome 3 awesome
```
4. new绑定
使用new来调用函数,或发生构造函数调用时,会自动执行下面的操作
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[原型]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function newMethos(Func, ...args) {
// 创建一个新对象
const obj = {};
if (Func.prototype) {
// 将新创建对象的__proto__指向Func.prototype
Object.setPrototypeOf(Func.prototype)
}
const res = Func.apply(obj, args)
if (typeof res === "function" || (typeof res === "object" && res !== null)) {
return res;
}
return obj;
}
使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。new时最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定
小结
如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象,按优先级排序
- 由
new调用?绑定到新创建的对象 - 由
call或apply(或者bind) 调用(硬绑定)?绑定到指定的对象 - 由
上下文对象调用?绑定到那个上下文对象 默认:在严格模式下绑定到undefined,否则绑定到全局对象如果想更安全地忽略this绑定,你可以使用一个DMZ对象,比如ø = Object.create(null),以保护全局对象
ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前代码中的 self = this 机制一样