JS核心知识点梳理上下文作用域闭包this上

引言满满的干货,面试必bei系列,参考大量资料,并集合自己的理解以及相关的面试题,对JS核心知识点中的作用域、闭包、this、上下文进行了梳理。由于篇幅有限,这里只对我认为最重要的知识做了介绍,一些常识性的东西大家可以参考高程。 上下文(execution context)又叫执行环境,环境。 执行环境定义了变量或者环境有权访问的其他数据,据定了它们的各自行为 --高程一个函数执行的时候,会产生一个属于自己的执行环境。环境里面有一个变量对象variable object(VO),OA里面存放着环境中定义的所有变量和函数,作用域链(scope chain),this。函数执行,环境产生被推入环境栈,函数执行完,环境出栈并被销毁(闭包例外),把控制权返回给之前的执行环境。 作用域js中的作用域是静态作用域,静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见(visibility);在这段区域以外该变量不可见(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。--wiki作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。--你不知道的javascript作用域就是执行的时候,给环境变量赋值的一种规则,这种规则在函数定义的时候就已经确定了,和运行无关。js只有全局作用域和函数作用域,没有块作用域。 变量提升我们把定一个变量的行为分为两个过程,声明和定义 var a = 1//实际执行的是下面两步var a a = 1var的变量声明会提升,没有var就是全局变量,let没,const有变量提升var的函数声明和赋值都提升 a //undefined 因为a的声明已经提升到最上面了var a = 1f() //alert 1function f () { alert (1)}有几个特殊的地方虽然平时不会这么写,但是面试题会遇到: 函数体中,return后面的代码不进行变量提升,但是return下面的代码要进行变量提升不管条件是否成立,都要进行变量提升;匿名函数不进行 变量提升;如果变量名字发生重复,那么不再重复声明,但是要重新定义;执行环境和作用域的关系很多人都分不清楚执行环境和作用域的关系。其实很简单,作用域和上下文完全是两个不相干的东西。作用域是一种规格,声明函数的时候就已经确定了。执行环境是函数执行的时候产生的,函数在执行环境中执行。大家看下面例子 alert(a) //a is not defined执行的时候VO里面没有a,因为根据VO作用域链【windows】,按照规则找不到a。 var a = 1alert(a) // alert 1执行的时候,根据规则,从VO作用域链【windows】头部window作用域开始找a,找到a了,a为1,则vo中a设置为1,所以alert 1 var a = 1function foo() { var a = 100 alert(a) }foo() // alert 100执行的时候,根据规则,从VO作用域链【windows-foo】头部foo作用域开始找a,找到a了,a为100,则vo中a设置为100,所以alert 100 var a = 1function foo() { alert(a) }foo() // alert 100执行的时候,根据规则,从VO作用域链【windows-foo】头部foo作用域开始找,没找到a。根据规则,沿上层作用域(也就是window)开始找,找到a了,aw为1。则vo中a设置为1,所以alert 1 ...

May 18, 2019 · 1 min · jiezi

还是不明白JavaScript - 执行环境、作用域、作用域链、闭包吗?

JavaScript中的执行环境、作用域、作用域链、闭包一直是一个非常有意思的话题,很多博主和大神都分享过相关的文章。这些知识点不仅比较抽象,不易理解,更重要的是与这些知识点相关的问题在面试中高频出现。之前我也看过不少文章,依旧是似懂非懂,模模糊糊。最近,仔细捋了捋相关问题的思路,对这些问题的理解清晰深入了不少,在这里和大家分享。这篇文章,我会按照执行环境、作用域、作用域链、闭包的顺序,结合着JS中函数的运行机制来梳理相关知识。因为这样的顺序刚好也是这些知识点相互关联且递进的顺序,同时这些知识点都又与函数有着千丝万缕的联系。这样讲解,会更容易让大家彻底理解,至少我就是这样理解清晰的。废话不再多说,我们开始。执行环境首先,我们还是要理解一下什么是执行环境,这也是理清后面问题的基础。执行环境是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。——《JavaScript高级程序设计》抽象!不理解!没关系,我来解释:其实,执行环境就是JS中提出的一个概念,它是为了保证代码合理运行采用的一种机制。一种概念…机制…更抽象,那它到底是什么?实际上,执行环境在JS机制内部就是用一个对象来表示的,称作执行环境对象,简称环境对象。那么,这个执行环境对象到底又是何时、怎么产生的呢?有以下两种情况:在页面中的脚本开始执行时,就会产生一个“全局执行环境”。它是最外围(范围最大,或者说层级最高)的一个执行环境,对应着一个全局环境对象。在Web浏览器中,这个对象就是Window对象。当一个函数被调用的时候,也会创建一个属于该函数的执行环境,称作“局部执行环境”(或者称作函数执行环境),它也对应着自己的环境对象。因此,执行环境就分为全局执行环境和局部执行环境两种,每个执行环境都有一个属于自己的环境对象。既然执行环境是使用一个对象表示的,那么对象就有属性。我们来看看环境对象的三个有意思的属性。变量对象、[[scope]]、this。环境对象中的变量对象《JS高程》中明确说明,执行环境定义了变量或函数有权访问的其他数据。那么这些数据到底被放(存储)在哪里呢?其实,每个执行环境都有一个与之关联的变量对象,在环境中定义的所有变量和函数都保存在这个对象中。我们在代码无法访问这个对象,但解析器在处理数据时会在内部使用它。通俗地说就是:一个执行环境中的所有变量和函数都保存在它对应的环境对象的变量对象(属性)中。认识[[scope]]前先理解作用域在讲[[scope]]前,我们就需要先弄清楚什么是作用域了。因为作用域与[[scope]]之间存在着非常紧密的关系。《JS高程》中没有明确给出作用域的定义和描述。其实,作用域就是变量或者函数可以被访问的代码范围,或者说作用域就是变量和函数所起作用的范围。这样看来作用域也像是一个概念,它是用来描述一个区域(或者说范围)的。在JS中,作用域分为全局作用域、局部作用域两种。我们来看看这两种作用域的具体描述:①在页面中的脚本开始执行时,就会产生一个“全局作用域”。它是最外围(范围最大,或者说层级最高)的一个作用域。全局作用域的变量、函数可以在代码的任何地方访问到。②当一个函数被创建的时候,会创建一个“局部作用域”。局部作用域中的函数、变量只能在某些局部代码中可以访问到。看一个例子:var g = ‘Global’;function outer() { var out = ‘outer’; function inner() { var inn = ‘inner’; }} 上面这个例子,产生的作用域就如下图所示:请注意上面①、②这两段话!!!是不是觉得很熟悉,似曾相识?!没错,这两段话和介绍全局/局部执行环境(全局/局部环境对象)时候的描述几乎一摸一样!作用域是不是和环境对象有着千丝万缕的联系呢?与此同时,我们再仔细回忆一下:1、作用域就是变量或者函数可以被访问的代码范围。2、一个执行环境中定义的所有变量和函数都保存在它对应的环境对象中。结合上面所述,其实不难得出:尽管作用域的描述更像是一个概念,但如果一定要将它具象化,问它到底是什么东西,与执行环境有什么关系?其实,作用域所对应的(不是相等、等于)是环境对象中的变量对象。明白了这些,我们就可以来看看环境对象中的[[scope]]属性。环境对象中的[[scope]]首先,要明确的是,环境对象中的[[scope]]属性值是一个指针,它指向该执行环境的作用域链。到底什么是作用域链呢?作用域链本质上就是一个有序的列表,而列表中的每一项都是一个指向不同环境对象中的变量对象的指针。那么,这个作用域链到底是怎么形成的呢?它里面指向变量对象的指针的顺序又是如何规定的呢?我们用下面这个简单的例子说明。var g = ‘Hello’;function inner() { var inn = ‘Inner’; var res = g + inn; return res;}inner();当执行了inner();这一行代码后,代码执行流进入inner函数内部,此时,JS内部会先创建inner函数的局部执行环境,然后创建该环境的作用域链。这个作用域链的最前端,就是inner执行环境自己的环境对象中的变量对象,作用域链第二项,就是全局环境的环境对象中的变量对象。这条作用域链如下图所示:形成了这样的作用域链之后,就可以有秩序地访问一个变量了。以这个例子为例:当执行inner();进入函数体内后,执行g + inn;一行,需要访问变量g、inn,此时JS内部机制就会沿着这条作用域链查找所需变量。在当前inner函数的作用域中找到了变量inn,值为’Inner’,查找终止。但是却没有找到变量g,于是沿着作用域链向上查找,进入全局作用域,在全局变量对象中找到了变量g,值为’Hello’,查找终止。计算得出res为’HelloInner’,并在最后返回结果。与上面所讲机制完全相同,如果是多层执行环境嵌套,则作用域链是这么形成的:当代码执行进入一个执行环境时,JS内部会开始创建该环境的作用域链。作用域链的最前端,始终都是当前执行环境的执行环境对象中的变量对象。如果这个环境是局部执行环境(函数执行环境),则将其活动对象作为变量对象。作用域链中的下一个是来自外层环境对象的变量对象,而再下一个则是来自再外层环境对象的变量对象…… 这样一直延续到全局环境对象的变量对象。所以,全局执行环境的变量对象始终都是作用域链中的最后一个对象。讲到这里,可能你已经对执行环境、执行环境对象、变量对象、作用域、作用域链的理解已经他们之间的关系有了一个较清晰的认识。也有可能,对这么多的抽象问题还是有些懵懵懂懂。没关系,我们用下面这一张图,将上面的所有内容串联起来,来直观感受和理解他们。var g = ‘Global’;function outer() { var out = ‘outer’; function inner() { var inn = ‘inner’; } inner();}outer();对于这张图,有一些需要注意的地方:当函数调用时,才会创建函数的执行环境和它的环境对象,再创建函数的活动对象,再创建函数环境的作用域链。上图中间一列变量对象中,outer、inner的变量对象其实是该函数的活动对象。全局环境是没有活动对象的,只有在函数环境中,才会使用函数的活动对象来作为它的变量对象。函数的活动对象,是在函数创建时使用函数内置的arguments类数组和其他命名参数来初始化的。所以实际上,函数的变量对象中应该还包含一个指向arguments类数组的指针。有了对作用域、作用域链的理解,最后,我们来说一说闭包。闭包什么是闭包闭包就是有权访问另一个函数作用域中的变量的函数。——《JavaScript高级程序设计》对于闭包,最简单的大白话可以这么理解:①外部函数声明内部函数,内部函数引用外部函数的局部变量,这些变量不会被释放!——这是我曾经看到的别人的说法或者这么理解:②当在一个函数中返回另一个函数的时候(是返回一个函数,不是返回函数的调用或者函数的执行结果),就会形成闭包,被返回的这个函数就叫做闭包函数。——这是我自己的理解上面两句话看似不同,其实本质是一样的。来看一个最简单的闭包的例子:function sum() { var num1 = 100; // 这里返回的是函数(体),不是函数的调用 return function(num2) { return num1 + num2; }}// 此时result指向sum返回的那个匿名函数// 注意!此时该匿名函数并没有被执行let result = sum();result(200);那么,上面几行代码,为什么就会形成闭包呢?我们来分析一下,代码执行中JS内部到底做了什么?首先,有一点必须明确,就是一般情况下,一个函数执行完内部的代码,函数调用时所创建的执行环境、环境对象(包括变量对象、[[scope]]等)都会被销毁,它们的生命周期就只有函数调用到函数执行结束这一段时间。但是上面的例子,就会出现例外。当执行sum()时,调用该函数,创建它的环境对象,其中作用域链中第一项是自己环境的变量对象,第二项是全局环境的变量对象。当创建匿名函数的时候,也会创建匿名函数的环境对象,其中作用域链第一项是自己环境的变量对象,第二项是sum环境的变量对象,第三项是全局变量对象。这时,问题就来了。按说,当函数sum执行完return之后,他的执行环境、变量对象、作用域链都会被销毁。可是这时候却不能销毁他的变量对象,因为返回的匿名函数(此时由result指向该函数)并没有执行,这个匿名函数的作用域链中还引用着sum函数的变量对象。换句话说,即使,sum执行完了,其执行环境的作用域链会被销毁,但是它的变量对象还会保存在内存中,我们在sum函数外部,还能访问到它内部的变量num1、num2,这就是形成闭包的真正原因。但是,当result()执行完后,这些变量对象、作用域链就会被销毁。闭包存在的问题因为闭包形成后,会在函数执行完仍将他的变量对象保存在内存中,当引用时间过长或者引用对象很多的时候,会占用大量内存,严重影响性能。来看下面的例子:(这个例子曾经是Tencent微众银行的笔试原题,出现在《JS高程》的7.2.3章节——P184)function assignHandler() { var element = document.getElementById(“someElement”); element.onclick = function(){ alert(element.id); };}assignHandler函数中定义的匿名函数是作为element元素的事件处理函数的,且内部使用了element元素(访问元素的id`),因此assignHandler函数执行完,对于element的引用也会一直存在,element元素会一直保存在内存中。将上面的例子改成下面这样,就能解决这个问题。function assignHandler(){ var element = document.getElementById(“someElement”); // 这里获取element的id,为其创建一个副本 // 这样是为了在下面事件处理函数中解除对element元素的引用 var id = element.id; element.onclick = function(){ alert(id); }; // 将element置为null,断开对element元素的引用 // 这样方便垃圾回收机制回收element所占的内存 element = null;} ...

March 29, 2019 · 1 min · jiezi