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

结合作用域执行上下文图解闭包

一 作用域相关      作用域是一套规则,用来管理引擎如何查找变量。在es5之前,js只有全局作用域及函数作用域。es6引入了块级作用域。但是这个块级别作用域需要注意的是不是{}的作用域,而是let,const关键字的块作用域。 1作用域1.1 全局作用域      在全局环境下定义的变量,是挂载在window下的。如下代码所示: 1.2 函数作用域       在函数内定义的变量,值在函数内部才生效,在函数外引用会报RefrenceError的错误       注意区分RefrenceError及TypeError。RefrenceError是在作用域内找不到,而TypeError则是类型错误。如果只是定义了变量a 直接调用便会报TypeError的错误。 1.3 块作用域       es新增的关键字let,const是作用在块级作用域。但是在js内{}形成的块,是不具有作用域的概念的。如下所示,虽然for循环有一个{}包裹的块,但是在块外面还是可以访问i的。 2 作用域链       所谓作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证当前执行环境对符合访问权限的变量和函数的有序访问。而作用域的最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。       如上图所示,会形成一个inner作用域到outer作用域到全局作用域的作用域链。当我们在执行inner函数的时候,需要outName的变量,在自己的作用域内找不到,便会顺着作用域链往上找,直到找到全局作用域。在这个例子中,往上查找到outer作用域的时候便找到了。       简单测试1:如下图所示的代码,大家觉得会输出什么呢?      虽然fn的调用是在show内调用的,但是因为fn所在的作用域是全局作用域,它的x的值会顺着作用域链去全局作用域中啊,即x会输出10。这里需要注意的一点是,变量的确定是在函数定义时候确定的,而不是函数运行时。 二 执行上下文相关       函数每次被调用时,都会产生一个新的执行上下文环境。全局上下文是存在栈中的。而处于栈顶的全局上下文一旦执行完就会自动出栈。如下图所示的代码。       首先是全局上下文入栈,然后开始执行可执行代码。遇到outer(),激活outer()的上下文;       第二步,outer的上下文入栈。开始执行outer内的可执行代码,直到遇到inner()。激活inner()的上下文;       第三步,inner的上下文入栈。开始执行inner内的可执行代码。执行完毕之后inner出栈。       第四步,inner的上下文出栈。outer内继续执行可执行代码。如果一直没有其他的执行上下文,执行完毕即可出栈; ...

June 15, 2019 · 2 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

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深入理解this作用域问题以及newletvarconst对this作用域的影响

理解this作用域《javascript高级程序设计》中有说到: this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象调用时,this等于那个对象。不过,匿名函数具有全局性,因此this对象同常指向window针对于匿名函数this具有全局性的观点仍是有争议的,可参考 https://www.zhihu.com/questio... 关于闭包经常会看到这么一道题: var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } };console.log(object.getNameFunc()());//result:The Window<font color="green">在这里,getNameFunc return了1个匿名函数,可能你会认为这就是输出值为<font color="blue">The Window</font>的原因 </font> 但是,我们再来尝试写1个匿名函数 var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return this.funAA; }, funAA:function(){ return this.name } }; console.log(object.getNameFunc()(),object.funAA())可以发现,同样是匿名函数,却输出了<font color="blue">The Window, My Object</font> 在作用域链中,执行函数时会创建一个称为“运行期上下文(execution context)”的内部对象,运行期上下文定义了函数执行时的环境。个人认为是因为<font color="red">函数执行在window作用域</font>下,getNameFunc的作用域链被初始化为<font color="red">window的[[Scope]]</font>所包含的对象,导致输出结果为window.name 对作用域链不是很了解的同学,可以查看这边文章【Javascript】深入理解javascript作用域与作用域链 实践是检验真理的唯一标准,让我们用代码测试一下 var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return this.funAA(); }, funAA:function(){ return this.name } };console.log(object.getNameFunc(),object.funAA())可以发现,输出了 <font color="blue">My Object, My Object</font> getNameFunc仍为匿名函数,但是return的是this.funAA(),此时,this.funAA变成了在getNameFunc作用域中执行,而不是在window下,验证了我们之前的猜想: ...

April 30, 2019 · 1 min · jiezi

JS篇你不知道的-JS-知识点总结一

typeof null 为 ”object” 解释不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null 的二进制表示都是0,自然前三位都是0,所以执行 typeof null 时,会返回 ”object”。传统编程语言编译的三个步骤分词/词法分析解析/语法分析代码生成变量的赋值操作编译阶段 -- 首次编译器会在当前作用域中声明一个变量(变量提升),如果之前没用声明过。运行阶段 -- 运行时引擎会在作用中查找该变量,如果能够找到就会对它赋值(LHS查询。作用域作用域是一套规则,用于确定在何处以及如何查找变量。如果查找的目的是对变量赋值,那么就会使用 LHS 查询。如果查找的目的是获取变量的值,那么就会使用 RHS 查询。无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。闭包当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。thisthis 在任何情况下都不指向函数的词法作用域。在 JavaScript 中作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在 JavaScript 引擎的内部。this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈),函数的调用方法,传入的参数等信息。this 就是记录其中的一个属性,会在函数执行的过程中用到。this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。调用栈 — 就是为了到达当前执行位置所调用到的所用函数。bind()会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。new 操作符使用 new 来调用函数时,会自动执行下面的操作: 创建一个新的空对象;这个对象会被执行[[原型]]连接;这个新对象会绑定到函数调用的 this;如果函数没用返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。判断 this可以根据下面的顺序来进行判断: 函数是否在 new 中调用(new绑定)如果是的话,this 是绑定的是新创建的对象。 var bar = new foo();函数是否是通过 call apply (显示绑定)或者硬绑定调用,如果是的话,this绑定的是指定的对象。 var bar = foo.call(obj2);函数是否在某个上下文对象中调用(隐式调用),如果是的话,this绑定的是那个上下文对象。 var bar = obj.foo();若果都不是的话,使用默认绑定,如果在严格模式下,就绑定到 undefined,否则绑定到全局对象上。 var bar = foo();ES6 新增可计算属性名var prefix = "foo";var myObject = { [prefix + "bar"]: "hello", [prefix + "baz"]: "world" }myObject["foobar"]; // hellomyObject["foobaz"]; // worldin / hasOwnProperty() -- 判断某个对象是否存在某个属性in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。hasOwnProperty() 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。forEach() / every() / sone() -- 历数组的值forEach() 会遍历数组中的所有值并忽略回调函数的返回值(忽略返回值)。every() 方法测试数组的所有元素是否都通过了指定函数的测试(返回值是false终止)。some() 方法测试是否至少有一个元素通过由提供的函数实现的测试(返回值是true终止)。for...in -- 遍历数组下标/对象可枚举属性不保证 key 顺序。for...of -- 遍历可迭代对象的值在可迭代对象上(包括 Array,Map,Set,String,TypedArray,arguments 对象等)上创建一个迭代循环,调用自定义迭代钩子自定义的 @@iterator 对象 ,并为每个不同属性的值执行语句。var randoms = {
[Symbol.iterator]: function() { return {
next: function() { return { value: Math.random() }; } }; } }; var randoms_pool = []; for (var n of randoms) { randoms_pool.push( n ); // 防止无限运行! if (randoms_pool.length === 100) break; } 类的继承和多态多态并不表示子类和父类有关联,子类得到的只是父类的一份副本,类的继承其实就是复制。属性的设置和屏蔽var myObject = {}; myObject.foo = "bar";如果 myObject 对象中包含名为 foo 的普通数据访问属性,这条赋值语句只会修改已有的属性值。如果 foo 不是直接存在于 myObject 中,[[Prototype]] 链就会被遍历,类似 [[Get]] 操作。如果原型链上找不到 foo,foo 就会被直接添加到 myObject 上。然而,如果 foo 存在于原型链上层,赋值语句 myObject.foo = "bar" 的行为就会有些不同 (而且可能很出人意料)。如果属性名 foo 既出现在 myObject 中也出现在 myObject 的 [[Prototype]] 链上层,那 么就会发生屏蔽。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性,因为 myObject.foo 总是会选择原型链中最底层的 foo 属性。转载请注明出处,如果想要了解更多,请搜索微信公众号:webinfoq。 ...

April 26, 2019 · 1 min · jiezi

一网打尽 JavaScript 的作用域

一网打尽 JavaScript 的作用域翻译:疯狂的技术宅https://medium.freecodecamp.o…-javascript-cbd957022652本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章作用域决定了变量的生命周期和可见性,变量在作用域范围之外是不可见的。JavaScript 的作用域包括:模块作用域,函数作用域,块作用域,词法作用域和全局作用域。全局作用域在任何函数、块或模块范围之外定义的变量具有全局作用域。可以在程序的任意位置访问全局变量。当启用模块系统时,创建全局变量会变得困难,但仍然可以做到这一点。可以在 HTML 中定义一个变量,这个变量需要在函数之外声明,这样就可以创建一个全局变量:<script> let GLOBAL_DATA = { value : 1};</script>console.log(GLOBAL_DATA);当没有模块系统时,创建全局变量会容易很多。在任何文件中的函数外声明的变量都是全局变量。全局变量贯穿于程序的整个生命周期。另一种创建全局变量的方法是在程序的任意位置使用 window 全局对象:window.GLOBAL_DATA = { value: 1 };这样 GLOBAL_DATA 变量会随处可见。console.log(GLOBAL_DATA)不过你也知道这种做法是不好的。模块作用域如果不启用模块,在所有函数之外声明的变量是全局变量。在模块中,在函数外部声明的变量都是隐藏的,除非显式导出,否则不可用于其他模块。导出使函数或对象可用于其他模块。在这个例子中,我从模块文件 sequence.js 中导出了一个函数:// in sequence.jsexport { sequence, toList, take };当前模块可以通过导入来使用其他模块的函数或对象成。import { sequence, toList, toList } from “./sequence”;在某种程度上,我们可以认为模块是一个自动执行的函数,它将 import 的数据作为输入,然后返回 export 的数据。函数作用域函数作用域意味着在函数中定义的参数和变量在函数内的任何位置都可见,但是在函数外部不可见。下面是一个自动执行的函数,被称为IIFE。(function autoexecute() { let x = 1;})();console.log(x);//Uncaught ReferenceError: x is not definedIIFE 的意思是立即调用函数表达式,是一个在定义后立即运行的函数。用 var 声明的变量只有函数作用域。更重要的是,用 var 声明的变量被提升到其作用域的顶部。通过这种方式,可以在声明之前访问它们。看看下面的代码:function doSomething(){ console.log(x); var x = 1;}doSomething(); //undefined这种事不会发生在 let 中。用 let 声明的变量只能在定义后访问。function doSomething(){ console.log(x); let x = 1;}doSomething();//Uncaught ReferenceError: x is not defined用 var 声明的变量可以在同一作用域下多次重新声明:function doSomething(){ var x = 1 var x = 2; console.log(x);}doSomething();用 let 或 const 声明的变量不能在同一作用域内重新声明:function doSomething(){ let x = 1 let x = 2;}//Uncaught SyntaxError: Identifier ‘x’ has already been declared也许我们可以不必关心这一点,因为 var 已经开始变得过时了。块作用域块作用域用花括号定义。它由 { 和 } 分隔。用 let 和 const 声明的变量可以受到块作用域的约束,只能在定义它们的块中访问。思考下面这段关于 let 块范围的代码:let x = 1;{ let x = 2;}console.log(x); //1相反,var 声明不受块作用域的约束:var x = 1;{ var x = 2;}console.log(x); //2另一个常见问题是在循环中使用类似 setTimeout() 的异步操作。下面的循环代码将显示五次数字 5。(function run(){ for(var i=0; i<5; i++){ setTimeout(function logValue(){ console.log(i); //5 }, 100); }})();带有 let 声明的 for 循环语句在每次循环都会创建一个新的变量并设置到块作用域。下一段循环代码将会显示 0 1 2 3 4 5。(function run(){ for(let i=0; i<5; i++){ setTimeout(function log(){ console.log(i); //0 1 2 3 4 }, 100); }})();词法作用域词法作用域是内部函数访问定义它的外部作用域的能力。看一下这段代码:(function autorun(){ let x = 1; function log(){ console.log(x); }; function run(fn){ let x = 100; fn(); } run(log);//1})();log 函数是一个闭包。它从父函数 autorun() 引用 x 变量,而不是 run() 函数中的 x 变量。闭包函数可以访问创建它的作用域,而不是它自己的作用域。autorun() 的局部函数作用域是 log() 函数的词法作用域。作用域链每个作用域都有一个指向父作用域的链接。当使用变量时,JavaScript 会向下查看作用域链,直到它找到所请求的变量或者到达全局作用域(即作用域链的末尾)。看下面这个例子:let x0 = 0;(function autorun1(){ let x1 = 1; (function autorun2(){ let x2 = 2; (function autorun3(){ let x3 = 3; console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3 })(); })();})();内部函数 autorun3() 可以访问本地 x3 变量。还可以从外部函数访问变量 x1 和 x2 和全局变量 x0 。如果找不到变量,它将在严格模式下返回错误。“use strict”;x = 1;console.log(x)//Uncaught ReferenceError: x is not defined非严格模式也被称为“草率模式”,它会草率的创建一个全局变量。x = 1;console.log(x); //1总结在全局作用域中定义的变量可在程序的任何位置使用。在模块中,在函数外部声明的变量都是隐藏的,除非被显式导出,否则不可用于其他模块。函数作用域意味着函数中定义的参数和变量在函数的任意位置都可见用 let 和 const 声明的变量具有块作用域。 var 没有块作用域。本文首发微信公众号:jingchengyideng欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章欢迎继续阅读本专栏其它高赞文章:12个令人惊叹的CSS实验项目世界顶级公司的前端面试都问些什么CSS Flexbox 可视化手册过节很无聊?还是用 JavaScript 写一个脑力小游戏吧!从设计者的角度看 ReactCSS粘性定位是怎样工作的一步步教你用HTML5 SVG实现动画效果程序员30岁前月薪达不到30K,该何去何从7个开放式的前端面试题React 教程:快速上手指南 ...

April 2, 2019 · 2 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

循环中的异步&&循环中的闭包

原文链接在这之前先要了解一下for循环中let 和var的区别var 是函数级作用域或者全局作用域,let是块级作用域看一个例子 function foo() { for (var index = 0; index < array.length; index++) { //..循环中的逻辑代码 } console.log(index);//=>5 } foo() console.log(index)//Uncaught ReferenceError: index is not definedfoo函数下的index输出5,全局下的index不存在现在我们把var 换为let function foo() { for (let index = 0; index < array.length; index++) { //..循环中的逻辑代码 } console.log(index)//Uncaught ReferenceError: index is not defined } foo()报错了,index不在foo函数作用域下,当然肯定也不会再全局下因为var和let的这个区别(当然var和let的区别不止于此)所以导致了下面的这个问题关于var的 const array = [1, 2, 3, 4, 5] function foo() { for (var index = 0; index < array.length; index++) { setTimeout(() => { console.log(index); }, 1000); } } foo()看下输出关于let的 const array = [1, 2, 3, 4, 5] function foo() { for (let index = 0; index < array.length; index++) { setTimeout(() => { console.log(index); }, 1000); } } foo()看下输出因为var和let 在作用域上的差别,所以到这了上面的问题使用var 定义变量的时候,作用域是在foo函数下,在for循环外部,在整个循环中是全局的,每一次的循环实际上是为index赋值,循环一次赋值一次,5次循环完成,index最后的结果赋值就为5;就是被最终赋值的index,就是5;let的作用局的块级作用局,index的作用域在for循环内部,即每次循环的index的作用域就是本次循环,下一次循环重新定义变量index;所以index每次循环的输出都不同这里还有另外一个问题,setTimeout,这是一个异步,这就是我们今天要讨论的循环中的异步setTimeout(func,time)函数运行机制setTimeout(func,time)是在time(毫秒单位)时间后执行func函数。浏览器引擎按顺序执行程序,遇到setTimeout会将func函数放到执行队列中,等到主程序执行完毕之后,才开始从执行队列(队列中可能有多个待执行的func函数)中按照time延时时间的先后顺序取出来func并执行。即使time=0,也会等主程序运行完之后,才会执行。一个需求,一个数组array[1,2,3,4,5],循环打印,间隔1秒上面的let是循环打印了12345,但是不是间隔1s打印的,是在foo函数执行1s后,同时打印的方式一 放弃for循环,使用setInterval function foo(){ let index = 0; const array = [1, 2, 3, 4, 5] const t = setInterval(()=>{ if (index < array.length) { console.log(array[index]); } index++; }, 1000); if (index >= array.length) { clearInterval(t); } } foo()我们上面说到,当for循环遇到了var,变量index的作用域在foo函数下,循环一次赋值一次,5次循环完成,index最后的结果赋值就为5;就是被最终赋值的index,就是5;方式二,引入全局变量代码执行顺序是,先同步执行for循环,再执行异步队列,在for循环执行完毕后,异步队列开始执行之前,index经过for循环的处理,变成了5。所以我们引入一个全局变量j,使j在for循环执行完毕后,异步队列开始执行之前,依然是0,在异步执行时进行累加 var j = 0; for (var index = 0; index < array.length; index++) { setTimeout(() => { console.log(j); j++; }, 1000 * index) }方式三 for循环配合setTimeout(常规思路,不赘述,面试必备技能) const array = [1, 2, 3, 4, 5] function foo() { for (let index = 0; index < array.length; index++) { setTimeout(() => { console.log(index); }, 1000*index); } } foo()方式四,通过闭包实现开始讨论方式四之前我推荐先阅读一遍我之前写过一篇文章谈一谈javascript作用域我们对上面的问题再次分析,for循环同步执行,在for循环内部遇到了setTimeout,setTimeout是异步执行的,所以加入了异步队列,当同步的for循环执行完毕后,再去执行异步队列,setTimeout中有唯一的一个参数数index方式三可行,是因为let是块级作用域,每次for执行都会创建新的变量index,for循环执行完毕后,异步执行之前,创建了5个独立的作用域,5个index变量,分别是0,1,2,3,4,相互独立,互不影响,输出了预期的结果如果说每次循环都会生成一个独立的作用域用来保存index,问题就会得到解决,所以,我们通过闭包来实现 const array = [1, 2, 3, 4, 5] function foo() { for (var index = 0; index < array.length; index++) { function fun(j) { setTimeout(function () { console.log(j); }, 1000 * j); } fun(index) } } foo()setTimeout中的匿名回调函数中引用了函数fun中的局部变量j,所以当fun执行完毕后,变量j不会被释放,这就形成了闭包当然我们可以对此进行一下优化 const array = [1, 2, 3, 4, 5] function foo() { for (var index = 0; index < array.length; index++) { (function(j) { setTimeout(function () { console.log(j); }, 1000 * j); })(index) } } foo()将foo函数改为匿名的立即执行函数,结果是相同的总结for循环本身是同步执行的,当在for循环中遇到了异步逻辑,异步就会进入异步队列,当for循环执行结束后,才会执行异步队列当异步函数依赖于for循环中的索引时(一定是存在依赖关系的,不然不会再循环中调动异步函数)要考虑作用域的问题,在ES6中使用let是最佳的选择,当使用var时,可以考虑再引入一个索引来替代for循环中的索引,新的索引逻辑要在异步中处理也可以使用闭包,模拟实现let在实际开发过程中,循环调用异步函数,比demo要复杂,可能还会出现if和else判断等逻辑,具体的我们下次再续参考通过for循环每隔两秒按顺序打印出arr中的数字setTimeOut和闭包《你不知道了JavaScript》上卷 ...

March 4, 2019 · 2 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

【前端面试】作用域和闭包

题目说一下对变量提升的理解说明this的几种不同使用场景创建10个a标签,点击的时候弹出来相应的序号如何理解作用域实际开发中闭包的应用2. 知识点2.1 执行上下文范围:一段script或者一个函数全局:变量定义、函数声明 script函数:变量定义、函数声明、this、arguments (执行之前)函数声明和函数表达式的区别:a(); //报错 函数表达式 变量声明 会提前。var a = function(){}b(); // 不报错 函数声明function b(){}变量定义时会默认把他的变量声明提升:(仅限于他的执行上下文,比如一段script和一个函数中)console.log(a);var a = 0;实际上是var a;console.log(a);a = 0;2.2 thisthis要在执行时才能确认,定义时无法确认。 var a = { name:‘a’, fn:function(){ console.log(this.name); } } a.fn(); // a a.fn.apply({name:‘b’}); // b a.fn.call({name:‘b’}); var fn1 = a.fn(); fn1(); // undefinedthis的使用场景构造函数中(指向构造的对象) function Fun(name){ this.name = name; } var f = new Fun(‘a’); console.log(f.name);对象属性中(指向该对象)普通函数中(指向window)call apply bind var fun = function (name){ console.log(this); console.log(name); }.bind({a:1}); fun(“name”);arguments中的this:var length = 10;function fn(){ alert(this.length)}var obj = { length: 5, method: function(fn) { arguments0 }}obj.method(fn)//输出1这里没有输出5,也没有输出10,反而输出了1,有趣。这里arguments是javascript的一个内置对象(可以参见mdn:arguments - JavaScript),是一个类数组(就是长的比较像数组,但是欠缺一些数组的方法,可以用slice.call转换,具体参见上面的链接),其存储的是函数的参数。也就是说,这里arguments[0]指代的就是你method函数的第一个参数:fn,所以arguments0的意思就是:fn()。不过这里有个疑问,为何这里没有输出5呢?我method里面用this,不应该指向obj么,至少也会输出10呀,这个1是闹哪样?实际上,这个1就是arguments.length,也就是本函数参数的个数。为啥这里的this指向了arguments呢?因为在Javascript里,数组只不过使用数字做属性名的方法,也就是说:arguments0的意思,和arguments.0()的意思差不多(当然这么写是不允许的),你更可以这么理解:arguments = { 0: fn, //也就是 functon() {alert(this.length)} 1: 第二个参数, //没有 2: 第三个参数, //没有 …, length: 1 //只有一个参数}所以这里alert出来的结果是1。如果要输出5应该咋写呢?直接 method: fn 就行了。2.3 作用域没有块级作用域 if(true){ var name = “test” } console.log(name);尽量不要在块中声明变量。只有函数级作用域2.4 作用域链自由变量 当前作用域没有定义的变量 即为自由变量。自由变量会去其父级作用域找。是定义时的父级作用域,而不是执行。 var a = 100; function f1(){ var b = 200; function f2(){ var c = 300; console.log(a); //自由变量 console.log(b); //自由变量 console.log(c); } f2(); }; f1();2.5 闭包 一个函数中嵌套另外一个函数,并且将这个函数return出去,然后将这个return出来的函数保存到了一个变量中,那么就创建了一个闭包。闭包的两个使用场景1.函数作为返回值 function fun(){ var a = 0; return function(){ console.log(a); //自由变量,去定义时的父级作用域找 } } var f1 = fun(); a = 1000; f1();2.函数作为参数 function fun(){ var a = 0; return function(){ console.log(a); //自由变量,去定义时的父级作用域找 } } function fun2(f2){ a = 10000 f2(); } var f1 = fun(); fun2(f1);具体解释看 高级-闭包中的说明闭包的两个作用:能够读取其他函数内部变量的函数可以让函数内部的变量一直保存在内存中实际应用场景1:闭包可以将一些不希望暴露在全局的变量封装成“私有变量”。假如有一个计算乘积的函数,mult函数接收一些number类型的参数,并返回乘积结果。为了提高函数性能,我们增加缓存机制,将之前计算过的结果缓存起来,下次遇到同样的参数,就可以直接返回结果,而不需要参与运算。这里,存放缓存结果的变量不需要暴露给外界,并且需要在函数运行结束后,仍然保存,所以可以采用闭包。上代码:function calculate(param){ var cache = {}; return function(){ if(!cache.parame){ return cache.param; }else{ //缓存计算…. //cache.param = result //下次访问直接取 } }}实际应用场景2延续局部变量的寿命img 对象经常用于进行数据上报,如下所示:var report = function( src ){ var img = new Image(); img.src = src;};report( ‘http://xxx.com/getUserInfo' );但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在 bug,在这些浏览器下使用 report 函数进行数据上报会丢失 30%左右的数据,也就是说, report 函数并不是每一次都成功发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的调用结束后, img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; }})();闭包缺点:浪费资源!3. 题目解答3.1 说一下对变量提升的理解变量定义和函数声明注意函数声明和函数表达式的区别变量定义时会默认把他的变量声明提升:(仅限于他的执行上下文,比如一段script和一个函数中)console.log(a);var a = 0;实际上是var a;console.log(a);a = 0;3.2 说明this的几种不同使用场景构造函数中(指向构造的对象)对象属性中(指向该对象)普通函数中(指向window)call apply bind3.3 创建10个a标签,点击的时候弹出来相应的序号实现方法1:用let声明i var body = document.body; console.log(body); for (let i = 0; i < 10; i++) { let obj = document.createElement(‘i’); obj.innerHTML = i + ‘<br>’; body.appendChild(obj); obj.addEventListener(‘click’,function(){ alert(i); }) }实现方法2 包装作用域 var body = document.body; console.log(body); for (var i = 0; i < 10; i++) { (function (i) { var obj = document.createElement(‘i’); obj.innerHTML = i + ‘<br>’; body.appendChild(obj); obj.addEventListener(‘click’, function () { alert(i); }) })(i) }3.4 实际开发中闭包的应用能够读取其他函数内部变量的函数可以让函数内部的变量一直保存在内存中封装变量,权限收敛应用1var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; }})();用于防止变量销毁。应用2 function isFirstLoad() { var arr = []; return function (str) { if (arr.indexOf(str) >= 0) { console.log(false); } else { arr.push(str); console.log(true); } } } var fun = isFirstLoad(); fun(10); fun(10);将arr封装在函数内部,禁止随意修改,防止变量销毁。

February 16, 2019 · 2 min · jiezi

深入学习js之——词法作用域和动态作用域

开篇当我们在开始学习任何一门语言的时候,都会接触到变量的概念,变量的出现其实是为了解决一个问题,为的是存储某些值,进而,存储某些值的目的是为了在之后对这个值进行访问或者修改,正是这种存储和访问变量的能力将状态给了程序。我们的程序中到处都充斥着对于状态的判断,根据不同的状态执行不同的逻辑。我们试想一下,如果没有状态这个概念,程序虽然也能够执行一些简单的任务,但是它会受到很多的限制,所能完成的功能是有限制的,举个例子,没有状态你是如何执行循环语句?没有状态如何更加优雅地使用逻辑结构?仔细想想,好像是寸步难行,当然引入变量后帮我们解决了这个问题。但是,引入变量和状态的概念之后会引起几个问题:这些变量住在哪里?换句话说,它们存储在哪里?最重要的是,程序需要它们的时候如何找到它们?今天我们就一起学习一下这套存储和查找变量的规则,这套规则我们称之为:作用域。作用域我们来拆解一下这个词语,所谓的“域”我们可以理解为:范围、区域,加上“作用”两个字所要表述的问题就是作用的范围、区域,比如国家的行政区域划分是为了便于管理,类比到程序源代码中作用域的出现也是为了便于对于变量做管理。好,这里我们简单做一下总结:定义:作用域是指程序源代码中定义变量的区域。作用:作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。在javaScript中的应用 :JavaScript采用词法作用域(lexical scoping),也就是静态作用域。那什么又是 词法作用域或者静态作用域呢?请继续往下看静态作用域与动态作用域因为javaScript采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。让我们看一个例子来理解词法作用域和动态作用域之间的区别:var value = 1;function foo() { console.log(value);}function bar() { var value = 2; foo();}bar();// 结果是 ???上面的代码中:1.我们首先定义了一个value,并赋值为1;2.声明一个函数foo,函数的功能是打印 value 这个变量的值;3.声明一个函数bar,函数内部重新创建了一个变量 value 这个变量赋值为2;在函数内部执行了 foo() 这个函数;4.执行 bar() 这个函数假设javaScript采用静态作用域,让我们分析下执行过程:执行foo函数,首先从 foo 函数内部查找是否有变量 value ,如果没有就根据书写的位置,查找上面一层的代码,我们发现value等于1,所以结果会打印 1。假设javaScript采用动态作用域,让我们分析下执行过程:执行foo函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。上面在区分静态作用于和动态作用域的时候,我们已经说了如果是静态作用域,那么函数在书写定义的时候已经确定了,而动态作用域是函数执行过程中才确定的。JavaScript采用的是静态作用域,所以这个例子的结果是 1。我们在控制台中输入执行上面的函数,检验一下执行结果果然是 1。动态作用域那什么语言是采用的动态的作用域呢? 其实bash 就是动态作用域,我们可以新建一个 scope.bash 文件将下列代码放进去,执行一下这个脚本文件:#!/bin/bashvalue=1function foo () { echo $value;}function bar () { local value=2; foo;}bar上面代码运行的结果输出2很好解释,虽然在代码最上层定义了 value并赋值为1,但是在调用foo函数的时候,在查找 foo 内部没有 value 变量后,会在foo 函数执行的环境中继续查找,也就是在bar 函数中查找,很幸运我们找到了。思考最后,让我们看一个《JavaScript权威指南》中的例子:// 例1:var scope = “global scope”;function checkscope(){ var scope = “local scope”; function f(){ return scope; } return f();}checkscope();// 例2:var scope = “global scope”;function checkscope(){ var scope = “local scope”; function f(){ return scope; } return f;}checkscope()();让我们来分析一下上面例1的代码:1、定义一个变量 scope 并赋值 global scope;2、声明一个函数 checkscope ,在这个函数中 定义一个变量 scope 并赋值 local scope;3、在checkscope 函数中 又定义一个函数 f ,这个函数 只做了一件事:返回scope 这个变量;4、最后返回并执行 f 这个函数;5、调用checkscope按照我们上面解释的javaScript中静态作用域理解,在执行 checkscope 这个函数的时候在函数内部执行的是f 这个函数,首先在 f 这个函数内部查找 scope 这个变量发现没有,继续在定义函数f的上面一层查找,发现在checkscope 这个函数作用域内 找到了scope的值 直接返回,至于 checkscope外面定义的scope没有理睬。让我们来分析一下上面例2的代码:1、定义一个变量 scope 并赋值 global scope;2、声明一个函数 checkscope 在这个函数中 定义一个变量 scope 并赋值 local scope;3、在checkscope 函数中 又定义一个函数 f 这个函数 只做了一件事:返回scope 这个变量;4、最后单纯的返回 f 这个函数;5、调用checkscope按照我们上面解释的javaScript中静态作用域理解,在执行 checkscope 这个函数的时候在函数内返回了函数f实际是在最外面调用的f但是由于javaScript是采用的词法作用域,因此函数的作用域基于函数创建的位置。而引用《JavaScript权威指南》的回答就是:JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。但是在这里真正想让大家思考的是:虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?敬请期待下面一篇关于javaScript 中的执行上下文栈的相关内容。参考:1、《你不知道的Javascript上卷》2、JavaScript深入之词法作用域和动态作用域 ...

February 2, 2019 · 1 min · jiezi

Javascript 闭包详解

闭包特性函数嵌套函数函数内部可以引用外部的参数和变量参数和变量不会被垃圾回收机制回收闭包的作用具体作用是有权访问函数内部的变量,最常见的就是函数内部创建另一个函数,通过另一个函数访问这个函数的局部的变量。缺点:就是常驻内存,会增大内存的使用量,使用不当会造成内存泄露。一般函数执行完毕,局部活动对象就会被销毁,内存中仅仅保存全局作用域,但是闭包会长期驻扎在内存。js垃圾回收在javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收;(Garbage Collection),计算机科学中一种自动释放不再被使用的内存空间的机制。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。全局变量 count++ 累加var count = 0;function testCount(){ count++; console.log(count);}testCount();//result 1testCount();//result 2局部变量++ 不累加function testCount(){ var count=0; count++; console.log(count);}testCount();//result 1testCount();//result 1//到这里会问玩毛线呢 这个我们懂 I know。 我只是想通过这两个例子来说明闭包的用处和好处。局部变量count++累加function testCount(){ var count=0; return function(){ count++; console.log(count); }}var plus = testCount(); //函数赋值给变量plus(); //plus函数调用一次,结果为1,相当于testCount()();plus(); //plus调用第二次,结果为2,实现了局部变量累加了。//闭包会使变量始终保存在内存中,如果使用不当会增大内存消耗。

January 9, 2019 · 1 min · jiezi

一眼看穿????JS变量,作用域和内存问题

这篇文章将梳理下环境,作用域链,变量对象和活动对象,以及内存管理问题。基本类型和引用类型的值我们都知道JS中的数据类型有两大类,基本数据类型和引用数据类型,下面从三个方面来解剖他们①保存方式基本类型的值是指简单的数据段,引用类型的值是指那些可能由多个值构成的对象。基本类型按值访问可以直接操作保存在变量中实际的值引用类型按引用地址访问保存在内存中的对象,而JS不能不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,所以说在实际操作过程中操作的是对象的引用,而不是实际的对象。②复制变量值基本类型在复制变量值的时候,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。也就是说基础类型的值复制给新变量后,会在栈内存中开辟一个新的地址空间去存储值,原值和复制值参与任何操作都互不影响引用类型在复制变量值的时候,同样会在栈内存中开辟一个新的地址空间去存储值,只不过,引用类型复制的是指针,原值和复制值的指针指向同一堆内存中存储的值,也就是说着两个变量实际上将引用同一对象,因此改变其中一个变量,就会影响到另一个变量。③传递参数先了解一个基本原则,ECMAScript中所有函数的参数都是按值传递的,千万不能觉得在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。根据这个原则,如果参数值是基本类型的,在函数内部修改值,并不会影响到函数外部的值,但如果是引用类型的,参数依旧是值传递,只不过传递的是栈内存的地址值,因此函数内部的修改会影响到函数外部的值。下面看一个????let obj_value = { a: 1, b: 2}function func(val) { val.a = 3 val.c = 6 console.log(val) // {a: 3, b: 2, c: 6}}console.log(obj_value) // {a: 1, b: 2}func(obj_value)console.log(obj_value) // {a: 3, b: 2, c: 6}下面????能证明引用类型的参数也是按值传递的function func(obj) { obj.a = 1 obj = {} obj.a = 2}let test = {}func(test)console.log(test.a) // 1上面的????,按照我们理解应该打印出a=2,但事与愿违,首先,test在函数func中新增了一个a属性并赋值为1,此时,obj中传递的是引用类型在栈内存中存储的地址值,也就是说函数内的obj复制的是test地址,他们两个共同指向一个对象,因此通过obj新增,修改删除操作都会反映到函数外部,接下来再看函数内的第二条语句,obj={},这就不得了了,这是重写,也就是说它会抹去obj原本存储的地址值,这就切断了test和obj共同指向一个对象这个联系,因此第三条语句,obj.a=2就是函数内部的事情了。所以总结一句话,引用类型的增删改操作与其关联所有对象都会受到波及和影响,重新就会切断自身与其余对象的联系检测类型typeof()(只适用于基本类型,不适用于对象)typeof函数可用于检测string,number,boolean,undefined,function还是symbol,但如果变量的值是引用类型或null,则typeof会返回object。ECMA-262规定任何在内部实现[[call]]方法的对象都应该在应用typeof操作符时返回"function"对于正则表达式类型的typeof检测,在IE和Firefox中会返回object,其余的返回function。let func = function() {}console.log(typeof (’’)) // stringconsole.log(typeof (1)) // numberconsole.log(typeof (true)) // booleanconsole.log(typeof (undefined)) // undefinedconsole.log(typeof ({})) // objectconsole.log(typeof (null)) // objectconsole.log(typeof (Symbol(’’))) //symbolconsole.log(typeof (func)) // functioninstanceof(只适用于对象,不适用于基本类型)instanceof操作符用于判断是什么类型的对象如果变量是给定引用类型的实例,那么instanceof操作符就会返回true根据规定,所有的引用类型的值都是Object的实例,因此在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true,instanceof操作符检测基本类型的值,该操作符始终会返回false,因为基本类型不是对象。执行环境及作用域执行环境也称为环境,它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样,在web浏览器中,全局执行环境被认为是window对象,因此,在浏览器中,创建的所有全局变量和函数都是作为window对象的属性和方法。ECMAScript程序中执行流的控制机制每个函数都有各自的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。作用域链代码在环境中执行,就会创建变量对象的作用域链,作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在的环境的变量对象,如果这个环境是函数,则将其活动对象作为变量对象。什么是活动对象呢?活动对象实际就是变量对象在真正执行时的另一种形式。活动对象一开始只包含一个变量,即arguments对象。作用域中的下一个变量对象来自外部环境,再下一个变量对象来自下一个环境,层层嵌套,一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象环境的访问是沿着作用域链进行的,作用域链是单向的,即由里到外,内部环境可以访问外部环境,反之不行。变量对象和活动对象的概念变量对象(VO)变量对象是与执行上下文对应的概念,定义执行上下文中的所有变量,函数以及当前执行上下文函数的参数列表,也就是说变量对象定义着一个函数内定义的参数列表、内部变量和内部函数变量对象的内部顺序是参数列表->内部函数->内部变量变量对象的创建过程检查当前执行环境的参数列表,建立Arguments对象。检查当前执行环境上的function函数声明,每检查到一个函数声明,就在变量对象中以函数名建立一个属性,属性值则指向函数所在的内存地址。检查当前执行环境上的所有var变量声明,每检查到一个var声明,如果VO(变量对象)中已存在function属性名,则调过,不存在就在变量对象中以变量名建立一个属性,属性值为undefined。变量对象是在函数被调用,但是函数尚未执行的时刻被创建的,这个创建变量对象的过程实际就是函数内数据(函数参数,内部变量,内部函数)初始化的过程。活动对象(AO)未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。全局变量对象我们上面说的都是函数上下文中的变量对象,是根据执行上下文中的数据(参数、变量、函数)确定其内容的,全局上下文中的变量对象则有所不同。以浏览器为例,全局变量对象是window对象,全局上下文在执行前的初始化阶段,全局变量、函数都是被挂载倒window上的。执行上下文的生命周期延长作用域链执行环境的类型就两种——全局和局部(函数)延长作用域链的意思是在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。延长方法(以下两个语句都会在作用域链的前端添加一个变量对象):try-catch语句的catch块:会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。with语句:会指定的对象添加到作用域链中通过with语句延长作用域链function addLink() { let name = ‘george’ with(local) { var url = href + name // 此时通过with语句将local对象添加到addLink环境的头部,因此在addLink中就有权可以访问local对象的属性和方法 } return url }没有块级作用域JS只有函数作用域和全局作用域,没有块级作用域。声明变量var声明的变量会自动被添加到最接近的环境中在函数内部,最接近的环境就是函数的局部环境,在with语句中,最接近的环境是函数环境,初始化变量若没有通过var声明,该变量会自动被添加到全局环境。查询表示符当某个环境中为了读取和写入一个标识符时,必须通过搜索来确定标识符实际代表什么。搜索过程从作用域链的前端开始,沿着作用域链向上查找,一直追溯到全局环境变量对象,找到标识符,搜索过程停止,反之,返回undefined。垃圾收集JS具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存收集原理找出那些不再继续使用的变量,然后释放其占用的内存,为此垃圾收集器会按照固定的时间间隔(或代码中预定的收集时间),周期性地执行这一操作。收集方法标记清除引用计数标记清除(最常用的垃圾收集方式)原理:垃圾收集器在运行的时候回给存储在内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记,最后删除被标记的变量。标记清除算法将“不再使用的对象”定义为“无法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。引用计数原理:通过名字很好理解,引用计数,就是跟踪记录每个值被引用的次数,当引用次数为0时,将其删除。计数方法:当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值被赋给另一个变量,则该值的引用次数加1,相反,包含这个值引用的变量又取的另一个值,则这个值的引用次数减1。引用计数的严重问题——循环引用循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。当出现循环引用的时候,引用次数永远不可能为0,这会导致内存得不到回收。解决方法:手动断开不需要的引用,即,将引用对象置为null立即执行垃圾回收函数IE中:window.CollectGarbage()Opera7或更高版本:window.opera.collect()管理内存分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少对于浏览器而言,确保占用最少的内存可以让页面获得更好的性能。优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据,一旦数据不再有用,最好通过将其值设置为null来释放其引用,这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时自动被解除引用。 ...

December 9, 2018 · 1 min · jiezi

JavaScript的作用域详解

作用域作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的变量并不总是有效/可用的,而限定这个变量的可用性的代码范围就是这个变量的作用域。通俗一点就是我要把我的变量分成一坨一坨保管起来,有些地方只能用这几个变量,有些地方只能用另外几个变量,而这个分开的一坨一坨的区域就是作用域那这个作用域什么时候用到的呢?没错就是编译的时候让我们来看看编译的大概流程词法分析(这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块)语法分析(这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”)代码生成(将这棵“树” 转换为可执行代码,将我们写的代码变成机器指令并执行)比起上面这些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等,但是大体上也是差不多的流程那我们要编译var a = 2的话,是‘谁’来执行编译的过程呢?当当当当引擎:负责整个编译运行的全部过程。编译器:负责词法分析以及代码生成。作用域:负责收集维护所有声明的标识符,确定当前执行代码对标识符的访问权限。当我们看到var a = 2的时候,我们认为是一条声明,但是对于引擎来说,这是两个完全不一样的声明,分为下面两部分1.遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a(严格模式下报错)。2.接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。可以看到,编译的时候,编译器和引擎需要询问作用域,所求变量是否存在,然后根据查询结果来进行不同的操作作用域嵌套上面我们展示了只有一个作用域,变量的声明和赋值过程。实际情况中,我们通常需要同时顾及几个作用域。当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止;但是反过来,外层的作用域无法访问内层作用域的变量,如果可以的话那不就全都是全局变量了吗嘿嘿嘿function foo(a) {console.log( a + b );}var b = 2;foo( 2 ); // 4当引擎需要变量b的时候,首先在foo的作用域中查找,发现没有b的踪影,于是就跑出来,往上面一层作用域走一走,发现了这个b原来在全局作用域里待着,那可不得一顿引用!如果全局作用域也没有b的话,那就得报错了,告诉写代码的傻子“你猪呢?一天到晚净会写bug!”。第一层楼代表当前的执行作用域,也就是你所处的位置。建筑的顶层代表全局作用域。变量引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止函数作用域可以看到我们在上面生成两层作用域(一层foo一层全局)的时候用了函数。因为JavaScript的函数可以产生一层函数作用域。上代码!function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar( b * 3 );}foo( 2 ); // 2, 4, 12我们来分析一下上面几行代码。这个例子里面包含了三层逐级嵌套的作用域,其中两个函数生成了两层嵌套作用域。1.包含着整个全局作用域,其中只有一个标识符: foo 。2.包含着 foo 所创建的作用域,其中有三个标识符: a 、 bar 和 b 。3.包含着 bar 所创建的作用域,其中只有一个标识符: c 。由于bar是最内层的作用域,如果在它作用域内的查询不到它需要的值,它会逐级往外查询外层作用域的同名变量。如果查询到了则取用块级作用域尽管函数作用域是最常见的作用域单元,当然也是现行大多数 JavaScript 中最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码。(如果你会其他一些语言你就会发现一个花括号不就一个块级作用域了吗)我们来看看JavaScript中的花括号for(var i=0;i<5;i++){console.log(window.i)} //0 1 2 3 4你惊奇的发现,妈耶,我这个var不等于白var嘛,反正都是全局变量(如果你没在函数内使用的话)。是的JavaScript就是这么的高端兼灵性~(滑稽)if的花括号和for是一样的,不做赘述。那我们怎样整一个独立作用域?然后我又不想一直声明函数JavaScript有四种方式可以产生块级作用域。withtry/catchletconst让我们来介绍一下这四种东西吧1.首先是with,算了,垃圾,不讲。好处不多,坏处倒是挺多,有兴趣百度用法不建议使用2.然后是try/catch, ES3 规范中规定 try / catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效try{throw 2}catch(a){console.log(a)};console.log(a);//Uncaught ReferenceError3.let,这个是es6引入的新关键字,非常香看下面可以和上面的var i的循环做对比for(let j=0;j<5;j++)(console.log(window.j));//undefined *54.这个跟let差不多,但是是用来定义常量的。const a = 5;a = 6;//报错ok这个很敢单让我们来学习下一部分提升在最开始之前,我们先来学习一下两种报错。ReferenceError 异常TypeError第一种的出现是因为遍历了所有的作用域都查找不到变量,第二种是找到了这个变量,但是对这个变量的值进行了错误的操作,比如试图对一个非函数类型的值进行函数调用我们先来看看下面的代码会输出什么a = 2;var a;console.log( a );你可能会以为,我先给a赋值了2,然后var a又给a赋值了undefined,所以会输出undefined。但是这个输出了2。我们再来看一题console.log( a );var a = 2;这个时候你可能认为会报ReferenceError异常,因为使用在前,使用的时候a还没有定义,作用域肯定也找不到a,但是这个却输出了undefined。Why?为了搞明白这个问题,我们需要回顾一下前面关于编译器的内容。回忆一下,引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。因此,正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明: var a; 和 a = 2; 。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。上面的第一段代码就可以看做var a;a = 2;console.log(a)第二段代码则可以看成var a;console.log(a);//此时a还没赋值,所以是undefineda = 2;打个比方,这个过程就好像变量从它们在代码中出现的位置被“移动”到了最上面(变量所在作用域)。这个过程就叫作提升。我们从上面可以看到变量声明的提升,那么对于函数声明呢?当然是no趴笨啦foo();function foo() { console.log( a ); // undefined var a = 2;}但是,需要注意的是,函数声明会被提升,但是函数表达式却不会。foo(); // 不是 ReferenceError, 而是 TypeError!var foo = function bar() {// …};这个就相当于var foo;foo(); // 此时foo肯定是undefined啦,undefined()? 对undefined值进行函数调用显然是错误操作!TypeError!foo = function bar() {// …};既然函数声明和变量声明都会被提升,那它们两个哪个提升到更前面呢?是函数!!函数作为JavaScript的一名大将,确实是有一些牌面。foo(); // 1var foo;function foo() { console.log( 1 );}foo = function() { console.log( 2 );};我们可以将上面看成function foo() { console.log( 1 );}var foo;//重复声明,可以去掉foo(); // 1foo = function() { console.log( 2 );};注意:后面的声明会覆盖前面的声明。foo(); // 3function foo() { console.log( 1 );}var foo = function() { console.log( 2 );};foo();//2function foo() { console.log( 3 );}相当于function foo() { console.log( 1 );}function foo() { console.log( 3 );}var foo;foo(); // 3foo = function() { console.log( 2 );};foo();//2闭包我们刚刚讲那么多,相信大家都已经知道并且深信,作用域只能一层一层往外查询,不能往里走,那我如果要找一个函数里的变量值呢?那可咋整啊?很简单,我们不能往里走,但是我们可以再给这个函数里面整一层作用域,这样函数里面的子作用域不就可以访问它的变量了吗?perfectfunction foo() { var a = 2; function bar() { console.log( a ); // 2 } return bar;}var baz = foo();执行了foo()就返回了一个bar;现在相当于baz=bar;baz();//2这里我们需要获取a的值,我们就在里面写一个函数bar,显然这个bar是有权利访问a的,那我们返回这个有权利访问a的函数不就顶呱呱了吗?在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收(频繁使用闭包可能导致内存泄漏)。谁在使用这个内部作用域?原来是 bar() 本身在使用。拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。来点练习题第一题var tt = ‘aa’; function test(){ alert(tt); var tt = ‘dd’; alert(tt); } test();第二题var a = 100;function test(){ console.log(a); a = 10; console.log(a);}test();console.log(a);第三题var a=10; function aaa(){ alert(a);}; function bbb(){ var a=20; aaa();}bbb();答案:undefined dd100 10 1010参考文献《你不知道的JavaScript》最后有什么错误或者建议可以在评论区告诉我谢谢 ...

September 26, 2018 · 2 min · jiezi