揭秘JavaScript中“神秘”的this关键字

当我开始学习JavaScript时,花了一些时间来理解JavaScript中的this关键字而且可以快速识别this关键字所指向的对象。我发现理解this关键字最困难的事情是,您一般会忘记在您已阅读或观看过一些JavaScript课程或资源中解释的不一样案例状况。在ES6中引入箭头函数后,事情变得更加混乱,由于箭头函数this以不一样的方式处理关键字。我想写这篇文章来陈述我学到的东西,并尝试以一种能够帮助任何正在学习JavaScript而且难以理解this关键字的人的方式来解释它。数组

您可能知道,执行任何JavaScript行的环境(或scope)称为“执行上下文”。Javascript运行时维护这些执行上下文的堆栈,而且当前正在执行存在于该堆栈顶部的执行上下文。this变量引用的对象每次更改执行上下文时都会更改。浏览器

默认状况下,执行上下文是全局的,这意味着若是代码做为简单函数调用的一部分执行,则该this变量将引用全局对象。在浏览器的状况下,全局对象是window对象。例如,在Node.js环境中,this值是一个特殊对象globalbash

例如,尝试如下简单的函数调用:闭包

function foo () {
  console.log("Simple function call");
  console.log(this === window);
}
foo();
复制代码

调用foo(),获得输出:app

“Simple function call”
true
复制代码

证实这里的this指向全局对象,此例中为window函数

注意,若是实在严格模式下,this的值将是undefined,由于在严格模式下全局对象指向undefined而不是windowpost

试一下以下示例:学习

function foo () {
  'use strict';
  console.log("Simple function call");
  console.log(this === window);
}
foo();
复制代码

输出:ui

“Simple function call”
false
复制代码

咱们再来试下有构造函数的:this

function Person(first_name, last_name) {
    this.first_name = first_name;
    this.last_name = last_name;
  
    this.displayName = function() {
        console.log(`Name: ${this.first_name} ${this.last_name}`);
    };
}
复制代码

建立Person实例:

let john = new Person('John', 'Reid');
john.displayName();
复制代码

获得结果:

"Name: John Reid"
复制代码

这里发生了什么?当咱们调用 new PersonJavaScript会在Person函数内建立一个新对象并把它保存为this。接着,first_name, last_namedisplayName 属性会被添加到新建立的this对象上。以下:

你会注意到在Person执行上下文中建立了this对象,这个对象有first_name, last_namedisplayName 属性。但愿您能根据上图理解this对象是如何建立并添加属性的。

咱们已经探讨了两种相关this绑定的普通案例我不得不提出下面这个更加困惑的例子,以下函数:

function simpleFunction () {
    console.log("Simple function call")
    console.log(this === window); 
}
复制代码

咱们已经知道若是像下面这样做为简单函数调用,this关键字将指向全局对象,此例中为window对象。

simpleFunction()
复制代码

所以,获得输出:

“Simple function call”
true
复制代码

建立一个简单的user对象:

let user = {
    count: 10,
    simpleFunction: simpleFunction,
    anotherFunction: function() {
        console.log(this === window);
    }
}
复制代码

如今,咱们有一个simpleFunction属性指向simpleFunction函数,一样添加另外一个属性调用anotherFunction函数方法。

若是调用user.simpleFunction(),获得输出:

“Simple function call”
false
复制代码

为何会这样呢?由于simpleFunction()如今是user对象的一个属性,因此this指向这个user对象而不是全局对象。

当咱们调用user.anotherFunction,也是同样的结果。this关键字指向user对象。因此,console.log(this === window);应该返回false:

false
复制代码

再来,如下操做会返回什么呢?

let myFunction = user.anotherFunction;
myFunction();
复制代码

如今,获得结果:

true
复制代码

因此这又发生了什么?在这个例子中,咱们发起普通函数调用。正如以前所知,若是一个方法以普通函数方式执行,那么this关键字将指向全局对象(在这个例子中是window对象)。因此console.log(this === window);输出true

再看一个例子:

var john = {
    name: 'john',
    yearOfBirth: 1990,
    calculateAge: function() {
        console.log(this);
        console.log(2016 - this.yearOfBirth);
        function innerFunction() {
            console.log(this);
        }
        innerFunction();
    }
}
复制代码

调用john.calculateAge()会发生什么呢?

{name: "john", yearOfBirth: 1990, calculateAge: ƒ}
26
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
复制代码

calculateAge函数内部, this 指向 john对象,可是,在innerFunction函数内部,this指向全局对象(本例中为window),有些人认为这是JS的bug,可是规则告诉咱们不管什么时候一个普通函数被调用时,那么this将指向全局对象。

...

我所学的JavaScript函数也是一种特殊的对象,每一个函数都有call, apply, bind方法。这些方法被用来设置函数的执行上下文的this值。

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.displayName = function() {
        console.log(`Name: ${this.firstName} ${this.lastName}`);
    }
}
复制代码

建立两个实例:

let person = new Person("John", "Reed");
let person2 = new Person("Paul", "Adams");
复制代码

调用:

person.displayName();
person2.displayName();
复制代码

结果:

Name: John Reed
Name: Paul Adams
复制代码

call:

person.displayName.call(person2);
复制代码

上面所作的事情就是设置this的值为person2对象。所以,

Name: Paul Adams
复制代码

apply:

person.displayName.apply([person2]);
复制代码

获得:

Name: Paul Adams
复制代码

callapply惟一的区别就是参数的传递形式,apply应该传递一个数组,call则应该单独传递参数。

咱们用bind来作一样的事情,bind返回一个新的方法,这个方法中的this指向传递的第一个参数。

let person2Display = person.displayName.bind(person2);
复制代码

调用person2Display,获得Name: Paul Adams结果。

...

箭头函数

ES6中,有一个新方法定义函数。以下:

let displayName = (firstName, lastName) => {
    console.log(Name: ${firstName} ${lastName});
};
复制代码

不像一般的函数,箭头函数没有他们自身的this关键字。他们只是简单的使用写在函数里的this关键字。他们有一个this词法变量。

ES5:

var box = {
    color: 'green', // 1
    position: 1, // 2
    clickMe: function() { // 3
        document.querySelector('body').addEventListener('click', function() {
            var str = 'This is box number ' + this.position + ' and it is ' + this.color; // 4
            alert(str);
        });
    }
}
复制代码

若是调用:

box.clickMe()
复制代码

弹出框内容将是This is box number undefined and it is undefined'.

咱们一步一步来分析是怎么回事。在//1//2行,this关键字能访问到colorposition属性由于它指向box对象。在clickMe方法内部,this关键字能访问到colorposition属性由于它也指向box对象。可是,clickMe方法为querySelector方法定义了一个回调函数,而后这个回调函数以普通函数的形式调用,因此this指向全局对象而非box对象。固然,全局对象没有定义colorposition属性,因此这就是为何咱们获得了undefined值。

咱们能够用ES5的方法来修复这个问题:

var box = {
    color: 'green',
    position: 1,
    clickMe: function() {
        var self = this;
        document.querySelector('body').addEventListener('click', function() {
            var str = 'This is box number ' + self.position + ' and it is ' + self.color;
            alert(str);
        });
    }
}
复制代码

添加 var self = this,建立了一个可使用指向box对象的this关键字的闭包函数的工做区。咱们仅仅只须要在回调函数内使用self变量。

调用:

box.clickMe();
复制代码

弹出框内容This is box number 1 and it is green

怎么使用箭头函数可以达到上述效果呢?咱们将用箭头函数替换点击函数的回调函数。

var box = {
    color: 'green',
    position: 1,
    clickMe: function() {
        document.querySelector('body').addEventListener('click', () => {
            var str = 'This is box number ' + this.position + ' and it is ' + this.color;
            alert(str);
        });
    }
}
复制代码

箭头函数的神奇之处就是共享包裹它的this词法关键字。因此,本例中外层函数的this共享给箭头函数,这个外层函数的this关键字指向box对象,所以,colorposition属性将是有正确的green1值。

再来一个:

var box = {
    color: 'green',
    position: 1,
    clickMe: () => {
        document.querySelector('body').addEventListener('click', () => {
            var str = 'This is box number ' + this.position + ' and it is ' + this.color;
            alert(str);
        });
    }
}
复制代码

oh!如今又弹出了‘This is box number undefined and it is undefined’.。为何?

click事件监听函数闭包的this关键字共享了包裹它的this关键字。在本例中它被包裹的箭头函数clickMeclickMe箭头函数的this关键字指向全局对象,本例中是window对象。因此this.colorthis.position将会是undefined由于window对象没有positioncolor属性。

我想再给你看个在不少状况下都会有帮助的map函数,咱们定义一个Person构造函数方法以下:

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.displayName = function() {
        console.log(`Name: ${this.firstName} ${this.lastName}`);
    }
}
复制代码

Person的原型上添加myFriends方法:

Person.prototype.myFriends = function(friends) {
    var arr = friends.map(function(friend) {
        return this.firstName + ' is friends with ' + friend;
    });
    console.log(arr);
}
复制代码

建立一个实例:

let john = new Person("John", "Watson");
复制代码

调用john.myFriends(["Emma", "Tom"]),结果:

["undefined is friends with Emma", "undefined is friends with Tom"]
复制代码

本例与以前的例子很是类似。myFriends函数体内有this关键字指向回调对象。可是,map闭包函数内是一个普通函数调用。因此map闭包函数内this指向全局对象,本例中为window对象,所以this.firstNameundefined。如今,咱们试着修复这个状况。

  1. myFriends函数体内指定this为其它变量如self,以便map函数内闭包使用它。
Person.prototype.myFriends = function(friends) {
    // 'this' keyword maps to the calling object
    var self = this;
    var arr = friends.map(function(friend) {
        // 'this' keyword maps to the global object
        // here, 'this.firstName' is undefined.
        return self.firstName + ' is friends with ' + friend;
    });
    console.log(arr);
}
复制代码
  1. map闭包函数使用bind
Person.prototype.myFriends = function(friends) {
    // 'this' keyword maps to the calling object
    var arr = friends.map(function(friend) {
        // 'this' keyword maps to the global object
        // here, 'this.firstName' is undefined.
        return this.firstName + ' is friends with ' + friend;
    }.bind(this));
    console.log(arr);
}
复制代码

调用bind会返回一个map回调函数的副本,this关键字映射到外层的this关键字,也就是是调用myFriends方法,this指向这个对象。

  1. 建立map回调函数为箭头函数。
Person.prototype.myFriends = function(friends) {
    var arr = friends.map(friend => `${this.firstName} is friends with ${friend}`);
    console.log(arr);
}
复制代码

如今,箭头函数内的this关键字将共享不曾包裹它的词法做用域,也就是说实例myFriends

全部以上解决方案都将输出结果:

["John is friends with Emma", "John is friends with Tom"]
复制代码

...

在这一点上,我但愿我已经设法使this关键字概念对您来讲有点平易近人。在本文中,我分享了我遇到的一些常见状况以及如何处理它们,但固然,在构建更多项目时,您将面临更多状况。我但愿个人解释能够帮助您在接近this关键字绑定主题时保持坚实的基础。若是您有任何问题,建议或改进,我老是乐于学习更多知识并与全部知名开发人员交流知识。请随时写评论,或给我留言!

相关文章
相关标签/搜索