logan70 / Blog

写博客的地方,觉得有用的给个Star支持一下~
81 stars 9 forks source link

作用域与闭包 - this的原理以及几种不同使用场景的取值 #27

Open logan70 opened 4 years ago

logan70 commented 4 years ago

this的原理以及几种不同使用场景的取值

了解函数

在具体谈论this取值的各种情况前,我们先来看看一个函数从创建到执行的过程中对我们了解this有帮助的一些规范信息。

函数的this模式

函数创建阶段会标记该函数的this模式,有以下三种模式,由上往下进行判定,详见 ECMAScript#FunctionInitialize

  1. lexical :箭头函数的this模式标记为 lexical
  2. strict :严格模式下函数的this模式标记为 strict
  3. global :其他情况的函数的this模式标记为 global

函数的执行

无论函数通过何种方式调用,最终JS引擎都会通过函数对象内部的 [[Call]] ( thisArgument, argumentsList ) 方法来调用函数并执行。第一个参数为指定this的值,不传则为undefined,第二个参数为函数被调用时的参数列表,详见 ECMAScript#[[Call]]

函数的this绑定

函数调用的初始阶段,会进行this的绑定,具体表现为以下步骤,详见 ECMAScript#BindThis 。:

  1. 如果函数的this模式为lexical,不进行绑定;
  2. 如果函数的this模式为strict,则this绑定值严格等于传入的thisArgument
  3. 如果函数的this模式为globalthisArgumentnullundefined,则this绑定值为全局对象;
  4. 如果函数的this模式为globalthisArgument不为nullundefined,则this绑定值为thisArgument

    这一步中如果传入的thisArgument为基本类型值,会进行装箱操作

不同使用场景的this取值

JavaScript函数中this取值主要区分以下几个情况:

  1. 函数的普通调用
  2. 函数作为对象方法调用
  3. 函数作为构造函数调用
  4. 函数通过callapplybind间接调用
  5. 箭头函数的调用

函数的普通调用

函数普通调用时,未指定this的值,thisArgumentundefined,this的值分两种情况:

function looseFn() {
  console.log(this)
}

function strictFn() {
  'use strict'
  console.log(this)
}

looseFn()  // <- window
strictFn() // <- undefined

函数作为对象方法调用

函数作为对象方法调用时,会将该对象作为thisArgument,所以this为函数所在对象。

ECMA定义规范 -> Abstract operation Call on Objects

var myName = 'global'
const obj = {
  myName: 'obj',
  getMyName() {
    console.log(this.myName)
  }
}

obj.getMyName() // <- 'obj'

函数作为构造函数调用

函数作为构造函数调用时,会将构造的对象作为thisArgument,所以this为构造的对象。

ECMA定义规范 -> Abstract operation Construct

function Person(name) {
  this.name = name
  console.log(this)
}

const person = new Person('Logan')
// <- Person {name: "Logan"}

函数通过callapplybind间接调用

这个不难理解,即通过指定thisArgument的值来改变this的指向。

var name = 'global'
function logName() {
  console.log(this.name)
}

logName() // <- 'global'
logName.call({ name: 'call' }) // <- 'call'
logName.apply({ name: 'apply' }) // <- 'apply'
// 注意bind返回一个函数,而不是直接调用
logName.bind({ name: 'bind' })() // <- 'bind'

箭头函数的调用

箭头函数this模式为lexical,执行时不进行this绑定,所以箭头函数中this的值取决于作用域链上最近的this值。

需要注意的是,箭头函数没有this绑定,所以使用callapplybind无法改变箭头函数内this的指向。

function genArrowFn() {
    return () => {
        console.log(this)
    }
}

const arrowFn1 = genArrowFn()
arrowFn1()                  // <- window

const arrowFn2 = genArrowFn.call({ a: 1 })
arrowFn2()                  // <- { a: 1 }

// `call`、`apply`、`bind`无法改变箭头函数内this的指向,仍然在作用域链上寻找
arrowFn1.call({ a: 2 })     // <- window
arrowFn2.apply({ a: 2 })    // <- { a: 1 }
arrowFn2.bind({ a: 2 })()   // <- { a: 1 }

测试练习

var name = 'window'

const person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
const person2 = { name: 'person2' }

person1.show1()                     // person1 函数作为对象方法调用,this指向对象
person1.show1.call(person2)         // person2 使用call间接调用函数,this指向传入的person2

person1.show2()                     // window  箭头函数无this绑定,在全局环境找到this,指向window
person1.show2.call(person2)         // window  间接调用改变this指向对箭头函数无效

person1.show3()()                   // window  person1.show3()返回普通函数,相当于普通函数调用,this指向window
person1.show3().call(person2)       // person2 使用call间接调用函数,this指向传入的person2
person1.show3.call(person2)()       // window  person1.show3.call(person2)仍然返回普通函数

person1.show4()()                   // person1 person1.show4调用对象方法,this指向person1,返回箭头函数,this在person1.show4调用时的词法环境中找到,指向person1
person1.show4().call(person2)       // person1  间接调用改变this指向对箭头函数无效
person1.show4.call(person2)()       // person2  改变了person1.show4调用时this的指向,所以返回的箭头函数的内this解析改变