前言: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. 生信-使用NCBI进行目的基因的引物设计

    使用NCBI进行目的基因的引物设计 全文概述 利用生信工具进行目的基因的引物设计,使用了NCBI进行筛选与设计引物,使用 idtdna对筛选出的DNA进行检查.本文分享了如何筛选出高质量的基因引物,帮 ...

  2. Feign输出Info级别日志

    背景 spring cloud netfix组件中,feign相关的日志默认是不会输出的,需要自定义配置才能输出,并且Feign只对Debug基本的日志做出响应, 实际业务需要输出Info级别的日志, ...

  3. 19 IO流(十六)——Commons工具包,FileUtils(一)

    Commons包的API:自己查吧懒得传云 Commons包的导入方法 Commons是一个java的IO开源工具,导入方法: 从apache.org下载commons包 解压 copy其中的comm ...

  4. Java集合对比

    1.array和ArrayList 的区别?1.1:ArrayList是Array的复杂版本1.2:数组不能扩容集合可以扩容1.3:存储的数据类型:Array只能存储相同数据类型的数据,而ArrayL ...

  5. 【C#】课堂知识点#4

    1.回顾类中基本结构. 成员分为: a.(数据成员) , b.(方法成员) 数据成员: 字段 方法成员:方法,构造函数,属性,索引器,运算符. 属性的作用: 对字段进行访问提供get,set方法. 类 ...

  6. tkinter学习笔记_06

    12.弹窗 messagebox import tkinter as tk from tkinter import messagebox root = tk.Tk() root.title(" ...

  7. 在 centos 上安装 virutalbox

    目录 简介 centos 6.x 安装 virtual box Step 1 – Add Required Yum Repositories Step 2 – Install Required Pac ...

  8. C# HtmlAgilityPack+Selenium爬取需要拉动滚动条的页面内容

    现在大多数网站都是随着滚动条的滑动加载页面内容的,因此单纯获得静态页面的Html是无法获得全部的页面内容的.使用Selenium就可以模拟浏览器拉动滑动条来加载所有页面内容. 前情提要 C#HtmlA ...

  9. C# vb .net实现高斯模糊

    在.net中,如何简单快捷地实现Photoshop滤镜组中的高斯模糊效果呢?答案是调用SharpImage!专业图像特效滤镜和合成类库.下面开始演示关键代码,您也可以在文末下载全部源码: 设置授权 第 ...

  10. Java Web 深入分析(11) JVM(1)

    前言 Java启动后作为一个进程运行在操作系统中,该进程要分配的内存有以下几个: 1.Java堆: 存储java内存区域,堆大小是在jvm启动时就像操作系统申请完成,其中 -Xmx和-Xms 分别表示 ...