彻底理清 this 去哪儿
ES6 中引入的箭头函数和 Class 让 this 更加令人头疼了,通过这遍博客完整的好好的理一理他的指向。
this 是当前执行上下文的一个属性,在非严格模式下总是指向一个对象,在严格模式下可以是任意值。在开发的时候只会出现在两个地方,代码顶级(全局)和函数內。
本文主要以执行上下文来分类说明。
全局上下文
在全局环境中,this 始终指向全局对象 global,在浏览器里的全局对象就是 window。
this === window; // true
this === globalThis; // true
函数上下文
在函数内部(箭头函数除外),this 的值取决于函数被调用的方式。
一般有七种方式来调用函数:
- 作为函数
- 作为方法
- 作为构造函数
- 通过 call() 和 apply() 方法间接调用
- 作为事件处理函数
- 原生 API 的回调
下面分别进行说明。
函数调用
非严格模式时 this 指向 window,严格模式下为 undefined
function getThis() {
return this === window;
}
function getThisInStrictMode() {
"use strict";
return this === undefined;
}
getThis(); // true
getThisInStrictMode(); // true
方法调用
简单来说就是谁调用,this 就指向谁
普通定义方法的示例
function getName() {
return this.name;
}
let student = {
name: "XDX",
getName: getName,
};
let student2 = {
name: "小明",
getName: getName,
};
student.getName(); // XDX
student2.getName(); // 小明
getter 与 setter 中的 this 也是相同的表现
MDN 的示例:
function sum() {
return this.a + this.b + this.c;
}
var o = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3;
},
};
Object.defineProperty(o, "sum", {
get: sum,
enumerable: true,
configurable: true,
});
console.log(o.average, o.sum); // 2, 6
构造函数调用
首先我们简单复习一下 new 操作符做了什么
- 新建一个空对象
- 新对象继承构造函数的 prototype 属性
- 将 this 指向新对象并执行构造函数的内容
- 返回 this(若函数没有指定返回值或者返回一个原始值)
首先是一个正常返回 this 的构造函数的示例
function Factory() {
// 等同于 return this
}
Factory.prototype.name = "XDX";
Factory.prototype.callName = function () {
return this.name;
};
let ins = new Factory();
ins.name; // XDX
ins.callName(); // XDX 符合方法调用
let callCache = ins.callName;
callCache(); // undefined 符合函数调用
返回一个对象,相当于构造函数啥也没做,就是返回这个新对象
function FactoryWithReturn() {
return { name: "小明" };
}
FactoryWithReturn.prototype.name = "XDX";
let ins2 = new FactoryWithReturn();
ins2.name; // 小明
默认情况下,构造函数默认尝试初始化新创建的对象,即使看起来像一个方法的调用
let fake = {
name: "fake",
create: Factory,
};
let ins3 = new fake.create(); // 尽管调用上下文看似是fake,但其实是新建的那个对象
ins3.name; // XDX
间接调用
JS 中可以使用函数对象的 call 和 apply 显式的指定调用所需的 this 值,也就是说任何函数都可以作为任何对象的方法来调用。
在严格模式中,第一个实参会被作为 this 的值(null、undefined 和原始值也不例外)。而在非严格模式下,null 和 undefined 会被 window 代替,原始值会被响应的包装对象替代。
两个方法只有传参数的区别,这里以 call 为例说明 this 的指向。
function getName() {
return this.name;
}
let student1 = {
name: "小明",
};
let student2 = {
name: "小红",
};
getName.call(student1); // 小明
getName.call(student2); // 小红
事件处理函数调用
dom 事件触发时,执行的回调的 this
DOM 事件处理函数
this 指向触发此事件的 DOM 对象。针对 addEventListener 和 onevent 类。
内联事件处理函数
- 函数语句直接运行时,this 指向当前 DOM 元素
- 函数语句被包裹在函数里运行时与函数直接运行结果相同。没有设置内部函数的 this,所以它指向 global/window 对象(严格模式下为 undefined)
原生 API 的回调
大部分原生 API 的回调函数 this 指向 global/window(严格模式下也是)。可用 bind 等改变 this。
例如 setTimeout、setInterval 和 requestAnimationFrame
类上下文
this 在类(Class)中的表现与在函数中类似。Class 中所有非静态的方法都会被添加到 this 的原型中。
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
method() {}
static staticMethod() {}
}
new Example(); // ['constructor', 'method']
子类/派生类不会自动创建 this,除非返回的是一个对象或者没有构造函数,否则必须调用 super()生成 this。在调用 super() 之前引用 this 会抛出错误。
class Base {}
class Good extends Base {}
class AlsoGood extends Base {
constructor() {
return {};
}
}
class Bad extends Base {
constructor() {}
}
new Good();
new AlsoGood();
new Bad(); // ReferenceError
类的方法默认为严格模式。根据这一点,下面示例说明上面的函数调用的 this 结论依旧正确
// 类
class Person {
showThis() {
// 'use strict' 默认此处是严格模式
return this;
}
}
const showThis = new Person().showThis;
showThis(); // undefined
// 构造函数
function PersonFactory() {}
PersonFactory.prototype.showThis = function () {
return this;
};
const showThisFactory = new PersonFactory().showThis;
showThisFactory(); // Window
使用 bind 指定 this 的指向
this 不能在运行时被直接赋值,简单来说只要你将 this 放到赋值号的左边就会立即报错。在 ES5 中函数对象新增了 bind 方法实现这一功能(Function.prototype.bind())。
当函数 fun 调用 bind 方法时,需要传入一个对象 o,该返回一个新的函数 funNew。funNew 被调用时,实际上是在执行 fun,且此时 fun 中的 this 会指向 o。
bind 只生效一次。
function getName() {
return this.name;
}
let student = {
name: "XDX",
getName: function () {
return this.name;
},
};
let getXDXName = getName.bind(student);
getXDXName(); // XDX
// 等同于
student.getName(); // XDX
// 将getXDXName的调用其理解为student.getName()时,表现与前面讨论相同
let student2 = {
name: "小明",
getName: getXDXName,
};
getXDXName = getXDXName.bind(student2); // bind 只生效一次
student2.getName(); // XDX
getXDXName.call(student2); // XDX
bind 方法不仅将函数绑定至第一个实参,还会将其他实惨绑定到原始函数的形参上,利用该特性可以实现一种常见的函数式编程技术,函数柯里化。此处暂不展开。
箭头函数
箭头函数单独拎出来讲一下。箭头函数没有自己的 this,argument,不能用作构造函数。
箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this(在 js 中,只有函数作用域)。
箭头函数的 this 在创建时确定,bind、call 和 apply 都无法改变箭头函数的 this 的指向。
// 注意,在bar执行的时候,箭头函数 x 才创建,此时箭头函数会继承其上一级作用域 bar 方法的this
var obj = {
bar: function () {
var x = () => this;
return x;
},
};
var fn = obj.bar(); // 此时创建了箭头函数,其 this 继承了 bar 的 this ,即 obj
console.log(fn() === obj); // true,不是 Window或者undefined(严格模式)
// 另一种情况
var fn2 = obj.bar; // 此时 bar 还未执行,箭头函数未创建
var fn3 = fn2(); // 此时 bar 执行了,this 为 window
console.log(fn3() == window); // true
构造函数中的 this 也是相同的道理,不再赘述。
总结
确定一个函数的 this 到底指向哪,可以通过思考以下几个问题得到答案:
- 是否为箭头函数
- 是否调用了 bind
- 执行的上下文是什么
- 是在该上下文中的哪种情况
本文参考MDN this 说明和《JavaScript 权威指南》完成~