JS基础篇之做用域、执行上下文、this、闭包

这是一篇很短的文章,介绍了js几个比较重要的概念,适合通勤路上快速阅读加深理解和记忆。javascript

做用域和执行上下文

做用域:

js中的做用域是词法做用域,即由 函数声明时 所在的位置决定的。词法做用域是指在编译阶段就产生的,一整套函数标识符的访问规则。(区别于词法做用域,动态做用域是在函数执行的时候确认的,js的没有动态做用域,但js的this很像动态做用域,后面会提到。词法做用域的概念十分重要,请多加记忆并理解。) 说到底js的做用域只是一个“空地盘”,其中并无真实的变量,可是却定义了变量如何访问的规则。前端

做用域链本质上是一个指向变量对象的指针列表,它只引用不包含实际变量对象,是做用域概念的延申。做用域链定义了变量在当前上下文访问不到的时候如何沿做用域链继续查询的一套规则。java

执行上下文:

执行上下文是指 函数调用时 产生的变量对象,这个变量对象咱们没法直接访问,可是能够访问其中的变量、this对象等。例如:es6

let fn, bar; // 一、进入全局上下文环境
bar = function(x) {
  let b = 5;
  fn(x + b); // 三、进入fn函数上下文环境
};
fn = function(y) {
  let c = 5;
  console.log(y + c); //四、fn出栈,bar出栈
};
bar(10); // 二、进入bar函数上下文环境
复制代码

每次函数调用时,都会产生一个新的执行上下文环境,JavaScript引擎会以栈的方式来处理它们,这个栈,咱们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前处于活动状态的正在执行的上下文,也称为活动对象(running execution context,图中蓝色的块),区别与底下被挂起的上下文(变量对象)。

总结:做用域是在函数声明的时候就肯定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的环境。也就是说做用域定义了执行上下文中的变量的访问规则,执行上下文在这个做用域规则的前提下进行变量查找,函数引用等具体操做。bash

理解函数执行过程

函数的执行过程分红两部分,一部分用来生成执行上下文环境,肯定this的指向、声明变量以及生成做用域链;另外一部分则是按顺序逐行执行代码。闭包

  • 创建执行上下文阶段(发生在:函数被调用时 && 函数体内的代码执行前 )
  1. 生成变量对象,顺序:建立 arguments 对象 --> 建立function函数声明 --> 建立var变量声明
  2. 生成做用域链
  3. 肯定this的指向
  • 函数执行阶段
  1. 逐行执行代码,这个阶段会完成变量赋值,函数引用,以及执行其余代码。

this指向

关于js的this关键字,我记得第一次接触仍是在作前端半年或一年的时候(哈哈我就是这么水)。那时候徐哥(java大佬)教我在绑定click事件的时候把this传给事件处理函数,相似<button onclick="handle(this)">确认</button>,我当时就懵了,this是什么鬼?!今后正式开启了我三年的js痛苦之旅:封装啊、闭包啊、面向对象啊、继承啊等等等等。this的指向说来讲去其实只有四种:app

let fn = function(){
  alert(this.name)
}
let obj = {
  name: '',
  fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
复制代码
  1. 方法1中直接调用函数fn(),这种看着像光杆司令的调用方式,this指向window(严格模式下是undefined)。
  2. 方法2中是点调用obj.fn(),此时this指向obj对象。点调用中this指的是点前面的对象。
  3. 方法3中利用call函数把fn中的this指向了第一个参数,这里是obj。即利用callapplybind函数能够把函数的this变量指向第一个参数。
  4. 方法4中用new实例化了一个对象instance,这时fn中的this就指向了实例instance

若是同时发生了多个规则怎么办?其实上面四条规则的优先级是递增的:函数

fn() < obj.fn() < fn.call(obj) < new fn()ui

首先,new调用的优先级最高,只要有new关键字,this就指向实例自己;接下来若是没有new关键字,有call、apply、bind函数,那么this就指向第一个参数;而后若是没有new、call、apply、bind,只有obj.foo()这种点调用方式,this指向点前面的对象;最后是光杆司令foo() 这种调用方式,this指向window(严格模式下是undefined)。this

es6中新增了箭头函数,而箭头函数最大的特点就是没有本身的this、arguments、super、new.target,而且箭头函数没有原型对象prototype不能用做构造函数(new一个箭头函数会报错)。由于没有本身的this,因此箭头函数中的this其实指的是包含函数中的this。不管是点调用,仍是call调用,都没法改变箭头函数中的this

闭包

js的闭包是新手的噩梦,在学js的前三年,我查阅了无数的博文,苦苦搜索闭包的概念,然而最终一无所得。MDN上这样定义闭包:闭包是函数和声明该函数的词法环境的组合。

what?能说人话吗?

很长时间以来我对闭包都停留在“定义在一个函数内部的函数”这样肤浅的理解上。事实上这只是闭包造成的必要条件之一。直到后来看了kyle大佬的《你不知道的JAVASCRIPT》上册中关于闭包的定义,我才豁然开朗:

当函数可以记住并访问所在的词法做用域时,就产生了闭包。

let single = (function(){
  let count = 0
  return {
    plus(){
      count++
      return count
    },
    minus(){
      count--
      return count
    }
  }
})()
single.plus() // 1
single.minus() // 0
复制代码

这是个单例模式,这个模式返回了一个对象single,对象中包含两个函数plusminus,而这两个函数都用到了所在词法做用域中的变量count,因此在函数执行结束时count所在的执行环境不会被销毁,这就产生了闭包。每次调用single.plus()或者single.minus(),就会对闭包中的count变量进行修改,这两个函数保持住了对所在的词法做用域的引用。

闭包实际上是一种特殊的函数,它能够访问函数内部的变量,还可让这些变量的值始终保持在内存中,不会在函数调用后被垃圾回收机制清除。

看个经典安利:

// 方法1
for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
复制代码

方法1中,循环设置了五个定时器,一秒后定时器中回调函数将执行,打印变量i的值。毋庸置疑,一秒以后i已经递增到了5,因此定时器打印了五次5 。(定时器中并无找到当前做用域的变量i,因此沿做用域链找到了全局做用域中的i

方法2中,因为es6的let会建立局部做用域,因此循环设置了五个做用域,而五个做用域中的变量i分布是1-5,每一个做用域中又设置了一个定时器,打印一秒后变量i的值。一秒后,定时器从各自父做用域中分别找到的变量i是1-5 。这是个利用闭包解决循环中变量发生异常的新方法。

最后

我真的学不动了。

相关文章
相关标签/搜索