this对于很多前端新人和一些经验丰富的人来说,都很容易产生困惑。很多时候由于没有指向预期上下文导致的。
这篇文章将深入了解javascript this机制。了解this更改指向的所有方式,从为什么许多库使用 "use strict" 到箭头函数如何获取其外部执行环境的this。
看完这篇文章,你会学习到
- javascript 如何隐式设置this上下文,以及我们如何设置它
- 函数调用位置决定该函数的 this 值
- 如何调用具有this值的函数
- 如何创建始终与特定this上下文相关联的函数
- 箭头函数和类如何处理this
看完这篇文章,希望你能更了解this,日常使用的时候不再那么畏手畏脚。
全局上下文中的this
与其他语言相比,javaScript this机制表现有些不同,它并不是总是直观的,让我们看看在全局执行环境如何工作的。在任何函数之外使用this,它的指向都是全局对象。
在浏览器中 this === window 返回是true,严格模式下表现相同;
在Node的命令行中 this === global 也是true,但是它仅在node RELP中生效,因为在node文件执行时this的指向是 module.exports ,具体原因可以参考 Node.js 启动方式:一道关于全局变量的题目引发的思考
console.log(this === module.exports) // true
函数调用中的this
大多数情况下,函数中 this值是由所调用的函数决定的。这意味着,每次我们执行函数时,this的值都有可能不同。看一个简单的例子
function func(){
console.log(this===global); //true
}
func();
如果不在严格模式下运行,普通函数调用将会将this值设置为global。
'use strict'
function func(){
console.log(this===undefined); //true
}
func();
function func(){
'use strict'
console.log(this===undefined); //true
}
func();
如果处于严格模式下 this 会被设置为undefined。文件是否处于严格模式不重要,主要取决于函数是否处于严格模式。
如果你的函数在严格模式下编写的,而一些使用的第三方库却不是,那这种区别就会对你产生影响。接下来看看,为什么严格模式下 this === undefined 是有意义的。
看下面这段代码
function Person(firstName,lastName){
this.firstName=firstName;
this.lastName=lastName;
}
const person = Person("Jane","Doe");
console.log(person) // undefined
console.log(window.firstName) // Jane
console.log(window.lastName) // Doe
注意,这段代码并不是在严格模式下运行的,如果在严格模式下运行,会直接报错,因为我们无法将属性设置给undefined,这样可以防止我们意外创建全局变量。
大写的函数名对于我们来说是个提示,所以我们应该把它当作构造函数来调用,使用new来处理。现在就正常了,也不会污染全局变量。
'use strict'
function Person(firstName,lastName){
this.firstName=firstName;
this.lastName=lastName;
}
const person = new Person("Jane","Doe");
console.log(person) // {firstName:'Jane',lastName:'Doe'}
console.log(window.firstName) // undefined
console.log(window.lastName) // undefined
在构造函数中调用 this
在javaScript中,前面带有new操作符的函数调用会变成一个构造函数调用,当一个函数作为构造函数调用时,会为我们创建一个全新的对象,然后这个新对象会作为this的上下文返回。
new
关键字会进行如下的操作:
-
创建一个空的简单 JavaScript 对象(即**{}**);
-
为步骤 1 新创建的对象添加属性**__proto__**,将该属性链接至构造函数的原型对象;
-
将步骤 1 新创建的对象作为this的上下文;
-
如果该函数没有返回对象,则返回this。
在我们的例子中,
function Person(firstName, lastName) {
console.log(this);
this.firstName = firstName;
console.log(this);
this.lastName = lastName;
console.log(this);
// return this;
}
const person = new Person("Jane", "Doe");
我们可以看到这人对象是如何一步步被组合起来的,在控制台也能看到构造函数的名称。这表明新对象已经被链接到构造函数的原型。注意,我们的构造函数并不包含 return 语句,根据上面的new 我们知道,我们的新对象会自动返回。当然你也可以在构造函数中返回其他对象,不常用,但也是有意义的,例如 构造单例模式时候,或者在开发环境中,我们可以将返回的对象包裹在一个代理中,在开发人员错误使用该对象时发出警告。
// 将变量直接挂在构造函数上面,最终将其返回
function Singleton(name) {
if(typeof Singleton.instance === 'object') {
return Singleton.instance
}
// 正常创建实例
this.name = name
return Singleton.instance = this
}
const a = new Singleton('Jane')
const b = new Singleton('Wood')
console.log(a===b) //true Singleton {name: 'Jane'}
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
注意⚠️,如果我们试图在构造函数中返回除对象以外的任何东西,javaScript引擎会简单忽略我们提供的值,而是返回新对象,还是参考 new 流程。
方法调用中的this
当一个函数作为对象的方法被调用时,该函数的this设置为该方法所调用的对象。这里,我们在调用person.sayHi(),因此 sayHi方法中的this 指的是person
const person = {
firstName: "John",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
};
person.sayHi(); //Hi, my name is John!
person就是方法调用的接收器,这个接收器不受函数定义地点的影响,例如我们可以单独定义这个函数,并在之后将其附加到person。我们仍然可以写person.sayHi(),因为person仍然是该方法的接收者。
function sayHi(){
console.log(`Hi, my name is ${this.firstName}!`);
}
const person = {firstName:"John"};
person.sayHi = sayHi;
person.sayHi(); //Hi, my name is John!
有时候,调用位置是一个属性链,看起来像 foo.bar.say()。在这种情况下,接收器是方法之前最直接的属性,也就是下方的father,开头的foo.bar并不影响我们的绑定。
const person = {
firstName: "John",
father:{
firstName:"James",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
}
};
person.father.sayHi();
注意⚠️,一个最常见的错误使用是当一个方法丢失其接收器时。再看下最初的例子,如果我们将sayHi方法存储到一个变量中,然后将这个变量作为函数调用,我们预期的接收器就会消失。这个时候 this 将引用全局对象,而不是person。这是因为调用时我们只是作为普通函数在调用,而且还不是在严格模式下。
const person = {
firstName: "John",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
};
const greet=person.sayHi;
greet()//Hi, my name is undefined!
常见的这种丢失接收器还有我们将一个方法作为回调传给另一个参数时,例如setTimeout
const person = {
firstName: "John",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
};
setTimeout(person.sayHi,1000); //Hi, my name is undefined!
setTimeout将调用我们的函数,并将this设置为全局对象,这肯定不是我们想要的,一般解决方案时添加一个封装函数,这样一来,person.sayHi仍然作为一个方法被调用,而且不会丢失预期接收者。另一个方案是使用bind方法,它允许我们将this与一个特定对象绑定,具体我们之后再详细讨论。
const person = {
firstName: "John",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
};
setTimeout(function (){
person.sayHi()
},1000); //Hi, my name is John!
// setTimeout(person.sayHi.bind(person),1000);
使用 .call() 或 .apply()来指定this
之前的sayHi函数我们怎样让person作为接收者,但是又不想将sayHi定义到person上呢?我们可以使用call方法来做到这一点,call 方法是定义在函数原型上的,因此每个函数都可以使用。
function sayHi(){
console.log(`Hi, my name is ${this.firstName}!`);
}
const person = {firstName:"John"};
sayHi.call(person) //Hi, my name is John!
正如我们看到这样,我们将person作为call的第一个参数穿进去,这个参数通常被称为this参数,另外我们也可以通过apply方法来实现,apply方法也定义在函数原型上。两者不同之处在于参数传递方式不同。除了this参数外,我们还需指定传递给函数的参数,通常我们调用一个方法时,我们会使用object.method(arg1,arg2)。
apply(this,[args,arg2])的第二个参数为数组或类数组。
call()是apply()的一颗语法糖,作用和apply()一样,同样可实现继承,唯一的区别就在于call()接收的是参数列表,而apply()则接收参数数组。
一个简单的记忆点,apply的参数是 类Array 都是A开头的,call不是。
注意⚠️,在严格模式之外使用call或者apply ,如果将this参数设置为null 或者undefined,javaScript引擎会将其忽略并使用全局对象代替。所以建议在严格模式下编写所有代码。可以看一下下面代码是不是符合你的预期。
"use strict";
function fun() { return this; }
console.log(fun() === undefined);
console.log(fun.call(2) === 2);
console.log(fun.apply(null) === null);
console.log(fun.call(undefined) === undefined);
console.log(fun.bind(true)() === true);
使用.bind()方法来硬绑定函数this值
当我们试图将一个方法作为回调传递给另一个参数时,通常会失去该方法的预期接收者。例如之前setTimeout将this设置为全局对象,这不是我们想要的。
我们可以通过 bind 方法来解决这个问题,bind 将创建一个新的sayHi函数,并将this值永远设置成了person。这种机制也被称为硬绑定(显示绑定)
const person = {
firstName: "John",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
};
setTimeout(person.sayHi.bind(person),1000);
即使我们将其提取到一个变量中,this参数依然和person相关联,一旦函数绑定,它的this值就不能再被改变,call 和 apply 也不行
const person = {
firstName: "John",
sayHi() {
console.log(`Hi, my name is ${this.firstName}!`);
}
};
const greet=person.sayHi.bind(person);
greet() //Hi, my name is John!
const otherPerson = {
firstName:"James"
}
greet.call(otherPerson) //Hi, my name is John!
greet.apply(otherPerson) //Hi, my name is John!
接下来我们自己来实现一个bind函数,以更好的理解它是如何工作的。
1、bind被定义在函数原型上,接收一个this参数
2、返回一个函数
Function.prototype.customBind = function(thisArg){
return function(){
}
}
使用func 存储对原始函数的引用。
然后在返回函数内部使用apply来调用,
处理使用者传递过来的原始参数
Function.prototype.customBind = function(thisArg){
const func = this;
return function(...args){
return func.apply(thisArg,agrs)
}
}
function.bind(thisArg[, arg1[, arg2[, ...]]])
arg1, arg2, ...
当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
Function.prototype.customBind = function(thisArg,...fixedArgs){
const func = this;
return function(...args){
return func.apply(thisArg,[...fixedArgs,...args]);
}
}
这只是一个简单的实现,并不规范。
使用箭头函数捕获this
箭头函数不仅在语法上与其他javascript函数不同,而且还有特殊的this行为。箭头函数没有自己的this,相反,它使用其封闭执行上下文中的this。
无非明确设置箭头函数的this绑定,如果尝试使用call、apply或者bind,都会被忽略。换句话说,我们如何调用箭头函数并不重要,它的this始终指向创建箭头函数所指向的this值。
我们也不能使用箭头函数作为构造函数,因为构造函数,我们通常会将属性分配给this。给箭头函数包围执行上下文this 分配属性没有意义。
const outerThis = this;
const func = () => {
console.log(this === outerThis);
};
func(); //true
func.call(null);//true
func.apply(undefined);//true
func.bind({})();//true
这样一来,当我们想要在回调中访问this时,箭头函数的这种透明 this 绑定特别有用,考虑这个Counter对象,假设我们希望每一秒count属性递增1,可以使用setInterval并提供一个回调,使this.count增加
const Counter={
count:0,
incrementPeriodically(){
setInterval(()=>{this.count++},1000)
}
Counter.incrementPeriodically()
如果不使用箭头函数,你会发现其实它绑定的还是全局对象
在类中使用this
这里有一个简单的Person类,它定义了一个构造函数和之前见过的sayHi方法。
创建一个类的实例并调用sayHi方法。
class Person{
constructor(firstName,lastName){
this.firstName = firstName;
this.lastName = lastName;
}
sayHi(){
console.log(`Hi, my name is ${this.firstName}!`);
}
}
const person = new Person('Jane','Doe');
person.sayHi(); //Hi, my name is Jean!
如你所见,一切正常,在构造函数中this指向该类新创建的实例。当我们调用person.sayHi方法时,我们把sayHi()作为方法调用,person作为接收者,因此 this 的绑定是正确的。然而,如果我们存储了对sayHi方法的引用,并在之后将其作为函数调用,我们发现再一次的失去了这个方法的接收者。
class Person{
constructor(firstName,lastName){
this.firstName = firstName;
this.lastName = lastName;
}
sayHi(){
console.log(`Hi, my name is ${this.firstName}!`);
}
}
const person = new Person('Jane','Doe');
const greet = person.sayHi;
greet();
这是因为类隐含在严格模式中,我们将greet作为普通函数调用,没有自动绑定发生。
我们也可以手动调用bind()来将这个sayHi函数与Person绑定。
class Person{
constructor(firstName,lastName){
this.firstName = firstName;
this.lastName = lastName;
}
sayHi(){
console.log(`Hi, my name is ${this.firstName}!`);
}
}
const person = new Person('Jane','Doe');
const greet = person.sayHi.bind(person);
greet();
或者我们也可以使用箭头函数,这样我们就不需要再手动bind了
class Person {
sayHi = () => {
console.log(`Hi, my name is ${this.firstName}!`);
};
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
const person = new Person("John", "Doe");
const greet = person.sayHi;
greet();