玩转 JavaScript 之详解 this

概述

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

this 是什么?

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

为何用 this?

如下是一段代码实例,其中用到了 thisapp

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 执行环境相比,就越复杂,固然以上只是一个应用场景之一。post

调用栈与调用点

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

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

baz()  // 1. baz的调用点
复制代码

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

重点!this 的指向规则

如今咱们知道了调用点,而调用点决定了函数调用期间 this 指向哪里的种规则,因此排好队一个一个来分析吧~this

1. 默认绑定

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

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

相关文章
相关标签/搜索