玩转 JavaScript 之详解 this

2,464 阅读6分钟

概述

this 关键字作为 JavaScript 中自动定义的特殊标识符,是我们不得不去面对、了解的知识点,很多初学者对 this 关键字可能会有含糊不清的感觉,但其实稍微理一理, this 并不复杂、不混乱。

this 是什么?

概述中我们说了 this 是 JavaScript 中的一个特殊关键字,this 和执行上下文相关,当一个函数被调用时,会建立一个活动记录,也称为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是如何被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。 但是本文中并不准备往深层次分析其到底是什么,而是说明在应用场景和浅层原理中分析 this 到底是什么。 在分析之前,我们先来看一看当初的我们为什么会用 this。

为什么用 this?

以下是一段代码实例,其中用到了 this

let me = {
  name: 'seymoe'
}
function toUpperCase() {
  return this.name.toUpperCase()
}

toUpperCase.call(me)  // 'SEYMOE'

当然以上代码也完全可以不用 this 而采用传参的形式实现。

let me = {
  name: 'seymoe'
}
function toUpperCase(person) {
  return person.name.toUpperCase()
}

toUpperCase(me)  // 'SEYMOE'

在这里为什么用 this 而不用传参的形式,是因为 this 机制用更优雅的方式隐含的传递一个对象的引用,可以拥有更干净的 API 设计和简单复用。使用模式越复杂,通过明确参数传递执行环境和传递 this 执行环境相比,就越复杂,当然以上只是一个应用场景之一。

调用栈与调用点

this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定和函数声明的位置无关,反而和函数被调用的方式有关,被调用的这个位置就叫调用点。所以我们分析 this 是什么的时候就必须分析调用栈(使我们到达当前执行位置而被调用的所有方法的堆栈)和调用点。

function baz() {
  // 调用栈是‘baz’,调用点是全局作用域
  console.log('baz')
  bar()   2. // bar的调用点
}
function bar() {
  // 调用栈是‘baz - bar’,调用点位于baz的函数作用域内
  console.log('bar')
}

baz()  // 1. baz的调用点

以上应该比较容易理解 bazbar 函数相对应的调用点和调用栈。

重点!this 的指向规则

现在我们知道了调用点,而调用点决定了函数调用期间 this 指向哪里的种规则,所以排好队一个一个来分析吧~

1. 默认绑定

顾名思义,就是 this 没有其他规则适用时的默认规则,独立函数调用就是最常见的情况。

var a = 2
function foo() {
  console.log(this.a)
}
function bar() {
  foo()
}
foo()  // 2
bar()  // 2

foo 是一个直白的毫无修饰的函数引用调用,所以默认绑定了全局对象,当然如果是严格模式 "use strict" this 将会是 undefined

注意: 虽然是基于调用点,但只要foo的内容没在严格模式下,那就默认绑定全局对象。

var a = 2
function foo() {
  console.log(this.a)
}
(function (){
  "use strict";
  foo()  // 2
})()

2. 隐含绑定

调用点是否拥有一个环境对象,或(拥有者、容器对象)。

function foo() {
  console.log(this.a)
}

let obj = {
  a: 2,
  foo: foo
}
obj.foo()  // 2

当一个方法引用存在一个环境对象,隐含规则为该对象应该被用于这个函数调用的this绑定。

隐含绑定的情况下,容易出现丢失的情况!当隐含绑定丢失了它的绑定,意味着它会回退到默认绑定,下面是例子:

var a = 3
function foo() {
  console.log(this.a)
}

let obj = {
  a: 2,
  foo: foo
}

let bar = obj.foo
bar()  // 3

// 另一种微妙的情况
function doFoo(fn) {
    fn && fn()
}

doFoo(obj.foo)  // 3

函数的参数传递只是一种隐含的赋值,fn是foo函数的一个引用,而调用fn则是毫无掩饰的调用一个函数,默认绑定规则

3. 明确绑定

隐含绑定需要我们改变对象自身包含一个函数的引用来使 this 隐含的绑定到这个对象上,默认绑定也是不确定的情况,但是很多时候我们希望能够明确的使一个函数调用时使用某个特定对象作为 this 绑定,而不在这个对象上放置一个函数引用属性。

这个时候,callapply 就该上场了。

JavaScript 中几乎所有的函数都能访问这两个方法,这两个方法接收的第一个参数都是一个用于 this 的对象,之后用这个指定的 this 来调用函数,这种方式就叫明确绑定。

function foo() {
    console.log(this.a)
}
let obj = {
    a: 2
}
foo.call(obj)   // 2

一种明确绑定的变种可以保证一个函数始终被obj调用,无论如何也不会改变,这种方式叫硬绑定,通过 bind 方法实现。

var obj = {
  a: 2
}
function foo(something) {
    console.log(this.a, something)
    return this.a + something
}
var bar = foo.bind(obj)
bar(' is a number.')  // 2 ,'is a number.'

我们注意到采用 bind 方式进行硬绑定时,该方法返回一个函数,这和 callapply 是有所区别的。

4. new 绑定

传统面向对象语言中,通过 new 操作符调用构造函数会生成一个类实例。在 JavaScript 中其实没有构造器、类的概念,new 调用的函数仅仅只是一个函数,只是被 new 调用时改变了行为。所以不存在构造器函数,只存在函数的构造器调用。

new 操作符调用时会创建一个全新对象,连接原型链,并将这个新创建的对象设置为函数调用的 this 绑定,(默认情况)自动返回这个全新对象。

function Foo(a) {
  this.a = a
}
let bar = new Foo(2)
console.log(bar.a)  // 2

优先级顺序

以上的规则在适用时存在优先级,级别如下:

硬绑定 > new 绑定 > 明确绑定 > 隐含绑定 > 默认绑定

所以我们已经能够总结出判定 this 的一般流程了。

判定 this 一般流程

  • 如果是 new 调用,this 是新构建的对象;
  • callapplybind ,this 是明确指定的对象;
  • 是用环境对象(或容器)调用的,this 是这个容器;
  • 默认绑定,严格模式为 undefined,否则是global(全局)对象。

箭头函数中的 this

单独将箭头函数中的 this 列出来是因为并不能因为 this 在箭头函数中就有特殊的指向,而是因为箭头函数不会像普通函数去使用 this, 箭头函数的 this 和外层的 this 保持一致。这种保持一致是强力的,无法通过 callapplybind来改变指向。

const obj = {
  a: () => {
    console.log(this)
  }
}
obj.a() // window
obj.a.bind({})()  // window

测验

最后,下面这个简单的测验,并不是很绕很难的面试题,有兴趣不妨做做,评论区回复答案一起探讨一下~

var a = 1
var obj = {
  a: 2,
}
var bar

obj.foo = foo
bar = obj.foo

function foo() {
  var a = 3
  console.log(this.a)
}

foo()  // 1. ???

;(function (a) {
  "use strict";
  foo()  // 2. ???
  bar.bind(a).call(obj)  // 3. ???
})(this)

obj.foo()  // 4. ???
obj.foo.call(this)  // 5. ???
bar()  // 6. ???
bar.apply(obj)  // 7. ???

var b = new foo()  // 8. ???
console.log(b.a)  // 9. ???

玩转 JavaScript 系列

写作是一个学习的过程,尝试写这个系列也主要是为了巩固 JavaScript 基础,并尝试理解其中的一些知识点,以便能灵活运用。本篇同步发布在「端技」公众号,如果有错误或者不严谨的地方,请务必给予指正,十分感谢!

整个系列会持续更新,不会完结。

全目录

1. 玩转 JavaScript 之 数据类型

2. 玩转 JavaScript 之不得不懂的原型

2. 玩转 JavaScript 之详解 this