关于前端:深入理解JavaScriptthis关键字

36次阅读

共计 6152 个字符,预计需要花费 16 分钟才能阅读完成。

先说论断: 谁调用它,this 就指向谁

前言

在讲 Function、作用域 时,咱们都讲到了 this,因为 JavaScript 中的作用域是词法作用域,在哪里定义,就在哪里造成作用域。而与词法作用域绝对应的还有一个作用域叫动静作用域,调用时去寻找它所处的地位。那个时候笔者就说 this 机制和动静作用域很像

对于 this

为什么应用 this

咱们解释一下为什么要应用 this,用一个例子

function identify() {return this.name.toUpperCase();
}

function speak() {var greeting = "Hello, I'm" + identify.call(this);
    console.log(greeting);
}

var me = {name: 'johan',};

var you = {name: 'elaine',};

identify.call(me); // JOHAN
identify.call(you); // ELAINE

speak.call(me); // Hello, I'm JOHAN
speak.call(you); // Hello, I'm ELAINE

这段代码能够在不同的上下文对象(me 和 you)中重复使用函数 identity() 和 speak(),不必针对每个对象编写不同版本的函数

如果不实用 this,那就须要给 identify() 和 speak() 显式传入一个上下文对象

function identify(context) {return context.name.toUpperCase();
}

function speak(context) {var greeting = "Hello, I'm" + identify(context);
    console.log(greeting);
}

identify(you); // ELAINE
speak(me); // Hello, I'm JOHAN

看到这里你兴许明确了,this 是一种更为优雅的”传递”对象援用的形式。这个例子还过于简略,当你遇到 n 个函数(或叫办法)之间的调用时,显式传值无疑会变得凌乱。除此之外,在原型中,构造函数会主动引入适合的上下文对象是极为重要的

this 到底是什么

this 到底是一种什么样的机制

  1. this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件;
  2. this 的绑定和函数申明的地位没有任何关系,只取决于函数的调用形式;
  3. 当一个函数被调用时,JavaScript 会创立一执行上下文,携带所有的信息(包含 this、词法环境、变量环境)。this 就是执行上下文(context)中的一条信息,它代表是谁调用它

调用形式

正如下面所讲,this 是在运行时绑定的,它的上下文取决于函数调用时的各个条件。在 JavaScript 中函数的调用有以下几种形式:作为对象办法调用,作为函数调用,作为结构函数调用,和应用 call / apply / bind 调用。上面咱们依照调用形式不同,别离探讨 this 的含意

作为对象办法调用

在 JavaScript 中,函数也是对象,因而函数能够作为一个对象的属性,此时该函数被称为该对象的办法,在调用这种调用形式时,this 被天然绑定到该对象

var people = {
    name: 'elaine',
    age: 28,
    sayName: function () {console.log(this.name);
    },
};
people.sayName(); // elaine

作为函数调用

函数也能够间接被调用,此时 this 绑定到全局对象。在浏览器中,window 就是该全局对象。比方上面的例子:函数被调用时,this 被绑定到全局对象,接下来执行赋值语句,相当于隐式的申明了一个全局变量,这显然不是调用者心愿的

function sayAge(age) {this.age = age;}
sayAge(5);
// age 曾经成为一个值为 5 的全局变量 

对于外部函数,即申明在另外一个函数体内的函数,这种绑定到全局对象的形式会产生另外一个问题。咱们以前文所写的 people 对象为例,这次咱们心愿在 sayName 办法内定义一个函数,函数打印年龄。发现 people.age 没有扭转,而全局多了一个 age 变量

var people = {
    name: 'elaine',
    age: 28,
    sayName: function (age) {var sayAge = function (age) {this.age = age;};
        sayAge(age);
    },
};
people.sayName(5);
people.age; // 28
age; // 5

这属于 JavaScript 的设计缺点,正确的设计形式是外部函数的 this 应该绑定到其外层函数对应的对象上,为了躲避这一设计缺点,咱们的方法是变量代替的形式,约定俗成,该变量个别被称为 that

var people = {
    name: 'elaine',
    age: 28,
    sayName: function (age) {
        var that = this;
        var sayAge = function (age) {that.age = age;};
        sayAge(age);
    },
};
people.sayName(5);
people.age; // 5
age; // 没有定义 

作为箭头函数调用

当然,咱们应用 ES6 中的箭头函数时,感觉它也能实现同样的成果

var people = {
    name: 'elaine',
    age: 28,
    sayName: (age) => {console.log(this)
        var sayAge = function (age) {this.age = age;};
        sayAge(age);
    },
};
people.sayName(5);
people.age; // 28
age; // 5

可答案却不如人意,箭头函数不应该没有 this 吗,它的 this 不是须要在内部词法环境中找吗

其实箭头函数很简略,和咱们之前说作用域时谈到的动静作用域和动态作用域(词法作用域)有关系。this 自身的机制和动静作用域很像,而箭头函数的呈现,某种程度上躲避了 JavaScript 的设计缺点(现实中的设计形式应该是外部函数的 this 应该绑定到其外层函数对应的对象上)

var people = {
    name: 'eliane',
    age: 28,
    sayName: () => console.log(this.name, this),
    sayName2: function () {console.log(this.name, this);
    },
};
people.sayName(); //  '', Window
people.sayName2(); // elaine, {name: 'eliane', age: 28}

应用箭头函数后,就不必管调用者是谁,它只关怀在哪里定义

var foo = {
    bar: {a: () => console.log(this),
    },
};
foo.bar.a(); // window

回头看这题:

var people = {
    name: 'elaine',
    age: 28,
    sayName: (age) => {console.log(this)
        var sayAge = function (age) {this.age = age;};
        sayAge(age);
    },
};

箭头函数下,它函数下的 this 指向的是内部词法环境,与谁调用无关。而这题中 sayName 函数中的打印 this,往外找只能找到 window

如果要实现题目的性能,应该将打印放在 sayAge 中,这样,this 才会指向它的外层 sayName 函数

var people = {
    name: 'elaine',
    age: 28,
    sayName: function(age) {var sayAge = (age) => {console.log(this)
               this.age = age;
        };
        sayAge(age);
    },
};
people.sayName(5);
people.age; // 5
age; // 没有定义 

作为结构函数调用

JavaScript 反对面向对象编程,与支流的面向对象编程语言不同,JavaScript 并没有类(Class)的概念,而是应用基于原型(prototype-base)的继承形式。同样约定俗称,首字母大写的函数被称为构造函数,咱们应用 new 调用时,this 会绑定到实例对象上

function People(name, age) {
    this.name = name;
    this.age = age;
}
var elaine = new People('elaine', 28)
console.log(elaine) // {name: "elaine", age: 28}

应用 call / apply / bind 调用

让咱们再一次重申,在 JavaScript 中函数也是对象,对象则有办法,call、apply、bind 就是函数对象的办法。这三个办法异样弱小,他们容许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该办法。让咱们看一个具体的例子:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function(name, age) {
        this.name = name;
        this.age = age;
    }
}
var elaine = new Person('elaine', 28);
var johan = {name: 'johan', age: 28};
elaine.sayName('elaine1', 281);
elaine.sayName.apply(johan, ['johan1', 281])
// 如果用 call elaine.sayName.call(johan, 'johan1', 281)
console.log(elaine.name) // elaine1;
console.log(elaine.age) // 281
console.log(johan) // {name: "johan1", age: 281}

在下面的例子中,咱们应用构造函数生成了一个对象 elaine,该对象同时具备 sayName 办法;应用对象字面量创立了另一个对象 johan,咱们看到应用 apply 能够将 elaine 上的办法利用到 johan 上,这时候 this 也被绑定到对象 johan 上,另一个 call 也具备雷同的性能,不同的是最初的参数不是作为一个数组对立传入,而是离开传入的

回过头来看,apply 和 call 的语义就是 elaine 的办法 sayName 作用于 johan,sayName 须要传入的参数,我从第二个参数开始传值;或者说 johan 调用 elaine 的 sayName 办法,从第二个参数开始传值

call、apply、bind 具备掰弯 this 指向的能力。无关 call/apply/bind 更具体的介绍,笔者会在这篇文章——call、apply、bind 三大将(后续文章会写道)中具体刻画

函数的执行环境

咱们之前始终在讲一件事,this 是如何被调用的,也说了 this 是什么,那么咱们来看看,当一个函数被执行时会产生什么

一个函数被执行时,会创立一个执行环境(或叫执行上下文,英文名 ExecutionContext),函数所有的行为都产生在此执行环境中,构建该执行环境时,JavaScript 首先会创立 arguments 变量,其中蕴含调用函数时传入的参数。接下来创立作用域链。而后初始化变量,首先初始化函数的形参表,值为 arguments 变量中对应的值,如果 arguments 变量中没有对应值,则该形参初始化为 undefined。如果该函数中含有外部函数,则初始化这些外部函数。如果没有,持续初始化该函数内定义的局部变量,须要留神的是此时这些变量初始化为 undefined,其赋值操作在执行环境(ExecutionContext)创立胜利后,函数执行时才会执行,这点对于咱们了解 JavaScript 中的变量作用域十分重要

最初是 this 变量赋值,如前所述,会依据函数调用形式的不同,赋给 this 全局对象,以后对象等。至此函数的执行环境(ExecutionContext)创立胜利,函数开始逐行执行,所需变量均从之前构建好的执行环境(ExecutionContext)中读取。其更具体地介绍会在 执行上下文与调用栈(后续文章会写道)一文中具体介绍

this 有什么作用

全局执行上下文中:this 指向了 window 对象,不便咱们来调用全局 window 对象

函数执行上下文中:this 指向了调用该函数的对象,缩小的参数的传递,原来如何须要在函数外部操作被调用对象,当然还须要将对象作为参数传递进去,而又了 this,就不须要了,间接拿 this 就能够操作该调用对象的属性

总结

构造函数就是个模板,this 将来会指向 new 进去的对象。创立 Person 的实例时,this.name 将援用新创建的对象,并将一个名为 name 的属性放入新对象中

this 其实很好了解,它就是一个代词,示意“这个”

生存中遇到一些事物法则,咱们演绎总结,得出结论,用一个名词代替这个法则,例如马太效应,墨菲定律,咱们约定俗成,这个词就是示意这些意。这样一形象,彼此信息耗费就缩小了。this 其实很好了解,this 就代指”这个“

var foo = {value: 1,};
function bar() {console.log(this.value);
}
bar();

调用函数 bar,函数中的 this 就默认代指 window。window 上没有 value,那后果就是 undefined。

var foo = {value: 1,};
function bar() {console.log(this.value);
}
bar.call(foo);

call/apply 能硬核掰弯 this 指向,将 this 指向第一个参数,所以这段代码中,this 代指 foo,foo 上有 value,所以打印后果是 1

针对 JavaScript 中的 this 指向问题,知乎上有人已经答复过:

  • this 的灵便指向,属于 JavaScript 本人创造的语言
  • this 指向存在的问题是公认的
  • this 的这种设计既不利于代码可读性,也不利于性能优化,齐全可对其世家强制性
  • this 设计问题的更远,是产品营销需要与设计师集体偏好之间的抵触

this 是万恶之源,大家都是(词法)动态作用域,就它玩动静

参考资料

  • 重学 this 关键字
  • 残缺梳理 this 指向
  • 面试三板斧——this

系列文章

  • 深刻了解 JavaScript- 开篇
  • 深刻了解 JavaScript-JavaScript 是什么
  • 深刻了解 JavaScript-JavaScript 由什么组成
  • 深刻了解 JavaScript- 所有皆对象
  • 深刻了解 JavaScript-Object(对象)
  • 深刻了解 JavaScript-new 做了什么
  • 深刻了解 JavaScript-Object.create
  • 深刻了解 JavaScript- 拷贝的机密
  • 深刻了解 JavaScript- 原型
  • 深刻了解 JavaScript- 继承
  • 深刻了解 JavaScript-JavaScript 中的始皇
  • 深刻了解 JavaScript-instanceof——找祖籍
  • 深刻了解 JavaScript-Function
  • 深刻了解 JavaScript- 作用域

正文完
 0