前言:JS 的作用域、执行上下文、this、闭包是老生常谈的话题,也是新手比较懵懂的知识点。当然即便你作为老手,也未必真的能理解透彻这些概念。

一、作用域和执行上下文

作用域:

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

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

执行上下文:

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

let fn, bar; // 1、进入全局上下文环境
bar = function(x) {
let b = 5;
fn(x + b); // 3、进入fn函数上下文环境
};
fn = function(y) {
let c = 5;
console.log(y + c); //4、fn出栈,bar出栈
};
bar(10); // 2、进入bar函数上下文环境

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

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

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

理解函数的执行过程

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

建立执行上下文阶段:(发生在 函数被调用时 && 函数体内的代码执行前 )

  1. 生成变量对象,顺序:创建 arguments 对象 --> 创建function函数声明 --> 创建var变量声明
  2. 生成作用域链
  3. 确定this的指向

函数执行阶段:

  1. 逐行执行代码,这个阶段会完成变量赋值,函数引用,以及执行其他代码。

二、this指向

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

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()

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

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() //
single.minus() //

这是个单例模式,这个模式返回了一个对象并赋值给变量single,变量single中包含两个函数plusminus,而这两个函数都用到了所在词法作用域中的变量count。正常情况下count和所在的执行上下文会在函数执行结束时被销毁,但是由于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 。这是个利用闭包解决循环中变量发生异常的新方法。

最后

我真的学不动了。

作者:幻灵尔依
链接:https://juejin.im/post/5cf8612df265da1bcb4f1bf8
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

JS基础篇之作用域、执行上下文、this、闭包的更多相关文章

  1. JS底层知识理解之执行上下文篇

    JS底层知识理解之执行上下文篇 一.什么是执行上下文(Execution Context) 执行上下文可以理解为当前代码的执行环境,它会形成一个作用域. 二.JavaScript引擎会以什么方式去处理 ...

  2. 前端面试题目汇总摘录(JS 基础篇)

    JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string typeof null; // o ...

  3. JS基础篇--sort()方法的用法,参数以及排序原理

    JS基础篇--sort()方法的用法,参数以及排序原理   sort() 方法用于对数组的元素进行排序,并返回数组.默认排序顺序是根据字符串Unicode码点.语法:arrayObject.sort( ...

  4. js的基础(平民理解的执行上下文/调用堆栈/内存栈/值类型/引用类型)

    与以前的切图比较,现在的前端开发对js的要求似乎越来越高,在开发中,我们不仅仅是要知道如何运用现有的框架(react/vue/ng), 而且我们对一些基础的知识的依赖越来越大. 现在我们就用平民的方法 ...

  5. 前端面试题目汇总摘录(JS 基础篇 —— 2018.11.02更新)

    温故而知新,保持空杯心态 JS 基础 JavaScript 的 typeof 返回那些数据类型 object number function boolean undefined string type ...

  6. js基础篇——call/apply、arguments、undefined/null

    a.call和apply方法详解 call方法: 语法:call([thisObj[,arg1[, arg2[,   [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象 ...

  7. js 基础篇(点击事件轮播图的实现)

    轮播图在以后的应用中还是比较常见的,不需要多少行代码就能实现.但是在只掌握了js基础知识的情况下,怎么来用较少的而且逻辑又简单的方法来实现呢?下面来分析下几种不同的做法: 1.利用位移的方法来实现 首 ...

  8. JS基础知识(作用域/垃圾管理)

    1.js没有块级作用域 if (true) { var color = “blue”; } alert(color); //”blue” for (var i=0; i < 10; i++){ ...

  9. Vue.js基础篇实战--一个ToDoList小应用

    距离开始学Vue已经过去一个多月了,总想把学到的东西柔和在一起,做点东西出来,于是有了这个Todolist小应用. 使用vuex 纯粹基础,没有用到web pack,vuex,npm,下次把它改造一下 ...

随机推荐

  1. Prometheus入门到放弃(6)之AlertManager进阶

    前面几个篇幅,我们介绍了alertmanger报警配置,在实际运维过程中,我们都会遇到,报警的重复发送,以及报警信息关联性报警.接下来我们就介绍下通过alertmanger对告警信息的收敛.一.告警分 ...

  2. SAS学习笔记58 单元格格式化设计

    单元格行_row_ 对于行单元格,主要就通过_row_这么一个自动变量的方式,来对单元格所有行进行格式化设计 例如,对性别为“男”的单元格所在行颜色设定为红色: 单元格列_col_ 将_row_改成_ ...

  3. es6新特性-解构表达式、Lambda表达式、局部变量及map/reduce方法

    循环内的变量在循环外可见,不合理: let定义的变量是局部变量: const修饰的是常量,不允许再次修改,类似于java中的static: 解构表达式:

  4. Ubuntu 18.04 Server 配置静态ip

    刚在虚拟机里面状态了一个 Ubunut 18.04 Server 作为我的服务器,我习惯使用静态ip首先再virtualbox中设置虚拟机网络的连接方式为桥接模式进入ubuntu虚拟机根据我的印象直接 ...

  5. java之hibernate之单向的多对多关联映射

    这篇 单向的多对多关联映射 1.如何在权限管理中,角色和权限之间的关系就是多对多的关系,表结构为: 2.类结构 Permission.java public class Permission impl ...

  6. 【CF1095F】 Make It Connected(最小生成树)

    题目链接 如果没有特殊边的话显然答案就是权值最小的点向其他所有点连边. 所以把特殊边和权值最小的点向其他点连的边丢一起跑最小生成树就行了. #include <cstdio> #inclu ...

  7. mysql 存储过程、视图---创建、调用、删除

    之前一直用的是Sql Server数据库,最近偶然机会接触到mysql.这里总结了关于mysql 存储过程.视图的“创建.调用.删除”示例 ============================== ...

  8. React学习:状态(State) 和 属性(Props)

    State 与 Props 区别props 是组件对外的接口,state 是组件对内的接口.组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法 ...

  9. Nginx 操作响应头信息的实现

    前置条件:需要编译 ngx_http_headers_module 模块,才支持 header 头信息操作 add_header 意思为将自定义的头信息的添加到响应头,指令为 add_header n ...

  10. linux设置网卡速率

    ethtool # ethtool ethX //查询ethX网口基本设置 # ethtool –h //显示ethtool的命令帮助(help) # ethtool –i ethX //查询ethX ...