JS篇JavaScript-执行上下文和提升

我们通常将 JavaScript 归类为动态或解释执行语言,但实际上它也是一门编译语言,它有自己的编译器形式,运行在 JavaScript 引擎中。 每个 Web 浏览器都有自己的 JavaScript 引擎形式:Chrome 有 V8,Mozilla 有 SpiderMonkey 等。这些 JavaScript 引擎的共同点都是将 JavaScript 代码转换为编译器可以理解的语言,然后执行它。 执行上下文 Execution Context当 JavaScript 代码运行的时候,运行 JavaScript 代码的环境形成了执行上下文 ,执行上下文决定代码可以访问哪些变量、函数、对象等。 我们将执行上下文简单视为运行当前代码的 environment / scope,我们知道作用域分为 global scope 和 local scope。 类似的,执行上下文也分为不同的类型: 全局执行上下文 - 代码首次执行时候的默认环境,在代码的整个执行过程中,只用一个全局执行上下文。 函数执行上下文 - 每当执行流程进入到一个函数体内部的时候,就会创建一个函数执行上下文,可以有任意数量的函数执行上下文。 执行栈/调用栈JavaScript 是单线程的,浏览器只分配给 JavaScript 一个主线程,一次只能执行一个任务(函数),因此它在执行栈中对其他操作(事件和函数执行)形成一个任务队列,排队等候执行。 每当在浏览器中加载脚本时,栈 stack 中的第一个元素就是全局执行上下文。当有函数执行时,将创建一个函数执行上下文,并将其置于全局执行上下文之上。一旦函数执行完成,它就会从执行堆栈中弹出,并将控制权交给它下面的上下文中。结合上面说到的,我们看一个例子: var name = "global variable";console.log(name)function func1() { console.log("func1 被调用了。") func2();}function func2() { console.log("func2 被调用了。");}func1(); ...

July 13, 2019 · 2 min · jiezi

js深入三作用域链与闭包

在之前我们根绝对象的原型说过了js的原型链,那么同样的js 万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链 作用域链首先先来回顾一下之前讲到的原型链的寻找机制,就是实例会先从本身开始找,没有的话会一级一级的网上翻,直到顶端没有就会报一个undefined 同样的js的机制就是这样的,函数在执行的时候会先函数本身的上下文的变量对象中查找,没有的话,也会从这个函数被创建的时候的父级的执行上下文的变量对象中去找(词法环境),一直找到全局上下文的变量对象(比如客户端的window对象),这个多层的执行上下文的链式关系就是函数的作用域链 盗一张图 作用域被创建的时机大家可以看到,我在控制台声明了一个函数,并且打印了他,这个a函数的里边有一个[[scope]]属性,这是一个内部属性,当一个函数被创建的时候,会保存所有的父级的变量对象(词法环境)到这个里边,比如说上图中 就有一个global 属性展开后,往下找你会发现很多我们常见的属性和方法,比如alert等等 如图 函数作用域的生命周期姑且叫他生命周期,我是这么理解的,当进入一个函数的上下文,经历了创建阶段之后,就会把函数的作用域链创建出来,直到销毁这个上下文,这个作用域链也是存在的 先来一个正经的例子 function a(){ var aaa = 'aaa'; return aaa;}checkscope();这个函数的生命周期是这样的 首先函数被创建,先把函数的作用域链保存到函数的[[scope]]属性上边a.[[scope]] = [ globalContext.VO//这个也就是我们上边图片里边看的golbal];globalContext 全局上下文 VO 这个之前没有介绍 是Variable object的简称,也就是之前经常提到的变量对象还有一个AO ,这个AO指的是函数被激活的时候(被执行)得活动对象创建完成之后,执行到a函数,创建了a函数得执行上下文,并压入执行栈里边现在执行栈里边已经有了两个执行上下文一个globalContext还有一个aContext 到了a函数之后,首先会做一些列得准备工作,就是之前讲到得函数得arguments,this等等首先第一步复制之前得[[scope]]属性,创建作用域链 aContext = { Scope: a.[[scope]],}然后开始初始化活动变量 argments对象 形参,函数声明,变量声明等等 最后把把活动变量也塞到作用域链中去 以上,一个函数得准备工作就算是做完了,然后下一步就是函数得执行阶段 之前讲过,在之后阶段得时候函数会根据代码给之前得活动对象赋值,然后执行里边得代码,直到执行完毕最后,函数执行完毕,函数得上下文被从上下文栈中弹出销毁在弹出得最后时候,a函数得结构大概长成这个样子 aContext = { AO: { arguments: { length: 0 }, }, Scope: [AO, [[Scope]]]}接下来我们在举一个不正经得例子,就是为了证明一下作用域链即使在函数被销毁后,也会存在这么一个事实 闭包首先什么是闭包,闭包是指在一个函数内部能够访问不是函数得参数,也不是局部变量得函数,所以广义得讲我们用的所有得函数都是可算作是闭包,都能访问全局变量。。。 不过工作中不是这样子得,说正题,给上边得问题举个例子 var item = '1'function a(){ var item = '2' function b(){ return item } return b;}var foo = a();foo();试着猜想一下这段代码得执行过程 ...

July 10, 2019 · 1 min · jiezi

7JavaScript-函数高级作用域与作用域链

JavaScript函数高级——作用域与作用域链一、作用域作用域个数 = n(定义的函数个数) + 1(全局作用域)(1)理解 就是一块"地盘", 一个代码段所在的区域。它是静态的(相对于上下文对象), 在编写代码时就确定了。(2)分类 全局作用域函数作用域ES6中新增了块级作用域(3)作用 隔离变量,不同作用域下同名变量不会有冲突。 二、作用域与执行上下文(1)区别1 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时。全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建。函数执行上下文环境是在调用函数时, 函数体代码执行之前创建。(2)区别2 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化。上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放。(3)联系 上下文环境(对象)是从属于所在的作用域。全局上下文环境==>全局作用域函数上下文环境==>对应的函数作用域 三、作用域链(1)理解 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)查找变量时就是沿着作用域链来查找的。(2)查找一个变量的查找规则 1)在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2)。2)在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3)。3)再次执行2)的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常。var a = 2;function fn1() { var b = 3; function fn2() { var c = 4; console.log(c); console.log(b); console.log(a); console.log(d); } fn2();}fn1(); 四、作用域_面试题面试题1 var x = 10;function fn() { console.log(x);}function show(f) { var x = 20; f();}show(fn); // 结果 10// 由于fn()的作用域中没有找到属性x,则会去fn()的上一级作用域也就是全局作用域中找,找到x=10,因此打印10.面试题2 ...

June 27, 2019 · 1 min · jiezi

javascript系列javascript深入理解作用域作用域链闭包的面试题解

一、概要作用域和作用域链是js中非常重要的特性,关系到理解整个js体系,闭包是对作用域的延伸,其他语言也有闭包的特性。 那什么是作用域?作用域指的是一个变量和函数的作用范围。 1、js中函数内声明的所有变量在函数体内始终是可见的; 2、在ES6中有全局作用域和局部作用域,但是没有没有块级作用域(catch只在其内部生效); 3、局部变量的优先级高于全局变量。 二、作用域我们来举几个栗子: 2.1变量提升var scope="global";function scopeTest(){ console.log(scope); var scope="local" }scopeTest(); //undefined上面的代码输出是undefined,这是因为局部变量scope变量提升了,等效于下面 var scope="global";function scopeTest(){ var scope; console.log(scope); scope="local" }scopeTest(); //undefined注意,如果在局部作用域中忘记var,那么变量就被声明为全局变量。 var scope="global";function scopeTest(){ console.log(scope); scope="local" }scopeTest(); //globalvar scope="global";function scopeTest(){ scope="local" console.log(scope);}scopeTest(); //local2.2没有块级作用域和我们其他常用语言不同的是,js中没有块级作用域 var data = [];for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); };}data[0](); // 3data[1](); // 3data[2](); // 32.3作用域链每个函数都有自己的执行上下文环境,当代码在这个环境中执行时候,会创建变量对象的作用域链, 那什么是作用域链?作用域链式是一个对象列表。 作用域链的作用?他保证了变量对象的有序访问。 作用域链开始的地方:当前代码执行环境的变量对象,常被称之为“活跃对象”(AO),变量的查找会从第一个链的对象开始,如果对象中包含变量属性,那么就停止查找,如果没有就会继续向上级作用域查找,直到找到全局对象中,如果找不到就会报ReferenceError。 2.4闭包function createClosure(){ var name = "jack"; return { setStr:function(){ name = "rose"; }, getStr:function(){ return name + ":hello"; } }}var builder = new createClosure();builder.setStr();console.log(builder.getStr()); //rose:hello上面在函数中反悔了两个闭包,这两个闭包都维持着对外部作用域的引用,因此不管在哪调用都是能够访问外部函数中的变量。在一个函数内部定义的函数,闭包中会将外部函数的自由对象添加到自己的作用域中,所以可以通过内部函数访问外部函数的属性,这就是js模拟私有变量的一种方式。 ...

May 29, 2019 · 2 min · jiezi

javascript深入理解从作用域链理解闭包

一、概要红宝书(P178)对于闭包的定义:闭包就是有权访问另外一个函数作用域中变量的函数。 MDN,对于闭包的定义:闭包就是指能够访问自由变量的函数。 那么什么是自由变量?自由变量就是在函数中使用,但既不是函数参数arguments,也不是函数的局部变量的变量,就是说另外一个函数作用域中的变量。 闭包组成?闭包 = 函数 + 函数能够访问的变量 文章首发地址于sau交流学习社区:https://www.mwcxs.top/page/57... 二、分析举个栗子: var a = 1;function foo() { console.log(a);}foo();foo函数可以访问到变量a,但是a并不是foo函数的局部变量,也不是foo函数的参数,所以a就是自由变量,那么函数foo+foo函数可以访问自由变量a不就是构成了一个闭包嘛。 我们再来看一个栗子: function getOuter(){ var date = '1127'; function getDate(str){ console.log(str + date); //访问外部的date } return getDate('今天是:'); //"今天是:1127"}getOuter();其中date不是函数getDate的参数,也不是局部变量,所以date是自由变量。 总结起来就是两点: 1、是一个函数(比如:内部函数从父函数中返回) 2、能够访问上级函数作用域中的变量(哪怕上级函数的上下文已经销毁) 然后我们再来看一个栗子(来自《JavaScript权威指南》)来分析: var scope = "global scope";function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f;}var foo = checkscope(); // foo指向函数ffoo();这时候需要我们来分析一下这段代码中执行上下文栈和执行上下文的变化情况。 简要的分析一下执行过程: 1、进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈; 2、全局执行上下文初始化; 3、执行checkscope函数,创建sheckscope函数执行上下文,checkscope执行上下文被压入执行上下文栈; 4、checkscope执行上下文初始化,创建变量对象,作用域链,this等; ...

May 27, 2019 · 2 min · jiezi

javascript系列javascript深入浅出图解作用域链和闭包

一、概要对于闭包的定义(红宝书P178):闭包就是指有权访问另外一个函数的作用域中的变量的函数。 关键点: 1、闭包是一个函数 2、能够访问另外一个函数作用域中的变量 二、闭包特性对于闭包有下面三个特性: 1、闭包可以访问当前函数以外的变量 function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //访问外部的date} return getDate('今天是:'); //"今天是:815"}getOuter(); 2、即使外部函数已经返回,闭包仍能访问外部函数定义的变量 function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //访问外部的date} return getDate; //外部函数返回}var today = getOuter();today('今天是:'); //"今天是:815"today('明天不是:'); //"明天不是:815" 3、闭包可以更新外部变量的值 function updateCount(){ var count = 0; function getCount(val){ count = val;console.log(count);} return getCount; //外部函数返回}var count = updateCount();count(815); //815count(816); //816 三、作用域链javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其他数据,决定它们各自的行为。每一个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。你可以把它当做Javascript的一个普通对象,但是你只能修改它的属性,却不能引用它。 变量对象也是有父作用域的。 作用域链定义:当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不再存在父作用域了,这就是作用域链。 ...

May 23, 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

作用域链、垃圾回收机制、闭包及其应用(oop)

执行环境、变量对象 / 活动对象、作用域链执行环境(executioncontext,为简单起见,有时也称为“环境”)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variableobject),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scopechain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activationobject)作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。—- 摘自 JavaScript高级程序设计理论说完,直接上代码。function Fn() { var count = 0 function innerFn() { count ++ console.log(‘inner’, count) } return innerFn}var fn = Fn()document.querySelector(’#btn’).addEventListener(‘click’, ()=> { fn() Fn()()})1、 浏览器打开,进入全局执行环境,也就是window对象,对应的变量对象就是全局变量对象。在全局变量对象里定义了两个变量:Fn和fn。2、当代码执行到fn的赋值时,执行流进入Fn函数,Fn的执行环境被创建并推入环境栈,与之对应的变量对象也被创建,当Fn的代码在执行环境中执行时,会创建变量对象的一个作用域链,这个作用域链首先可以访问本地的变量对象(当前执行的代码所在环境的变量对象),往上可以访问来自包含环境的变量对象,如此一层层往上直到全局环境。Fn的变量对象里有两个变量:count和innerFn,其实还有arguments和this,这里先忽略。然后函数返回了innerFn函数出去赋给了fn。3、手动执行点击事件。首先,执行流进入了fn函数,实际上是进入了innerFn函数,innerFn的执行环境被创建并推入环境栈,执行innerFn代码,通过作用域链对Fn的活动对象中的count进行了+1,并且打印。执行完毕,环境出栈。然后,执行流进入了Fn函数,Fn的执行跟第2步的一样,返回了innerFn。接着执行了innerFn函数,innerFn的执行跟前面的一样。每一次点击都执行了fn, Fn, innerFn,而fn和innerFn其实是一样逻辑的函数,但控制台打印出来的结果却有所不同。点击了3次的结果,接下来进入闭包环节。闭包垃圾回收机制先介绍下垃圾回收机制。离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。“标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。—- 摘自 JavaScript高级程序设计通俗点说就是:1、函数执行完了,其执行环境会出栈,其变量对象自然就离开了作用域,面临着被销毁的命运。但是如果其中的某个变量被其他作用域引用着,那么这个变量将继续保持在内存当中。2、全局变量对象在浏览器关闭时才会被销毁。接下来看看上面的代码。对了先画张图。现在就解释下为什么会有不同的结果。Fn()() — 执行Fn函数,return了innerFn函数并立即执行了innerFn函数,因为innerFn函数引用了Fn变量对象中的count变量,所以即使Fn函数执行完了,count变量还是保留在内存中。等innerFn执行完了,引用也随之消失,此时count变量被回收。所以每次运行Fn()(),count变量的值都是1。fn() — 从fn的赋值开始说起,Fn函数执行后return了innerFn函数赋值给了fn。从这个时候开始Fn的变量对象中的count变量就被innerFn引用着,而innerFn被fn引用着,被引用的都存在于内存中。然后执行了fn函数,实际上执行了存在于内存中的innerFn函数,存在于内存中的count++。执行完成后,innerFn还是被fn引用着,由于fn是全局变量除了浏览器关闭外不会被销毁,以至于这个innerFn函数没有被销毁,再延申就是innerFn引用的count变量也不会被销毁。所以每次运行fn函数实际上执行的还是那个存在于内存中的innerFn函数,自然引用的也是那个存在于内存中的count变量。不像Fn()(),每次的执行实际上都开辟了一个新的内存空间,执行的也是新的Fn函数和innerFn函数。闭包的用途1、通过作用域访问外层函数的私有变量/方法,并且使这些私有变量/方法保留再内存中2、避免全局变量的污染3、代码模块化 / 面向对象编程oop举个例子function Animal() { var hobbies = [] return { addHobby: name => {hobbies.push(name)}, showHobbies: () => {console.log(hobbies)} }}var dog = Animal()dog.addHobby(’eat’)dog.addHobby(‘sleep’)dog.showHobbies()定义了一个Animal的方法,里面有一个私有变量hobbies,这个私有变量外部无法访问。全局定义了dog的变量,并且把Animal执行后的对象赋值给了dog(其实dog就是Animal的实例化对象),通过dog对象里的方法就可以访问Animal中的私有属性hobbies。这么做可以保证私有属性只能被其实例化对象访问,并且一直保留在内存中。当然还可以实例化多个对象,每个实例对象所引用的私有属性也互不相干。当然还可以写成构造函数(类)的方式function Animal() { var hobbies = [] this.addHobby = name => {hobbies.push(name)}, this.showHobbies = () => {console.log(hobbies)}}var dog = new Animal() ...

March 19, 2019 · 1 min · jiezi

JS基础——作用域链与执行环境

每一个函数存在一个[[Scope]]内部属性,包含了一个函数被创建得作用域中对象得集合,这个集合为函数得作用域链。例如下面的全局函数:fucntion add(num1, num2){ var sum = num1 + num2; return sum;}当函数add被创建时,它的作用域链中便插入了一个对象变量,里面包含所有在全局范围内定义的变量。函数add的作用域会在函数执行时用到,函数每次执行都会创建一个执行环境的内部对象,每个执行环境都有自己的作用域链。函数运行时,一个被称为活动对象的新对象就为执行环境创建好了,里面包含了函数的所有局部变量,命名参数,参数集合以及this。假设执行var total = add(5,10),其对应的作用域链如下:函数执行过程中,变量的查找时从作用域头部开始查找,如果找到就是使用改变量的值。如果找不到就继续从作用域下一个对象查找,直到找到改变量。如过匹配不到,则为undefined。当函数执行完成,活动对象也会随之销毁。当频繁使用全局变量时,可以先使用一个局部变量保存起来,之后直接访问局部变量可以减少查找次数提高效率。例如:function initUI(){ var doc = document, bd = doc.body, links = doc.getElementsByTagName(“a”); ….}资料:《高性能JavaScript》第2章节

February 27, 2019 · 1 min · jiezi

深入学习js之——作用域链

开篇作用域是每种计算机语言最重要的基础之一,因此要想深入的学习JavaScript,作用域和作用域链就是个绕不开的话题。在《深入学习js之—-执行上下文栈》中我们提到过,当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。对于每个执行上下文,都有三个重要属性:变量对象(Variable object,VO)作用域链(Scope chain)this今天重点聊聊作用域链。作用域细说作用域链之前,我们首先来聊聊作用域,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种(局部作用域又称为函数作用域)。作用域链代码在当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。函数创建在《深入学习js之——词法作用域和动态作用域》中讲到,函数的作用域在函数定义的时候就决定了——即JavaScript采用的是静态作用域。这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!举个例子:function foo(){ function bar(){ … }}函数创建时,各自的[[scope]]为:foo.[[scope]] = [ globalContext.VO];bar.[[scope]] = [ fooContext.AO, globalContext.VO];函数激活当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。这时候执行上下文的作用域链,我们命名为 Scope:Scope = [AO].concat([[Scope]]);至此,作用域链创建完毕。通过例子深刻理解以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:var scope = “global scope”;function checkscope(){ var scope2 = ’local scope’; return scope2;}checkscope();执行过程如下:1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]];checkscope.[[scope]] = [ globalContext.VO];2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈;ECStack = [ checkscopeContext, globalContext];3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链;checkscopeContext = { Scope: checkscope.[[scope]],}4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明;checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]],}5.第三步:将活动对象压入 checkscope 作用域链顶端;checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]]}6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值;checkscopeContext = { AO: { arguments: { length: 0 }, scope2: ’local scope’ }, Scope: [AO, [[Scope]]]}7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出;ECStack = [ globalContext];参考:《JavaScript深入之作用域链》《深入了解JavaScript,从作用域链开始》欢迎添加我的个人微信讨论技术和个体成长。欢迎关注我的个人微信公众号——指尖的宇宙,更多优质思考干货 ...

February 23, 2019 · 1 min · jiezi

JS 总结之函数、作用域链

JavaScript 中,函数实际上是一个对象。???? 声明JavaScript 用 function 关键字来声明一个函数:function fn () {}变体:函数表达式:var fn = function () {}这种没有函数名的函数被称为匿名函数表达式。???? return函数可以有返回值function fn () { return true}位于 return 之后的任何代码都不会执行:function fn () { return true console.log(false) // 永远不会执行}fn() // true没有 return 或者只写 return,函数将返回 undefined:function fn () {}fn() // undefined// 或者function fn () { return}fn() // undefined⛹ 参数函数可以带有限个数或者不限个数的参数// 参数有限function fn (a, b) { console.log(a, b)}// 参数不限function fn (a, b, …, argN) { console.log(a, b, …, argN)}没有传值的命名参数,会被自动设置为 undefined// 参数有限function fn (a, b) { console.log(b) // undefined}fn(1)???? arguments函数可以通过内部属性 arguments 这个类数组的对象来访问参数,即便没有命名参数:// 有命名参数function fn (a, b) { console.log(arguments.length) // 2}fn(1, 2)// 无命名参数function fn () { console.log(arguments[0], arguments[1], arguments[2]) // 1, 2, 3}fn(1, 2, 3)⛳️ 长度arguments 的长度由传入的参数决定,并不是定义函数时决定的。function fn () { console.log(arguments.length) // 3}fn(1, 2, 3)如果按定义函数是决定个的,那么此时的 arguments.length 应该为 0 而不为 3。???? 同步arguments 对象中的值会自动反应到对应的命名参数,可以理解为同步,不过并不是因为它们读取了相同的内存空间,而只是保持值同步而已。function fn (a) { console.log(arguments[0]) // 1 a = 2 console.log(arguments[0]) // 2 arguments[0] = 3 console.log(a) // 3}fn(1)严格模式下,重写 arguments 的值会导致错误。 ???? callee通过 callee 这个指针访问拥有这个 arguments 对象的函数function fn () { console.log(arguments.callee) // fn}fn()???? 类数组长的跟数组一样,可以通过下标访问,如 arguments[0],却无法使用数组的内置方法,如 forEach 等:function fn () { console.log(arguments[0], arguments[1]) // 1, 2 console.log(arguments.forEach) // undefined}fn(1, 2)通过对象那章知道,可以用 call 或者 apply 借用函数,所以 arguments 可以借用数组的内置方法:function fn () { Array.prototype.forEach.call(arguments, function (item) { console.log(item) })}fn(1, 2)// 1// 2对于如此诡异的 arguments,我觉得还是少用为好。???? this、 prototype具体查看总结:《关于 this 应该知道的几个点》《原型》???? 按值传递引用《JavaScript 高级程序设计》4.1.3 的一句话:ECMAScript 中所有函数的参数都是按值传递的,也就是说,把函数外部的值复制给函数内部的参数,就和把一个变量复制到另一个变量一样。???? 基本类型的参数传递基本类型的传递很好理解,就是把变量复制给函数的参数,变量和参数是完全独立的两个个体:var name = ‘jon’function fn (a) { a = ‘karon’ console.log(‘a: ‘, a) // a: karon}fn(name)console.log(’name: ‘, name) // name: jon用表格模拟过程:栈内存 堆内存 name, a jon 将 a 复制为其他值后:栈内存 堆内存 name jon a karon ???? 引用类型的参数传递var obj = { name: ‘jon’}function fn (a) { a.name = ‘karon’ console.log(‘a: ‘, a) // a: { name: ‘karon’ }}fn(obj)console.log(obj) // name: { name: ‘karon’ }嗯?说好的按值传递呢?我们尝试把 a 赋值为其他值,看看会不会改变了 obj 的值:var obj = { name: ‘jon’}function fn (a) { a = ‘karon’ console.log(‘a: ‘, a) // a: karon}fn(obj)console.log(obj) // name: { name: ‘jon’ }???? 真相浮出水面参数 a 只是复制了 obj 的引用,所以 a 能找到对象 obj,自然能对其进行操作。一旦 a 赋值为其他属性了,obj 也不会改变什么。用表格模拟过程:栈内存 堆内存 obj, a 引用值 { name: ‘jon’ } 参数 a 只是 复制了 obj 的引用,所以 a 能找到存在堆内存中的对象,所以 a 能对堆内存中的对象进行修改后:栈内存 堆内存 obj, a 引用值 { name: ‘karon’ } 将 a 复制为其他值后:栈内存 堆内存 obj 引用值 { name: ‘karon’ } a ‘karon’ 因此,基本类型和引用类型的参数传递也是按值传递的???? 作用域链理解作用域链之前,我们需要理解执行环境 和 变量对象。???? 执行环境执行环境定义了变量或者函数有权访问的其它数据,可以把执行环境理解为一个大管家。执行环境分为全局执行环境和函数执行环境,全局执行环境被认为是 window 对象。而函数的执行环境则是由函数创建的。每当一个函数被执行,就会被推入一个环境栈中,执行完就会被推出,环境栈最底下一直是全局执行环境,只有当关闭网页或者推出浏览器,全局执行环境才会被摧毁。???? 变量对象每个执行环境都有一个变量对象,存放着环境中定义的所有变量和函数,是作用域链形成的前置条件。但我们无法直接使用这个变量对象,该对象主要是给 JS 引擎使用的。具体可以查看《JS 总结之变量对象》。???? 作用域链的作用而作用域链属于执行环境的一个变量,作用域链收集着所有有序的变量对象,函数执行环境中函数自身的变量对象(此时称为活动对象)放置在作用域链的最前端,如:scope: [函数自身的变量对象,变量对象1,变量对象2,…, 全局执行环境的变量对象]作用域链保证了对执行环境有权访问的所有变量和函数的有序访问。var a = 1function fn1 () { var b = 2 console.log(a,b) // 1, 2 function fn2 () { var c = 3 console.log(a, b, c) // 1, 2, 3 } fn2()}fn1()对于 fn2 来说,作用域链为: fn2 执行环境、fn1 执行环境 和 全局执行环境 的变量对象(所有变量和函数)。对于 fn1 来说,作用域链为: fn1 执行环境 和 全局执行环境 的变量对象(所有变量和函数)。总结为一句:函数内部能访问到函数外部的值,函数外部无法范围到函数内部的值。引出了闭包的概念,查看总结:《JS 总结之闭包》???? 箭头函数ES6 新语法,使用 => 定义一个函数:let fn = () => {}当只有一个参数的时候,可以省略括号:let fn = a => {}当只有一个返回值没有其他语句时,可以省略大括号:let fn = a => a// 等同于let fn = function (a) { return a}返回对象并且没有其他语句的时候,大括号需要括号包裹起来,因为 js 引擎认为大括号是代码块:let fn = a => ({ name: a })// 等同于let fn = function (a) { return { name: a }}箭头函数的特点:没有 this,函数体内的 this 是定义时外部的 this不能被 new,因为没有 this不可以使用 arguments,可以使用 rest 代替不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。???? 参考《JavaScript 深入之参数按值传递》 by 冴羽《ECMAScript 6 入门》函数的扩展 - 箭头函数 by 阮一峰《JavaScript 高级程序设计》第三章 基本概念、第四章 4.1.3 传递参数、第四章 4.2 执行环境及作用域 ...

December 24, 2018 · 3 min · jiezi