
log函数
-电工公式
2023年2月15日发(作者:三周年祭文)c++log函数_普通函数
普通函数
使函数不同于其他普通对象的绝对性特点是函数存在⼀个[[Call]]内部属性。
[[Call]]属性是函数独有的,表明该对象可以被执⾏。由于仅函数拥有该属性,ECMAScript定义typeof操作符对任何具有[[Call]]属性的对象
返回“function”。
functionadd(a,b){
returna+b
}
(typeofadd);//function
这也让很多⼈误以为函数与对象并列为数据类型的原因。但是函数是⾪属于对象的。
定义函数
函数声明
⼀个函数定义函数定义(也称为函数声明函数声明,或函数语句函数语句)由⼀系列的function关键字组成,依次为:
函数的名称。eg:add
函数参数列表,包围在括号中并由逗号分隔。eg:(a,b)
定义函数的JavaScript语句,⽤⼤括号{}括起来。eg:{returna+b}
例如,以下的代码定义了⼀个简单的add函数:
functionadd(a,b){
returna+b
}
(typeofadd);//function
函数add有两个参数分别是a和b,这个函数只有⼀个语句,它表明该函数将函数的参数(即a和b)相加后返回。函数的return语句确定了函
数的返回值:
returna+b;
函数表达式
虽然上⾯的函数声明在语法上是⼀个语句,但函数也可以由函数表达式创建。这样的函数可以是匿名匿名的,它不必有⼀个名称,我们称这样的
函数为匿名函数匿名函数。例如,我们在普通对象⾥创建的函数对象。
letfnObj=function(){}
我们把上⾯通过函数声明的add函数改成函数表达式
letadd=function(a,b){
returna+b
}
然⽽,函数表达式也可以提供函数名,并且可以⽤于在函数内部代指其本⾝,或者在调试器堆栈跟踪中识别该函数:
varfactorial=functionfac(n){
returnn<2?1:n*fac(n-1)
};
(factorial(3));//6
当将函数作为参数传递给另⼀个函数时,函数表达式很⽅便。
⽐如我们的sort排序函数就接受⼀个函数作为参数。我们就可以通过函数表达式定义函数,把它作为参数传递进去。
letarr=[3,2,1]
(arr);//[3,2,1]
letasceFn=function(a,b){
returna-b
}
arr=(asceFn)
//等价于下⾯
//arr=(function(a,b){returna-b})
(arr);//[1,2,3]
在JavaScript中,可以根据条件来定义⼀个函数。⽐如下⾯的代码,当age>=18的时候才会定义palyFn:
letage=18
letplayFn
if(age>=18){
playFn=function(){
('可以播放');
}
}
playFn()//可以播放
letage=11
letplayFn
if(age>=18){
playFn=function(){
('可以播放');
}
}
(playFn);//undefined
playFn()//报错playFnisnotafunction
为什么要提上⾯这个呢?因为相对与函数表达式,作为函数声明存在函数提升的机制。
函数提升
函数声明和var创建变量⼀样,会被提升⾄上下⽂(要么是该函数被声明时所在的函数的范围(即函数域函数域),要么是全局范围)的顶部。但值
得值得的是:
变量提升只是声明变量却未定义
函数提升时该函数已被声明且定义
(obj);//undefined
(fn);//[Function:fn]
varobj=1
functionfn(){
('Hi');
}
因为这样我们就可以在函数定义前调⽤函数,关于调⽤函数将在下⾯讲解。
fn()//Hi
functionfn(){
('Hi');
}
那就会引⼊⼀个新的问题,怎么判断该函数是函数声明还是函数表达式呢?
区分函数声明和表达式最简单的⽅法是:看function关键字前⾯有⽆其他词,如果有就是函数表达式,没有的话就是函数声明
(fn);//undefined
fn()//报错
varfn=functionfn(){
('Hi');
}
(fn);//报错fnisnotdefined
(functionfn(){
('Hi');
})();
上⾯这⾥是⼀个⽴即执⾏函数,我们将在函数作⽤域⾥再次讨论
箭头函数
在ES6后,我们⼜多了⼀种定义函数的⽅式,即箭头函数。需要注意的是,箭头函数有⼏种不同的写法。以下代码中的箭头函数和前⾯所见的
匿名函数⾮常相似。不同之处在于缺少了function关键字,并且参数列表的右侧多了⼀个箭头=>。
letadd=(a,b)=>{
returna+b
}
(add(1,2));//3
(typeofadd);//function
简写
只有⼀个参数的时候
varsayHi=name=>{
(`I'm${name}`);
}
sayHi('wcdaren')//I'mwcdaren
//等效于
varsayHi=(name)=>{
(`I'm${name}`);
}
sayHi('wcdaren')//I'mwcdaren
函数只有return语句
varadd=(a,b)=>a+b
(add(1,2));//3
//等效于
varadd=(a,b)=>{
returna+b
}
(add(1,2));//3
区别
虽然箭头函数看起来和常规的匿名函数很相似,但它们本质上完全不同:
箭头函数不能显式地命名,尽管现代运⾏环境会将箭头函数所赋予的变量名作为函数名;
箭头函数不能⽤作构造函数,也没有prototype属性,这意味着不能对它们使⽤new关键字;
箭头函数没有arguments对象,但是可以访问包含它的函数的arguments对象
此外,箭头函数会绑定到所在词法作⽤域中,因此它们不会改变this的指向。
这些区别我们会在后⾯慢慢介绍
调⽤函数
定义⼀个函数并不会⾃动的执⾏它。定义了函数仅仅是赋予函数以名称并明确函数被调⽤时该做些什么。调⽤调⽤函数才会以给定的参数真正执
⾏这些动作。例如,⼀旦你定义了函数add,你可以如下这样调⽤它:
functionadd(a,b){
returna+b
}
letadd1=function(a,b){
returna+b
}
letadd2=(a,b)=>{returna+b}
//如果函数体只有return我们可以⽤下⾯的简写
letadd3=(a,b)=>a+b
(add(1,2));//3
(add1(1,2));//3
(add2(1,2));//3
(add3(1,2));//3
上述语句通过提供参数1和2来调⽤函数。函数执⾏完它的语句会返回值3。
参数
JavaScript函数的另⼀个独特之处在于你可以给函数传递任意数量的参数却不造成错误。
letfn=function(a,b,c){
returna
}
(fn(1,2,3,4,5));//1
那是因为函数参数实际上被保存在⼀个被称为arguments的类似数组的对象中。如同⼀个普通的JavaScript数组,arguments可以⾃由增长
来包含任意个数的值,这些值可通过数字索引来引⽤。arguments的length属性会告诉你⽬前有多少个值。
arguments对象⾃动存在于函数中。也就是说,函数的命名参数不过是为了⽅便,并不真的限制了该函数可接受参数的个数。
使⽤arguments对象
在介绍arguments前,我们要知道⼀点箭头函数没有arguments对象。
在函数内,你可以按如下⽅式找出传⼊的参数:
arguments[i]
其中i是参数的序数编号(即,数组索引),以0开始。所以第⼀个传来的参数会是arguments[0]。参数的数量由表⽰。
使⽤arguments对象,你可以处理⽐声明的更多的参数来调⽤函数。这在你事先不知道会需要将多少参数传递给函数时⼗分有⽤。你可以
⽤来获得实际传递给函数的参数的数量,然后⽤arguments对象来取得每个参数。
例如,设想有⼀个⽤来连接字符串的函数。唯⼀事先确定的参数是在连接后的字符串中⽤来分隔各个连接部分的字符。该函数定义如下:
functionjoin(separator){
letret=''
//注意这⾥我们是从1开始,因为arguments[0]是separator
for(leti=1;i<;i++){
ret+=arguments[i]+separator
}
returnret
}
(join(',','first','second','third'));//first,second,third,
我们发现最后的third后⾯还跟着⼀个,如果我们不要这个逗号还需要特殊处理,那能不能有更优雅的⽅式呢?那就是数组的join
(['first','second','third'].join(','));
//first,second,third
但是arguments变量只是类数组对象类数组对象,并不是⼀个数组。称其为类数组对象是说它有⼀个索引编号和length属性。尽管如此,它并不拥有
全部的Array对象的操作⽅法。
y(arguments)永远返回false。
functionfn(a,b,c){
(arguments);//[Arguments]{'0':1,'1':2}
(typeofarguments);//object
(y(arguments));//false
}
fn(1,2)
更多信息请阅读arguments
将类数组转换为数组
Array#
那既然不是数组,我们就把它变成数组,通过Array#ray#⽅法可以把arguments对象转换为真正的数组,那么我们可以调⽤数组
中的join⽅法
关于call我们会在构造函数再讲到
functionjoin(){
varlist=(arguments)
(',')
}
(join('first','second','third'));
//'first,second,third'
扩展运算符
扩展运算符可以将可遍历对象转换为数组,能够在数组或函数调⽤中轻松展开表达式。以下⽰例使⽤...arguments将函数参数转换为⼀个数
组字⾯量。
functionjoin(){
([...arguments]);
//['first','second','third']
(y([...arguments]));
//true
return[...arguments].join(',')
}
(join('first','second','third'));
//first,second,third
剩余参数
在上⾯类数组的问题⾥,通过Array#ray#或者扩展运算符扩展运算符只是表⾯解决遍历的问题,即把类数组转换为数组。此外,我们还知道箭
头函数没有arguments对象,那对于箭头函数如何实现上述的功能呢?
在ES6中添加了剩余参数语法允许将不确定数量的参数表⽰为数组,且可在箭头函数中使⽤。
在函数的最后⼀个参数前添加...可以将该参数转变为⼀个特殊的“剩余参数”。当剩余参数是函数中的唯⼀参数时,它会获取所有传⼊函数
的参数。这与上述使⽤.slice处理的结果相同,但不需要依赖于arguments,直接在参数列表中指定即可。
functionjoin(...list){
(',')
}
(join('first','second','third'));
//first,second,third
letjoin=(...list)=>(',')
(join('first','second','third'));
//first,second,third
这⾥尽管参数…list只有⼀个参数,但是因为添加了剩余参数是不可以简写去掉()的
剩余参数前⾯的参数不会包含在list参数中
functionjoin(separator,...list){
(separator)
}
(join(',','first','second','third'));
//first,second,third
默认参数
在JavaScript中,函数参数的默认值是undefined。然⽽,在某些情况下设置不同的默认值是有⽤的。这时默认参数可以提供帮助。
在过去,⽤于设定默认的⼀般策略是在函数的主体测试参数值是否为undefined,如果是则赋予⼀个值。如果在下⾯的例⼦中,调⽤函数时
没有实参传递给b,那么它的值就是undefined,于是计算a*b得到、函数返回的是NaN:
functionmultiply(a,b){
b=(typeofb!=='undefined')?b:1;
returna*b;
}
multiply(5);//5
使⽤默认参数,在函数体的检查就不再需要了。现在,你可以在函数头简单地把1设定为b的默认值:
functionmultiply(a,b=1){
returna*b;
}
multiply(5);//5
箭头函数的参数也可以指定默认值。当为箭头函数的参数指定默认值时,哪怕只有⼀个参数,也需要⽤圆括号将参数列表包裹起来。
vardouble=(input=0)=>input*2
与某些编程语⾔不同,我们可以为任何⼀个参数设置默认值,⽽不是只能给最后⼀个参数设置。
functionsumOf(a=1,b=2,c=3){
returna+b+c
}
(sumOf(undefined,undefined,4))
//<-1+2+4=7
在JavaScript中,向函数传递包含多个属性的options对象参数是再常见不过的。如果调⽤函数时没有传⼊,可以为其设定⼀个默认值对象
options,如下所⽰。
letdefaultOptions={name:'⽆名⽒',age:0}
functionperson(per=defaultOptions){
();
();
}
person()//⽆名⽒0
该⽅法存在⼀个问题,如果person的使⽤者传⼊⼀个options对象,则所有的默认值都会失效。
letdefaultOptions={name:'⽆名⽒',age:0}
functionperson(per=defaultOptions){
();
();
}
person({age:22})//undefined22
了解更多默认参数的信息。
解构
与只提供⼀个默认值相⽐,更好的⽅法是解构整个options,并在解构模式中为每个属性指定默认值。通过使⽤这个⽅法,我们不通过options
对象就能引⽤options中的每个选项,但也因此不再能够直接引⽤options,这在某些情况下可能会产⽣问题。
functionperson({name='⽆名⽒',age=0}){
(name);
(age);
}
person({age:22})//⽆名⽒22
在这种情况下,如果使⽤者没有传⼊options对象,则默认值会再次缺失。也就是说,如果没有传⼊options对象参数,person就会报错。为
options添加⼀个空对象作为默认值即可避免该问题,如下所⽰。这是因为解构空对象时已经提供了默认值。
functionperson({name='⽆名⽒',age=0}){
(name);
(age);
}
person()//报错TypeError:Cannotdestructureproperty`name`of'undefined'or'null'.
functionperson({name='⽆名⽒',age=0}={}){
(name);
(age);
}
person()//⽆名⽒0
除了默认值,我们还可以在函数参数中使⽤解构来描述函数能够处理的对象结构。思考以下代码,假设有⼀个包含多个属性的person对象。
person对象描述了其姓名、年龄、配偶、⽣⽇以及学历。
letperson={
name:{
lastName:'gao',
firstName:'wcdaren',
},
age:22,
partner:'gugu',
date:'1996-07-15',
qualification:'本科'
}
如果只想在某个函数中提取对象的某些属性作为参数,可以通过解构提前显式地引⽤这些属性。这样做的好处是,看到函数声明时,就能知道函
数需要使⽤哪些属性。提前解构所需要的每个属性时,当输⼊不正确时,就很容易被发现。以下⽰例展⽰了如何在参数列表中指定需要的所有
属性,这样我们对getPersonNameAndAgeAPI能够处理的参数就⼀⽬了然了。
letperson={
name:{
lastName:'gao',
firstName:'wcdaren',
},
age:22,
partner:'gugu',
date:'1996-07-15',
qualification:'本科'
}
functiongetPersonNameAndAge({name,age}){
(ame);
(age);
}
getPersonNameAndAge(person)//wcdaren22
参数传递
我们再回到函数的调⽤,在函数的调⽤中,如果有参数传递,就有⼀个将实参(actualparameter)映射到形参(formalparameter)的过
程。如何把实参映射到⾏参,就有不同的机制,常见的有以下两种
按值传递(pass-by-value或call-by-value)
函数收到的是实际参数的值——按位拷贝(bitwisecopy)
按引⽤传递(pass-by-reference或call-by-reference)
函数收到的是实际参数的引⽤——内存地址
此外还有拷贝-恢复(copy-restore)、按名调⽤(call-by-name)、宏展开(macroexpansion)等机制
我们写⼀段代码来思考
functionchange(obj){
obj={name:'忘尘'}
}
letobj={name:'wcdaren'}
change(obj)
(obj);
{name:'wcdaren'}是⼀个object引⽤类型,如果js是按引⽤将obj传递给change,即传递的是obj⾃⾝的地址,即CCCFFF000。那
change函数⾥的
obj={name:'忘尘'}
就会改变CCCFFF000这个地址上obj的值,即把AAAFFF111指向另⼀个对象。
但是,输出的结果是wcdaren。即,js中不是按引⽤传递的。因为函数⾥对形参的改变,并没有影响到实参。实际上JS,只是把obj的值,
即AAAFFF111拷贝了⼀份,赋给了形参。
从图中我们能更清楚的,函数传递,只是把实参的值,拷贝⼀份给形参,即形参的改变是⽆法影响实参的形参的改变是⽆法影响实参的。
但是,我们却可以改变传⼊对象的内容
functionchange(obj){
obj['age']=22
}
letobj={name:'wcdaren'}
change(obj)
(obj);//{name:'wcdaren',age:22}
从图中,可以很明显的看出,因为实参与⾏参都指向同⼀个对象,当形参对obj对象的修改,会影响到实参的输出。
这⾥分享⼀篇知乎关于两者传递⽅式的讨论,虽然⾥⾯讨论的是java,但是js和java⼀样,参数传递都是只有按值传递:链接
《你不知道的javascript(中)》书中于提到的通过值复制或者通过引⽤复制,实际上都是上述所说的按值传递,只不过是把值传递
终点的值值再⼀次细分为,简单值(scalarprimitive)和复合值(compoundvalue)
函数作为参数
我们在上⾯说到了按值传递,接下来我们讨论⼀下这个值字。既然谈到值,就不可避免的要提到它的对⽴⾯,引⽤。那什么是值什么是引⽤
呢?
值:具有某种类型的数据
引⽤:可⽤来获取特定数据的值
如果把类型做以区分,就有值类型,和引⽤类型。
值类型:能直接⽅法的数据类型,如我们说到的11数字,'wcdaren'字符串
引⽤类型:借助引⽤才能被访问的数组类型,如,对象。
我们常说的obj是⼀个对象,实际是说obj它指向了⼀个对象。
因为在js中,变量是没有类型的obj此时可以存储⼀个引⽤类型,即指向⼀个对象,我们也可以给他重新赋值,存储⼀个值类型。
functionchange(obj){
obj['age']=22
}
letobj={name:'wcdaren'}
change(obj)
(typeofobj);//object
obj=22
(typeofobj);//number
既然变量是⽆类型的,那对于变量来说,存储什么数据都是⼀样的,所以对于变量⾃⾝⽽⾔,它存储的是⼀个值类型还是⼀个引⽤类型,它
是不知道的,它只是知道,它存储了⼀个值(此值包括值类型和引⽤类型)。
那按变量的视⾓来看,“所有的东西都是值”。
既然函数是⼀个对象,那函数的存储也应该如上图所⽰,既然我们可以按值传递把对象传递进去,那函数也可以作为参数传递进去。
⾼阶函数
在函数式语⾔中(JS中),我们把以函数作为参数的函数或者函数最终返回(return)⼀个函数的函数称为⾼阶函数。
最常见的莫过于排序函数
letarr=[2,1,11,5,19]
(arr);
//[2,1,11,5,19]
((a,b)=>a-b)
(arr);
//[1,2,5,11,19]
关于⾼阶函数,我们还会单独开⼀篇来讲
形参赋值与函数提升
我们在前⾯说到了js是按值传递的,这个传递实际上就是在函数内部创建⼀个变量来接收传递进来的值。
既然形参是作为⼀个变量存在,那么let就不能重复定义了。
varname='忘尘'
functionsayHi(name){
letname
(`I'm${name}`);
}
sayHi(name)//报错:Identifier'name'hasalreadybeendeclared
变量提升中,只是提前定义变量名,并为定义,如果变量名存在,就不重复定义。
那此时如果函数作为参数,且我们函数⾥⼜存在函数声明,就会涉及到是形参赋值先还是函数提升先的问题。
varfn1=function(){
('fn1');
}
functiontest(fn1){
fn1()//fn1
fn1=function(){
('fn2');
}
fn1()//fn2
}
test(fn1)
从上⾯代码中,可以看出是形参赋值先于函数提升形参赋值先于函数提升。
表达式参数
vari=100
(i+=20,i*=2,'value:'+i);
//120240'value:240'
(i);
//240
js允许表达式作为参数,那既然我们说参数是按值传递的,那么如果参数是⼀个表达式的话,为了保持按值传递,我们必须对表达式进⾏运
算⽅可得到值,所以最后的输⼊结果就如上所⽰。
我们⼜称这为⾮惰性求值。对于函数来说,如果⼀个参数是需要⽤到时,才会完成求值(或取值),那么他就是“惰性求值”,反之
则是“⾮惰性求值”。
函数作⽤域
规避冲突
我们知道在全局作⽤域下,存在着不同的标志符(变量,函数),我们在引进⼀个函数或者在⾃⼰写⼀个函数的时候,函数⾥⾯就会有新的
标志符,如果新增的标志符不与全局下的标志符区分,那将会带来获取上的冲突,⽐如,变量覆盖?重复定义?
所以这个时候,我们就需要开辟⼀个新的作⽤域,即函数作⽤域,来存放函数⾥的标志符。
varwcdaren={
age:22
}
functionsayHi(){
varwcdaren={
age:11
}
(`I'mwcdaren,${}yearsold`);
}
sayHi()//I'mwcdaren,11yearsold
在以上的例⼦中,我们当然是期望输出wcdaren的,这是符合正常的⼈期望的.
⽐如读书时,1班有个⼩明,2班也有个⼩明,此时⽼师在1班上课,在讲台上说:⼩明起来回答问题。
我们决不可能说:⽼师!你是叫1班的⼩明呢还是2班的⼩明呢?
这是因为我们在上课时,默认是把该班作为⼀个整体,那函数执⾏的时候也是默认把函数本⾝{}⾥的东西作为⼀个整体,这个整体我们就称
为函数作⽤域函数作⽤域。
但是,如果该作⽤域不存在我们所需要的标志符,我们⾃然是会往上级作⽤域查找。
⽐如,⽼师说:叫忘尘过来修下电脑。
如果在班⾥没有忘尘这个⼈,我们当然会思考着忘尘这个⼈不是班上的⼈,可能是指电脑⽼师。
varwcdaren={
job:'电脑⽼师'
}
functioncallWc(){
();
}
callWc()//电脑⽼师
这说明,函数作⽤域是可以访问上级作⽤域(这⾥指全局作⽤域)下的标志符的。
更具体的访问机制,将在作⽤域链再介绍
反过来,在全局作⽤域下是⽆法访问函数作⽤域下的标志符的。
functionschool(){
letwcdaren={
age:22
};
}
();//报错:wcdarenisnotdefined
这当然也是符合⼈们的期望的,⽐如:作为上课的⽼师,当然不希望在上课期间,⾃⼰的学⽣就被叫了出去,不晓得是做什么事去了。
⽴即执⾏函数表达式(IIFE)
从上⾯知道,函数内部的代码,形成了⼀个函数作⽤域,外部作⽤域⽆法访问函数内部的任意内容。避免了函数作⽤域中的标志符不与全局
作⽤域的标志符产⽣冲突。
回归本质,可以说成,我们⽤函数对任意代码进⾏包装,避免了标志符(变量,函数)冲突。
但我们上⾯使⽤函数,都是通过函数声明的⽅式,这种⽅式会额外产⽣两个问题:
我们在全局作⽤域下额外的添加了新的标志符,
对“隐藏”起来的代码,我们还必须显⽰地通过函数名才能执⾏⾥⾯的代码。
varwcdaren={age:22}
//添加了新的标志符sayHi
functionsayHi(){
varwcdaren={age:11}
(`I'mwcdaren,${}yearsold`);
}
//通过执⾏来获取sayHi函数内部的数据
sayHi()//I'mwcdaren,11yearsold
⽽下⾯这段代码,就完美的解决了上述的问题。
varwcdaren={age:22};
(functionsayHi(){
varwcdaren={age:11}
(`I'mwcdaren,${}yearsold`);
})()//I'mwcdaren,11yearsold
(functionfoo(){..})():由于函数声明被包含在⼀对()括号内部,因此成为了⼀个函数表达式,通过在末尾加上另外⼀个()可以⽴即执⾏这个函
数。
第⼀个()将函数声明变成函数表达式
说明不会有函数提升
第⼆个()执⾏了这个函数。
我们把(functionfoo(){...})()这种模式称为IIFE称为⽴即执⾏函数表达式(ImmediatelyInvokedFunctionExpression)⽴即执⾏函数表达式(ImmediatelyInvokedFunctionExpression)。
还可以写成(function(){...}()),两者⼀致的。
既然是函数表达式,就如我们先前所以,可以给它添加函数名或者不添加函数名,以下代码和上⾯代码的执⾏结果⼀致。
varwcdaren={age:22};
(function(){
varwcdaren={age:11}
(`I'mwcdaren,${}yearsold`);
})()//I'mwcdaren,11yearsold
虽然可以这么写,但是我们还是推荐使⽤具名函数具名函数(即含有函数名),因为匿名函数匿名函数(即没有函数名)具有以下缺点:
1.匿名函数在栈追踪中不会显⽰出有意义的函数名,使得调试很困难。
2.如果没有函数名,当函数需要引⽤⾃⾝时只能使⽤已经过期的引⽤,⽐如在递归中。另⼀个函数需要引⽤⾃⾝的例⼦,
是在事件触发后事件监听器需要解绑⾃⾝。
3.匿名函数省略了对于代码可读性/可理解性很重要的函数名。⼀个描述性的名称可以让代码不⾔⾃明。
参数
如果,我们把(function(){…})当作函数表达式,那我们就可以使⽤函数表达式(参数)来运⾏该函数并传⼊参数。
varobj={name:'wcdaren'};
(functionIIFE(pp){
varobj={name:'忘尘'}
('obj'+);
('pp'+);
})(obj)
('global'+);
//obj忘尘
//ppwcdaren
//globalwcdaren
既然能传参数,那当然也可以把函数作为参数传进去。通过这样传递,我们就可以很明显的看出执⾏顺序,先执⾏IIFE函数,然后IIFE函数
执⾏时才会把作为参数传进来的函数执⾏,完美符合从上到下的阅读顺序。
vara=1;
(functionIIFE(def){
('⼀');//⼀
vara=2
(a);//2
def(a)
})(functiondef(c){
('⼆');//⼆
(a);//?
(c);//2
})
//⼀2⼆?2
在上⾯这段代码中,我们都可以很明显的看出结果,⽽第9⾏(a);这⾥的a是哪个a就会让⼤家困惑。是使⽤全局作⽤域下的a=
1呢还是使⽤IIFE作⽤域下的a=2呢?为了解决这个问题,我们先学习下⾯的变量查找
变量查找
我们知道函数执⾏会形成⼀个新的作⽤域,即函数作⽤域。
嵌套函数
在上⾯的例⼦中,我们的上级作⽤域⼀直是全局作⽤域,接下来我们要讲的是如果函数是⼀个嵌套函数呢?
因为如果存在嵌套函数,它的上级作⽤域就不能简单的认为是全局作⽤域了。
functionA(x){
functionB(y){
functionC(z){
(x+y+z);
}
C(3);
}
B(2);
}
A(1);//6
/**
*6(1+2+3)
*/
对于B函数和C函数,上级作⽤域已经不是全局作⽤域了。
如果我们把作⽤域看做是下⾯图中⼀个平⾯,那开辟⼀个新的作⽤域是需要建⽴在原来的平⾯(作⽤域)基础上。
按编程的思维是,在函数执⾏开辟⼀个作⽤域前,我们的函数⼀定是在某⼀作⽤域下定义好了的。
所以函数形成新的作⽤域是建⽴在函数定义时所在的作⽤域。
函数作⽤域可以访问上级作⽤域(即函数定义时所在的的作⽤域)可访问的标志符。
重点:上级作⽤域,是指函数定义时所在的作⽤域,与函数在哪执⾏⽆关
这句话就涉及到⼀个嵌套问题,⽐如上图中的。
如果我说绿⾊作⽤域可以访问上级作⽤域,即橙⾊作⽤域可访问的标志符。
那对于橙⾊作⽤域,它可访问的标志符就有它本⾝内部及它的上级作⽤域,即黄⾊作⽤域可访问的
那对于黄⾊作⽤域,它可访问的标志符就有它本⾝内部及它的上级作⽤域,即全局作⽤域。
我们完全可以把上⾯这种嵌套思考成是⼀个链,⽐如->->->
即函数作⽤域可以访问该链上后续作⽤域⾥的标志符,于是我们把这条链叫做作⽤域链作⽤域链。
那在上图中就存在三条作⽤域链
->->->
->
->->
那根据定义,我们⾃然是知道紫⾊作⽤域和黄⾊作⽤域及青⾊作⽤域,三者是独⽴的,相同点只是他们都可以访问全局作⽤域。
我们再回到作⽤域的本质,为了避免标志符的冲突,我们对作⽤域下的标志符进⾏了隐藏,那对于作⽤域链来说,就会涉及到如果后续每个
作⽤域⾥都有我们需要的标志符的话,我们应该选择哪个标志符呢?
当然是⼀找到就返回呀,这样就避免了很多混乱的问题。
例⼦1
//下⾯的变量定义在全局作⽤域(globalscope)中
varnum1=20,
num2=3,
name="Chamahk";
//本函数定义在全局作⽤域
functionmultiply(){
returnnum1*num2;
}
multiply();//返回60
//嵌套函数的例⼦
functiongetScore(){
varnum1=2,
num2=3;
functionadd(){
returnname+"scored"+(num1+num2);
}
returnadd();
}
getScore();//返回"Chamahkscored5"
multiply()执⾏,multiply函数作⽤域中没有num1和num2变量,通过作⽤域链查询到全局作⽤域下的num1和num2,即20和3,
所以返回20*3即60
getScore()执⾏,声明定义好变量和函数后,返回add(),因为返回需要⼀个值,所以先执⾏add函数
add()执⾏,返回name+'scored'+(num1+num2),
因为在本⾝add函数作⽤域中没有name与num1和num2变量
通过作⽤域链查询,在getScore函数作⽤域中发现了num1和num2,即2和3,所以关于num1和num2的查询就会停⽌了
但是name在getScore函数作⽤域中仍未查询到,继续按作⽤域链向上查询,在全局作⽤域中发现name变量,即Chamahk。
此时,全部变量已经确定,最后把所得值代⼊表达式中"Chamahk"+"scored"+(2+3),计算后返回Chamahkscored5
例⼦2
这时候我们回到我们在前⾯抛出的问题
vara=1;
(functionIIFE(def){
('⼀');//⼀
vara=2
(a);//2
def(a)
})(functiondef(c){
('⼆');//⼆
(a);//?
(c);//2
})
//⼀2⼆12
这⾥有点难以理解的是函数表达式作为参数,且直接放在()⾥⾯。
因为我们知道参数是按值传递的,即()传递的值是函数表达式的值(地址),所以此时(即在全局作⽤域下),我们已经定义好了该函数
(即函数def),只不过我们并没有把值赋给⼀个变量,⽽是直接做为实参传递⽽已。这意味着,该函数定义的地⽅是在全局作⽤域下,所
以如果该后续函数被执⾏了执⾏,它的上级作⽤域是全局作⽤域。
例⼦3
varn=10;
functionfn(){
varn=20;
functionf(){
n++;
(n);
}
f();
returnf;
}
varx=fn();
x();
x();
(n);
这个就当⾃我练习,我就不写答案了。
内存管理与闭包
像C语⾔这样的底层语⾔⼀般都有底层的内存管理接⼝,⽐如malloc()和free()。另⼀⽅⾯,JavaScript具有⾃动垃圾收集机制,也就是说,
执⾏环境会负责管理代码执⾏过程中使⽤的内存。
内存⽣命周期
不管什么程序语⾔,内存⽣命周期基本是⼀致的:
1.分配你所需要的内存
2.使⽤分配到的内存(读、写)
3.不需要时将其释放归还
所有语⾔第⼆部分都是明确的。第⼀和第三部分在底层语⾔中是明确的,但在像JavaScript这些⾼级语⾔中,⼤部分都是隐含的。
JavaScript的内存分配
值的初始化
为了不让程序员费⼼分配内存,JavaScript在定义变量时就完成了内存分配。
varn=123;//给数值变量分配内存
vars="azerty";//给字符串分配内存
varo={
a:1,
b:null
};//给对象及其包含的值分配内存
//给数组及其包含的值分配内存(就像对象⼀样)
vara=[1,null,"abra"];
functionf(a){
returna+2;
}//给函数(可调⽤的对象)分配内存
//函数表达式也能分配⼀个对象
ntListener('click',function(){
oundColor='blue';
},false);
通过函数调⽤分配内存
有些函数调⽤结果是分配对象内存:
vard=newDate();//分配⼀个Date对象
vare=Element('div');//分配⼀个DOM元素
有些⽅法分配新变量或者新对象:
vars="azerty";
vars2=(0,3);//s2是⼀个新的字符串
//因为字符串是不变量,
//JavaScript可能决定不分配内存,
//只是存储了[0-3]的范围。
vara=["ouaisouais","nannan"];
vara2=["generation","nannan"];
vara3=(a2);
//新数组有四个元素,是a连接a2的结果
栈内存和堆内存
为了区分不同类型值存储的位置,我们⼜把内存分为以下两种类型:
堆内存堆内存:存储引⽤数据类型值(对象:键值对函数:代码字符串)栈内存栈内存:提供JS代码执⾏的环境和存储基本类型值
使⽤值
使⽤值的过程实际上是对分配内存进⾏读取与写⼊的操作。读取与写⼊可能是写⼊⼀个变量或者⼀个对象的属性值,甚⾄传递函数的参数。
当内存不再需要使⽤时释放
⼤多数内存管理的问题都在这个阶段。在这⾥最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发⼈员来确定在程
序中哪⼀块内存不再需要并且释放它。
⾼级语⾔解释器嵌⼊了“垃圾回收器”,它的主要⼯作是跟踪内存的分配和使⽤,以便当分配的内存不再使⽤时,⾃动释放它。这只能是⼀
个近似的过程,因为要知道是否仍然需要某块内存是⽆法判定的(⽆法通过某种算法解决)
垃圾回收
如上⽂所述⾃动寻找是否⼀些内存“不再需要”的问题是⽆法判定的。因此,垃圾回收实现只能有限制的解决⼀般问题。本节将解释必要的
概念,了解主要的垃圾回收算法和它们的局限性。
引⽤
垃圾回收算法主要依赖于引⽤的概念。在内存管理的环境中,⼀个对象如果有访问另⼀个对象的权限(隐式或者显式),叫做⼀个对象引⽤
另⼀个对象。例如,⼀个Javascript对象具有对它原型的引⽤(隐式引⽤)和对它属性的引⽤(显式引⽤)。
在这⾥,“对象”的概念不仅特指JavaScript对象,还包括函数作⽤域(或者全局词法作⽤域)。
何时销毁
因为这⾥我们只是为了引进闭包的概念,不对内存管理进⾏深⼊的探讨,我们可以简单的认为,当⼀个对象(包括作⽤域)不再被引⽤时,
就会⾃⾏销毁,释放内存。
函数作⽤域
⼀般情况下,当函数执⾏完成,所形成的私有作⽤域(栈内存)都会⾃动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销
毁的情况。
我们思考下⾯三个例⼦
functionMyObject(obj){
varfoo=function(){
('wcdaren');
}
if(!obj)return
=foo;
}
//⽰例1
MyObject()
//⽰例2
MyObject(newObject())
//⽰例3
varobj=newObject()
MyObject(obj)
⽰例1
MyObject()被调⽤,在函数内部有⼀个匿名函数的实例被创建,并被赋值给foo变量,但因为参数obj为undefined,执⾏return,所以该函数实
例没有被返回到MyObject()函数外。因此MyObject()执⾏结束后,MyObject函数作⽤域内的数据未被外部引⽤,因此MyObject函数作⽤
域销毁,foo引⽤指向的匿名函数也被销毁。
⽰例2
传⼊参数obj是⼀个有效的对象,于是匿名函数被赋值给,因此建⽴了⼀个引⽤。在MyObject()执⾏结束的时候,该匿名函数与
MyObject()都不能被销毁。但随后,由于传⼊的对象未被任何变量引⽤,因此⽴即销毁,的引⽤得以释放。这时foo指向的匿名
函数没有任何引⽤、MyObject()内部也没有其它数据被引⽤,因此开始销毁过程
⽰例3
在⽰例3中开始的过程与⽰例2⼀致,但由于obj是⼀个在MyObject()之外具有独⽴⽣存周期(不受MyObject函数作⽤域影响)的外部
变量,JavaScript引擎必须对这种持有MyObject()函数作⽤域中的foo变量(所指向的匿名函数实例)的关联关系加以持续地维护,直到该变
量被销毁,或它的指定⽅法()被重置、删除时,它对foo的引⽤才会得以释放。例如:
删除操作不适合于使⽤var声明的变量,因为var声明的变量不能被delete操作删除
//1.重新置值时,关联关系被清除
=newFunction()
//2.删除成员时,关联关系也被清除
//3.变量销毁(或重新置值)导致的关联关系清除
obj=null
从上⾯三个例⼦中,我们可以得知如果函数执⾏完成,当前形成的栈内存(作⽤域)中,某些内容被栈内存(作⽤域)以外的变量引⽤了,此
时栈内存(作⽤域)不会销毁。
在不会销毁的作⽤域⾥,还有⼀个全局栈内存只有在页⾯关闭的时候才会被释放掉,这是显⽽易见的,因为我们的代码都在该环境执
⾏,只有真正退出了,才会释放此处的内存
闭包定义
这个时候我们再来谈下闭包是什么?
⾸先,所谓包,指函数与其周围的环境变量捆绑打包;所谓闭,指这些变量是封闭的,只能为该函数所专⽤。
总的来说,闭包就是⼀种能保留当初创建时环境变量的函数闭包就是⼀种能保留当初创建时环境变量的函数。在上⾯的例⼦3中,即下⾯这段代码
functionMyObject(obj){
varfoo=function(){
('wcdaren');
}
if(!obj)return
=foo;
(obj);
}
varobj=newObject()
MyObject(obj)
MyObject函数执⾏后,在该作⽤域⾥的环境变量(这⾥特指⾥⾯创建的匿名函数),在函数执⾏完毕后并没有销毁,所以我们称该函数为
⼀个闭包。
关于闭包,我们还会单独使⽤⼀篇⽂章介绍
递归和函数堆栈
递归
⼀个函数可以指向并调⽤⾃⾝。有三种⽅法可以达到这个⽬的:
1.函数名
3.作⽤域下的⼀个指向该函数的变量名
在严格模式下,第5版ECMAScript(ES5ES5)禁⽌使⽤()。当⼀个函数必须调⽤⾃⾝的时候,避免使⽤
(),(),通过要么给函数表达式⼀个名字,要么使⽤⼀个函数声明。
例如,思考⼀下如下的函数定义:
varfoo=functionbar(){
//statementsgohere
};
在这个函数体内,以下的语句是等价的:
()
()
()
调⽤⾃⾝的函数我们称之为递归函数递归函数。在某种意义上说,递归近似于循环。两者都重复执⾏相同的代码,并且两者都需要⼀个终⽌条件(避
免⽆限循环或者⽆限递归)。例如以下的循环:
varx=0;
while(x<10){//"x<10"是循环条件
//dostuff
x++;
}
可以被转化成⼀个递归函数和对其的调⽤:
functionloop(x){
if(x>=10)//"x>=10"是退出条件(等同于"!(x<10)")
return;
//dosomething
loop(x+1);//递归调⽤
}
loop(0);
不过,有些算法并不能简单的⽤迭代来实现。例如,获取树结构中所有的节点时,使⽤递归实现要容易得多:
functionwalkTree(node){
if(node===null)//
return;
//dosomethingwithnode
for(vari=0;i<;i++){
walkTree(odes[i]);
}
}
跟loop函数相⽐,这⾥每个递归调⽤都产⽣了更多的递归。
将递归算法转换为⾮递归算法是可能的,不过逻辑上通常会更加复杂,⽽且需要使⽤堆栈。事实上,递归函数就使⽤了堆栈:函数堆栈。
这种类似堆栈的⾏为可以在下例中看到:
functionfoo(i){
if(i<0)
return;
('begin:'+i);
foo(i-1);
('end:'+i);
}
foo(3);
//输出:
//begin:3
//begin:2
//begin:1
//begin:0
//end:0
//end:1
//end:2
//end:3
预定义函数
JavaScript语⾔有好些个顶级的内建函数:
eval()
eval()eval()⽅法会对⼀串字符串形式的JavaScript代码字符求值。
uneval()
uneval()uneval()⽅法创建的⼀个Object的源代码的字符串表⽰。
isFinite()
isFinite()isFinite()函数判断传⼊的值是否是有限的数值。如果需要的话,其参数⾸先被转换为⼀个数值。
isNaN()
isNaN()isNaN()函数判断⼀个值是否是NaN。注意:isNaN函数内部的强制转换规则⼗分有趣;另⼀个可供选择的是ECMAScript6中定义
(),或者使⽤typeof来判断数值类型。
parseFloat()
parseFloat()parseFloat()函数解析字符串参数,并返回⼀个浮点数。
parseInt()
parseInt()parseInt()函数解析字符串参数,并返回指定的基数(基础数学中的数制)的整数。
decodeURI()
decodeURI()decodeURI()函数对先前经过encodeURI函数或者其他类似⽅法编码过的字符串进⾏解码。
decodeURIComponent()
decodeURIComponent()decodeURIComponent()⽅法对先前经过encodeURIComponent函数或者其他类似⽅法编码过的字符串进⾏解码。
encodeURI()
encodeURI()encodeURI()⽅法通过⽤以⼀个,两个,三个或四个转义序列表⽰字符的UTF-8编码替换统⼀资源标识符(URI)的某些字符来进⾏编码
(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)。
encodeURIComponent()
encodeURIComponent()encodeURIComponent()⽅法通过⽤以⼀个,两个,三个或四个转义序列表⽰字符的UTF-8编码替换统⼀资源标识符(URI)的每个字
符来进⾏编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)。
escape()
已废弃的escape()escape()⽅法计算⽣成⼀个新的字符串,其中的某些字符已被替换为⼗六进制转义序列。使⽤encodeURI或者
encodeURIComponent替代本⽅法。
unescape()
已废弃的unescape()unescape()⽅法计算⽣成⼀个新的字符串,其中的⼗六进制转义序列将被其表⽰的字符替换。上述的转义序列就像escape⾥介
绍的⼀样。因为unescape已经废弃,建议使⽤decodeURI()或者decodeURIComponent替代本⽅法。