关于闭包:go的defer和闭包例子说明非内部实现

用几个例子阐明golang的闭包函数,联合defer应用,配合对应代码及文末总结。 函数阐明输入e1defer调用,相当于是拿到了以后err变量的快照,即注册defer函数的时候,将当下err的值塞入到defer中start err1e2defer 调用,然而一个闭包函数,且闭包函数有传参,闭包捕捉以后err的值依然是 start err2(闭包捕捉的是变量值的拷贝),且闭包内的值变量扭转不会影响内部err的值(详见见e5)start err2e3defer 调用,闭包内的变量和匿名函数外的变量是专用的,没有传递形参,没有传递形参,与上下文共享defer3 errore4defer 调用,在函数 e4 中,当你将 err 作为参数传递给闭包函数时,实际上是创立了一个闭包函数的正本,这个正本在闭包外部独立于内部作用域。这种行为是因为闭包在捕捉内部变量时,会将内部变量的以后值复制到闭包外部,造成一个闭包环境,当初了解了闭包的概念了吧。具体来说,在 defer 语句执行的时候,闭包函数会将 err 的以后值(即 "start err4")复制到闭包外部的参数中。之后,无论内部作用域的 err 是否产生扭转,闭包外部的参数值都会放弃不变,因为闭包曾经捕捉了一个快照start err4 e5传值的状况下,闭包内的值变量扭转不会影响内部err的值,(相互独立)now err is start err5 start err5CHANGE MEe6闭包没有传值,拿到的err是最初赋值的,now err is start err6 defer6 error CHANGE MEpackage mainimport ( "errors" "fmt")func e1(){ err := errors.New("start err1") defer fmt.Println(err) err = errors.New("defer1 error") return}func e2(){ err := errors.New("start err2") defer func(e error) { fmt.Println(e) }(err) err = errors.New("defer2 error") return}func e3(){ err := errors.New("start err3") //闭包内的变量和匿名函数外的变量是专用的,没有传递形参,没有传递形参,与上下文共享 defer func() { fmt.Println(err) }() err = errors.New("defer3 error") return}func e4(){ var err error err = errors.New("start err4") //闭包内的变量和匿名函数外的变量是专用的,然而如果传了形参,那就和上文的共用了 //在函数 e4 中,当你将 err 作为参数传递给闭包函数时,实际上是创立了一个闭包函数的正本,这个正本在闭包外部独立于内部作用域。这种行为是因为闭包在捕捉内部变量时,会将内部变量的以后值复制到闭包外部,造成一个闭包环境 //具体来说,在 defer 语句执行的时候,闭包函数会将 err 的以后值(即 "start err4")复制到闭包外部的参数中。之后,无论内部作用域的 err 是否产生扭转,闭包外部的参数值都会放弃不变,因为闭包曾经捕捉了一个快照。 defer func(err error) { fmt.Println(err) }(err) err = errors.New("defer4 error") return}func e5(){ err := errors.New("start err4") defer func(err error ) { err=errors.New(err.Error()+"CHANGE ME") fmt.Println(err) }(err) fmt.Println("now err is ",err) err = errors.New("defer5 error") return}func e6() { err := errors.New("start err6") defer func() { err = errors.New(err.Error() + " CHANGE ME") fmt.Println(err) }() fmt.Println("now err is ", err) err = errors.New("defer6 error") return}func main(){ e1() e2() e3() e4() e5() e6()}变量作用域和闭包:Go 语言中的变量作用域由代码块决定。变量在其定义的代码块内可见。闭包是一个函数值,它能够捕捉其定义时四周的作用域内的变量。闭包能够在定义之外被调用,依然拜访并批改捕捉的变量。 ...

August 20, 2023 · 1 min · jiezi

关于闭包:JavaScript闭包

本文将带你用正确姿态对待JavaScript闭包。在 JavaScript 中闭包形容的是 function 中外层作用域的变量被内层作用域援用的场景,闭包为内层作用域保留了外层作用域的变量。要了解闭包,首先要晓得 JS词法作用域 是如何工作的。 JS词法作用域(lexical scoping)来看这段代码: let name = 'John';function greeting() { let message = 'Hi'; console.log(message + ' '+ name);}变量 name 是全局变量。它能够在任何中央调用,包含在 greeting 函数外部。 变量 message 是局部变量,只能在 greeting 函数外部调用。 如果你尝试从 greeting() 内部拜访 message 变量,会抛出一个谬误: ReferenceError: message is not defined比拟有意思的是 函数外部的作用域是能够嵌套的,如下: function greeting() { let message = 'Hi'; function sayHi() { console.log(message); } sayHi();}greeting();// Higreeting() function 创立了一个局部变量 message 和一个部分函数 sayHi()。 sayHi() 是 greeting() 的一个外部办法,只能在 greeting() 外部拜访。sayHi() 能够拜访 greeting() 的 message 变量。在 greeting() 外部调用了 sayHi(),打印出了变量 message 的值。 ...

April 24, 2022 · 1 min · jiezi

关于闭包:闭包与直接return的区别

间接return: 闭包返回:1,闭包不是为了让函数内部拿到外部变量。而是为了爱护公有变量2,return进去的是一个值,不是变量自身,此处的return是获得公有变量值的一种办法,跟闭包没有严格关系

May 10, 2021 · 1 min · jiezi

关于闭包:JS-总结之闭包

从《JS 总结之函数、作用域链》一文中晓得作用域链的作用,保障了对所有变量对象的有序拜访。 ???? 问题函数外的是无法访问函数外部的变量,有时候要用到怎么办?咱们的配角,闭包就是能够解决这个问题。 ???? 什么是闭包援用 MDN 上的解释: 闭包是函数和申明该函数的词法环境的组合。援用 《JavaScript 高级程序设计(第 3 版)》上的解释: 闭包是指有权拜访另一个函数作用域中的变量的函数。???? 相同点这两个解释都在说着同一件事,闭包能拜访申明时函数所在的环境中的变量和函数。 那具体是因为什么才会让闭包拜访到环境中的变量和函数,这得靠两兄弟:变量对象和作用域链。 变量对象 当执行函数的时候,会创立和初始化一个函数执行环境,在函数执行环境中,全局执行环境的变量对象(Variable Object,缩写为 VO)不能间接拜访,此时由激活对象(Activation Object,缩写为 AO)表演 VO 的角色。 变量对象专门用来梳理和记住这些变量和函数,是作用域链造成的前置条件 。但咱们无奈间接应用这个变量对象,该对象次要是给 JS 引擎应用的。具体能够查看《JS 总结之变量对象》。 变量对象就相当于一个寄存的仓库,获取到外面的货色,还得须要去获取这些的门路,这就是作用域链的事件了。 作用域链 然而,光有变量对象可实现不了闭包的造成,怎样才能让函数拜访到,这得靠作用域链,作用域链的作用就是让函数找到变量对象外面的变量和函数。具体能够查看《JS 总结之函数、作用域链》 ???? 不同点尽管都是讲闭包,但 MDN 下面讲的是申明该函数的词法环境,而 JS 高程讲的是拜访另一个函数作用域中,从解释上的不同,闭包便有了实践中的闭包( MDN )和实际中的闭包( JS 高程)之分。 ???? 实践中的闭包依据 MDN 的解释写个例子: var a = 1function fn() { console.log(a)}fn()函数 fn 和函数 fn 的词法作用域形成了一个闭包。然而这不是一般的函数吗? 在《JavaScript 权威指南》中也失去证实: 从技术的角度讲,所有 JavaScript 函数都是闭包???????? 实际中的闭包汤姆大叔翻译的文章中讲,实际中的闭包须要满足以下两个条件: 即便创立它的上下文曾经销毁,它依然存在(比方,外部函数从父函数中返回)在代码中援用了自在变量什么是上下文?即函数的执行环境。 什么是自在变量?即函数的词法作用域中的变量和函数,而不是函数自身的参数或者局部变量,或者说是所在函数的变量对象中的变量和函数。 这两点和 JS 高程中讲的闭包的解释不约而同。 ...

January 11, 2021 · 2 min · jiezi

关于闭包:js闭包的理解

任何在函数中定义的变量,都能够认为是公有变量,因为不能在函数的内部拜访这些变量 (变量的作用域) 1.当在函数外部定义了其余函数时,就创立了闭包。 闭包是一个函数,闭包会携带蕴含它的函数的作用域 例子: a函数内进行return 另一个b函数,被蕴含的b函数因为作用域链拿到了下级a函数作用域的公有变量,当在内部调用a函数并且执行a函数的返回值b函数时,也就能在内部顺利拿到了a函数中作用域的公有变量。当a函数执行完后外面的流动对象不会销毁,因为b函数还在援用a函数作用域上的流动对象。直到b函数销毁后,外面的流动对象才被销毁。 2.在后盾执行环境中,闭包的作用域链蕴含着它本人的作用域、蕴含函数的作用域和全局作用域。 3.通常,函数的作用域及其所有变量都会在函数执行完结后被销毁。 然而,当函数返回了一个闭包时,这个函数的作用域将会始终在内存中保留到闭包不存在为止。 闭包的作用:1.能够应用自执行函数与函数的联合全新的闭包作用域2.生成全新的公有作用域,爱护函数内的变量平安。应用闭包的公有作用域代替全局变量,避免全局净化(在封装js库的时候就有用到)3.在es5能够隔离 for循环中的var i变量的作用域4.在内存中维持一个变量。

November 29, 2020 · 1 min · jiezi

关于闭包:温故而知新篇之JavaScript忍者秘籍第二版学习总结三闭包和作用域

前言这本书的电子版我曾经在学习总结第一篇曾经放了下载链接了,能够去查看温故而,知新篇之《JavaScript忍者秘籍(第二版)学习总结(一)——函数篇 你自律用在什么中央,什么中央就会成就你。要记住当你快顶不住的时候,磨难也快顶不住了。 加油吧,兄弟们 先来一个自增函数看看 var addnum= function(){ var num=0; // 闭包内 参数私有化 return function(){ return num++ }}const Addnum= addnum()Addnum()console.log(Addnum()) // 1console.log(Addnum()) // 2封装公有变量 function Ninja() { var feints = 0; this.getFeints = function() { return feints; }; this.feint = function() { feints++; };}var ninja1 = new Ninja();ninja1.feint();通过执行上下文来跟踪代码具备两种类型的代码,那么就有两种执行上下文:全局执行上下文和函数执行上下文。二者最重要的差异是: 全局执行上下文只有一个,当JavaScript程序开始执行时就曾经创立了全局上下文;而函数执行上下文是在每次调用函数时,就会创立一个新的。定义变量的关键字与词法环境关键字varvar globalNinja = "Yoshi"; //⇽--- 应用关键字var定义全局变量function reportActivity() { var functionActivity = "jumping"; //⇽--- 应用关键字var定义函数外部的局部变量 for (var i = 1; i < 3; i++) { var forMessage = globalNinja + " " + functionActivity; //⇽--- 应用关键字var在for循环中定义两个变量 console.log(forMessage === "Yoshi jumping", "Yoshi is jumping within the for block"); //⇽--- 在for循环中能够拜访块级变量,函数内的局部变量以及全局变量 console.log(i, "Current loop counter:" + i); } console.log(i === 3 && forMessage === "Yoshi jumping", "Loop variables accessible outside of the loop"); //⇽--- 然而在for循环内部,依然能拜访for循环中定义的变量 }reportActivity();console.log(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); //⇽--- 函数内部无法访问函数外部的局部变量”这源于通过var申明的变量实际上总是在间隔最近的函数内或全局词法环境中注册的,不关注块级作用域。 ...

November 12, 2020 · 2 min · jiezi

使用-JS-及-React-Hook-时需要注意过时闭包的坑文中有解决方法

作者:Dmitri Pavlutin译者:前端小智来源:dmitripavlutin上个月自己花了 1300 买了阿里的服务器来学习 node 及对应的框架,在 11 号之前它们有做活动,1300 的配置现在一年只要 86 元,三年只要229元,真心觉得很划算了,可以点击下面链接进行参与: https://www.aliyun.com/1111/2... 为了保证的可读性,本文采用意译而非直译。 1. JS 中的闭包下面定义了一个工厂函数 createIncrement(i),它返回一个increment函数。之后,每次调用increment函数时,内部计数器的值都会增加i。 function createIncrement(i) { let value = 0; function increment() { value += i; console.log(value); } return increment;}const inc = createIncrement(1);inc(); // 1inc(); // 2createIncrement(1) 返回一个增量函数,该函数赋值给inc变量。当调用inc()时,value 变量加1。 第一次调用inc()返回1,第二次调用返回2,依此类推。 这挺趣的,只要调用inc()还不带参数,JS 仍然知道当前 value 和 i 的增量,来看看这玩意是如何工作的。 原理就在 createIncrement() 中。当在函数上返回一个函数时,有会有闭包产生。闭包捕获词法作用域中的变量 value 和 i。 词法作用域是定义闭包的外部作用域。在本例中,increment() 的词法作用域是createIncrement()的作用域,其中包含变量 value 和 i。 无论在何处调用 inc(),甚至在 createIncrement() 的作用域之外,它都可以访问 value 和 i。 ...

November 5, 2019 · 3 min · jiezi

闭包全面详解

什么是闭包最原始定义闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来。 //根据定义,包含变量的函数就是闭包function foo() { var a = 0;}cosole.log(a) // Uncaught ReferenceError: a is not defined《JavaScript高级程序设计》对闭包定义闭包是指有权访问另一个函数作用域中的变量的函数 //访问上层函数的作用域的内层函数就是闭包function foo() { var a = 2; function bar() { console.log(a); } bar();}foo();《JavaScript权威指南》对闭包定义函数对象可以通过作用域链相互关联起来,函数体内部变量可以保存在函数作用域内,这就是闭包。 var global = "global scope"; //全局变量function checkscope() { var scope = "local scope"; //局部变量 function f() { return scope; //在作用域中返回这个值 }; return f();}checkscope(); // 返回 "local scope"《你不知道的JavaScript》这样描述当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 //fn3就是fn2函数本身。执行fn3能正常输出name//这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。function fn1() { var name = 'iceman'; function fn2() { console.log(name); } return fn2;}var fn3 = fn1();fn3();MDN 上面这么说:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。简单说就是指那些能够访问自由变量的函数。 ...

August 8, 2019 · 2 min · jiezi

js函数闭包了解一下两道题测一下水平

介绍你下你理解的闭包?不管怎样!我最近听到很多次!感觉是不好好总结一下没法面对那些犀利的追问!如果觉得闭包理解的很透彻,就直接跳到最后看题目! 1.闭包概念小红书的解释闭包是有权访问另一个函数作用域中的变量的函数。明白了吗?就是一个函数,一个可以访问其他函数中变量的函数。所以常见的创建闭包的方式就是在一个函数内部创建另一个函数。 function bag(num){ return function(){ return num }}var bagc = bag(12)console.log(bagc()) //12可以看到在bag内部的匿名函数可以访问外部bag函数的变量num。 2.闭包的用处闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。 function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2;}var result=f1();result(); // 999 nAdd(); //变量n被保存了result(); // 1000上面是阮一峰在文档中的一个例子,读取函数内部变量我觉得用处一般吧,让变量保持在内存中的用处倒是不少,像经常使用的that=this等。下面看一个常见的问题: for(var i = 0;i <10;i++){ setTimeout(()=>{ console.log(i) },1000) } //上面的代码我们希望按照索引值打印,结果却打印了10个10,为什么就不解释了,i是全局变量。 //换成下面的写法,就能解决问题,正是因为闭包 让变量的值始终保持在内存中,每个i都存在了num这个局部变量中 for(var i = 0;i <10;i++){ (function(num){ setTimeout(()=>{ console.log(num) },1000) })(i) }3.使用闭包需要注意的点闭包虽然在解决一些问题上有好处,但是由此引发的一些问题要注意,而且由于闭包会携带外部函数作用域,所以内存占用比较大,所以尽量少用、慎用闭包。 1.变量问题正是因为闭包可以使用外部变量,所以下面的代码中,返回的匿名函数中对变量i的使用将会是最终的值,数组中存放的函数的返回值将都会是10。 function test() { var result = []; for(var i = 0; i<10; i++){ result.[i] = function () { return i; } } return result}需要将上述代码改写成如下: ...

August 7, 2019 · 2 min · jiezi

JavaScript闭包

基本概念函数和声明该函数的词法环境的组合。闭包包含了函数也包含了声明该函数的词法环境(作用域)。 闭包实际上是将函数与其所操作的某些数据(环境)关联起来,这些数据或者是环境可以理解为它的一个作用域。因此我们可以达到一个能够访问另一个函数作用域的变量的函数的目的。 //定义局部变量function count() { let count = 0; return function() { count = count + 1; console.log(count); }}var getCount = count();getCount();getCount();// 1// 2特点闭包一定return一个function,只有function才会有一个封闭的命名空间。 函数嵌套函数函数内部可以引用外部的参数和变量参数和变量不会被垃圾回收机制回收用途匿名自执行函数 - 只需要执行一次,其内部变量无需维护 (function(){ // })();封装回调保存作用域(缓存结果) for(var i = 1; i < 5; i++) { setTimeout((function(i) { return function() { console.log(i); } })(i), i * 1000) }模拟私有方法 var myNameSpace = (function () { // 私有计数器变量 var myPrivateVar = 0; /* 记录所有参数的私有函数 */ var myPrivateMethod = function (foo) { console.log(foo + myPrivateVar); }; return { // 公有变量 myPublicVar: 'foo', // 调用私有变量和方法的公用函数 * myPublicFunction: function (bar) { // 增加私有计数器值 myPrivateVal ++; myPrivateMethod(bar); } }; })();常见误区在循环中创建闭包 ...

July 26, 2019 · 1 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

12内存溢出与内存泄漏

内存溢出与内存泄露一、内存溢出一种程序运行出现的错误。当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。 二、内存泄漏占用的内存没有及时释放。内存泄露积累多了就容易导致内存溢出。常见的内存泄露: 意外的全局变量。没有及时清理的计时器或回调函数。闭包// 1. 内存溢出var obj = {}for (var i = 0; i < 10000; i++) { obj[i] = new Array(10000000) console.log('-----')}// 2. 内存泄露// 意外的全局变量,没有加varfunction fn() { a = new Array(10000000) console.log(a)}fn()// 3. 没有及时清理的计时器或回调函数var intervalId = setInterval(function () { //启动循环定时器后不清理 console.log('----')}, 1000)// clearInterval(intervalId)// 4. 闭包function fn1() { var a = 4 function fn2() { console.log(++a) } return fn2}var f = fn1()f()// f = null

June 27, 2019 · 1 min · jiezi

8JavaScript-函数高级闭包

JavaScript函数高级——闭包一、引子实例<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>00_引入</title></head><body> <button>测试1</button> <button>测试2</button> <button>测试3</button><!--需求: 点击某个按钮, 提示"点击的是第n个按钮"--><script type="text/javascript"> var btns = document.getElementsByTagName('button') //遍历加监听 /* for (var i = 0,length=btns.length; i < length; i++) { var btn = btns[i] btn.onclick = function () { alert('第'+(i+1)+'个') } }*/ /* for (var i = 0,length=btns.length; i < length; i++) { var btn = btns[i] //将btn所对应的下标保存在btn上 btn.index = i btn.onclick = function () { alert('第'+(this.index+1)+'个') } }*/ //利用闭包 for (var i = 0,length=btns.length; i < length; i++) { (function (j) { var btn = btns[j] btn.onclick = function () { alert('第'+(j+1)+'个') } })(i) }</script></body></html>二、理解闭包(1)如何产生闭包? ...

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

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

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

引言满满的干货,面试必bei系列,参考大量资料,并集合自己的理解以及相关的面试题,对JS核心知识点中的作用域、闭包、this、上下文进行了梳理。上一篇介绍了作用域和上下文。因为感觉这两个概念互相纠缠,上下文的生成会依赖作用域规则。本篇重点介绍闭包和this。 this先介绍this,因为我觉得this最简单了,掌握住分析的方法,依照方法去分析,毫无难度。 为什么引入this因为我们解耦,为什么要解耦?因为我们要复用!举个例子:我费了九牛二虎之力写了一个方法,可以对某个数组a进行复杂的操作 var a = [xxx,xxx,....]function foo (){ a xxx // do something to a xxx a // do something to a}这个方法只能a用 耦合性太强。当其他数组b想用这个方法的时候由于方法里面的操作对象固定是a导致失败。当然我也不能写成b,因为如果数组c要用难道我们再改成c?怎么办,显然这个操作的对象不能是固定的,应该说最好是一个变量,谁调用,这个变量就是谁。this就这么产生了!所以说this的可变是业务的需要,我们要的就是它的可变。当然你要是掌握不了它的变化规则,那么对你来说引入this就是一场灾难了。 this的规则总原则: 函数中的this,指的是当前函数的执行主体;谁让函数执行的,那么this就指向谁 在全局作用域下,this指向window;函数体中的this,看函数执行前有没有".",如果有,那么点前面是谁,this就指向谁;如果没有“.”,那么会指向window;如果给元素的事件行为绑定方法,那么方法中的this,就会指向当前被绑定的那个元素;回调函数中的this指向window;自执行函数中的this永远指向window;改变thisapply call bind可以改变this 问题来了,为什么要改变this? 还记得我之前说的引入this是为了进行更好的复用吗?js里面没有类,但是通过原型、继承js在努力模仿类。同一个类当然能通过继承复用代码,不同类不继承的情况下怎么复用呢?通过改变this。举个例子,Array类有reverse方法。只要是数组,都继承了Array的reverse方法,可以直接调用。 [1,2,3].reverse() //[3,2,1]现在有个类数组arguments,由于不属于Array类,没有办法继承reverse方法,但是我就是想用,咋办?改变this Array.prototype.reverse.call(argumengts)闭包在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 wiki闭包这个东西咋说呢,不同的程序员,不同的资料都有不同的理解。你把它理解为一个函数,也可以把它理解为函数+执行环境。 我们这里不纠结闭包的定义。而是关注闭包的现象,应用,再结合相关面试题去攻克它,最后谈一下我对闭包的思考。 现象话说了一箩筐,你倒是给我上代码呀..... 各位看官息怒,小的再多说几句 之前我们说过了,函数执行,生成执行环境。函数执行完,销毁执行环境。嗯,听上去很正常,不用的东西就销毁嘛。 但是如果函数执行完,该函数上下文还用用怎么办,有用的东西肯定不敢销毁了。这个就是闭包的现象,那么能引起这个现象的鄙人就把它理解为闭包! function foo () { var a = 1 return function bar () { a++ console.log(a) }}var b = foo() //函数执行完,我就问你foo的上下文你敢销毁吗?b() // 2b() // 3大家看foo执行完的结果赋值给了b,也就是函数bar赋值给了b,未来我可能让b执行的,也就是让bar执行,所以我需要保证bar能访问上下文不被销毁。bar能访问的上下文实际上是foo执行时候的上下文。所以foo执行完以后上下文不会被销毁,会继续存在。 ...

May 18, 2019 · 1 min · jiezi

在事务中使用闭包优化代码结构

闭包函数PHP官方文档对于闭包函数的定义: 匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。当然,也有其它应用的情况。简单来说,闭包函数也是一种数据类型,可以直接使用变量来存储、传参、调用等等。 事务事务简单来说就是一个核心: 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。一般情况下,每一条SQL的执行情况都需要进行判断,如果执行成功则继续,否则回滚事务。以下是PDO事务代码: $pdo = new PDO('mysql:host=localhost;dbname=demo', 'root', 'root');try { $pdo->beginTransaction(); //todo 业务代码 $pdo->commit();} catch (\Exception $e) { $pdo->rollBack(); throw $e;}PHP实现几乎所有事务都需要如此处理,但是这样重复代码太多,实际上只需要关心的部分是 业务代码 部分,使用闭包函数可以很好的解决这个问题。 闭包函数可以理解为具体的业务逻辑,不带任何事务相关操作,如果出现异常,会自动回滚事务。 PHP的简单实现代码如下: function transaction(PDO $pdo, callable $callable){ try { $pdo->beginTransaction(); $result = call_user_func($callable, $pdo); $pdo->commit(); return $result; } catch (\Exception $e) { $pdo->rollBack(); throw $e; }}PHP调用方法代码如下: transaction($pdo, function (PDO $pdo) { return $pdo->query('INSERT INTO `test` VALUES (1)');});结语使用闭包函数去简化样板代码在生产中是很常见的,具体的设计模式应该是类似“模板方法模式”。 来自我的博客:https://www.ddhigh.com/2019/0... ...

May 16, 2019 · 1 min · jiezi

JavaScript高级程序设计第3版读书笔记-第7章-函数表达式

定义函数表达式的方式有两种: 函数声明。它的重要特征就是 函数声明提升(function declaration hoisting) 即在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。函数表达式。// 函数声明function functionName(params) { ...}// 函数表达式有几种不同的方式,下面是最常见的一种var functionName = function(params) { ...}上面这种形式看起来好像是常规的变量赋值语句。而右边这种形式创建的函数叫做 匿名函数 (anonymous function)(有时候也叫 拉姆达函数 lambda),因为function关键字后面没有标识符。函数表达式与其他表达式一样,在使用前必须先赋值,否则会导致出错。sayHi(); // 错误,函数还不存在var sayHi = function () { console.log('Hi!');};表面上看,下面的代码没有问题,condition为true时,使用一个定义,否则使用另一个定义。实际上,在ECMAScript中属于无效语法,JavaScript引擎会尝试修正错误,将其转换为合理状态。但问题是浏览器尝试修正错误的做法并不一致。大多数浏览器会返回第二个声明,忽略condition的值;Firefox会在condition为true的时候返回第一个声明。因此这种做法很危险,不应该出现在你的代码中。// 不要这样做!if (condition) { function sayHi() { console.log('Hi!'); }} else { function sayHi() { console.log('Yo!'); }}上述代码改为函数表达式就没有问题// 可以这样做var sayHi;if (condition) { sayHi = function() { console.log('Hi!'); }} else { sayHi = function() { console.log('Yo!'); }}能够创建函数再赋值给变量,也就能把函数作为其他函数的返回值。createComparisonFunction() 就返回了一个匿名函数。function createComparisonFunction (propertyName) { return function (object1, object2) { var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } }}在把函数当成值来使用的情况下,都可以使用匿名函数。不过,这并不是匿名函数唯一的用途。递归递归函数是在一个函数通过名字调用自身的情况下构成的// 经典的递归阶乘函数function factorial (num) { if (num <= -1) { return 1; } else { return num * factorial(num - 1); }}虽然递归阶乘函数表面看没有什么问题,但下面的代码却可能导致它出错// 把factorial() 函数保存在变量anotherFactorial中var anotherFactorial = factorial;// 将factorial设置为null// 现在指向原始函数的引用只剩下anotherFactorialfactorial = null;// 原始函数必须执行factorial()// 但factorial不再是函数,所以导致出错anotherFactorial(4); // throw error!可以使用 arguments.callee (指向正在执行函数的指针)实现函数的递归调用// 非严格模式function factorial (num) { if (num <= -1) { return 1; } else { return num * arguments.callee(num - 1); }}但在严格模式下不能通过脚本访问 arguments.callee,会导致出错。可以使用命名函数表达式来达成相同的结果var factorial = (function f(num) { if (num <= -1) { return 1; } else { return num * f(num - 1); }})闭包匿名函数 和 闭包 是两个概念,容易混淆。 闭包 是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,仍以前面的 createComparisonFunction() 函数为例function createComparisonFunction (propertyName) { return function (object1, object2) { // 下面两行代码访问了外部函数中的变量propertyName // 即使这个内部函数被返回了,而且是在其他地方被调用了 // 它仍然可以访问变量 propertyName var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } }}上述例子,即使内部函数被返回了,在其他地方调用,它仍然可以访问propertName。因为这个内部函数的作用域链中包含 createComparisonFunction() 的作用域。要搞清楚其中细节,必须从理解函数被调用的时候都会发生什么入手。第4章介绍过 作用域链。当某个函数被 调用 时会发生下列事情: ...

May 15, 2019 · 5 min · jiezi

JavaScript-闭包指南

翻译:疯狂的技术宅https://medium.freecodecamp.o...本文首发微信公众号:前端先锋欢迎关注,每天都给你推送新鲜的前端技术文章 闭包是函数创建时作用域内所有变量的集合。要使用闭包,需要在另一个函数中创建一个函数,这种函数被称为嵌套函数。内部函数可以访问外部函数作用域中的变量(依靠闭包可以访问外部函数作用域),即使在返回外部函数之后也是如此。每次创建嵌套函数时都会创建闭包。 在继续了解闭包之前,首先了解一下JavaScript中的作用域链。 通常,有两种类型的作用域: 全局作用域局部作用域在JavaScript中,函数内部的变量在外部是不可见的。但是在块内的变量(if 或 while 之类)是可见的。 因此,JavaScript有函数作用域。没有块作用域。 var a = 10;function app(){ var b = 2; console.log(a); // 10 console.log(b); // 2}console.log(b); // ReferenceError: b is not definedapp();正像我们已知的那样,a 是一个全局变量并且 b 是一个局部变量,它是app函数独有的。 我们无法从局部作用域之外获取局部变量的值。 使用嵌套函数 —— 函数内部的函数var a = 10;function app(){ var b = 2; var d = 3; function add(){ var c = a + b; } return add;}var x = app();console.dir(x);在这里app是父函数,add函数是子函数。 代码中没有用 console.log 而是用了console.dir 来输出指定JavaScript对象的所有属性,这有助于开发人员获取对象的属性变量 x 被分配给app函数,app函数返回add函数。因此我们可以看到add函数的所有对象属性。如果在浏览器中查看控制台,可以在Scopes数组中看到Closure对象。 ...

May 5, 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变量存储机制,深度理解闭包

对于下面的script代码,它们在内存中是怎样分配的呢? var a = 123; // 赋值运算符,赋的是内存地址var b = "hello";function f() {} // 函数f就在函数(方法)定义区f(); // 函数调用时在函数(方法)缓存区占用内存var f1 = function() {} // 变量名f1存在栈内存中,无名函数在函数(方法)定义区function Person(){} // 函数Person存在函数定义区var p = new Person(); // new关键字代表后面的内存创建在堆中var n = null; // 变量里面存的地址是堆中的null对象var u = undefined; // 变量里面没有存地址typeof xx === "object";// 堆中typeof xx === "function";// 函数(方法)缓存区typeof xx === "number";// 池(常量)typeof xx === "string";// 池(常量)typeof xx === "boolean";// 池(常量)typeof xx === "undefined";// 栈中内存图下面是对应的内存图: 闭包闭包:在函数缓存区有永久的生命周期通过调用一个函数,让它在函数缓存区有永久的生命周期 ...

April 23, 2019 · 1 min · jiezi

PHP闭包的理解与介绍

看过许多关于PHP中闭包的讲解,每个文档想要表达的意思大体相同,但是理解起来很费劲,我根据自身理解加以描述,有更好的理解请指出众所周知,大家都知道PHP的闭包是function () use (){};本文分为3步1:讲解闭包的使用2:闭包实例3:闭包总结1、讲解闭包的使用1:闭包中的use使用-上篇 function () use($param){}; $param = 1;$data = function () use ($param){ var_dump($param); };$data();$param = 2;$data();// 输出结果为11分析结果:use的用法相当于对象(类的用法),$data(),表示实例化一次,然而语言有着文本流的特性,第二次$data()没有重新实例,实例的时候取到的值是1,所以输出结果为上面结果;2:闭包中的use使用-下篇$param = 1;$data = function () use ($param){ var_dump($param); };$data();$param = 2;$data = function () use ($param){ var_dump($param); };$data();// 输出结果为12分析结果:文本流的形式,又实例化一次变量所以存储变量为2,最终输出为2;3:闭包中function()后面的括号,use前面的括号用法 function ($obj) use($param){}; $param = 1; $data = function ($obj) use ($param) { var_dump($obj); var_dump($param); }; $data(2); $data(3); $param = 2; $data(4); // 输出 2 1 3 1 4 1分析结果:function后面的括号相当于函数的用法,每次调用函数时传入数据一样,每次都需要传入,后面的use表示实例一次后不变,所以obj随时改变。2、闭包实例例子1: $arr = [ ‘米’ => [‘咸粥’, ‘甜粥’, ‘米饭’], ‘面’ => [‘面条’, ‘花卷’, ‘馒头’], ]; $param = ‘’; $bag = function ($data) use ($param) { $l = count($data); return $data[rand(0, $l-1)]; }; $eat_arr = []; foreach ($arr as $key => $value) { $each_arr[] = ‘吃’.$key.’:’.$bag($value); } echo implode(’,’, $each_arr); // 输出 吃米:米饭, 吃面:面条 吃米:甜粥, 吃面:馒头例子2: $arr = [ ‘米’ => [‘咸粥’, ‘甜粥’, ‘米饭’], ‘面’ => [‘面条’, ‘花卷’, ‘馒头’], ]; $eat_arr = []; foreach ($arr as $key => $value) { $bag = function () use ($value) { $l = count($value); return $value[rand(0, $l-1)]; }; $each_arr[] = ‘吃’.$key.’:’.$bag(); } echo implode(’,’, $each_arr); // 输出 吃米:甜粥, 吃面:面条 吃米:甜粥, 吃面:花卷 3、闭包总结闭包总结到最后,就是与函数不同的地方就是多加了一个use中间值,使用的时候注意一点是function后面的()为可变变量,use()里面的变量为实例一次后不改动的变量,循环里面写闭包这种方法不是很好用,相当于每次都实例,所以闭包的写法可以先定义一个方法在调用。闭包与函数真正的区别:函数:封装一次多处调用。闭包:只限于本方法使用,耦合度低到忽略。 ...

April 11, 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

GO 匿名函数和闭包

匿名函数:顾名思义就是没有名字的函数。很多语言都有如:java,js,php等,其中js最钟情。匿名函数最大的用途是来模拟块级作用域,避免数据污染的。今天主要讲一下Golang语言的匿名函数和闭包。匿名函数示例:1、package mainimport ( “fmt”)func main() { f:=func(){ fmt.Println(“hello world”) } f()//hello world fmt.Printf("%T\n", f) //打印 func()}2、带参数package mainimport ( “fmt”)func main() { f:=func(args string){ fmt.Println(args) } f(“hello world”)//hello world //或 (func(args string){ fmt.Println(args) })(“hello world”)//hello world //或 func(args string) { fmt.Println(args) }(“hello world”) //hello world}3、带返回值package mainimport “fmt"func main() { f:=func()string{ return “hello world” } a:=f() fmt.Println(a)//hello world}4、多个匿名函数package mainimport “fmt"func main() { f1,f2:=F(1,2) fmt.Println(f1(4))//6 fmt.Println(f2())//6}func F(x, y int)(func(int)int,func()int) { f1 := func(z int) int { return (x + y) * z / 2 } f2 := func() int { return 2 * (x + y) } return f1,f2}闭包(closure)闭包:说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕。示例:1、package mainimport “fmt"func main() { a := Fun() b:=a(“hello “) c:=a(“hello “) fmt.Println(b)//hello world fmt.Println(c)//worldhello hello }func Fun() func(string) string { a := “world” return func(args string) string { a += args return a }}2、package mainimport “fmt"func main() { a := Fun() d := Fun() b:=a(“hello “) c:=a(“hello “) e:=d(“hello “) f:=d(“hello “) fmt.Println(b)//hello world fmt.Println(c)//worldhello hello fmt.Println(e)//hello world fmt.Println(f)//worldhello hello}func Fun() func(string) string { a := “world” return func(args string) string { a += args return a }}注意两次调用F(),维护的不是同一个a变量。3、package mainimport “fmt"func main() { a := F() a0//0xc00004c080 3 a1//0xc00004c080 3 a2//0xc00004c080 3}func F() []func() { b := make([]func(), 3, 3) for i := 0; i < 3; i++ { b[i] = func() { fmt.Println(&i,i) } } return b}闭包通过引用的方式使用外部函数的变量。例中只调用了一次函数F,构成一个闭包,i 在外部函数B中定义,所以闭包维护该变量 i ,a[0]、a[1]、a[2]中的 i 都是闭包中 i 的引用。因此执行,i 的值已经变为3,故再调用a0时的输出是3而不是0。4、如何避免上面的BUG ,用下面的方法,注意和上面示例对比。package mainimport “fmt"func main() { a := F() a0 //0xc00000a0a8 0 a1 //0xc00000a0c0 1 a2 //0xc00000a0c8 2}func F() []func() { b := make([]func(), 3, 3) for i := 0; i < 3; i++ { b[i] = (func(j int) func() { return func() { fmt.Println(&j, j) } })(i) } return b}或者package mainimport “fmt"func main() { a := F() a0 //0xc00004c080 0 a1 //0xc00004c088 1 a2 //0xc00004c090 2}func F() []func() { b := make([]func(), 3, 3) for i := 0; i < 3; i++ { j := i b[i] = func() { fmt.Println(&j, j) } } return b}每次 操作仅将匿名函数放入到数组中,但并未执行,并且引用的变量都是 i,随着 i 的改变匿名函数中的 i 也在改变,所以当执行这些函数时,他们读取的都是环境变量 i 最后一次的值。解决的方法就是每次复制变量 i 然后传到匿名函数中,让闭包的环境变量不相同。5、package mainimport “fmt"func main() { fmt.Println(F())//2}func F() (r int) { defer func() { r++ }() return 1}输出结果为2,即先执行r=1 ,再执行r++。6、递归函数还有一种情况就是必须用都闭包,就是递归函数。package mainimport “fmt"func F(i int) int { if i <= 1 { return 1 } return i * F(i-1)}func main() { var i int = 3 fmt.Println(i, F(i))// 3 6}7、斐波那契数列(Fibonacci)这个数列从第3项开始,每一项都等于前两项之和。package mainimport “fmt"func fibonaci(i int) int { if i == 0 { return 0 } if i == 1 { return 1 } return fibonaci(i-1) + fibonaci(i-2)}func main() { var i int for i = 0; i < 10; i++ { fmt.Printf("%d\n”, fibonaci(i)) }}小结:匿名函数和闭包其实是一回事儿,匿名函数就是闭包。匿名函数给编程带来灵活性的同时也容易产生bug,在使用过程当中要多注意函数的参数,及可接受的参数的问题。links目录 ...

March 28, 2019 · 2 min · jiezi

大话javascript 3期:闭包

一、什么是闭包1.闭包的定义闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境(包含自由变量)。环境由闭包创建时在作用域中的任何局部变量组成。闭包是指有权访问另外一个函数作用域中的变量的函数闭包是函数以及函数声明所在的词法环境的组合。由此,我们可以看出闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量举个例子:var a = 1;function foo() { console.log(a);}foo();foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛2.闭包的概念function fa(){ var va = “this is fa”; function fb(){ console.log(va); } return fb;}var fc = fa();fc();//“this is fa"其实,简单点说,就是在 A 函数内部,存在 B 函数, B函数 在 A 函数 执行完毕后再执行。B执行时,访问了已经执行完毕的 A函数内部的变量和函数。由此可知:闭包是函数A的执行环境以及执行环境中的函数B组合而构成的。变量都储存在其所在执行环境的活动对象中,所以说是函数A的执行环境。当函数A执行完毕后,函数B再执行,B的作用域中就保留着函数A的活动对象,因此B中可以访问A中的变量,函数,arguments对象。此时产生了闭包。大部分书中,都把函数B称为闭包,而在谷歌浏览器中,把A函数称为闭包。3.闭包的本质之前说过,当函数执行完毕后,局部活动对象就会被销毁。其中保存的变量,函数都会被销毁。内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况就不同了。以上面的例子来说,函数fb和其所在的环境函数fa,就组成了闭包。函数fa执行完毕后,按道理说, 函数fa执行环境中的 活动对象就应该被销毁了。但是,因为函数fa执行时,其中的函数fb被返回,被变量fc引用着。导致,函数fa的活动对象没有被销毁。而在其后fc()执行,就是函数fb执行时,构建的作用域中保存着函数fa的活动对象,因此,函数fb中可以通过作用域链访问函数fa中的变量。其实,简单的说:就是fa函数执行完毕了,其内部的fb函数没有执行,并返回fb的引用,当fb再次执行时,fb的作用域中保留着fa函数的活动对象。二、闭包的作用闭包的特点是读取函数内部局部变量,并将局部变量保存在内存,延长其生命周期。作用通过闭包,在外部环境访问内部环境的变量。使得这些变量一直保存在内存中,不会被垃圾回收。以使用闭包实现以下功能:1.解决类似循环绑定事件的问题在实际开发中,经常会遇到需要循环绑定事件的需求,比如在id为container的元素中添加5个按钮,每个按钮的文案是相应序号,点击打印输出对应序号。其中第一个方法很容易错误写成:var container = document.getElementById(‘container’);for(var i = 1; i <= 5; i++) { var btn = document.createElement(‘button’), text = document.createTextNode(i); btn.appendChild(text); btn.addEventListener(‘click’, function(){ console.log(i); }) container.appendChild(btn);}虽然给不同的按钮分别绑定了事件函数,但是5个函数其实共享了一个变量 i。由于点击事件在 js 代码执行完成之后发生,此时的变量 i 值为6,所以每个按钮点击打印输出都是6。为了解决这个问题,我们可以修改代码,给各个点击事件函数建立独立的闭包,保持不同状态的i。var container = document.getElementById(‘container’);for(var i = 1; i <= 5; i++) { (function(_i) { var btn = document.createElement(‘button’), text = document.createTextNode(_i); btn.appendChild(text); btn.addEventListener(‘click’, function(){ console.log(_i); }) container.appendChild(btn); })(i);}注:解决这个问题更好的方法是使用 ES6 的 let,声明块级的局部变量。2.封装私有变量(1) 经典的计数器例子:function makeCounter() { var value = 0; return { getValue: function() { return value; }, increment: function() { value++; }, decrement: function() { value–; } }}var a = makeCounter();var b = makeCounter();b.increment();b.increment();b.decrement();b.getValue(); // 1a.getValue(); // 0a.value; // undefined每次调用makeCounter函数,环境是不相同的,所以对b进行的increment/decrement操作不会影响a的value属性。同时,对value属性,只能通过getValue方法进行访问,而不能直接通过value属性进行访问。(2) 经典的循环闭包面试题for (var i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i1000);}正常预想下,上面这段代码我们以为是分别输出数字1-5,每秒一个。但实际上,运行时输出的却是每秒输出一个6,一共五次。Why?for循环有一个特点,就是“i判断失败一次才停止”。所以,i在不断的自加1的时候,直到i等于5,i才失败,这时候循环体不再执行,会跳出,所以i等于5没错。那么为什么5次循环的i都等于5?原因就是setTimeout()的回调,也就是console.log(i);被压到任务队列的最后,for循环是同步任务,所以先执行,等于是空跑了5次循环。于是,i都等于5之后,console.log(i);刚开始第一次执行,当然输出全是5。根据setTimeout定义的操作在函数调用栈清空之后才会执行的特点,for循环里定义了5个setTimeout操作。而当这些操作开始执行时,for循环的i值,已经先一步变成了6。因此输出结果总为6。而我们想要让输出结果依次执行,我们就必须借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值即可。简单来说,原因是,延迟函数的回调会在循环结束时才执行。根据作用域的工作原理,循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,实际上只有一个i。解决办法方法1:用立即执行函数模拟块级作用域利用立即执行函数和函数作用域来解决,用自执行函数传参,这样自执行函数内部形成了局部作用域,不受外部变量变化的影响。我们可以通过立即执行函数创建作用域。(立即执行函数会通过声明并立即执行一个函数来创建作用域)。 for (var i=1; i<=5; i++) { (function(i) { setTimeout( function timer() { console.log(i); }, i1000 ); })(i) }// 1// 2// 3// 4// 5方法2:利用闭包function makeClosures(i){ //这里就和 内部的匿名函数构成闭包了 var i = i; //这步是不需要的,为了让看客们看的轻松点 return function(){ console.log(i); //匿名没有执行,它可以访问i的值,保存着这个i的值。 }}for (var i=1; i<=5; i++) { setTimeout(makeClosures(i),i1000); //这里简单说下,这里makeClosures(i), 是函数执行,并不是传参,不是一个概念 //每次循环时,都执行了makeClosures函数,都返回了一个没有被执行的匿名函数 //(这里就是返回了5个匿名函数),每个匿名函数都是一个局部作用域,保存着每次传进来的i值 //因此,每个匿名函数执行时,读取i值,都是自己作用域内保存的值,是不一样的。所以,就得到了想要的结果}//1//2//3//4//5方法3:利用块级作用域ES6引入的let在循环中不止会被声明一次,在每次迭代都会声明:for (let i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i1000);}因为使用let,导致每次循环都会创建一个新的块级作用域,这样,虽然setTimeout 中的匿名函数内没有 i 值,但它向上作用域读取i 值,就读到了块级作用域内 i 的值。三、闭包的问题使用闭包会将局部变量保持在内存中,所以会占用大量内存,影响性能。所以在不再需要使用这些局部变量的时候,应该手动将这些变量设置为null, 使变量能被回收。当闭包的作用域中保存一些DOM节点时,较容易出现循环引用,可能会造成内存泄漏。原因是在IE9以下的浏览器中,由于BOM 和DOM中的对象是使用C++以COM 对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略,当出现循环引用时,会导致对象无法被回收。当然,同样可以通过设置变量为null解决。举例如下:function func() { var element = document.getElementById(’test’); element.onClick = function() { console.log(element.id); };}func 函数为 element 添加了闭包点击事件,匿名函数中又对element进行了引用,使得 element 的引用始终不为0。解决办法是使用变量保存所需内容,并在退出函数时将 element 置为 null。function func() { var element = document.getElementById(’test’), id = element.id; element.onClick = function() { console.log(id); }; element = null;}四、应用场景:模块与柯里化模块也是利用了闭包的一个强大的代码模式。function CoolModule(){ var something=“cool”; var anothor=[1,2,3]; function doSomething(){ console.log(something); } function doAnthor(){ console.log(anothor.join(”!")); } return{ doSomethig:doSomething, doAnothor:doAnother };}var foo=CoolMOdule();foo.doSomething();//coolfoo.doAnother();//1!2!3模块有2个主要特征:为创建内部作用域而调用了一个包装函数;包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。关于模块的引入import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上;module会将整个模块的API导入并绑定到一个变量上;export会将当前模块的一个标识符导出为公共API。 ...

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

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

原文链接在这之前先要了解一下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

面试官问我:什么是JavaScript闭包,我该如何回答

闭包,有人说它是一种设计理念,有人说所有的函数都是闭包。到底什么是闭包?这个问题在面试是时候经常都会被问,很多小白一听就懵逼了,不知道如何回答好。这个问题也有很多朋友在公众号给李老师留言了,问题表达方式不一样,都是终归到一点,就是对闭包没有很清晰的理解。大家经常去网上找相关资料,但是对闭包的说法都是各种各样的,让大家对闭包的定义没有一个概念。所以今天我们来一起讲讲什么是闭包,帮助大家理解,今天的内容可以直接收藏起来。方便以后看。什么是闭包(Closure)简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。MDN 上面这么说:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。这种官方的概念是比较难理解的,在面试的时候说出来也不是很专业,因为没办法有个具体的逻辑。我个人认为,理解闭包的关键在于:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象,这就是闭包的重要概念。产生一个闭包创建闭包最常见方式,就是在一个函数内部创建另一个函数。下面例子中的 closure 就是一个闭包:闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。闭包的注意事项.通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。从上述代码可以看到add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。最后通过 null 释放了 add5 和 add10 对闭包的引用。在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收; 如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。闭包只能取得包含函数中任何变量的最后一个值大家看一下上面这个代码,arr数组中包含了10个匿名函数,每个匿名函数都能访问外部函数的变量i,那么i是多少呢?当arrFunc执行完毕后,其作用域被销毁,但它的变量对象仍保存在内存中,得以被匿名访问,这时i的值为10。要想保存在循环过程中每一个i的值,需要在匿名函数外部再套用一个匿名函数,在这个匿名函数中定义另一个变量并且立即执行来保存i的值。这时最内部的匿名函数访问的是num的值,所以数组中10个匿名函数的返回值就是1-10。闭包中的this对象在上面这段代码中,obj.getName()()实际上是在全局作用域中调用了匿名函数,this指向了window。这里要理解函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this就指向哪里。window才是匿名函数功能执行的环境。如果想使this指向外部函数的执行环境,可以这样改写:在闭包中,arguments与this也有相同的问题。下面的情况也要注意:obj.getName();这时getName()是在对象obj的环境中执行的,所以this指向obj。(obj.getName = obj.getName)赋值语句返回的是等号右边的值,在全局作用域中返回,所以(obj.getName = obj.getName)();的this指向全局。要把函数名和函数功能分割开来。内存泄漏闭包会引用包含函数的整个变量对象,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。所以我们有必要在对这个元素操作完之后主动销毁。函数内部的定时器当函数内部的定时器引用了外部函数的变量对象时,该变量对象不会被销毁。闭包的应用应用闭包的主要场合是:设计私有的方法和变量。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和函数内定义的其他函数。把有权访问私有变量的公有方法称为特权方法(privileged method)。在这里,我们需要理解两个概念:模块模式(The Module Pattern):为单例创建私有变量和方法。单例(singleton):指的是只有一个实例的对象。JavaScript 一般以对象字面量的方式来创建一个单例对象。上面是普通模式创建的单例,下面使用模块模式创建单例:匿名函数最大的用途是创建闭包,并且还可以构建命名空间,以减少全局变量的使用。从而使用闭包模块化代码,减少全局变量的污染。在这段代码中函数 addEvent 和 removeEvent 都是局部变量,但我们可以通过全局变量 objEvent 使用它,这就大大减少了全局变量的使用,增强了网页的安全性。运用闭包的关键闭包引用外部函数变量对象中的值;在外部函数的外部调用闭包。闭包的缺陷闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。最后 来一道有关闭包的面试题下面代码中,标记 ? 的地方输出分别是什么?大家结合今天讲解的内容,思考一下答案,大家可以把答案发到留意上吧。好了,今天的讲解就那么多,如果你还有什么前端问题想提问的,或者你想李老师下次给大家讲什么内容,可以直接留意提问,说不定下次文章就会讲解了。如果你觉得这篇文章对你有帮助,请转发点赞支持一下!

February 19, 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

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

JavaScript 进阶知识 - 高级篇

JS高级前言经过前面几篇文章的学习,相信大家已经对js有了大部分的理解了,但是要想真正的掌握好js,本篇才是关键。由于js高级阶段的知识点比较难理解,所以本篇文章花了大量的时间去理思路,有可能有一些知识点遗漏了,也有肯能有部分知识点写的不对,欢迎大家留言纠正。另外,大家在以后的学习中千万不要被一些难点所吓到,听说有些知识点很难,其实并不是真正的难,只要你静下心慢慢的理解,其实还是很简单的。1.异常处理常见的异常分类运行环境的多样性导致的异常(浏览器)语法错误,代码错误异常最大的特征,就是一旦代码出现异常,后面的代码就不会执行。1.1异常捕获捕获异常,使用try-catch语句:try{ // 这里写可能出现异常的代码}catch(e){ // e-捕获的异常对象 // 可以在此处书写出现异常后的处理代码}异常捕获语句执行的过程为:代码正常运行, 如果在try中出现了错误,try里面出现错误的语句后面的代码都不再执行, 直接跳转到catch中catch中处理错误信息然后继续执行后面的代码如果try中没有出现错误, 那么不走catch直接执行后面的代码通过try-catch语句进行异常捕获之后,代码将会继续执行,而不会中断。示例代码:console.log(‘代码开始执行’);try{ console.log(num); // num 在外部是没有定义的}catch(e){ console.log(e); console.log(‘我已经把错误处理了’);}console.log(‘代码结束执行’);效果图:从效果图中我们可以看到,num是一个没有定义的变量,如果没有放在try-catch代码块中,后面的‘代码结束执行’就不会被打印。通过把try-catch放在代码块中,出现错误后,就不会影响后面代码的运行了,他会把错误信息打印出来。注意:语法错误异常用try-catch语句无法捕获,因为在预解析阶段,语法错误会直接检测出来,而不会等到运行的时候才报错。try-catch在一般日常开发中基本用不到,但是如果要写框架什么的,用的会非常多。因为这个会让框架变得健壮异常捕获语句的完整模式异常捕获语句的完整模式为try-catch-finallytry { //可能出现错误的代码} catch ( e ) { //如果出现错误就执行} finally { //结束 try 这个代码块之前执行, 即最后执行}finally中的代码,不管有没有发生异常,都会执行。一般用在后端语言中,用来释放资源,JavaScript中很少会用到1.2抛出异常如何手动的抛出异常呢?案例:自己写的一个函数,需要一个参数,如果用户不传参数,此时想直接给用户抛出异常,就需要了解如何抛出异常。抛出异常使用throw关键字,语法如下:throw 异常对象;异常对象一般是用new Error(“异常消息”), 也可以使用任意对象示例代码:function test(para){ if(para == undefined){ throw new Error(“请传递参数”); //这里也可以使用自定义的对象 throw {“id”:1, msg:“参数未传递”}; }}try{ test();}catch(e){ console.log(e);}效果图:1.3异常的传递机制function f1 () { f2(); }function f2 () { f3();}function f3() { throw new Error( ’error’ );}f1(); // f1 称为调用者, 或主调函数, f2 称为被调用者, 或被调函数当在被调函数内发生异常的时候,异常会一级一级往上抛出。2.面向对象编程在了解面向对象编程之前,我们先来了解下什么是面向过程,什么是面向对象,他们之间的区别是什么。2.1 面向过程和面向对象的的对比举个例子:日常洗衣服1.面向过程的思维方式:面向过程编程:将解决问题的关注点放在解决问题的具体细节上,关注如何一步一步实现代码细节;step 1:收拾脏衣服step 2:打开洗衣机盖step 3:将脏衣服放进去step 4:设定洗衣程序step 5:开始洗衣服step 6:打开洗衣机盖子step 7:晒衣服2.面向对象的思维方式:面向对象编程:将解决问题的关注点放在解决问题所需的对象上,我们重点找对象;人(对象)洗衣机(对象)在面向对象的思维方式中:我们只关心要完成事情需要的对象,面向对象其实就是对面向过程的封装;示例代码:在页面上动态创建一个元素//面向过程//1-创建一个divvar div=document.createElement(‘div’);//2-div设置内容div.innerHTML=‘我是div’;//3-添加到页面中document.body.appendChild(div);//面向对象$(‘body’).append(’<div>我也是div</div>’);我们可以看出,jQ封装的其实就是对面向过程的封装。总结: 面向对象是一种解决问题的思路,一种编程思想。2.2 面向对象编程举例设置页面中的div和p的边框为'1px solid red'1、传统的处理办法// 1> 获取div标签var divs = document.getElementsByTagName( ‘div’ );// 2> 遍历获取到的div标签for(var i = 0; i < divs.length; i++) { //3> 获取到每一个div元素,设置div的样式 divs[i].style.border = “1px dotted black”;}// 4> 获取p标签var ps = document.getElementsByTagName(“p”);// 5> 遍历获取到的p标签for(var j = 0; j < ps.length; j++) { // 获取到每一个p元素 设置p标签的样式 ps[j].style.border = “1px dotted black”; }2、使用函数进行封装优化// 通过标签名字来获取页面中的元素 function tag(tagName) { return document.getElementsByTagName(tagName); }// 封装一个设置样式的函数 function setStyle(arr) { for(var i = 0; i < arr.length; i++) { // 获取到每一个div或者p元素 arr[i].style.border = “1px solid #abc”; } }var dvs = tag(“div”);var ps = tag(“p”);setStyle(dvs); setStyle(ps);3、使用面向对象的方式// 更好的做法:是将功能相近的代码放到一起 var obj = { // 命名空间 getEle: { tag: function (tagName) { return document.getElementsByTagName(tagName); }, id: function (idName) { return document.getElementById(idName); } // … }, setCss: { setStyle: function (arr) { for(var i = 0; i < arr.length; i++) { arr[i].style.border = “1px solid #abc”; } }, css: function() {}, addClass: function() {}, removeClass: function() {} // … } // 属性操作模块 // 动画模块 // 事件模块 // … };var divs = obj.getEle.tag(‘div’);obj.setCss.setStyle(divs);2.3 面向对象的三大特性面向对象的三大特性分别是:‘封装’,‘继承’,‘多态’。1、封装性对象就是对属性和方法的封装,要实现一个功能,对外暴露一些接口,调用者只需通过接口调用即可,不需要关注接口内部实现原理。js对象就是“键值对”的集合键值如果是数据( 基本数据, 复合数据, 空数据 ), 就称为属性如果键值是函数, 那么就称为方法对象就是将属性与方法封装起来方法是将过程封装起来2、继承性所谓继承就是自己没有, 别人有,拿过来为自己所用, 并成为自己的东西2.1、传统继承基于模板子类可以使用从父类继承的属性和方法。class Person { string name; int age;}class Student : Person {}var stu = new Student();stu.name即:让某个类型的对象获得另一个类型的对象的属性的方法2.2、js 继承基于对象在JavaScript中,继承就是当前对象可以使用其他对象的方法和属性。js继承实现举例:混入(mix)// 参数o1和o2是两个对象,其中o1对象继承了所有o2对象的“k”属性或者方法var o1 = {};var o2 = { name: ‘Levi’, age: 18, gender: ‘male’};function mix ( o1, o2 ) { for ( var k in o2 ) { o1[ k ] = o2[ k ]; }}mix(o1, o2);console.log(o1.name); // “Levi"3、多态性(基于强类型,js中没有多态)只做了解同一个类型的变量可以表现出不同形态,用父类的变量指向子类的对象。动物 animal = new 子类(); // 子类:麻雀、狗、猫、猪、狐狸…动物 animal = new 狗();animal.叫();2.4 创建对象的方式1、字面量 {}var student1 = { name:‘诸葛亮’, score:100, code:1,}var student2 = { name:‘蔡文姬’, score:98, code:2,}var student3 = { name:‘张飞’, score:68, code:3,}字面量创建方式,代码复用性太低,每一次都需要重新创建一个对象。2、Object()构造函数var student1 = new Object(); student1.name = ‘诸葛亮’; student1.score = 100; student1.code = 1;var student2 = new Object(); student2.name = ‘蔡文姬’; student2.score = 98; student2.code = 2; var student3 = new Object(); student3.name = ‘张飞’; student3.score = 68; student3.code = 3;代码复用性太低,字面量创建的方式其实就是代替Object()构造函数创建方式的。3、自定义构造函数自定义构造函数,可以快速创建多个对象,并且代码复用性高。// 一般为了区分构造函数与普通函数,构造函数名首字母大写function Student(name,score,code){ this.name = name; this.score = score; this.code = code;}var stu1 = new Student(‘诸葛亮’,100,1);var stu2 = new Student(‘蔡文姬’,98,2);var stu3 = new Student(‘张飞’,68,3);构造函数语法:构造函数名首字母大写;构造函数一般与关键字:new一起使用;构造函数一般不需要设置return语句,默认返回的是新创建的对象;this指向的是新创建的对象。构造函数的执行过程:new关键字,创建一个新的对象,会在内存中开辟一个新的储存空间;让构造函数中的this指向新创建的对象;执行构造函数,给新创建的对象进行初始化(赋值);构造函数执行(初始化)完成,会将新创建的对象返回。构造函数的注意点:构造函数本身也是函数;构造函数有返回值,默认返回的是新创建的对象;但是如果手动添加返回值,添加的是值类型数据的时候,构造函数没有影响。如果添加的是引用类型(数组、对象等)值的时候,会替换掉新创建的对象。function Dog(){ this.name=“哈士奇”; this.age=0.5; this.watch=function(){ console.log(‘汪汪汪,禁止入内’); } // return false; 返回值不会改变,还是新创建的对象 // return 123; 返回值不会改变,还是新创建的对象 // return [1,2,3,4,5]; 返回值发生改变,返回的是这个数组 return {aaa:‘bbbb’}; // 返回值发生改变,返回的是这个对象}var d1=new Dog(); // 新创建一个对象console.log(d1);构造函数可以当做普通函数执行,里面的this指向的是全局对象window。 function Dog(){ this.name=“husky”; this.age=0.5; this.watch=function(){ console.log(‘汪汪汪,禁止入内’); } console.log(this); // window对象 return 1;}console.log(Dog()); // 打印 12.5 面向对象案例通过一个案例,我们来了解下面向对象编程(案例中有一个prototype概念,可以学完原型那一章后再来看这个案例)。需求:实现一个MP3音乐管理案例;同种类型的MP3,厂家会生产出成百上千个,但是每个MP3都有各自的样式、使用者、歌曲;每个MP3都有一样的播放、暂停、增删歌曲的功能(方法);图解:示例代码: // 每个MP3都有自己的 主人:owner 样式:color 歌曲:list function MP3(name,color,list){ this.owner = name || ‘Levi’; // 不传值时默认使用者是‘Levi’ this.color = color || ‘pink’; this.musicList = list || [ {songName:‘男人哭吧不是罪’,singer:‘刘德华’}, {songName:‘吻别’,singer:‘张学友’}, {songName:‘对你爱不完’,singer:‘郭富城’}, {songName:‘今夜你会不会来’,singer:‘黎明’} ]; } // 所有的MP3都有 播放 暂停 音乐 增删改查的功能 MP3.prototype = { // 新增 add:function(songName,singer){ this.musicList.push({songName:songName,singer:singer}); }, // 查找 select:function(songName){ for(var i=0;i<this.musicList.length;i++){ if(this.musicList[i].songName == songName){ return this.musicList[i]; } } return null; // 如果没有搜索到返回null }, // 修改 update:function(songName,singer){ // 先找到这首歌 在修改 var result = this.select(songName); // 查找 if(result){ result.singer = singer; // 修改 } }, // 删除 delete:function(songName){ // 先找到音乐 splice(index,1) var result = this.select(songName); // 知道该音乐的索引值 // 删除 if(result){ var index = this.musicList.indexOf(result); this.musicList.splice(index,1); // 从指定索引值来删除数据 } }, // 显示 show:function(){ console.log(this.owner+‘的MP3’); for(var i=0;i<this.musicList.length;i++){ console.log(this.musicList[i].songName +’—’+this.musicList[i].singer); } } } var XiaoHong = new MP3(‘小红’); // 实例小红MP3 var XiaoMing = new MP3(‘小明’); // 实例小明MP3 var XiaoDong = new MP3(‘小东’); // 实例小东MP3 XiaoHong.add(‘十年’,‘陈奕迅’); // 小红的歌单里添加歌曲 XiaoDong.add(‘月亮之上’,‘凤凰传奇’); // 小东的歌单里添加歌曲 XiaoMing.musicList = [ // 小明的歌单替换 { songName:‘精忠报国’, singer:‘屠洪刚’ }, { songName:‘窗外’, singer:‘未知’ } ]; // 展示各自的歌单 XiaoHong.show(); XiaoMing.show(); XiaoDong.show();打印结果:3.原型3.1 传统构造函数存在问题通过自定义构造函数的方式,创建小狗对象:两个实例化出来的“小狗”,它们都用的同一个say方法,为什么最后是false呢?function Dog(name, age) { this.name = name; this.age = age; this.say = function() { console.log(‘汪汪汪’); }}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为false画个图理解下:每次创建一个对象的时候,都会开辟一个新的空间,我们从上图可以看出,每只创建的小狗有一个say方法,这个方法都是独立的,但是功能完全相同。随着创建小狗的数量增多,造成内存的浪费就更多,这就是我们需要解决的问题。为了避免内存的浪费,我们想要的其实是下图的效果:解决方法:这里最好的办法就是将函数体放在构造函数之外,在构造函数中只需要引用该函数即可。function sayFn() { console.log(‘汪汪汪’);}function Dog(name, age) { this.name = name; this.age = age; this.say = sayFn();}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为 true这样写依然存在问题:全局变量增多,会增加引入框架命名冲突的风险代码结构混乱,会变得难以维护想要解决上面的问题就需要用到构造函数的原型概念。3.2 原型的概念prototype:原型。每个构造函数在创建出来的时候系统会自动给这个构造函数创建并且关联一个空的对象。这个空的对象,就叫做原型。关键点:每一个由构造函数创建出来的对象,都会默认的和构造函数的原型关联;当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法,如果当前对象内未找到,就会去跟它关联的原型对象内进行查找;也就是说,在原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享;访问原型的方式:构造函数名.prototype。示例图:示例代码: 给构造函数的原型添加方法function Dog(name,age){ this.name = name; this.age = age;}// 给构造函数的原型 添加say方法Dog.prototype.say = function(){ console.log(‘汪汪汪’);}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);dog1.say(); // 汪汪汪dog2.say(); // 汪汪汪我们可以看到,本身Dog这个构造函数中是没有say这个方法的,我们通过Dog.prototype.say的方式,在构造函数Dog的原型中创建了一个方法,实例化出来的dog1、dog2会先在自己的对象先找say方法,找不到的时候,会去他们的原型对象中查找。如图所示:在构造函数的原型中可以存放所有对象共享的数据,这样可以避免多次创建对象浪费内存空间的问题。3.3 原型的使用1、使用对象的动态特性使用对象的动态属性,其实就是直接使用prototype为原型添加属性或者方法。function Person () {}Person.prototype.say = function () { console.log( ‘讲了一句话’ );};Person.prototype.age = 18;var p = new Person();p.say(); // 讲了一句话console.log(p.age); // 182、直接替换原型对象每次构造函数创建出来的时候,都会关联一个空对象,我们可以用一个对象替换掉这个空对象。function Person () {}Person.prototype = { say : function () { console.log( ‘讲了一句话’ ); },};var p = new Person();p.say(); // 讲了一句话注意:使用原型的时候,有几个注意点需要注意一下,我们通过几个案例来了解一下。使用对象.属性名去获取对象属性的时候,会先在自身中进行查找,如果没有,就去原型中查找;// 创建一个英雄的构造函数 它有自己的 name 和 age 属性function Hero(){ this.name=“德玛西亚之力”; this.age=18;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age= 30;Hero.prototype.say=function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();h1.say(); // 先去自身中找 say 方法,没有再去原型中查找 打印:‘人在塔在!!!‘console.log(p1.name); // “德玛西亚之力"console.log(p1.age); // 18 先去自身中找 age 属性,有的话就不去原型中找了使用对象.属性名去设置对象属性的时候,只会在自身进行查找,如果有,就修改,如果没有,就添加;// 创建一个英雄的构造函数function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age = 18;var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}console.log(h1.age); // 18h1.age = 30; // 设置的时候只会在自身中操作,如果有,就修改,如果没有,就添加 不会去原型中操作console.log(h1); // {name:“德玛西亚之力”,age:30}console.log(h1.age); // 30一般情况下,不会将属性放在原型中,只会将方法放在原型中;在替换原型的时候,替换之前创建的对象,和替换之后创建的对象的原型不一致!!!// 创建一个英雄的构造函数 它有自己的 name 属性function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的默认原型对象添加 say 方法Hero.prototype.say = function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}h1.say(); // ‘人在塔在!!!’// 开辟一个命名空间 obj,里面有个 kill 方法var obj = { kill : function(){ console.log(‘大宝剑’); }}// 将创建的 obj 对象替换原本的原型对象Hero.prototype = obj;var h2 = new Hero();h1.say(); // ‘人在塔在!!!‘h2.say(); // 报错h1.kill(); // 报错h2.kill(); // ‘大宝剑’画个图理解下:图中可以看出,实例出来的h1对象指向的原型中,只有say()方法,并没有kill()方法,所以h1.kill()会报错。同理,h2.say()也会报错。3.4 __proto__属性在js中以_开头的属性名为js的私有属性,以__开头的属性名为非标准属性。__proto__是一个非标准属性,最早由firefox提出来。1、构造函数的 prototype 属性之前我们访问构造函数原型对象的时候,使用的是prototype属性:function Person(){}//通过构造函数的原型属性prototype可以直接访问原型Person.prototype;在之前我们是无法通过构造函数new出来的对象访问原型的:function Person(){}var p = new Person();//以前不能直接通过p来访问原型对象2、实例对象的 proto 属性__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性,有了__proto__属性,就可以通过构造函数创建出来的对象直接访问原型。function Person(){}var p = new Person();//实例对象的__proto__属性可以方便的访问到原型对象p.proto;//既然使用构造函数的prototype和实例对象的__proto__属性都可以访问原型对象//就有如下结论p.proto === Person.prototype;如图所示:3、__proto__属性的用途可以用来访问原型;在实际开发中除非有特殊的需求,不要轻易的使用实例对象的__proto__属性去修改原型的属性或方法;在调试过程中,可以轻易的查看原型的成员;由于兼容性问题,不推荐使用。3.5 constuctor属性constructor:构造函数,原型的constructor属性指向的是和原型关联的构造函数。示例代码:function Dog(){ this.name=“husky”;}var d=new Dog();// 获取构造函数console.log(Dog.prototype.constructor); // 打印构造函数 Dogconsole.log(d.proto.constructor); // 打印构造函数 Dog如图所示:获取复杂类型的数据类型:通过obj.constructor.name的方式,获取当前对象obj的数据类型。在一个的函数中,有个返回值name,它表示的是当前函数的函数名;function Teacher(name,age){ this.name = name; this.age = age;}var teacher = new Teacher();// 假使我们只知道一个对象teacher,如何获取它的类型呢?console.log(teacher.proto.constructor.name); // Teacherconsole.log(teacher.constructor.name); // Teacher实例化出来的teacher对象,它的数据类型是啥呢?我们可以通过实例对象teacher.proto,访问到它的原型对象,再通过.constructor访问它的构造函数,通过.name获取当前函数的函数名,所以就能得到当前对象的数据类型。又因为.__proto__是一个非标准的属性,而且实例出的对象继承原型对象的方法,所以直接可以写成:obj.constructor.name。3.6 原型继承原型继承:每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。所以可以通过更改构造函数的原型属性来实现继承。继承的方式有多种,可以一个对象继承另一个对象,也可以通过原型继承的方式进行继承。1、简单混入继承直接遍历一个对象,将所有的属性和方法加到另一对象上。var animal = { name:“Animal”, sex:“male”, age:5, bark:function(){ console.log(“Animal bark”); }};var dog = {};for (var k in animal){ dog[k]= animal[k];}console.log(dog); // 打印的对象与animal一模一样缺点:只能一个对象继承自另一个对象,代码复用太低了。2、混入式原型继承混入式原型继承其实与上面的方法类似,只不过是将遍历的对象添加到构造函数的原型上。var obj={ name:‘zs’, age:19, sex:‘male’ }function Person(){ this.weight=50;}for(var k in obj){ // 将obj里面的所有属性添加到 构造函数 Person 的原型中 Person.prototype[k] = obj[k];}var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’面向对象思想封装一个原型继承我们可以利用面向对象的思想,将面向过程进行封装。function Dog(){ this.type = ‘yellow Dog’;}// 给构造函数 Dog 添加一个方法 extendDog.prototype.extend = function(obj){ // 使用混入式原型继承,给 Dog 构造函数的原型继承 obj 的属性和方法 for (var k in obj){ this[k]=obj[k]; }}// 调用 extend 方法Dog.prototype.extend({ name:“二哈”, age:“1.5”, sex:“公”, bark:function(){ console.log(‘汪汪汪’); }});3、替换式原型继承替换式原型继承,在上面已经举过例子了,其实就是将一个构造函数的原型对象替换成另一个对象。function Person(){ this.weight=50;}var obj={ name:‘zs’, age:19, sex:‘male’}// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’之前我们就说过,这样做会产生一个问题,就是替换的对象会重新开辟一个新的空间。替换式原型继承时的bug替换原型对象的方式会导致原型的constructor的丢失,constructor属性是默认原型对象指向构造函数的,就算是替换了默认原型对象,这个属性依旧是默认原型对象指向构造函数的,所以新的原型对象是没有这个属性的。解决方法:手动关联一个constructor属性function Person() { this.weight = 50;}var obj = { name: ‘zs’, age: 19, sex: ‘male’}// 在替换原型对象函数之前 给需要替换的对象添加一个 constructor 属性 指向原本的构造函数obj.constructor = Person;// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1 = new Person();console.log(p1.proto.constructor === Person); // true4、Object.create()方法实现原型继承当我们想把对象1作为对象2的原型的时候,就可以实现对象2继承对象1。前面我们了解了一个属性:proto,实例出来的对象可以通过这个属性访问到它的原型,但是这个属性只适合开发调试时使用,并不能直接去替换原型对象。所以这里介绍一个新的方法:Object.create()。语法: var obj1 = Object.create(原型对象);示例代码: 让空对象obj1继承对象obj的属性和方法var obj = { name : ‘盖伦’, age : 25, skill : function(){ console.log(‘大宝剑’); }}// 这个方法会帮我们创建一个原型是 obj 的对象var obj1 = Object.create(obj);console.log(obj1.name); // “盖伦"obj1.skill(); // “大宝剑"兼容性:由于这个属性是ECMAScript5的时候提出来的,所以存在兼容性问题。利用浏览器的能力检测,如果存在Object.create则使用,如果不存在的话,就创建构造函数来实现原型继承。// 封装一个能力检测函数function create(obj){ // 判断,如果浏览器有 Object.create 方法的时候 if(Object.create){ return Object.create(obj); }else{ // 创建构造函数 Fun function Fun(){}; Fun.prototype = obj; return new Fun(); }}var hero = { name: ‘盖伦’, age: 25, skill: function () { console.log(‘大宝剑’); }}var hero1 = create(hero);console.log(hero1.name); // “盖伦"console.log(hero1.proto == hero); // true4.原型链对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链。4.1 什么是原型链示例代码: 原型继承练习// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();// 创建一个 Preson 构造函数function Person() { this.name = ‘zs’; this.tool = function() { console.log(‘菜刀’); }}// 让 Person 继承 animal (替换原型对象)Person.prototype = animal;// 实例化一个 p 对象 var p = new Person();// 创建一个 Student 构造函数function Student() { this.score = 100; this.clickCode = function() { console.log(‘啪啪啪’); }}// 让 Student 继承 p (替换原型对象)Student.prototype = p;//实例化一个 student 对象var student = new Student();console.log(student); // 打印 {score:100,clickCode:fn}// 因为是一级级继承下来的 所以最上层的 Animate 里的属性也是被继承的console.log(student.weight); // 50student.eat(); // 蜂蜜蜂蜜student.tool(); // 菜刀如图所示:我们将上面的案例通过画图的方式展现出来后就一目了然了,实例对象animal直接替换了构造函数Person的原型,以此类推,这样就会形成一个链式结构的原型链。完整的原型链结合上图,我们发现,最初的构造函数Animal创建的同时,会创建出一个原型,此时的原型是一个空的对象。结合原型链的概念:“原型本身又是一个对象,所以原型也有原型”,那么这个空对象往上还能找出它的原型或者构造函数吗?我们如何创建一个空对象? 1、字面量:{};2、构造函数:new Object()。我们可以简单的理解为,这个空的对象就是,构造函数Object的实例对象。所以,这个空对象往上面找是能找到它的原型和构造函数的。// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();console.log(animal.proto); // {}console.log(animal.proto.proto); // {}console.log(animal.proto.proto.constructor); // function Object(){}console.log(animal.proto.proto.proto); // null如图所示:4.2 原型链的拓展1、描述出数组“[]”的原型链结构// 创建一个数组var arr = new Array();// 我们可以看到这个数组是构造函数 Array 的实例对象,所以他的原型应该是:console.log(Array.prototype); // 打印出来还是一个空数组// 我们可以继续往上找 console.log(Array.prototype.proto); // 空对象// 继续console.log(Array.prototype.proto.proto) // null如图所示:2、扩展内置对象给js原有的内置对象,添加新的功能。注意:这里不能直接给内置对象的原型添加方法,因为在开发的时候,大家都会使用到这些内置对象,假如大家都是给内置对象的原型添加方法,就会出现问题。错误的做法:// 第一个开发人员给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘哈哈哈’);}// 第二个开发人员也给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘啪啪啪’);}var arr = new Array();arr.say(); // 打印 “啪啪啪” 前面写的会被覆盖为了避免出现这样的问题,只需自己定义一个构造函数,并且让这个构造函数继承数组的方法即可,再去添加新的方法。// 创建一个数组对象 这个数组对象继承了所有数组中的方法var arr = new Array();// 创建一个属于自己的构造函数function MyArray(){}// 只需要将自己创建的构造函数的原型替换成 数组对象,就能继承数组的所有方法MyArray.prototype = arr;// 现在可以单独的给自己创建的构造函数的原型添加自己的方法MyArray.prototype.say = function(){ console.log(‘这是我自己添加的say方法’);}var arr1 = new MyArray();arr1.push(1); // 创建的 arr1 对象可以使用数组的方法arr1.say(); // 也可以使用自己添加的方法 打印“这是我自己添加的say方法”console.log(arr1); // [1]4.3 属性的搜索原则当通过对象名.属性名获取属性时,会遵循以下属性搜索的原则:1-首先去对象自身属性中找,如果找到直接使用,2-如果没找到,去自己的原型中找,如果找到直接使用,3-如果没找到,去原型的原型中继续找,找到直接使用,4-如果没有会沿着原型不断向上查找,直到找到null为止。5.Object.prototype成员介绍我们可以看到所有的原型最终都会继承Object的原型:Object.prototype。打印看看Object的原型里面有什么:// Object的原型console.log(Object.prototype)如图所示:我们可以看到Object的原型里有很多方法,下面就来介绍下这些方法的作用。5.1 constructor 属性指向了和原型相关的构造函数5.2 hasOwnProperty 方法判断对象自身是否拥有某个属性,返回值:布尔类型。示例代码:function Hero() { this.name = ‘盖伦’; this.age = ‘25’; this.skill = function () { console.log(‘盖伦使用了大宝剑’); }}var hero = new Hero();console.log(hero.name); // ‘盖伦’hero.skill(); // ‘盖伦使用了大宝剑’console.log(hero.hasOwnProperty(“name”)); // trueconsole.log(hero.hasOwnProperty(“age”)); // trueconsole.log(hero.hasOwnProperty(“skill”)); // trueconsole.log(hero.hasOwnProperty(“toString”)); // false toString是在原型链当中的方法,并不是这里对象的方法console.log(’toString’ in hero); // true in方法 判断对象自身或者原型链中是否有某个属性5.3 isPrototypeOf 方法对象1.isPrototypeOf(对象2),判断对象1是否是对象2的原型,或者对象1是否是对象2原型链上的原型。示例代码:var obj = { age: 18}var obj1 = {};// 创建一个构造函数function Hero() { this.name = ‘盖伦’;}// 将这个构造函数的原型替换成 objHero.prototype = obj;// 实例化一个 hero 对象var hero = new Hero();console.log(obj.isPrototypeOf(hero)); // true 判断 obj 是否是 hero 的原型console.log(obj1.isPrototypeOf(hero)); // false 判断 obj1 是否是 hero 的原型console.log(Object.prototype.isPrototypeOf(hero)); // true 判断 Object.prototype 是否是 hero 的原型// 注意 这里的 Object.prototype 是原型链上最上层的原型对象5.4 propertyIsEnumerable 方法对象.propertyIsEnumerable(‘属性或方法名’),判断一个对象是否有该属性,并且这个属性可以被for-in遍历,返回值:布尔类型。示例代码:// 创建一个构造函数function Hero (){ this.name = ‘盖伦’; this.age = 25; this.skill = function(){ console.log(‘盖伦使用了大宝剑’); }}// 创建一个对象var hero = new Hero();// for-in 遍历这个对象 我们可以看到分别打印了哪些属性和方法for(var k in hero){ console.log(k + ‘—’ + hero[k]); // “name-盖伦” “age-25” “skill-fn()”}// 判断一个对象是否有该属性,并且这个属性可以被 for-in 遍历console.log(hero.propertyIsEnumerable(’name’)); // trueconsole.log(hero.propertyIsEnumerable(‘age’)); // trueconsole.log(hero.propertyIsEnumerable(’test’)); // false5.5 toString 和 toLocalString 方法两种方法都是将对象转成字符串的,只不过toLocalString是按照本地格式进行转换。示例代码:// 举个例子,时间的格式可以分为世界时间的格式和电脑本地的时间格式var date = new Date();// 直接将创建的时间对象转换成字符串console.log(date.toString());// 将创建的时间对象按照本地格式进行转换console.log(date.toLocaleString());效果图:5.6 valueOf 方法返回指定对象的原始值。MDN官方文档6.静态方法和实例方法静态方法和实例方法这两个概念其实也是从面相对象的编程语言中引入的,对应到JavaScript中的理解为:静态方法: 由构造函数调用的在js中,我们知道有个Math构造函数,他有一个Math.abs()的方法,这个方法由构造函数调用,所以就是静态方法。Math.abs();实例方法: 由构造函数创建出来的对象调用的var arr = new Array();// 由构造函数 Array 实例化出来的对象 arr 调用的 push 方法,叫做实例方法arr.push(1);示例代码:function Hero(){ this.name=‘亚索’; this.say=function(){ console.log(‘哈撒ki’); }}Hero.prototype.skill=function(){ console.log(‘吹风’);}// 直接给构造函数添加一个 run 方法(函数也是对象,可以直接给它加个方法)Hero.run=function(){ console.log(‘死亡如风,常伴吾身’);}var hero = new Hero();hero.say();hero.skill(); //实例方法Hero.run(); //静态方法如果这个方法是对象所有的,用实例方法。一般的工具函数,用静态方法,直接给构造函数添加方法,不需要实例化,通过构造函数名直接使用即可;7.作用域“域”,表示的是一个范围,“作用域”就是作用范围。作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。7.1 块级作用域在ES5及ES5之前,js中是没有块级作用域的。{ var num = 123; { console.log( num ); // 123 }}console.log( num ); // 123上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。注意:会计作用域只在在ES5及ES5之前不起作用,但是在ES6开始,js中是存在块级作用域的。7.2 词法作用域词法( 代码 )作用域,就是代码在编写过程中体现出来的作用范围。代码一旦写好,不用执行,作用范围就已经确定好了,这个就是所谓词法作用域。在js中词法作用域规则:函数允许访问函数外的数据;整个代码结构中只有函数可以限定作用域;作用域规则首先使用提升规则分析;如果当前作用规则中有名字了,就不考虑外面的名字。作用域练习:第一题var num=250;function test(){ // 会现在函数内部查找有没有这个num变量,有的话调用,没有的话会去全局中查找,有就返回,没有就返回undefined console.log(num); // 打印 250}function test1(){ var num=222; test();}test1(); 第二题if(false){ var num = 123;}console.log(num); // undefined // {}是没有作用域的 但是有判断条件,var num会提升到判断语句外部 所以不会报错 打印的是undefined第三题var num = 123;function foo() { var num = 456; function func() { console.log( num ); } func();}foo(); // 456// 调用foo时,在函数内部调用了func,打印num的时候,会先在func中查找num 没有的时候会去外层作用域找,找到即返回,找不到即再往上找。第四题var num1 = 123;function foo1() { var num1 = 456; function foo2() { num1 = 789; function foo3 () { console.log( num1 ); // 789 自己的函数作用域中没有就一层层往上找 } foo3(); } foo2();}foo1();console.log( num1 ); // 123 7.3 变量提升(预解析)JavaScript是解释型的语言,但是它并不是真的在运行的时候逐句的往下解析执行。我们来看下面这个例子:func();function func(){ alert(“函数被调用了”);}在上面这段代码中,函数func的调用是在其声明之前,如果说JavaScript代码真的是逐句的解析执行,那么在第一句调用的时候就会出错,然而事实并非如此,上面的代码可以正常执行,并且alert出来"函数被调用了”。所以,可以得出结论,JavaScript并非仅在运行时简简单单的逐句解析执行!JavaScript预解析JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字var和function开头的语句块提前进行处理。关键问题是怎么处理呢?当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到当前作用域的开头。示例代码:函数名提升正常函数书写方式function func(){ alert(“函数被调用了”);}func();预解析之后,函数名提升func();function func(){ alert(“函数被调用了”);}示例代码:变量名提升正常变量书写方式alert(a); // undefined var a = 123;// 由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,// 如果没有预解析,代码应该会直接报错a is not defined,而不是输出值。不是说要提前的吗?那不是应该alert出来123,为什么是undefined?// 变量的时候 提升的只是变量声明的提升,并不包括赋值var a; // 这里是声明alert(a); // 变量声明之后并未有初始化和赋值操作,所以这里是 undefineda = 123; // 这里是赋值注意:特殊情况1、函数不能被提升的情况函数表达式创建的函数不会提升test(); // 报错 “test is not a function"var test = function(){ console.log(123);}new Function创建的函数也不会被提升test(); // 报错 “test is not a function"var test = new Function(){ console.log(123);}2、出现同名函数test(); // 打印 ‘好走的都是下坡路’// 两个函数重名,这两个函数都会被提升,但是后面的函数会覆盖掉前面的函数function test(){ console.log(‘众里寻她千百度,他正在自助烤肉….’);}function test(){ console.log(‘好走的都是下坡路’);}3、函数名与变量名同名// 如果函数和变量重名,只会提升函数,变量不会被提升console.log(test); // 打印这个test函数function test(){ console.log(‘我是test’);}var test=200;再看一种情况:var num = 1;function num () { console.log(num); // 报错 “num is not a function”}num();直接上预解析后的代码:function num(){ console.log(num);}num = 1;num();4、条件式的函数声明// 如果是条件式的函数申明, 这个函数不会被预解析test(); // test is not a functionif(true){ function test(){ console.log(‘只是在人群中多看了我一眼,再也忘不掉我容颜…’); }}预解析是分作用域的声明提升并不是将所有的声明都提升到window 对象下面,提升原则是提升到变量运行的当前作用域中去。示例代码:function showMsg(){ var msg = ‘This is message’;}alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”预解析之后:function showMsg(){ var msg; // 因为函数本身就会产生一个作用域,所以变量声明在提升的时候,只会提升在当前作用域下最前面 msg = ‘This is message’;}alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”预解析是分段的分段,其实就分script标签的<script>func(); // 输出 AA2;function func(){ console.log(‘AA1’);}function func(){ console.log(‘AA2’);}</script><script>function func(){ console.log(‘AA3’);}</script>在上面代码中,第一个script标签中的两个func进行了提升,第二个func覆盖了第一个func,但是第二个script标签中的func并没有覆盖上面的第二个func。所以说预解析是分段的。tip: 但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。函数预解析的时候是分段的,但是执行的时候不分段<script> //变量预解析是分段的 ,但是函数的执行是不分段 var num1=100; // test3(); 报错,函数预解析的时候分段,执行的时候才不分段 function test1(){ console.log(‘我是test1’); } function test2(){ console.log(‘我是test2’); }</script><script> var num2=200; function test3(){ console.log(’test3’); } test1(); // 打印 ‘我是test1’ 函数执行的时候不分段 console.log(num1); // 100</script>7.4 作用域链什么是作用域链?只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域, 即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。例如:function f1() { function f2() { }}var num = 456;function f3() { function f4() { }}示例代码:var num=200;function test(){ var num=100; function test1(){ var num=50; function test2(){ console.log(num); } test2(); } test1();}test(); // 打印 “50”如图所示:绘制作用域链的步骤:看整个全局是一条链, 即顶级链, 记为0级链看全局作用域中, 有什么变量和函数声明, 就以方格的形式绘制到0级练上再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为1级链然后在每一个1级链中再次往复刚才的行为变量的访问规则:首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.如果还没有再次往上刚找… 直到全局链( 0 级 ), 还没有就是 is not defined注意,同级的链不可混合查找来点案例练练手第一题:function foo() { var num = 123; console.log(num); //123}foo();console.log(num); // 报错第二题:var scope = “global”;function foo() { console.log(scope); // undefined var scope = “local”; console.log(scope); // ’local’}foo();// 预解析之后// var scope = “global”;// function foo() {// var scope;// console.log(scope); // undefined// scope = “local”;// console.log(scope); // local// }第三题:if(“a” in window){ var a = 10;}console.log(a); // 10// 预解析之后// var a;// if(“a” in window){// a = 10; // 判断语句不产生作用域// }// console.log(a); // 10第四题:if(!“a” in window){ var a = 10;}console.log(a); // undefined// 预解析之后// var a;// if(!“a” in window){// a = 10; // 判断语句不产生作用域// }// console.log(a); // undefined第五题// console.log(num); 报错 虽然num是全局变量 但是不会提升function test(){ num = 100; }test();console.log(num); // 100第六题var foo = 1;function bar() { if(!foo) { var foo = 10; } console.log(foo); // 10}bar();// 预解析之后// var foo=1;// function bar(){// var foo;// if(!foo){// foo=10;// }// console.log(foo); // 10// }// bar();8.FunctionFunction是函数的构造函数,你可能会有点蒙圈,没错,在js中函数与普通的对象一样,也是一个对象类型,只不过函数是js中的“一等公民”。这里的Function类似于Array、Object等8.1 创建函数的几种方式1、函数字面量(直接声明函数)创建方式function test(){ // 函数体} // 类似于对象字面量创建方式:{}2、函数表达式var test = function(){ // 函数体}3、Function构造函数创建// 构造函数创建一个空的函数var fn = new Function();fn1(); // 调用函数函数扩展名有没有一种可能,函数表达式声明函数时,function 也跟着一个函数名,如:var fn = function fn1(){}? 答案是可以的,不过fn1只能在函数内部使用,并不能在外部调用。var fn = function fn1(a,b,c,d){ console.log(‘当前函数被调用了’); // 但是,fn1可以在函数的内部使用 console.log(fn1.name); console.log(fn1.length); // fn1(); 注意,这样调用会引起递归!!! 下面我们会讲到什么是递归。}// fn1(); // 报错,fn1是不能在函数外部调用的fn(); // “当前函数被调用了”// 函数内部使用时打印:// “当前函数被调用了”// console.log(fn1.name); => “fn1”// console.log(fn1.length); => 48.2 Function 构造函数创建函数上面我们知道了如何通过Function构造函数创建一个空的函数,这里我们对它的传参详细的说明下。1、不传参数时// 不传参数时,创建的是一个空的函数var fn1 = new Function();fn1(); // 调用函数2、只传一个参数// 只传一个参数的时候,这个参数就是函数体// 语法:var fn = new Function(函数体);var fn2 = new Function(‘console.log(2+5)’);f2(); // 73、传多个参数// 传多个参数的时候,最后一个参数为函数体,前面的参数都是函数的形参名// 语法:var fn = new Function(arg1,arg2,arg3…..argn,metthodBody);var fn3 = new Function(’num1’,’num2’,‘console.log(num1+num2)’);f3(5,2); // 78.3 Function 的使用1、用Function创建函数的方式封装一个计算m - n之间所有数字的和的函数//求 m-n之间所有数字的和//var sum=0;//for (var i = m; i <=n; i++) {// sum+=i;//}var fn = new Function(’m’,’n’,‘var sum=0;for (var i = m; i <=n; i++) {sum+=i;} console.log(sum);’);fn(1,100); // 5050函数体参数过长问题:函数体过长时,可读性很差,所以介绍解决方法:1)字符串拼接符“+”var fn = new Function( ’m’, ’n’, ‘var sum=0;’+ ‘for (var i = m; i <=n; i++) {’+ ‘sum += i;’+ ‘}’+ ‘console.log(sum);’ );fn(1,100); // 50502)ES6中新语法“ ”,(在esc键下面)表示可换行字符串的界定符,之前我们用的是单引号或者双引号来表示一个字符串字面量,在ES6中可以用反引号来表示该字符串可换行。new Function( 'm', 'n', var sum=0; for (var i = m; i <=n; i++) { sum+=i; } console.log(sum);`);3)模板方式<!– 新建一个模板 –><script type=“text/template” id=“tmp”> var sum=0; for (var i = m; i <=n; i++) { sum += i; } console.log(sum);</script><script> // 获取模板内的内容 var methodBody = document.querySelector(’#tmp’).innerHTML; console.log(methodBody); var fn = new Function(’m’,’n’,methodBody); fn(2,6); // 20</script>2、eval 函数eval函数可以直接将把字符串的内容,作为js代码执行,前提是字符串代码符合js代码规范。这里主要是用作跟Function传参比较。eval 和 Function 的区别:Function();中,方法体是字符串,必须调用这个函数才能执行eval(); 可以直接执行字符串中的js代码存在的问题:性能问题因为eval里面的代码是直接执行的,所以当在里面定义一个变量的时候,这个变量是不会预解析的,所以会影响性能。// eval 里面的代码可以直接执行,所以下面的打印的 num 可以访问到它// 但是这里定义的 num 是没有预解析的,所以变量名不会提升,从而性能可能会变慢eval(‘var num = 123;’);console.log(num); // 123安全问题主要的安全问题是可能会被利用做XSS攻击(跨站脚本攻击(Cross Site Scripting)),eval也存在一个安全问题,因为它可以执行传给它的任何字符串,所以永远不要传入字符串或者来历不明和不受信任源的参数。示例代码: 实现一个简单的计算器<!– html 部分 –><input type=“text” class=“num1”><select class=“operator”> <option value="+">+</option> <option value=”-">-</option> <option value=”"></option> <option value=”/">/</option></select><input type=“text” class=“num2”><button>=</button><input type=“text” class=“result”><!– js 部分 –><script> document.querySelector(‘button’).onclick=function(){ var num1 = document.querySelector(’.num1’).value; var num2 = document.querySelector(’.num2’).value; var operator = document.querySelector(’.operator’).value; // result其实最终获得的就是 num1 + operator + num2的字符串 但是他能够直接执行并计算 var result = eval(num1 + operator + num2); //计算 document.querySelector(’.result’).value = result; //显示 }</script>效果图:8.4 Function 的原型链结构在7.2章节中我们知道函数也还可以通过构造函数的方式创建出来,既然可以通过构造函数的方式创建,那么函数本身也是有原型对象的。示例代码:// 通过Function构造函数创建一个函数testvar test = new Function();// 既然是通过构造函数创建的,那么这个函数就有指向的原型console.log(test.proto); // 打印出来的原型是一个空的函数console.log(test.proto.proto); // 空的函数再往上找原型是一个空的对象console.log(test.proto.proto.proto); // 再往上找就是null了// 函数原型链: test() —> Function.prototype —> Object.prototype —> null如图所示:通过上图,可以直观的看出,函数也是有原型的。那一个完整的原型链究竟是什么样子的呢?下面我们一起做个总结。8.5 完整的原型链绘制完整原型链的步骤:1、先将一个对象的原型画出来2、再把对象的原型的原型链画出来 ,到null结束3、把对象的构造函数的原型链画出来4、把Function和Object的原型关系给画出来示例代码:// 创建一个构造函数function Person(){ this.name = ‘Levi’; this.age = 18;}// 实例化一个对象var p = new Person();如图所示:总结:Function构造函数的原型,在Object的原型链上;Object构造函数的原型,在Function的原型链上;9.arguments对象在每一个函数调用的过程中, 函数代码体内有一个默认的对象arguments, 它存储着实际传入的所有参数。示例代码:// 封装一个加法函数function add(num1,num2){ console.log(num1+num2);}add(1); // NaNadd(1,2); // 3add(1,2,3); // 3在调用函数时,实参和形参的个数可以不一样,但是没有意义。在函数内部有个arguments对象(注意:是在函数内部),arguments是一个伪数组对象。它表示在函数调用的过程中传入的所有参数(实参)的集合。在函数调用过程中不规定参数的个数与类型,可以使得函数调用变得非常灵活性。function add(num1,num2){ console.log(arguments); // 打印的是一个伪数组}add(1,2,3,4); length:表示的是实参的个数;callee:指向的就是arguments对象所在的函数;示例代码:封装一个求最大值的函数,因为不知道需要传进多少实参,所以直接用伪数组arguments获取调用的实参function max(){ // 假使实参的第一个数字最大 var maxNum = arguments[0]; // 循环这个伪数组 for(var i = 0; i < arguments.length; i++){ if(maxNUm < arguments[i]){ maxNUm = arguments[i]; } return maxNum; } }// 调用console.log(max(1,9,12,8,22,5)); // 2210. 函数的四种调用模式四种调用模式分别是:“函数调用模式”、“方法调用模式”、“构造器调用模式”、“上下文调用模式”。其实就是分析this是谁的问题。只看函数是怎么被调用的,而不管函数是怎么来的。分析this属于哪个函数;分析这个函数是以什么方式调用的;什么是函数? 什么是方法?如果一个函数是挂载到一个对象中,那么就把这个函数称为方法如果一个函数直接放在全局中,由Window对象调用,那么他就是一个函数。// 函数function fn() {}var f = function() {};fn();f();// 方法var obj = { say: function() {}};obj.say();fn和f都是函数,say是一个方法10.1 函数模式函数模式其实就是函数调用模式,this是指向全局对象window的。this -> window示例代码:// 函数调用模式:// 创建的全局变量相当于window的属性var num = 999;var fn = function () { console.log(this); // this 指向的是 window 对象 console.log(this.num); // 999};fn();10.2 方法模式方法模式其实就是方法调用模式,this是指向调用方法的对象。this -> 调用方法的对象示例代码:// this指向的是obj var age = 38;var obj = { age: 18, getAge: function () { console.log(this); // this指向的是对象obj {age:18,getAge:f()} console.log(this.age); // 18 } };obj.getAge(); // getAge() 是对象 obj 的一个方法10.3 构造器模式构造器模式其实就是构造函数调用模式,this指向新创建出来的实例对象。this -> 新创建出来的实例对象示例代码:// this指向的是实例化出来的对象function Person(name){ this.name = name; console.log(this);}var p1 = new Person(‘Levi’); // Person {name: “Levi”}var p2 = new Person(‘Ryan’); // Person {name: “Ryan”}构造函数的返回值:如果返回的是基本类型function Person() { return 1;}var p1 = new Person();console.log(p1); // 打印Person {}构造函数内有返回值,且是基本类型的时候,返回值会被忽略掉,返回的是实例出来的对象。如果返回的是引用类型function Person() { return { name: ’levi’, age: 18 };}var p1 = new Person();console.log(p1); // 此时打印 Object {name: ’levi’, age: 18}构造函数内的返回值是一个引用类型的时候,返回的就是这个指定的引用类型。10.4 上下文(借用方法)模式上下文,即环境,用于指定方法内部的this,上下文调用模式中,this可以被随意指定为任意对象。上下文模式有两种方法,是由函数调用的:函数名.apply( … );函数名.call( … );1、apply 方法语法:fn.apply(thisArg, array);参数:第一个参数:表示函数内部this的指向(或者:让哪个对象来借用这个方法)第二个参数:是一个数组(或者伪数组),数组中的每一项都将作为被调用方法的参数示例代码:// 没有参数function fn (){ console.log(this.name);}var obj = { name : ‘Levi丶’}// this 指向 obj,fn 借用obj方法里面的 name 属性fn.apply(obj); // 打印 ‘Levi丶’// 有参数function fn (num1, num2){ console.log(num1 + num2);}var obj = {}// this 指向 obj,数组中的数据是方法 fn 的参数fn.apply(obj, [1 , 2]); // 打印 3注意:apply方法的第一个参数,必须是一个对象!如果传入的参数不是一个对象,那么这个方法内部会将其转化为一个包装对象。function fn() { console.log(this);}fn.apply(1); // 包装对象fn.apply(‘abc’); // 包装对象fn.apply(true); // 包装对象指向window的几种方式:function fn(){ }fn.apply(window);fn.apply();fn.apply(null);fn.apply(undefined);具体应用:求数组中的最大数// 以前的方法,假设第一项最大,然后与后面每一项比较,得到最大的项var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];var maxNum = arr[0];for(var i = 1; i < arr.length; i++) { if(maxNum < arr[i]) { maxNum = arr[i]; }}console.log(maxNum); // 777// 利用 内置对象的 apply 的方法var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];// max 是内置对象 Math 求最大值的一个方法var maxNum = Math.max.apply(null, arr);console.log(maxNum); // 777将传进的参数每一项之间用“-”连接// 思考:参数个数是用户随机传的,没有具体的一个值,这时候就需要用到 arguments 的概念了function fn (){ // 数组原型中有一个join方法,他的接收的参数是一个字符串 // join.apply的第一个参数指向 arguments 对象,第二个参数是jion方法需要的参数 return Array.prototype.join.apply(arguments, [’-’]);}var ret = fn(‘a’, ‘b’, ‘c’, ’d’, ’e’);console.log(ret); // ‘a-b-c-d-e'2、call 方法call方法的作用于apply方法的作用相同,唯一不同的地方就是第二个参数不同。语法:fn.apply(thisArg, parm1,parm2,parm3,…);参数:第一个参数:表示函数内部this的指向(或者:让哪个对象来借用这个方法)第二个及后面的参数:不是之前数组的形式了,对应方法调用的每一个参数示例代码:function fn(num1, num2, num3) { console.log(num1, num2, num3);}var obj = {};fn.call(obj, [1, 3, 9], 0, 1); // [1, 3, 9] 0 1fn.call(obj, [1, 3, 9]); // [1, 3, 9] undefined undefined3、apply 和 call 的区别两者在功能上一模一样,唯一的区别就是第二个参数传递的类型不一样。什么时候用apply?什么时候用call呢?其实用哪个都可以,在参数少的情况下,我们可以使用call方法,但是如果参数是伪数组或者是数组的时候,call方法就不适用了,还需要将伪数组中的每一项取出来作为方法的参数,此时apply更加实用。10.5 面试题分析面试题1:var age = 38;var obj = { age: 18, getAge: function() { function foo() { console.log(this.age); // 这里的this属于函数 foo; 打印 38 } foo(); // foo函是Window对象调用的 }};obj.getAge();面试题2:// 只看函数是怎么被调用的,而不管函数是怎么来的var age = 38;var obj = { age: 18, getAge: function() { alert(this.age); }};var f = obj.getAge;f(); // 函数是Window对象调用的,所以this指向Window对象。打印:38面试题3:var length = 10;function fn(){ console.log(this.length);}var obj = { length: 5, method: function (fn) { fn(); // window对象调用 打印 10 arguments0; // 方法调用模式,是arguments对象调用的 // this指向arguments,所以arguments.length = 2; (arguments.length:实参的个数)所以打印 2 }};obj.method(fn, 123);面试题4:怎么使用call或者apply方法实现构造函数的复用呢?function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}function Teacher(name, age, gender, workYear, subject) { this.name = name; this.age = age; this.gender = gender; this.workYear = workYear; this.subject = subject;}function Student(name, age, gender, stuNo, score) { this.name = name; this.age = age; this.gender = gender; this.stuNo = stuNo; this.score = score;}var tec = new Teacher(‘张老师’, 32, ‘male’, ‘7年’, ‘语文’);var stu = new Student(‘xiaowang’, 18, ‘male’, 10001, 99);console.log(tec); // Teacher {name: “张老师”, age: 32, gender: “male”, workYear: “7年”, subject: “语文”}console.log(stu); // Student {name: “xiaowang”, age: 18, gender: “male”, stuNo: 10001, score: 99}上面的代码中一个Teacher构造函数,一个Student构造函数,他们都有一些公共的属性,跟Person构造函数里面的属性重复,我们能否使用call或者apply方法,简化上面的代码呢?function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}function Teacher(name, age, gender, workYear, subject) { // 借用 Person 函数来给当前对象添加属性 Person.call(this, name, age, gender); // 这里的this指向的就是当前的Teacher构造函数 this.workYear = workYear; this.subject = subject;}function Student(name, age, gender, stuNo, score) { Person.call(this, name, age, gender); // 这里的this指向的就是当前的Student构造函数 this.stuNo = stuNo; this.score = score;}var tec = new Teacher(‘张老师’, 32, ‘male’, ‘7年’, ‘语文’);var stu = new Student(‘xiaowang’, 18, ‘male’, 10001, 99);console.log(tec); // Teacher {name: “张老师”, age: 32, gender: “male”, workYear: “7年”, subject: “语文”}console.log(stu); // Student {name: “xiaowang”, age: 18, gender: “male”, stuNo: 10001, score: 99}11.递归11.1 什么是递归什么是递归?递归就是函数直接自己调用自己或者间接的调用自己。举个例子:函数直接调用自己function fn(){ fn();}fn();函数间接调用自己function fn1(){ fn2();}function fn2(){ fn1();}递归示例代码:function fn (){ console.log(‘从前有座山,’); console.log(‘山里有座庙,’); console.log(‘庙里有个老和尚,’); console.log(‘老和尚给小和尚讲,’); fn();}fn(); // 产生递归,无限打印上面的内容这样做会进入到无限的死循环当中。11.2 化归思想化归思想是将一个问题由难化易,由繁化简,由复杂化简单的过程称为化归,它是转化和归结的简称。合理使用递归的注意点:函数调用了自身必须有结束递归的条件,这样程序就不会一直运行下去了示例代码: 求前n项的和求前n项的和其实就是:1 + 2 + 3 +…+ n;寻找递推关系,就是n与n-1, 或n-2之间的关系:sum(n) == n + sum(n - 1);加上结束的递归条件,不然会一直运行下去。function sum(n){ if(n == 1) return 1; // 递归结束条件 return n + sum(n - 1);}sum(100); // 打印 5050递推关系:11.3 递归练习1、求n的阶乘:思路:f(n) = n * f(n - 1);f(n - 1) = (n - 1) * f(n - 2);示例代码:function product(n){ if(n == 1) { return 1; } return n * product(n-1);}console.log(product(5)); // 打印 1202、求m的n次幂:思路:f(m,n) = m * f(m,n-1);示例代码:function pow(m,n){ if(n==1){ return m; } return m * pow(m,n-1);}console.log(pow(2, 10)); // 打印 10243、斐波那契数列思路:什么是斐波那契数列?1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55,… 数字从第三项开始,每一项都等于前两项的和。可得出公式:fn = f(n-1) + f(n-2),结束递归的条件:当n <= 2时,fn = 1。示例代码:function fib(n){ if(n<=2) return 1; // 结束递归的条件 return fib(n-1) + fib(n-2);}console.log(fib(5)); // 5console.log(fib(10)); // 55console.log(fib(25)); // 75025 // 数值太大会影响性能问题存在问题:数值太大时会影响性能,怎么影响的呢?function fib(n){ if(n<=2) return 1; return fib(n-1) + fib(n-2); // 当我们在计算一个值的时候,都是通过计算他的fib(n-1) 跟 fib(n-2)项之后再去进行相加,得到最终的值 // 这时候就需要调用两次这个函数,在计算fib(n-1)的时候,其实也是调用了两次这个函数,得出fib(n-1)的值}// 记录执行的次数var count=0;function fib(n){ count++; if(n<=2) return 1; return fib(n-1)+fib(n-2);}console.log(fib(5)); // 5console.log(count); // 9 求第五项的时候就计算了9次//console.log(fib(20)); // 6765//console.log(count); // 13529 求第20项的时候就计算了13529次这个问题在下面讲闭包的时候解决。4.获取页面所有的元素,并加上边框页面结构:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> </div> <div> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> </div></body></html>结构图:js代码:// 封装一个方法,获取到所有的标签,并且给这些标签加上边框function childrenTag(ele){ var eleArr = []; // 用于存放所有的获取到的标签 var elements = ele.children; // 获取传入元素下的直接子元素 (伪数组) for(var i = 0; i < elements.length; i++){ eleArr.push(elements[i]); // 获取子元素下的直接子元素 var temp = childrenTag(elements[i]); // 一层层的递推下去 eleArr = eleArr.concat(temp); // 将获取的子元素的拼接到一起 } return eleArr;}console.log(childrenTag(document.body)); // 打印的就是页面body下所有的标签// 获取所有标签var tags=childrenTag(document.body);// 给所有标签添加边框for(var i=0;i<tags.length;i++){ tags[i].style.border=‘1px solid cyan’;}效果图:12. JS 内存管理本章引用自:《MDN-内存管理》12.1 内存生命周期不管是什么程序语言,内存生命周期基本是一致的:分配你所需要的内存;使用分配到的内存(读、写);不需要时将其释放、归还。JavaScript 的内存分配:为了不让程序员费心分配内存,JavaScript在定义变量时就完成了内存分配。var n = 123; // 给数值变量分配内存var s = “Levi”; // 给字符串分配内存var o = { a: 1, b: null}; // 给对象及其包含的值分配内存// 给数组及其包含的值分配内存(就像对象一样)var a = [1, null, “abra”]; function f(a){ return a + 2;} // 给函数(可调用的对象)分配内存// 函数表达式也能分配一个对象someElement.addEventListener(‘click’, function(){ someElement.style.backgroundColor = ‘blue’;}, false);使用值:使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。当内存不再需要使用时释放:大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。12.2 垃圾回收如上所述,自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。本节将解释必要的概念,了解主要的垃圾回收算法和它们的局限性。1、引用:垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。在这里,“对象”的概念不仅特指JavaScript对象,还包括函数作用域(或者全局词法作用域)。2、引用计数垃圾收集:这是最天真的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。示例代码var o = { a: { b:2 }}; // 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o// 很显然,没有一个可以被垃圾收集var o2 = o; // o2变量是第二个对“这个对象”的引用o = 1; // 现在,“这个对象”的原始引用o被o2替换了var oa = o2.a; // 引用“这个对象”的a属性// 现在,“这个对象”有两个引用了,一个是o2,一个是oao2 = “yo”; // 最初的对象现在已经是零引用了 // 他可以被垃圾回收了 // 然而它的属性a的对象还在被oa引用,所以还不能回收oa = null; // a属性的那个对象现在也是零引用了 // 它可以被垃圾回收了限制:循环引用该算法有个限制:无法处理循环引用。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return “azerty”;}f();实际例子:IE 6, 7 使用引用计数方式对DOM对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:var div;window.onload = function(){ div = document.getElementById(“myDivElement”); div.circularReference = div; div.lotsOfData = new Array(10000).join(”*”);};在上面的例子里,myDivElement这个DOM元素里的circularReference属性引用了myDivElement,造成了循环引用。如果该属性没有显示移除或者设为null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的DOM元素,即使其从DOM树中删去了。如果这个DOM元素拥有大量的数据(如上的lotsOfData属性),而这个数据占用的内存将永远不会被释放。3、标记-清除算法这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。循环引用不再是问题了在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦div和其事件处理无法从根获取到,他们将会被垃圾回收器回收。限制: 那些无法从根对象查询到的对象都将被清除尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。一般情况下, 如果需要手动释放变量占用的内存, 就将这个变量赋值为:null13. 闭包了解闭包之前,先了解下另外两个知识点:1、函数基础知识1、函数内部的代码在调用的时候执行2、函数返回值类型可以是任意类型3、怎么理解函数的返回值将函数内部声明的变量暴露到函数外部函数内用来返回数据,相当于没有函数的时候直接使用该数据不同之处在于:函数形成作用域,变量为局部变量function foo() { var o = {age: 12}; return o;}var o1 = foo();// 相当于: var o1 = {age: 18};2、作用域的结论1、JavaScript的作用域是词法作用域2、函数才会形成作用域(函数作用域)3、词法作用域:变量(变量和函数)的作用范围在代码写出来的就已经决定, 与运行时无关4、函数内部可以访问函数外部的变量(函数外部不能访问函数内部的变量)5、变量搜索原则:从当前链开始查找直到0级链6、当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。13.1 闭包的概念闭包从字面意思理解就是闭合,包起来。简单的来说闭包就是,一个具有封闭的对外不公开的包裹结构或空间。在JavaScript中函数可以构成闭包。一般函数是一个代码结构的封闭结构,即包裹的特性。同时根据作用域规则, 只允许函数访问外部的数据,外部无法访问函数内部的数据,即封闭的对外不公开的特性。因此说函数可以构成闭包。闭包的其他解释在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。实例:function fn() { var num = 123; return function foo() { console.log(num); };}// bar1就是闭包的一个实例var bar1 = fn();// bar2就是闭包的另外一个实例var bar2 = fn();bar1(); // 123bar2(); // 123闭包的构成闭包包括两部分:1、函数体(函数自身的代码);2、环境(函数的作用域)。闭包的说明1、JS中函数形成了闭包2、闭包是函数作用域的应用3、对于闭包来说,只关注创建函数的作用域,不关注调用函数的位置闭包的作用对函数内部的变量起到保护作用除了返回的函数以外,没有任何手段能够获取或者修改这个变量的值13.2 闭包模型function foo() { var num = 0; // 函数会产生一个作用域,所以外部的程序想要访问函数内部的变量,一般情况下是不行的 // 通过闭包的方式可以使外部访问到函数内部的变量 // 具体做法就是在函数内部返回一个函数,并且这个函数使用了这个变量 // 当用户调用最外层的函数的时候,使用的这个变量就会随着返回的函数返回给用户 return function() { return ++num; };}// 函数foo的返回值就是一个函数,所以,就可以调用getNum这个函数了!var getNum = foo();console.log(getNum()); // 113.3 闭包的使用目标:想办法(在外部)访问到函数内部的数据利用函数返回值function foo() { var num = Math.random(); return num;}var num1 = foo();var num2 = foo();console.log(num1 === num2); // 随机数 相同的情况很小很小普通的函数返回值说明两次调用函数,返回的数据并不是同一个数据。原因:函数在每次调用的时候,内部的数据会被新创建一次游戏充值案例示例图片:示例代码:<button id=“pay”>充值</button><button id=“play”>玩游戏</button><script> // 需求: // 1-需要对充值的金额起到保护作用,这个存放数值的变量不能暴露在全局,否则谁都会去修改这个金额 // var money = 0; // 2-点击充值按钮的时候,每次充值10元 // 3-点击玩游戏按钮的时候,每玩一次金额减少一元 function fn (){ var money = 0; // money用来存储充值的钱,放在函数内部,不会暴露在全局 // 一般的闭包返回值是一个函数,但是这里有两个功能,一个是玩游戏,一个是充值; // 两个功能分开,但是金额之间还是关联的,所以这里返回一个对象,里面存放两个方法 return { // 充值的函数 recharge:function(value){ money += value; console.log(‘尊敬的黄金会员,您本次充值:’ + value, ‘,您的总余额为:’ + money); }, // 玩游戏的函数 play:function(){ if(money <= 0){ console.log(‘余额不足无法继续游戏,请充值!’); return; } money–; console.log(‘您还剩余 ’ + money + ’ 条命!’); } }; } var obj = fn(); // 点击“充值”按钮 var pay = document.getElementById(‘pay’); pay.addEventListener(‘click’, function () { obj.recharge(10); }); // 点击“玩游戏”按钮 var play = document.getElementById(‘play’) play.addEventListener(‘click’, function () { obj.play(); })</script>优化,多个角色进行充值玩游戏<div> <button id=“pay”>小明:充值</button> <button id=“play”>小明:玩游戏</button></div><div> <button id=“pay1”>小华:充值</button> <button id=“play1”>小华:玩游戏</button></div><script> // 1 需要对充值的钱起到保护作用 // var money = 0; // 2 充值: 每次充值20 // 3 玩游戏: 每玩一次,金额少1 // 整个fn()形成一个函数作用域,对里面的变量起到保护作用 function fn() { // money 用来存储充值的钱 var money = 0; // 充值的函数: function recharge(value) { // money += 20; money += value; console.log(‘尊敬的黄金会员,您本次充值:’ + value, ‘,您的总余额为:’ + money); } // 玩游戏的函数 function play() { money–; if (money < 0) { console.log(‘余额不足,请充值!’); } else { console.log(‘您还剩余 ’ + money + ’ 条命!’); } } return { recharge: recharge, play: play }; } // 小明充值玩游戏的函数 var obj; obj = fn(); // 小明玩游戏: var pay = document.getElementById(‘pay’); pay.addEventListener(‘click’, function () { obj.recharge(20); }); var play = document.getElementById(‘play’) play.addEventListener(‘click’, function () { obj.play(); }); // 小华(新的闭包实例): var obj1 = fn(); // 小华玩游戏: var pay1 = document.getElementById(‘pay1’); pay1.addEventListener(‘click’, function () { obj1.recharge(20); }); var play1 = document.getElementById(‘play1’) play1.addEventListener(‘click’, function () { obj1.play(); });</script>优化的案例我们可以看到,只要重新定义一个变量,接收函数 fn(),就能重新开辟一个新的空间,且多个用户之间不受任何影响。13.4 闭包里的缓存从内存看闭包函数调用也是需要内存的!因为函数中声明了一些变量,这些变量在函数调用过程中是可以使用的,所以, 这个变量是存储到了函数调用时候分配的内存中了!因为没有任何变量来引用这块内存,所以,函数调用结束。 函数调用占用的内存就会被回收掉。虽然,此时的函数有返回值(返回了一个普通的变量),并且这个函数调用结束以后这个函数占用的内存还是被回收了!但是, 存储函数的内存还在。闭包的内存占用:作用域的引用是对函数整个作用域来说的,而不是针对作用域中的某个变量!!!即便没有任何的变量,也是有作用域( 作用域的引用 )。function fn() { var num = 123; return function() { console.log(num); };}// 此时, 函数fn调用时候占用的内存, 是不会被释放掉的!!!var foo = fn();// 调用 foo() 此时, 因为返回函数的作用域对外层函数fn的作用域有引用// 所以, 即使是 fn() 调用结束了, 因为 返回函数作用域引用的关系, 所以// 函数fn()调用时候, 产生的内存是不会被释放掉的!foo();// 手动释放闭包占用的内存!foo = null;缓存介绍缓存:暂存数据方便后续计算中使用。缓存中存储的数据简单来说就是:键值对工作中,缓存是经常被使用的手段。目的:提高程序运行的效率我们只要是使用缓存,就完全信赖缓存中的数据。所以, 我们可以通过闭包来保护缓存。对于缓存来说,我们既要存储值,又要取值!存储的目的是为了将来取出来,在js中可以使用对象或者数组来充当缓存。如果是需要保持顺序的,那么就用数组,否则就用对象!// 创建一个缓存:var cache = {};// 往缓存中存数据:cache.name = ‘xiaoming’;cache[’name1’] = ‘xiaohua’;// 取值console.log(cache.name);console.log(cache[’name1’]);计算机中的缓存就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。缓存使用步骤首先查看缓存中有没有该数据,如果有,直接从缓存中取出来;如果没有就递归计算,并将结果放到缓存中递归计算斐波那契数列存在的问题前面在学习递归的时候,我们举了一个斐波那契数列的例子,但是当时说存在性能问题,我们重新看下这个问题。// 使用递归计算 菲波那契数列// 数列:1 1 2 3 5 8 13 21 34 55 89 。。。// 索引:0 1 2 3 4 5 6 7 8 9 10 。。。var count = 0;var fib = function (num) { count++; if (num === 0 || num == 1) { return 1; } return fib(num - 1) + fib(num - 2);};// 计算索引号为10的值, 一共计算了: 177 次// 计算索引号为11的值, 一共计算了: 287 次// 计算索引号为12的值, 一共计算了: 465 次// ….// 计算索引号为20的值, 一共计算了: 21891 次// 计算索引号为21的值, 一共计算了: 35421 次// …// 计算索引号为30的值, 一共计算了: 2692537 次// 计算索引号为31的值, 一共计算了: 4356617 次fib(31);console.log(count); // 4356617注意上面代码,count是用来记录程序运行时执行的次数,不明白的小伙伴可以返回递归那一章节,我专门画了一张图,可以理解下这个次数是怎么计算的。我们看下上面的代码的注释,求第20项跟21项的时候,虽然只相差一项,但是却多运算了一万多次,试想一下这里面存在的效率问题是多么的可怕。闭包和缓存解决计算斐波那契数列存在的问题其实主要的问题就是,数据重复运算。比如计算第五项的时候,他计算的是第三项跟第四项的和,这时的第三项跟第四项都是从一开始重新计算的,假如吧计算过得值保存下来,就不需要再重复的运算。运用缓存:将计算的值存储下来,减少运算次数,提高效率;使用闭包:从来保护缓存。// 记录计算的次数var count = 0;function fn() { // 缓存对象 var cache = {}; // 这个返回函数才是 递归函数! return function( num ) { count++; // 1 首先查看缓存中有没有 num 对应的数据 if(cache[num]) { // 说明缓存中有我们需要的数据 return cache[num]; } // 2 如果缓存中没有, 就先计算, 并且将计算的结果存储到缓存中 if(num === 0 || num === 1) { // 存储到缓存中 cache[num] = 1; return 1; } var temp = arguments.callee(num - 1) + arguments.callee(num - 2); cache[num] = temp; return temp; };}var fib = fn();var ret = fib(20);console.log(ret); // 10946console.log(‘计算了:’ , count, ‘次’); // 计算了: 39 次我们可以跟上面没有使用缓存,求斐波那契数列的比较一下,此时求第20项的时候,仅仅运算了39次,但是在之前却运行了21891次。上面的方法存在着一些的问题,每次在执行的时候,函数fn都要先被调用一次(var fib = fn();),下面进行优化:将fn转换成自执行函数(沙箱模式,下一章会讲),自执行函数的返回函数就是递归函数;判断缓存是否存在的条件进行优化,之前是通过判断缓存的值是否存在,来进行存、取值的,但是假如一个缓存的值是false的时候呢?岂不是if(false){}了,明明有值的时候,却不能取值了,所以玩我们只需要判断缓存里是否存在某个键就行。var fib = (function () { // 缓存对象 var cache = {}; // 这个返回函数才是 递归函数! return function (num) { // 1 首先查看缓存中有没有 num 对应的数据 /** if(cache[num]) { return cache[num]; } / // 只要缓存对象中存在 num 这个key, 那么结果就应该是 true if (num in cache) { // 说明缓存中有我们需要的数据 return cache[num]; } // 2 如果缓存中没有, 就先计算, 并且将计算的结果存储到缓存中 if (num === 0 || num === 1) { // 存储到缓存中 // cache[num] = 1 是一个赋值表达式, 赋值表达式的结果为: 等号右边的值! return (cache[num] = 1); } // arguments.callee 表示当前函数的引用 return (cache[num] = arguments.callee(num - 1) + arguments.callee(num - 2)); };})();var ret = fib(10)console.log(ret);什么是 arguments.callee?返回正被执行的function对象,也就是所指定的function对象的正文。callee属性是arguments 对象的一个成员,它表示对函数对象本身的引用,这有利于匿名函数的递归或者保证函数的封装性。function fn(a, b) { console.log(arguments);}fn(1, 2);我们可以看到,打印的arguments属性里面有哪些参数:前面几项是函数调用后传进来的实参;callee:f,它其实就是函数fn的引用,你可以理解为:arguments.callee()相当于fn();length就是实参的长度。再去看上面斐波那契的案例,它的递归函数是一个匿名函数,所以在这个函数里面自己调用自己的时候,就是使用的arguments.callee去引用的。14. 沙箱模式沙箱模式又称:沙盒模式、隔离模式。沙箱(sandbox)介绍:用于为一些来源不可信、具备破坏力或无法判定程序意图的程序提供试验环境。然而,沙盒中的所有改动对操作系统不会造成任何损失。14.1 沙箱模式的作用作用:对变量进行隔离问题:在js中如何实现隔离?ES6之前, JavaScritp中只有函数能限定作用域,所以,只有使用函数才能实现隔离。本质上还是对函数作用域的应用。14.2 沙箱模式模型使用自调用函数实现沙箱模式函数形成独立的作用域;函数只有被调用,内部代码才会执行;将全局污染降到最低。(function() { // … // 代码 // …})(); 14.3 沙箱模式应用最佳实践:在函数内定义变量的时候,将 变量定义 提到最前面。// 1 减少了window变量作用域的查找// 2 有利于代码压缩(function( window ) { var fn = function( selector ) { this.selector = selector; }; fn.prototype = { constructor: fn, addClass: function() {}, removeClass: function() {} }; // 给window添加了一个 $属性,值为: fn // 暴露数据的方式: window.$ = fn;})( window );14.4 沙箱模式的说明将代码放到一个立即执行的函数表达式(IIFE)中,这样就能实现代码的隔离;使用IIFE:减少一个函数名称的污染,将全局变量污染降到最低;代码在函数内部执行,函数内部声明的变量不会影响到函数外部;如果外部需要,则可以返回数据或把要返回的数据交给window。IIFE: Immediately Invoke Function Expression立即执行的函数表达式15. 工厂模式工厂模式是一种设计模式,作用是:隐藏创建对象的细节,省略了使用new创建对象。构造函数:构造函数创建之后,我们实例化一个对象的时候都是直接通过new创建出来的。function Person(name, age) { this.name = name; this.age = age;}var p1 = new Person(‘Levi’, 18); 工厂函数:工厂函数的核心就是隐藏这个new创建对象的细节。function Person(name, age) { this.name = name; this.age = age;}function createPerson(name, age) { return new Person(name, age);}var p2 = createPerson(‘Ryan’, 19);两段代码比较下来,我们可以看到,实例出来的p2对象没有直接使用new创建,而是通过一个函数的返回值创建出来的,这就是工厂模式。使用场合:jQuery中,我们用的“$”或者jQuery函数,就是一个工厂函数。/ Jquery 中的部分源码 */// jQuery 实际上是一个 工厂函数,省略了 new 创建对象的操作jQuery = function( selector, context ) { // jQuery.fn.init 才是jQuery中真正的构造函数 return new jQuery.fn.init( selector, context );}(本篇完) ...

December 27, 2018 · 19 min · jiezi

通过几个栗子认识 PHP 闭包

<!– TOC –>通过几个栗子认识PHP闭包一、栗子1 用作于回调二、栗子2 用作于变量赋值三、栗子3 从父作用域继承变量四、栗子4的前提条件,简单理解call_user_func_array()和call_user_func()方法1. call_user_func — 把第一个参数作为回调函数调用2. call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数五、栗子4 绑定闭包在指定对象1. Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。2. Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。六、参考资料<!– /TOC –>有收获的话请加颗小星星,没有收获的话可以 反对 没有帮助 举报三连示例代码本文地址本人能力有限,如遇到什么不对的地方还望指出修正,谢谢所有栗子的输出都使用symfony/var-dumpe美化了匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。当然,也有其它应用的情况。匿名函数目前是通过 Closure 类来实现的。一、栗子1 用作于回调$rs = preg_replace_callback(’/-([a-z])/’, function ($match) { return strtoupper($match[1]);}, ‘hello-world’);dump($rs); // “helloWorld"二、栗子2 用作于变量赋值$greet = function ($name) { dump($name);};dump($greet instanceof Closure); // true$greet(‘PHP’); // “PHP"三、栗子3 从父作用域继承变量$message = ‘hello’;$example = function () use ($message) { dump($message);};dump($example instanceof Closure); // true$example(); // “hello"四、栗子4的前提条件,简单理解call_user_func_array()和call_user_func()方法1. call_user_func — 把第一个参数作为回调函数调用function call_user_func ($function, …$parameter) {}该方法接收多个参数,第一个就是回调函数,可以是普通函数,也可以是闭包函数,后面的多个参数都是作为函数回调使用$rs = call_user_func(function (…$params) { return func_get_args();}, 1, 2, 3);dump($rs); // [1,2,3]2. call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数function call_user_func_array ($function, array $param_arr) {}该方法接收2个参数,第一个就是回调函数,可以是普通函数,也可以是闭包函数,后面的数组参数都是作为函数回调使用$rs = call_user_func_array(function (array $params) { return func_get_args();}, [1, 2, 3]);dump($rs); // [1,2,3]五、栗子4 绑定闭包在指定对象楼主见解是将方法绑定到指定类上,使得方法也可以使用类的属性和方法,非常适合配合__call()魔术方法和call_user_func_array方法一起使用1. Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。function bindTo($newthis, $newscope = ‘static’) { }<?phpnamespace PHP\Demo\Closure;class ClosureBindTo{ public function __call($name, $arguments) { if (count($arguments) > 1 && $arguments[0] instanceof \Closure) { return call_user_func_array($arguments[0]->bindTo($this), array_slice($arguments, 1)); } throw new \InvalidArgumentException(“没有这个方法”); }}// 测试public function testClosureBindTo(){ $obj = new ClosureBindTo(); $this->assertEquals(2, $obj->add(function (array $params) { return ++$params[0]; }, [1])); // 测试同一个实例 $newObj = $obj->test(function (array $params){ return $this; }, [1]); $this->assertTrue($newObj instanceof $obj);}2. Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。static function bind(Closure $closure, $newthis, $newscope = ‘static’) { }bind函数是bindTo的静态表示<?phpnamespace PHP\Demo\Closure;class ClosureBind{ private $methods = []; public function addMethod(string $name, \Closure $callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException(“第二个参数有误”); } $this->methods[$name] = \Closure::bind($callback, $this, get_class()); } public function __call(string $name, array $arguments) { if (isset($this->methods[$name])) { return call_user_func_array($this->methods[$name], $arguments); } throw new \RuntimeException(“不存在方法[{$name}]”); }}// 测试public function testClosureBind(){ $obj = new ClosureBind(); $obj->addMethod(‘add’, function (array $params) { return ++$params[0]; }); $this->assertEquals(2, $obj->add([1])); // 测试同一个实例 $obj->addMethod(’test’, function (array $params) { return $this; }); $this->assertTrue($obj->test([1]) instanceof $obj);}六、参考资料php手册 ...

December 26, 2018 · 2 min · jiezi

JS 总结之闭包

从《JS 总结之函数、作用域链》一文中知道作用域链的作用,保证了对所有变量对象的有序访问。???? 问题函数外的是无法访问函数内部的变量,有时候要用到怎么办?我们的主角,闭包就是可以解决这个问题。???? 什么是闭包引用 MDN 上的解释:闭包是函数和声明该函数的词法环境的组合。引用 《JavaScript 高级程序设计(第 3 版)》上的解释:闭包是指有权访问另一个函数作用域中的变量的函数。???? 相同点这两个解释都在说着同一件事,闭包能访问声明时函数所在的环境中的变量和函数。那具体是因为什么才会让闭包访问到环境中的变量和函数,这得靠两兄弟:变量对象和作用域链。变量对象当执行函数的时候,会创建和初始化一个函数执行环境,在函数执行环境中,全局执行环境的变量对象(Variable Object,缩写为 VO)不能直接访问,此时由激活对象(Activation Object,缩写为 AO)扮演 VO 的角色。变量对象专门用来梳理和记住这些变量和函数,是作用域链形成的前置条件 。但我们无法直接使用这个变量对象,该对象主要是给 JS 引擎使用的。具体可以查看《JS 总结之变量对象》。变量对象就相当于一个存放的仓库,获取到里面的东西,还得需要去获取这些的路径,这就是作用域链的事情了。作用域链然而,光有变量对象可完成不了闭包的形成,怎样才能让函数访问到,这得靠作用域链,作用域链的作用就是让函数找到变量对象里面的变量和函数。具体可以查看《JS 总结之函数、作用域链》???? 不同点虽然都是讲闭包,但 MDN 上面讲的是声明该函数的词法环境,而 JS 高程讲的是访问另一个函数作用域中,从解释上的不同,闭包便有了理论中的闭包( MDN )和实践中的闭包( JS 高程)之分。???? 理论中的闭包根据 MDN 的解释写个例子:var a = 1function fn() { console.log(a)}fn()函数 fn 和函数 fn 的词法作用域构成了一个闭包。但是这不是普通的函数吗?在《JavaScript 权威指南》中也得到证实:从技术的角度讲,所有 JavaScript 函数都是闭包???????? 实践中的闭包汤姆大叔翻译的文章中讲,实践中的闭包需要满足以下两个条件:即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)在代码中引用了自由变量什么是上下文?即函数的执行环境。什么是自由变量?即函数的词法作用域中的变量和函数,而不是函数本身的参数或者局部变量,或者说是所在函数的变量对象中的变量和函数。这两点和 JS 高程中讲的闭包的解释不谋而合。现在写个符合的例子:function fn() { var a = 1 function fn1() { console.log(a) } return fn1}var b = fn()b()当执行 b 的时候,创建它的执行环境 fn 早已经摧毁,但函数 b 还能访问到变量 a。好吧,我有点乱!要彻底明白这个是咋回事,要结合执行环境、活动变量和作用域链来看,让我们来看看这个例子的执行过程:???? 执行过程执行全局代码,创建全局执行环境 globalContext,将全局执行环境推入环境栈环境栈 = [globalContext]初始化全局执行环境globalContext = { VO: [global], Scope: [globalContext.VO], this: globalContext.VO}初始化的同时,创建 fn 函数,复制 全局执行环境的作用域链 到 fn 的 [[scope]] 属性fn.[[scope]] = [ globalContext.VO]执行 fn 函数,创建 fn 的执行环境 fnContext,并推入环境栈环境栈 = [fnContext, globalContext]初始化 fnContext:复制 fn 的 [[scope]] 创建 fn 作用域链创建变量对象,初始化变量对象,加入形参,函数,变量等,由于是函数,变量对象为活动对象将活动对象推入 fnContext 的 Scope 顶端fnContext = { AO: { arguments: {}, scope: undefined, fn1: reference to function fn1(){} }, Scope: [AO, globalContext.VO], this: undefined}初始化同时,创建 fn1 函数,复制 fnContext 的作用域链 到 fn1 的 [[scope]] 属性fn1.[[scope]] = [ fnContext.AO, globalContext.VO]执行完毕,推出 fnContext环境栈 = [globalContext]执行函数 b,也就是被返回的 fn1,创建 fn1 的执行环境 fn1Context,并推入环境栈环境栈 = [ fn1Context, globalContext]初始化 fn1Context:复制 fn1 的 [[scope]] 创建 fn1 作用域链创建变量对象,初始化变量对象,加入形参,函数,变量等,由于是函数,变量对象为活动对象将活动对象推入 fn1Context 的 Scope 顶端fn1Context = { AO: { arguments: {} }, Scope: [AO, fnContext.AO, globalContext.VO], this: undefined}执行完毕,推出 fn1Context环境栈 = [globalContext]???? 柳暗花明当执行函数 b 的时候,创建它的执行环境 fn 早已摧毁(步骤 6),只留下了它的活动变量 fnContext.AO 于内存中(步骤 5):fn1Context = { Scope: [AO, fnContext.AO, globalContext.VO]}fnContext.AO 存在 fn1 的作用域链中,所以能访问到fn1 的词法环境,这便形成了闭包。因此,闭包是变量对象和作用域链共同作用的结果。???? 参考《闭包》 by MDN《JavaScript 深入之执行上下文》 by 冴羽《JavaScript 深入之闭包》 by 冴羽《深入理解 JavaScript 系列(16):闭包(Closures)》 by 汤姆大叔《JavaScript 高级程序设计(第 3 版)》4.2 执行环境及作用域、7.2 闭包《JavaScript 权威指南》8.6 闭包 ...

December 26, 2018 · 2 min · jiezi