关于javascript:从这两道题重新理解JS的this作用域闭包对象

47次阅读

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

日常开发中,咱们常常用到 this。例如用 Jquery 绑定事件时,this 指向触发事件的 DOM 元素;编写 Vue、React 组件时,this 指向组件自身。对于老手来说,常会用一种意会的感觉去判断 this 的指向。以至于当遇到简单的函数调用时,就分不清 this 的真正指向。

本文将通过两道题去缓缓剖析 this 的指向问题,并波及到函数作用域与对象相干的点。最终给大家带来真正的实践剖析,而不是简简单单的一句话概括。

置信若是对 this 稍有钻研的人,都会搜到这句话:this 总是指向调用该函数的对象

然而箭头函数并不是如此,于是大家就会遇到如下各式说法:

  1. 箭头函数的 this 指向外层函数作用域中的 this。
  2. 箭头函数的 this 是定义函数时所在上下文中的 this。
  3. 箭头函数体内的 this 对象,就是定义时所在的对象,而不是应用时所在的对象。

各式各样的说法都有,乍看下感觉说的差不多。废话不多说,凭着你之前的了解,来先做一套题吧(非严格模式下)。

/** * Question 1 */

var name = 'window'

var 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)
  }
}
var person2 = {name: 'person2'}

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

大抵意思就是,有两个对象person1person2,而后花式调用 person1 中的四个 show 办法,预测真正的输入。

你能够先把本人预测的答案按程序记在本子上,而后再往下拉看正确答案。



正确答案选下:

person1.show1() // person1
person1.show1.call(person2) // person2

person1.show2() // window
person1.show2.call(person2) // window

person1.show3()() // window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window

person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2

比照下你刚刚记下的答案,是否有不一样呢?让咱们尝试来最开始那些实践来剖析下。

person1.show1()person1.show1.call(person2) 好了解,验证了 谁调用此办法,this 就是指向谁

person1.show2()person1.show2.call(person2) 的后果用下面的定义解释,就开始让人不了解了。

它的执行后果阐明 this 指向的是 window。那就不是所谓的定义时所在的对象。

如果说是外层函数作用域中的 this,实际上并没有外层函数了,外层就是全局环境了,这个说法也不谨严。

只有 定义函数时所在上下文中的 this这句话算能形容当初这个状况。

person1.show3是一个高阶函数,它返回了一个函数,分步走的话,应该是这样:

var func = person3.show()

func()

从而导致最终调用函数的执行环境是 window,但并不是 window 对象调用了它。所以说,this 总是指向调用该函数的对象 ,这句话还得补充一句: 在全局函数中,this 等于 window

person1.show3().call(person2)person1.show3.call(person2)() 也好了解了。前者是通过 person2 调用了最终的打印办法。后者是先通过 person2 调用了 person1 的高阶函数,而后再在全局环境中执行了该打印办法。

person1.show4()()person1.show4().call(person2)都是打印 person1。这如同又印证了那句:箭头函数体内的 this 对象,就是定义时所在的对象,而不是应用时所在的对象。因为即便我用过 person2 去调用这个箭头函数,它指向的还是 person1。

然而 person1.show4.call(person2)() 的后果又是 person2。this 值又产生扭转,看来上述那句形容又走不通了。一步步来剖析,先通过 person2 执行了 show4 办法,此时 show4 第一层函数的 this 指向的是 person2。所以箭头函数输入了 person2 的 name。也就是说,箭头函数的 this 指向的是 谁调用箭头函数的外层 function,箭头函数的 this 就是指向该对象,如果箭头函数没有外层函数,则指向 window。这样去了解 show2 办法,也解释的通。

这句话就对了么?在咱们学习的过程中,咱们总是想以总结法则的办法去总结论断,并且心愿论断越简略越容易形容就越好。实际上可能会错失真谛。

上面咱们再做另外一个类似的题目,通过构造函数来创立一个对象,并执行雷同的 4 个 show 办法。

/** * Question 2 */
var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {return function () {console.log(this.name)
    }
  }
  this.show4 = function () {return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

personA.show2()
personA.show2.call(personB)

personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()

同样的,依照之前的了解,再次预计打印后果,把答案记下来,再往下拉看正确答案。



正确答案选下:

personA.show1() // personA
personA.show1.call(personB) // personB

personA.show2() // personA
personA.show2.call(personB) // personA

personA.show3()() // window
personA.show3().call(personB) // personB
personA.show3.call(personB)() // window

personA.show4()() // personA
personA.show4().call(personB) // personA
personA.show4.call(personB)() // personB

咱们发现与之前字面量申明的相比,show2 办法的输入产生了不一样的后果。为什么呢?尽管说构造方法 Person 是有本人的函数作用域。然而对于 personA 来说,它只是一个对象,在直观感触上,它跟第一道题中的 person1 应该是截然不同的。JSON.stringify(new Person('person1')) === JSON.stringify(person1)也证实了这一点。

阐明构造函数创建对象与间接用字面量的模式去创建对象,它是不同的,构造函数创建对象,具体做了什么事呢?我援用红宝书中的一段话。参考 前端进阶面试题具体解答

应用 new 操作符调用构造函数,实际上会经验一下 4 个步骤:

  1. 创立一个新对象;
  2. 将构造函数的作用域赋给新对象(因而 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象增加属性);
  4. 返回新对象。

所以与字面量创建对象相比,很大一个区别是它多了构造函数的作用域。咱们用 chrome 查看这两者的作用域链就能清晰的晓得:

personA 的函数的作用域链从构造函数产生的闭包开始,而 person1 的函数作用域仅是 global,于是导致 this 指向的不同。咱们发现,要想真正了解 this,先得晓得到底什么是作用域,什么是闭包。

有简略的说法称闭包就是可能读取其余函数外部变量的函数。然而这是一种闭包景象的形容,而不是它的实质与造成的起因。

我再次援用红宝书的文字(便于了解,文字程序略微调整),来形容这几个点:

… 每个函数都有本人的执行环境(execution context,也叫执行上下文),每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保留在这个对象中。

… 当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。当代码在环境中执行时,会创立一个作用域链,来保障对执行环境中的所有变量和函数的有序拜访。函数执行之后,栈将环境弹出。

… 函数外部定义的函数会将蕴含函数的流动对象增加到它的作用域链中。

具体来说,当咱们 var func = personA.show3() 时,personAshow3 函数的流动对象,会始终保留在 func 的作用域链中。只有不销毁 func,那么show3 函数的流动对象就会始终保留在内存中。(chrome 的 v8 引擎对闭包的开销会有优化)

而构造函数同样也是闭包的机制,personAshow1 办法,是构造函数的外部函数,因而执行了 this.show3 = function () { console.log(this.name) }时,曾经把构造函数的流动对象推到了 show3 函数的作用域链中。

咱们再回到 this 的指向问题。咱们发现,单单是总结法则,或者用一句话概括,曾经难以正确解释它到底指向谁了,咱们得寻根究底。

红宝书中说道:

…this 援用的是函数执行的环境对象(便于了解,贴上英文原版:It is a reference to the context object that the function is operating on)。
… 每个函数被调用时都会主动获取两个非凡变量:this 和 arguments。外部在搜寻这个两个变量时,只会搜寻到其流动对象为止,永远不可能间接拜访内部函数中的这两个变量。

咱们看下 MDN 中箭头函数的概念:

一个箭头函数表达式的语法比一个函数表达式更短,并且不绑定本人的 thisargumentssupernew.target。… 箭头函数会捕捉其所在上下文的 this 值,作为本人的 this 值。

也就是说,一般状况下,this 指向调用函数时的对象。在全局执行时,则是全局对象。

箭头函数的 this,因为没有本身的 this,所以 this 只能依据作用域链往下层查找,直到找到一个绑定了 this 的函数作用域(即最靠近箭头函数的一般函数作用域,或者全局环境),并指向调用该一般函数的对象。

或者从景象来形容的话,即 箭头函数的 this 指向申明函数时,最靠近箭头函数的一般函数的 this。但这个 this 也会因为调用该一般函数时环境的不同而发生变化。导致这个景象的起因是这个一般函数会产生一个闭包,将它的变量对象保留在箭头函数的作用域中

故而 personAshow2办法因为构造函数闭包的关系,指向了构造函数作用域内的 this。而

var func = personA.show4.call(personB)

func() // print personB

因为 personB 调用了 personA 的 show4,使得返回函数 func 的作用域的 this 绑定为 personB,进而调用 func 时,箭头函数通过作用域找到的第一个明确的 this 为 personB。进而输入 personB。

讲了这么多,可能还是有点绕。总之,想充沛了解 this 的前提,必须得先明确 js 的执行环境、闭包、作用域、构造函数等基础知识。而后能力得出清晰的论断。

咱们平时在学习过程中,难免会更偏向于依据教训去推导论断,或者间接去找一些通俗易懂的描述性语句。然而实际上可能并不是最正确的后果。如果想真正把握它,咱们就应该寻根究底的去钻研它的外部机制。

正文完
 0