函数: 封装代码实现某个功能,最初思路解决代码重复度高的问题,类似于变量(就是一个筐,往里面填充内容即可) 【编程思想:高内聚、低耦合】【聚焦点:参数和返回值,后续就是函数的嵌套,才延生出作用域、闭包等问题,然后寻求解决方案】
1、函数的基本认知:
(1).函数声明、调用:function test(){}; test: 函数名/函数引用; test(); 函数执行;[function/var 都是关键字; 函数名命名规则:小驼峰规则(并非是语法规则,而是开发规范)]【functiontheFirstName(){} document.write(theFirstName);// 打印出函数体,绝对不是地址,区别于编译性语言,解释性语言是打印不出地址的】
(2).函数表达式:指代的便是匿名函数表达式;
(3).参数的使用:使函数功能更加丰富;[形参==> 函数内隐式var了多个变量,形参和实参都是变量,它们可以是变量的任何类型:原始值/引用值,取决于函数封装的代码要实现的功能]
参数个数问题、实参列表;[实参与形参传递时最好要相互对应,要不后续会遇到函数内部封装好的对象,event/error等的使用,避免出错]
映射规则、特殊性;[arguments:每个函数内部系统都会隐式定义一个实参列表,实参都有地方存放,console.log(arguments)]
例子:
(4).return(函数结束条件及返回值,如果不写 -> 函数末尾隐式添加 return undefined;)
(5).作用域:全局变量/局部变量的访问规则:
嵌套函数:里面可访问外面,外面不可访问里面;两个相互独立的函数也不可互相访问;
补充:定义函数的方式:
[1].函数声明;[2].函数表达式(匿名函数表达式);[3].使用对象Function,类似Number/Boolean/String/Array/Object;(了解即可)
[4].函数在对象中的使用,称为方法;[5].匿名函数function(){},箭头函数() => { }; 这两种一般作为回调函数使用(作为其他函数的参数),或者当作立即执行函数使用;
箭头函数:() => {}; => 表示函数中的大括号,{ } 函数体;( )函数参数,当函数只有一个形参()可省略,当函数体只有一条语句时,{}可省略,若有return,return单词也可省略;最好都别省略;
2、递归:有很强抽象规律使用,阶乘、斐波那契数列是典型的两个案例;[递归可以使代码更加简捷,缺点:运行速度较慢,往往有很强抽象规律才使用]
【1.找规律;2.找出口,防止进入死循环】
//使用过程中,只需要在函数体内写入:return 公式 就OK;(尽量做到灵活使用,但其确实不容易想到,后续有些地方会用到递归的思想,例如:算法)
3、预编译:解决代码执行顺序问题;【聚焦点:GO全局形成、AO函数执行前形成,聚焦在变量】
js运行三部曲: 语法(语义)分析、预编译、解释执行;
(1).语法分析:检查代码是否有低级错误,若出现低级语法错误,则不执行;[关于错误后还有更文]
(2).预编译:代码执行前一刻需要进行预编译,全局的预编译/函数执行前的预编译;[日常开发编写代码是视觉效果展示,计算机执行过程是遵循预编译的顺序执行,预编译生成的对象就是一个空间存储库]
(3).解释执行;
window全局对象的整体认知:【补充:undefined:声明了但没值,例如console.log(window.a);其也是对象中很特殊的一类现象,a is not defined; 压根就不存在该变量】
预编译:变量声明提升、函数声明整体提升都只是预编译中的现象而已;一般遇到代码执行问题,上述两条就可以解决,不容易解决时创建GO、AO对象分析;[GO对象就是window对象,表示全局]
[定义变量=变量声明+变量初始化(赋值) var a; //变量声明 a = 10; //变量初始化 var a=10; //定义变量 ]
先形成GO对象,再形成AO对象;[代码包含无非就是:变量、函数、语句,GO对象是在全局时候形成的,AO对象是函数执行前一刻形成的,一定要善于区分,函数输出的变量是全局还是AO]
升华例子1:函数执行前形成的AO对象中肯定会有变量b的声明,即使函数中该变量在if条件判断语句中,预编译过程中形成的对象就是后续代码执行的依据,符合条件的提升就OK;
升华例子2:关于代码执行顺序可以get到一些点(例子1显然返回函数、例子2因为有赋值,所以返回的是值),总之灵活应对;
【补充:如上console.log(bar);//输出整个函数;bar:函数名/函数引用;console.log(bar()); - - - > 输出函数执行结果】
----------->>>>>>生成的GO/AO对象放到什么地方?函数中里面的可访问外面的,外面的却不可访问里面的这类现象的底层逻辑是什么?[之前查找变量是从GO/AO对象中查找,底层实际是从函数的作用域链中去查找]
4、作用域、作用域链精解:【聚焦点是:函数嵌套时变量的查找】
【window:全局的域;函数都有自己的独立作用域】
每个函数都有自己的作用域链;产生的执行期上下文的集合形成了作用域链中,访问变量的顺序也遵循该链;里面可访问外面,外面无法访问里面;
一个函数只有执行的时候,产生AO对象,系统才能看到里面的东西,否则定义阶段根本就看不到里面的东西;只有执行的时候才开始读取函数里面的内容;
(1).函数也是对象(函数类对象):对象都有属性,有些属性我们可以直接访问test.name/prototype;但有些属性是不可直接进行访问,仅供javaScript引擎读取,[[scope]]就是其中的一个,test[[scope]];
[[scope]]:每个函数都有该属性,称为作用域,其内部存储了运行期上下文的集合,这个集合呈链式链接,就是作用域链;
运行期上下文:也称为执行期上下文,当函数执行的前一刻,会创建执行期上下文的内部对象,也就是预编译时产生的GO/AO对象;[函数每次执行时都会创建新的执行期上下文(对象),所以多次调用一个函数会创建多个执行期上下文,当函数执行完毕后,所产生的执行期上下文被销毁,这里的销毁指代的是引用销毁,而非代码直接销毁]
查找变量:从作用域链的顶端依次向下查找;
(2).过程剖析:
[1].函数定义的过程中函数的作用域便已经存在,即[[scope]]已经存在;首先生成GO对象,这时候a函数被定义,定义的过程中作用域链中 0 -->> GO对象;[函数定义的阶段(GO对象生成的过程),系统根本不会关注函数体里面的内容,不会解析,生成AO对象才开始分析函数体里面的内容]
[2].GO对象形成后,开始执行代码----->>a();--->>>函数执行前一刻形成AO对象,此时的作用域链中0 --->> AO对象; 1 --->>GO对象;新形成的AO对象会在作用域链的顶端;[执行过程中查找变量肯定是在作用链中去查找,先找自己AO对象,再查找GO对象,即从作用链顶端以此向下查找]
[3].a函数的执行过程中,b函数开始定义(预编译过程);定义的时候b函数会产生自己的[[scope]],其的作用域链环境是a函数给的,也就是a函数执行时候的环境;
[4].b函数执行中会产生自己的AO对象,然后放置到自己作用域链的最顶端;
[5].b函数执行结束后,销毁自己的执行期上下文对象即AO对象(回归到被定义的状态,如果有二次执行,再形成新的AO对象,执行的过程中创建AO对象,执行结束后销毁相应的AO对象即可)所谓的销毁并非是直接把AO对象抹掉了,而是把线剪断;[b函数作用域链中拿到的a函数执行过程中的作用域链,它们指向是相同的,只不过是生成自己的AO对象而已]
[函数内查找变量的过程:在函数自己的作用域链中自顶向下找,里层函数可访问外层函数的变量,但外层函数绝对不可能访问到里层函数的变量,外层函数作用域链中自己的AO与外层的环境,自己的AO对象只有里层函数的定义,定义阶段系统又不解析函数体内部,所以压根找不到函数里面的变量]
[6].b函数执行结束后,代码中a函数也执行结束了,a函数需要销毁自己的执行期上下文对象,回归到被定义的状态,销毁AO对象的同时,b函数定义也被销毁了,也就是b函数彻底结束了,作用域链中没东西了;
[7].若是a函数再次执行,需要从新开始,创建AO对象等一系列操作同上;若是函数内嵌套了多个函数,最里面的函数作用链会很长,当最外面函数执行完成后,里面的函数连定义状态都没有了,作用链为空; ---->>>>> 但有特殊情况,函数执行完成后却把里面的函数return出去了,形成闭包;
5、闭包:其是一类现象;函数嵌套中外部函数结束执行时,内部函数由于被return到了外部,依旧存活,内部函数生命周期比外部函数还长;【内部函数被保存到外部就会形成闭包,但这个过程很容易被忽视】
过程分析:[1].a函数结束执行后,return b;b函数依旧属于被定义的状态,环境是a函数执行时的作用域链,这时候b函数依旧可以操作外面函数的变量(作用域链的原因);[若是只是普通的在a函数外部定义的函数,由于作用域的原因,其必然是不能访问a函数中的变量的,但闭包可以]
闭包:函数执行结束会释放作用域链占用的空间,但若是函数嵌套多层,形成闭包则会导致原有作用域链不释放,占用内存空间(也称为内存泄漏【现象一致:都是占用内存,剩余内存少了】);闭包有坏处需要防治,但也有很好的应用场景来实现一些功能;
闭包的作用:
(1).实现公有变量:函数累加器;[return demo;demo的作用域链是add函数执行时的环境,指向的就是同一套,demo();每次指向环境没变只是创建了新AO对象而已,所以每次操作都会改变原来的值]
(2).可以做缓存;(缓存就是存储结构)
(3).可以实现封装、属性私有化;
(4).模块化开发、防止污染全局变量;,
闭包的bug以及解决方案:[下式是经常会遇到的一种闭包情况,高频触发,经常会被忽视] 考虑过程中需要结合预编译、作用域链、闭包的知识,开始写GO/AO对象分析,后续尽量抽象化去分析; myArr = [function (){console.log(i} ......] //里面10个相同的函数;myArr[j]();这时候才开始执行函数,i自身AO对象中没有,沿着作用域链找test中有,此时i为10;【里面函数与test() 形成10对1的闭包】
闭包的解决方案:立即执行函数(其也是唯一的解决方案);[每循环一次立即执行函数就执行,过后销毁,但销毁并非是把里面的代码给抹掉了,只是把引用抹掉了而已,循环了10次,有10个不同的立即执行函数] [立即执行函数虽然销毁,但最终把里面的函数弄了出来,其和立即执行函数形成了闭包,j自身没有就从立即执行函数中查找] 【里面函数与立即执行函数形成:1对1的闭包】
6.立即执行函数(也称为函数的自执行):该函数执行过后就销毁便不存在了;[js中提供立即执行函数来处理那些只执行一次的函数,有些函数内容巨长会占用内存][所谓的销毁并不是直接把代码抹掉了,而是把函数的引用抹掉]
[无论如何有的函数只执行一次(只被执行一次,我们想要其执行一次,或者执行一次返回结果的函数),我们称之为初始化函数或初始化功能,例如开发中的数据进行初始化等,初始化后该函数就没用了]
常规用法:[立即执行函数没有函数名,函数名没什么用处,写上倒也不会报错]
拓展(深挖底层原理):只有表达式才能被执行符号执行,执行后基本就是立即执行函数了; [function test(){}();//报错; test();不报错,test代表着函数体,为什么两者结果不同? 原因:test也是表达式,例如123是数字表达式];
var test = function(){ console.log('a');}();//表达式加上执行符号,执行过后就销毁console.log(test);//undefined;
回归到最初所提到的:(function (){} ()); (function (){})();这两种立即执行函数形式,可以理解为外面的括号将函数声明变为了表达式,然后被执行符号执行,变为立即执行函数;
升华例子:括号首先把函数声明变为了表达式,然后执行if条件判断;(开发中也没有人这么写)