前端笔记之JavaScript(四)关于函数、作用域、闭包那点事
一、自定义函数function
函数就是功能、方法的封装。函数能够帮我们封装一段程序代码,这一段代码会具备某一项功能,函数在执行时,封装的这一段代码都会执行一次,实现某种功能。而且,函数可以多次调用。
1.1函数的定义和调用
语法:
定义:把需要实现的功能预先做好 执行:需要的时候执行这个功能,而且还可以执行多次 |
定义:function myName(){} 执行:myName() |
【语法解释】:
function 定义函数的关键字 myName 函数名称 () 参数集 {} 函数体,执行的代码都放在{}里面 |
多条语句,组成一个“语句军团”,集体作战。
//定义一个函数,函数就是一组语句的集合 function haha(){ console.log(1); console.log(2); console.log(3); console.log(4); } haha();//调用haha函数 haha();//调用haha函数 haha();//调用haha函数
函数必须先定义,然后才能调用。
定义一个函数,用关键字function来定义,function就是英语“功能”的意思。表示这里面定义的语句,完成了一些功能。function后面有一个空格,后面就是函数名字,函数的名字也是关键字,命名规范和变量命名是一样的。名字后面有一对儿圆括号,里面放置参数。然后就是大括号,大括号里面是函数的语句。
function 函数名称(){
} |
函数如果不调用,里面的语句一辈子都不执行,等于白写。
调用函数的方法,就是函数名称加(),()是一个运算符,表示执行一个函数。
函数名称() |
一旦调用函数,函数内部的代码不管对错,都会执行。
能感觉到,函数是一些语句的集合,让语句称为一个军团,集体作战。要不出动都不出动,要出动就全动。
函数的意义1:在出现大量程序代码相同时候,可以为它门封装成一个function,这样只调用一次,就能执行很多语句。
1.2函数的参数
定义在函数内部的语句,都是相同的,但是实际上可以通过“参数”这个东西,来让语句有差别。
定义函数时,内部语句可能有一些悬而未决的量,就是变量,这些变量,要求在定义时都罗列在圆括号中:
function fun(a){ console.log("我第"+a+"次说你好!"); } fun(100); fun(1); fun(2); fun(3);
调用的时候,把这个变量真实的值,一起写在括号里,这样随着函数的调用,这个值也传给了a变量参数。
罗列在function圆括号中的参数,叫做形式参数;调用时传递的数值,叫做实际参数。
参数可以有多个,用逗号隔开。
function sum(a,b){ console.log(a + b); } sum(3,5); //8 sum(8,11); //19 sum("5",12); //512 sum(10); //NaN,因为a的值是10,b没有被赋值是undefined,10+undefined=NaN sum(10,20,30,40,88); //30,后面的参数没有变量接收
函数的意义2:在调用函数时,不用关心函数内部的实现细节,甚至这个函数是你网上抄的,可以运行。所以这个东西,给我们团队开发带来了好处。
定义函数的时候,参数是什么类型,不需要指定类型:
调用的时候,传进去什么类型,a、b变量就是什么类型
sum("5",12); //512,做的是连字符串运算 |
另外,定义和调用的时候参数个数可以不一样多,不报错。
sum(10); |
因为只给a变量赋值,b没有被赋值,b被隐式的var。b的值是undefined,10+undefined=NaN
sum(10,20,30,40,88); //30 |
只有前面两个参数被形参变量接收了,后面的参数没有变量接收,就被忽略了。
//封装一个函数,计算m+....+n的和 //比如m是4,n是15,4+5+6+7...+15 function sum(m,n){ var s = 0; //累加器,累加和 for(var i = m;i <= n;i++){ s+=i //s = s + i; } console.log(s); } sum(1,100); //计算1到100的和 sum(10,13); //计算10+11+12+13的和 sum(13,10); //输出0,所以你就知道函数顺序关机,定义顺序是什么,传递顺序就是什么。
1.3函数的返回值
函数可以通过参数来接收东西,还可以通过return关键字来返回值,“吐出”东西。
function sum(a,b){ return a+b; //现在这个函数的返回值就是a+b的和 } //sum没有输出功能,就要用console.log输出 console.log(sum(3,8)); //计算sum(3,8);实际上称为表达式,需要计算,计算后是11 console.log(sum(3,sum(4,5)));//输出12,实际上两次执行了sum函数,先执行内层的,计算出9,然后sum(3,9)就是12
函数有一个return的值,那么现在这个函数,实际上是一个表达式,换句话说这个函数就是一个值。
所以这个函数,可以当做其他的函数参数。
sum(3,sum(4,5)); |
程序从内层执行到外层,sum(3,9)
函数可以接收很多值,但是返回一个值。
函数的意义3:模块化编程,让复杂的逻辑变得更简单。
函数只能有唯一的return,有if语句除外。
程序遇见return,会做两件事:
1、立即返回结果,返回到调用它的地方
2、不执行return后面的代码。
function fun(){ console.log(1); console.log(2); //return; //返回一个空值,undefined return "你好啊!"; console.log(3); //这行语句不执行,因为函数已经return,所以会终止执行后面的代码。 } console.log(fun();
1.4函数模块化编程
实现前提:函数有返回值,可以作为其他函数执行时传的实参。
习惯将复杂工作,进行一步步的分工,将一部分工作的结果作为下一步工作的条件。
将程序中某个单独的功能制作成单独函数,这就是造轮子的过程。
业务逻辑上:将所有的轮子进行拼装。
将程序分成有层次的模块,制作过程中一部分函数要有返回值,执行结果作为另一些模块的参数、条件。
现在做一个程序,输出2~100的所有质数,所谓的质数,就是只有1和原数本身两个约数,没有其他约数。
把一个复杂的问题,拆分成一个个小问题,每个都是一个单独的函数:
逻辑思维:约数个数函数 → 判断质数函数 → 高层业务
编程需要逆向思维编程:制作约数个数函数 → 制作判断质数函数 → 高层业务
函数思维找质数:
//约数个数函数:能够传入一个数字,返回它的约数个数 function yueshugeshu(a){ //计算这个数字的约数个数 var count = 0; //累加约数个数 for(var i = 1;i <= a;i++){ if(a % i == 0){ //判断是否为约数 count++; } } return count; } //判断是否是质数,如果一个函数名字取名为is,就暗示了将返回布尔值 //要么返回true,要么返回false。 //接收一个参数m,返回是否为质数(true或false) function isZhishu(m){ if(yueshugeshu(m) == 2){ return true; }else{ return false; } } //寻找1~100的质数 for(var i = 1;i <= 100; i++){ if(isZhishu(i)){ //可以省略==true的判断 //isZhishu()给我们返回了true和false console.log(i); } }
利用函数验证哥德巴赫猜想:用户输入偶数拆分两个质数和:
哥德巴赫猜想:任何一个偶数,都可以拆分为两个质数的和。
现在要求,用户输入一个偶数,你把所有的质数拆分可能,写出来。
比如:
4 = 2 + 2
6 = 3 + 3
8 = 3 + 5
48 = 5 + 43
代码见案例:
约数个数函数,里面的细节不需要关心,它足够的鲁棒,就能返回约数个数。
上层的函数,可以使用下层的API:
//约数个数函数:能够传入一个数字,返回它的约数个数 function yueshugeshu(a){ //计算这个数字的约数个数 var count = 0; //累加约数个数 for(var i = 1;i <= a;i++){ if(a % i == 0){ //判断是否为约数 count++; } } return count; } //判断是否是质数,如果一个函数名字取名为is,就暗示了将返回布尔值 //要么返回true,要么返回false。 //接收一个参数m,返回是否为质数(true或false) function isZhishu(m){ if(yueshugeshu(m) == 2){ return true; }else{ return false; } } //哥德巴赫猜想,用户输入一个数字 //验证偶数是否能被拆分两个质数的和,拆分的思想就是穷举法 //比如用户输入48,那么就: //看看1、47是不是都质数 //看看2、46是不是都质数 //看看3、45是不是都质数 //... var even = parseInt(prompt("请输入一个偶数")); for(var i = 4;i < even;i++){ if(isZhishu(i) && isZhishu(even - i)){ console.log(even + "可以拆分为" + i + "与" + (even - i) + "的和"); } }
利用函数验证哥德巴赫猜想-一百万以内的偶数拆分:优化
function yueshugeshu(a){ ... } function isZhishu(m){ ... } //注意验证,验证偶数能否被拆成两个质数 waiceng:for(var i = 4 ; i <= 1000000 ; i+=2){ for(var j = 2 ; j < i ; j++){ if(isZhishu(j) && isZhishu(i - j)){ console.log(i +"可以拆分为"+ j +"与"+ (i - j) + "的和"); continue waiceng; } } }
1.5函数递归
函数可以自己调用自己,就是递归。
function haha(){ console.log("哈哈"); haha();//调用自己 } haha(); function sum(a){ if(a == 1){ return 1; }else{ return a + sum(a-1); //10 + 9 + 8 + sum(7) } } console.log(sum(10));
斐波那契数列就是经典的递归算法:
1 1、1、2、3、5、8、13、21、34、55、89、144、233... |
输出斐波那契数列
只需要一个函数,就可以搞定全部问题。
fib(n); 就能得到第n位的数字
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
...
fib(10) = 55
function fib(n){ if(n == 1 || n == 2){ return 1; }else{ return fib(n - 1) + fib(n - 2); } } // console.log(fib(10)); for(var i = 1;i <= 50;i++){ console.log(fib(i)); }
1.6函数表达式
定义函数除了使用function之外,还有一种方法,就是函数表达式。就是函数没有名字,称为“匿名函数”,为了今后能够调用它,我们把这个匿名函数,直接赋值给一个变量。
var haha = function(){ console.log("哈哈"); } // console.log(haha); haha(); //以后要调用这个函数,就可以直接使用haha变量调用。
等价于:
function haha(){ console.log("哈哈"); } haha();
如果这个函数表达式的function不是匿名,而是有名字的:
var haha = function xixi(){ console.log("哈哈"); } xixi(); //这是错误的 haha(); //这的对的
那么JS表现非常奇怪,在外部只能用haha()调用,xixi()会引发错误。
也就是说,JS这个奇怪的特性,给我们提了个醒,定义函数,只能用以下两种方法,不能杂糅:
function haha(){} |
var haha = function(){} |
错误的:
var haha = function xixi(){} |
1.7函数声明的提升(预解析)
//先调用,可以输出,因为有函数声明提升的特性 fun(); fun(); fun(); //后定义 function fun(){ console.log("我是函数!"); }
不会报错。
JS在执行前,会有一个预解析的过程,把所有的函数声明和变量的声明,都提升到了最开头,然后再执行第一行代码。所以function定义在哪里,都不重要,程序总能找到这个函数。
函数声明头可以提升,JS程序执行前,都会有一个函数预解释阶段,预解释阶段是自动进行的
函数优先:函数声明和变量声明都会被提升,但是面试常考的一个细节是:函数会被首先提升,然后才是变量。
函数提升是没节操的,无视if等语句的判断,强制提升
在JavaScript世界中,函数是一等公民。
函数声明会被提升,但是函数表达式却不会被提升:
fun(); var fun = function(){ //因为它是函数表达式,而不是function定义法 alert("我是函数!"); }
又给我们提了个醒,没有特殊的理由,都要用function haha(){}来定义函数。
函数优先:
aaa(); //现在这个aa到底是函数,还是变量5? console.log(aaa);//函数优先,遇见同名的标识符,预解析阶段一定把这个标识符给函数 var aaa = 5; //定义一个变量,是5 function aaa(){ alert("我是aaa函数!") }
面试题:
函数优先,现在foo这个标识符冲突了,一个函数叫foo,一个变量也叫foo。预解析阶段,如果遇见标识符冲突,这个标识符给函数。
1.8函数是一个引用类型
基本类型:Number、String、Boolean、undefined、null
引用类型:Object、function、array、RegExp、Math、Date。
function fun(){} var haha = function (){} console.log(typeof fun); //引用类型中的function类型 console.log(typeof haha);//引用类型中的function类型
函数也是一种类型,这个类型叫function,是引用类型的其中一种。
基本类型:保存值
引用类型:保存地址
现在变量a = 1,那么这个a变量里面存储1这个数字
//基本类型的赋值 var a = 1; var b = a; //b得到的值是a的副本,a把自己复制了一份,给了b b = 3; //改变了b的值,a不受影响 console.log(a); //1 console.log(b); //3
//引用类型的赋值: //定义了一变量a,引用了一个function //这个a变量存储的是这个匿名函数的内存地址 var a = function(){ alert("我是一根函数"); } var b = a; //就是把匿名函数的地址也给了b。 b.xixi = 1; //给b添加一个属性 console.log(a.xixi); //输出a的xixi属性,a也有这个属性了 //b的xixi属性和a的变量都改变了,因为都是指向同一个对象(同一个内存地址) b.xixi++; b.xixi++; b.xixi++; console.log(a.xixi); console.log(b.xixi);
总结:
预解释:在js中,代码从上到下执行之前,(浏览器默认)首先会把所有带var和function关键字的进行提前声明或者定义
var num=88;
声明(declare):相当于种树时"挖坑" var num; 只声明没有定义时,num的默认值是undefined
定义(defined):相当于种树时"栽树" num=88;(给变量赋值)
在预解释的时候,带var和带function的还不一样:
var:只是提前的声明(定义赋值的部分是在代码执行的时候完成的)
function:提前的声明+定义
在浏览器加载HTML页面时,首先会开辟一个供js代码执行的环境-->"全局作用域"(window/global)
栈内存(作用域):存储基本数据类型的值;提供js代码执行的环境;
堆内存:在js中,对于引用数据类型来说,首先会开辟一个新的内存空间,然后把代码存储到这个空间中,最后把空间的地址给相关的变量--->我们把新开辟的这个内存空间称为"堆内存"。
堆内存的作用:存储引用数据类型值
二、作用域
2.1函数能封闭住作业域
变量的作用域无非就两种:全局变量和局部变量。
2.1.1全局变量(全局作用域)
全局变量:在最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的。
言外之意:如果变量没有定义在任何的function中,那么它将在程序中任意范围内都有定义:
var a = 100; //定义在全局的变量,在程序任何一个角落都有定义 function fn(){ console.log("我是函数里面的语句,我认识全局变量a值为:" + a); } fn(); console.log("我是函数外面的语句,我认识全局变量a值为:" + a);
2.1.2局部变量(局部作用域)
局部变量:和全局作用域相反,局部作用域一般只在固定的代码片段内可访问,而对于函数外部是无法访问的。
例如:变量定义在function里面,这个变量就是局部变量,只在当前这个function函数内部能使用。在函数外部不能使用这个变量,出了这个function,就如同没有定义过一样。
function fn(){ var a = 100; //定义在函数的变量,局部变量 console.log("我是函数里面的语句,我认识变量a值为:" + a); } fn(); console.log("我是函数外面的语句,我认识变量a值为:" + a);
a被var在了function里面,所以现在这个a变量只能在红框范围内有定义:
在ES5语法中,JavaScript变量作用域非常简单,能关住作用域的只有一个,就是:函数。
【总结】:
● 定义在function里面的变量,叫做局部变量,只在function里面有定义,出了function没有定义的。
● 定义在全局范围内的,没写在任何function里面的,叫做全局变量,都认识。
【原理】:
全局变量在定义时,就会直接生成一个新的变量,在任何位置查找变量都有定义。
局部变量定义在函数内部,函数如果不执行,相当于内部的代码没写,局部变量等于从未定义过,在函数执行时,会在函数作用域内部立即定义了一个变量,使用完之后,变量立即被销毁。所以在外部永远找不到局部变量定义。
2.2作用域链
作用域链:根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
当遇见变量时,JS引擎会从其所在的作用域依次向外层查找,查找会在找到第一个匹配的标识符时停止。
在私有作用域中出现了变量,首先看是否为私有的,如果是私有变量,那么就用私有的即可。如果不是私有变量,则往当前作用域的上级作用域查找,如果上级作用域也没有,则继续往上查找....一直找到window为止。
//变量的作用域,就是它var的时候最内层的function function outer(){ var a = 1; //a的作用域是outer inner(); function inner(){ var b = 2; //b的作用域是inner console.log(a); //能输出1,a在本层没有定义,就往上找 console.log(b); //能输出2 } } outer(); console.log(a); //报错,因为a的作用域是outer
多层嵌套:如果有同名的变量,那么就会发生“遮蔽效应”:
var a = 10; //全局变量 function fn(){ console.log(a); //undefined,提升声明了局部变量 var a = 13; //把外层的a变量遮蔽了,这函数内部看不见外层的a console.log(a); //输出13,变量在当前作用域寻找,找到a定义为13 } fn(); fn(); fn(); console.log(a); //10,变量在当前作用域寻找,找到全局a
一个变量在使用的时候得几?就会在当前作用域去寻找它的定义,找不到,去上一层找,直到找到全局(window),如果全局也没有,就报错。这就是作用域链。
题目:
var a = 1; //全局变量 var b = 2; //全局变量 function outer(){ var a = 3; //遮蔽了外层的a,a局部变量 function inner(){ var b = 4; //遮蔽了外层的b,b局部变量 console.log(a); //① 输出3,a现在在当前层找不到定义的,所以就上一层寻找 console.log(b); //② 输出4 } inner(); //调用函数 console.log(a); //③ 输出3 console.log(b); //④ 输出2 b现在在当前层找不到定义的,所以就上一层寻找 } outer(); //执行函数,控制权交给了outer console.log(a); // ⑤ 输出1 console.log(b); // ⑥ 输出2
2.3不写var就自动成为全局变量了
需要注意,函数内部声明的时候,一定要用var命令,如果不用,实际上声明了一个全局变量。
function fn(){ a = 100;//这个a第一次赋值时,没有var,所以就自动在全局作用域var了一次 } fn(); console.log(a);//100 |
这是JS的机理,如果遇见一个标识符,从来没有var过,并赋值了:
a = 100; |
那么就会自动在全局作用域定义var a;
2.4函数的形参变量,会默认定义为这个函数的局部变量
var a = 0; var b = 0; function fn(a,b){ a = 3; b = 4; console.log(a,b); } fn(); console.log(a); console.log(b);
a,b就是fn内部的局部变量,只能在当前function函数内部使用,出了fn就没有定义。
2.5全局变量的作用
在函数内部使用自己的变量,尽量定义为局部。
全局变量有自己独特的用途:累加、传递。
累加:函数没执行一次,都要求变量在原来基础上发生变化。
功能1:通信,共同操作传递同一个变量
两个函数同时操作一个变量,一个增加,一个减少,函数和函数通信。
var num = 0; //全局变量 function add(){ num++; } function remove(){ num--; } add(); add(); add(); add(); remove(); remove(); add(); add(); console.log(num); //4
功能2:累加,重复调用函数的时候,不会重置
var num = 0; function baoshu(){ num++; console.log(num); } baoshu();//1 baoshu();//2 baoshu();//3
如果num定义在baoshu里面,每次执行完函数,作用域就被销毁,所以里面变量都是全新的。
function baoshu(){ var num = 0; num++; console.log(num); } baoshu(); //1 baoshu(); //1 baoshu(); //1
2.6函数定义也有作用域
function outer(){ var a = 10; function inner(){ //局部函数 console.log("哈哈"); } } outer(); inner(); //报错,因为全局作用域下,没有inner函数的定义 console.log(a);//报错
公式:
function 大(){ function 小(){ } 小(); 可以运行 } 小(); //不能运行,因为小函数定义在大函数里面,离开大函数就没有作用域。
三、闭包
闭包有两个作用:
1、可以读取自身函数外部的变量(沿着作用域链寻找)
2、可以让这些外部变量始终保存在内存中
3.1闭包
推导过程:之前已经学习过,inner这个函数不能在outer外面调用,因为outer外面没有inner定义。
当函数执行的时候,会形成一个新的私有作用域,来保护里面的私有变量不受外界干扰,我们把函数的这种保护机制--->"闭包"。
function fn(){ } fn(); function outer(){ var a = 100; function inner(){ console.log(a); } } outer(); inner(); //在全局作用域调用inner,全局没有inner的定义,所以报错
但是我们就想在全局作用域下,运行outer内部的inner,此时我们必须想一些奇奇怪怪的方法。
有一个简单可行的方法,就是让outer自己return掉inner。
非常经典的闭包案例,任何培训机构、书、讲闭包,一定是下面的案例:
function outer(){ var a = 888; function inner(){ console.log(a); //888 } return inner; //outer返回了inner的引用 } var inn = outer();//inn就是inner函数了 inn(); //执行inn,全局作用域下没有a的定义,但是函数闭包,能够把定义函数时的作用域一起记忆住,输出888
一个函数可以把自己内部的语句,和自己声明时,所处的作用域一起封装成了一个密闭的环境,就叫“闭包”。
每个函数都是闭包,每个函数天生都能够记忆自己定义时所处的作用域环境。但是,我们必须将这个函数,挪到别的作用域,才能更好的观察闭包。这样才能实验它有没有把作用域给“记住”。
我们发现,把一个函数从它定义的那个作用域,挪走,运行。嘿,这个函数居然能够记忆住定义时的那个作用域。不管函数走到哪里,定义时的作用域就带到了哪里。这就是闭包。
闭包在工作中是一个用来防止产生隐患的事情,而不是加以利用的性质。
因为我们总喜欢在函数定义的环境中运行函数。从来不会把函数往外挪。那为啥学习闭包,防止一些隐患,面试绝对考。
使用全局变量接收,返回函数:
var inn; //全局变量 function outer(){ var a = 250; var b = 500; //全局变量inn此时被赋值了一个函数 //这个函数此时将立即生成闭包,记忆住此时所处的环境 inn = function inner(){ console.log(a); console.log(b); } } outer(); var a = 300; var b = 400; inn();//一个函数在执行时,找闭包里面的变量,不会理会当前作用域
闭包题目1:
function outer(x){ function inner(y){ console.log(x+y) } return inner; } var inn = outer(3);//接收到了inner函数,inn就是inner inn(5); //8 inn(7); //10 inn(10); //13
闭包题目2:
function fun1(x,y){ function fun2(x){ console.log(x + y); } return fun2; } var f = fun1(3,4); //f就代表fun2 f(); //NaN f(6); //10
一般情况下:当函数执行会形成一个私有的作用域(形参赋值→预解析→代码执行),当这三步都进行完成后,浏览器会刚刚开辟的这个私有作用域进行回收,也就是说,函数执行完成,作用域立即销毁。
3.2闭包的性质
每次重新接收引用的函数时,闭包都是全新。
function outer(){ var count = 0; function inner(){ count++; console.log(count); } return inner; } var inn1 = outer(); var inn2 = outer(); //两个变量引用的是同一个inner函数,实际上两个引用变量中,都是全新闭包 inn1(); //1 inn1(); //2 inn1(); //3 inn1(); //4 inn2(); //1 inn2(); //2 inn1(); //5 inn1(); //6 inn1(); //7
无论它在何处被调用,它总是能访问定义时所处作用域中的全局变量。
每个新的函数,不管通过何种结构生成,闭包都是新的,作用域也是新的,语句也是新的。通过同一个结构组成两个不同的函数,直接不会互相影响。
实际应用:要考虑闭包对程序造成影响,了解原因,平时不会写个特殊结构。
3.3作用域销毁的问题
在函数中,return后面返回的值如果是一个函数,这个函数是不参与预解释的;函数体中return后面的代码也不执行,但是需要把后面的代码参预解释;
销毁的作用域:一般情况下,函数执行完成后,当前的作用域都立即销毁;
不销毁的作用域:
当函数执行时,在私有作用域中返回了一个引用数据类型的值(例如:函数、对象、数组...等)
并且在函数外面,有变量接收了这个返回值,此时当前的这个私有作用域就被占用了,这个作用域也不能销毁了;
作用域不销毁,里面的私有变量也不再销毁了。
不立即销毁的作用域:
当函数执行时,在私有作用域中返回了一个引用数据类型的值(例如:函数、对象、数组...等)
但是并没有变量在函数的外面接收,那么浏览器暂时先不销毁,等到浏览器空闲的时候,会自己销毁这个作用域。
文章都是本人学习时的笔记整理,希望看完后能对您有所帮助,欢迎大家提意见,多多交流。
也有些文章是转载的,如果存在转载文章且没有标注转载地址的,请与我联系,马上处理。
自由转载-非商用-非衍生-保持署名。