本文最后更新于:8 个月前
函数定义、参数、属性、返回值,箭头函数,递归,闭包
JavaScript(五)函数 一、定义函数 函数名,就是指向函数的指针。 这个性质决定了后面很多特性,例如:不能重载、函数可以作为变量赋值,等等
1 2 3 4 5 6 7 8 function add (num ){return num+1 ;}function add (num ){return num+2 ;}add (1 ); function one ( ){return 'hello' ;}let two = one one = null two ()
1.1 函数声明 1 2 3 function sum (n1,n2 ){ return n1+n2; }
具有函数声明提升 ,函数声明会在任何代码执行前,被添加到执行上下文,所以可以在定义之前使用函数,细节可以参见《JavaScript(一)基本语法》中【执行上下文】相关的内容
1.2 函数表达式 1 2 3 let sum = function (n1,n2 ){ return n1+n2; }
不具备函数声明提升 ,如果在定义之前调用它,会报错
二、函数参数 函数参数特性: ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。函数内部有一个隐藏的arguments对象,是类数组对象,用来保存传入的参数。
2.1 arguments对象
arguments对象由传入的参数决定
可以通过arguments.length
获得对象长度,可以通过arguments[0]的方式访问其中的元素
arguments的值会始终和对应的命名参数同步(见以下代码示例)
箭头函数没有arguments对象!!
有一个callee属性,指向arguments所在函数的指针
arguments不是数组对象,不能使用数组的方法,如forEach、map等
它是类数组对象,可以用for..of遍历
1 2 3 4 5 6 7 8 9 10 11 function doAdd (num1,num2 ){ arguments [1 ]=10 ; console .log (num2); }doAdd (1 ,2 ); function doAdd2 (num1,num2 ){ num2 = 10 ; console .log (arguments [1 ]); }doAdd2 (1 ,2 );
2.2 默认参数 只要在函数定义中的参数后面用=
就可以为参数赋一个默认值。默认参数放到最后面
1 2 3 4 5 6 function add (num1,num2=10 ){return num1+num2;}add (1 ); function add (num1=10 ,num2 ){return num1+num2;}add (undefined ,1 ); add (1 );
默认参数作用域
后面的参数,可以引用先定义的参数,但是不能反过来。
1 2 3 4 5 function add (num1=10 ,num2=num1 ){return num1+num2;}add (); function add (num1=num2,num2=10 ){return num1+num2;}add ();
2.3 参数扩展与收集 ES6新增了扩展操作符...
,既可以用于调用时传参,也可以用于定义函数参数
参数扩展
1 2 3 4 5 6 7 8 9 10 values = [1 ,2 ,3 ,4 ];function getSum ( ){ let sum=0 ; for (let i=0 ;i<arguments .length ;i++){sum+=arguments [i];} return sum; }getSum (...values);getSum (1 ,...values);getSum (1 ,...values,5 );
参数收集 ,用来接受数量不固定的参数
1 2 function getSum (firstValue,...values ){...}
values前面用...
标识,以数组的形式保存,b后面的参数,如果没有,那么values为空数组。
2.4 this 在标准函数中, this引用的是调用函数的上下文对象
在箭头函数中, this永远指向的都是定义箭头函数的上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function outer ( ){ inner (); console .log (2 ,this ) }function inner ( ){ console .log (0 ,arguments .callee .caller ); console .log (1 ,this ) }outer ();0 ƒ outer ( ){ inner (); console .log (2 ,this ) }1 Window 2 Window
三、函数属性 3.1、length 保存命名参数的个数,不包括带有默认值的参数、rest参数的个数
3.2、prototype属性 保存引用类型所有实例方法的地方,诸如toString()
3.3、apply方法 语法:函数名.apply(上下文对象,参数Array)
3.4、call方法 语法:函数名.call(上下文对象,[参数1,参数2,,,,]
,其中方括号[]
表示可选的意思
以上两个方法最强大的地方在于,用来改变函数调用上下文(即函数体内this)的能力,如下
1 2 3 4 5 window .color = 'red' ;let o = {color :'blue' };function sayColor ( ){console .log (this .color );}sayColor (); sayColor.apply (o);
利用apply,可以动态改变函数的行为
1 2 3 4 5 6 var count=0 ;var oldParseInt = parseInt ;window .parese = function ( ){ count+=1 ; oldParseInt.apply (null ,arguments ); }
可以在不修改源码的基础上,增加函数的功能
3.5、bind方法 bind() 方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。返回值: 返回一个原函数的拷贝,并拥有指定的 this
值和初始参数
1 2 3 4 window .color = 'red' ;let o = {color :'blue' };function sayColor ( ){console .log (this .color );}let objectSayColor = sayColor.bind (o);
bind 的一个常用用法是设置偏函数 。使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind()
的参数写在 this
后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function list ( ) { return Array .prototype .slice .call (arguments ); }function addArguments (arg1, arg2 ) { return arg1 + arg2 }var list1 = list (1 , 2 , 3 ); var result1 = addArguments (1 , 2 ); var leadingThirtysevenList = list.bind (null , 37 );var addThirtySeven = addArguments.bind (null , 37 );var list3 = leadingThirtysevenList (1 , 2 , 3 );var result2 = addThirtySeven (5 );var result3 = addThirtySeven (5 , 10 );
参考:Function.prototype.bind() - JavaScript | MDN (mozilla.org)
四、返回值 直接在函数体内使用return即可返回指定值
函数作为返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function callFunction (fun,argu ){ return fun (argu); }function add (num ){ return num+10 ; }callFunction (add,1 ); function callFunction (fun1,fun2,argu1,argu2 ){ return fun1 (argu1)+fun2 (argu2); }function add (num ){ return num+10 ; }callFunction (add,add,1 ,2 );
五、箭头函数 5.1 定义箭头函数
5.1.1 参数 1 2 3 x => {...} (x,y) => {...} (x=1 ) => {...}
5.1.2 函数体 1 2 (x,y) => x+y (x,y) => {...}
5.2 注意事项
不能使用arguments
、super
、new.target
不能作为构造函数
没有prototype
属性
六、递归函数 6.1 使用函数名 (不推荐!),因为一旦函数名的指向发生变化,内部递归就将发生错误
1 2 3 4 5 6 7 function factorial (num ){ if (num<=1 ){return 1 ;} else {return factorial (num-1 );} }let other = factorial; factorial = null ;other ();
6.2 使用arguments.callee 严格模式下无效
1 2 3 4 function factorial (num ){ if (num<=1 ){return 1 ;} else {return arguments .callee (num-1 );} }
6.3 使用命名函数表达式 最推荐的使用方式
1 2 3 4 const factioral = (function f (num ){ if (num<=1 ){return 1 ;} else {return f (num-1 );} })
七、函数闭包 7.1 什么是闭包 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。内部函数无法直接访问外部函数的this和arguments
1 2 3 4 5 6 7 8 9 10 11 function createCompareFunction (propertyName ){ return function (object1,object2 ){ let value1 = object1[propertyName]; let value2 = object2[propertyName]; if (value1<value2){return -1 ;} return 1 ; } }let compare = createCompareFunction ('name' );let result = compare ({name :'Mike' },{name :'Liu' });
调用createCompareFunction
将返回一个函数,调用完毕之后,它的作用域链会被销毁,但是它的活动对象仍然保存在内存中,直到匿名函数被销毁之后,才被销毁。所以使用闭包会占用更多的内存!
1 2 3 4 5 6 7 8 function lazy_sum (arr ){ var sum = function ( ){ return arr.reduce ((x,y )=> x+y); } return sum; }var f = lazy_sum ([1 ,2 ,3 ]); f ();
在这个例子中,我们在函数lazy_sum(arr)中由定义了一个函数sum(),sum()可以使用前者的参数和局部变量,当lazy_sum返回函数时,相关参数和局部变量都保存在了返回的函数中。f变量接收的是一个求和函数,并不是结果。调用f才能获得结果。
每次调用lazy_sum()都返回一个新的函数(即使传入相同的参数)
7.2 闭包的正确使用方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function count ( ){ var arr = []; for (var i=1 ;i<3 ;i++){ arr.push (function ( ){ return i*i; }) } return arr; }var results = count ();var f1 = results[0 ];var f2 = results[1 ];f1 (); f2 ();
期望的结果应该是1,4。但是实际结果为9,9。这是因为返回的函数引用了变量i,但是没有立即执行,等到执行的时候,i已经变成3了。返回的闭包不应该使用循环变量、或者后续会发生变化的变量 ,如果一定要引用循环变量,方法是再创建一个函数,用该函数的参数绑定循环变量当前的值。对上面代码的修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function count ( ){ var arr = []; for (var i=1 ;i<3 ;i++){ arr.push ((function (n ){ return function ( ){ return n*n; } })(i)); } return arr; }var results = count ();var f1 = results[0 ];f1 ();
返回的那个函数变量的形参对应的是f1()的实参,外层函数的形参对应的是count()的实参!
在push里面定义了一个匿名函数,然后在外层在用一对括号包裹起来,直接调用
(function(n){return function(){return n*n;}})(i);
通常,一个立即执行的匿名函数可以把函数体拆开
(function (x) {return x * x;})(3);
7.3 闭包的用途 闭包有非常强大的功能 。举个栗子:
7.3.1 封装私有变量 在面向对象的程序设计语言里,比如Java和C++,要在对象内部封装一个私有变量,可以用private
修饰一个成员变量。
在没有class
机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量
1 2 3 4 5 6 7 8 9 10 11 12 function create_counter (inital ){ var x = inital || 0 ; return { inc : function ( ){ x += 1 ; return x; } } }var c1 = create_counter (); c1.inc (); c1.inc ();
1 2 3 4 5 6 7 8 9 10 11 function count ( ){ var x = 0 ; return function ( ){ return x++; } }var f = count ();f (); f (); var f1 = count ();f1 ();
7.3.2 多参数 to 单参数 还可以将多参数的函数变成单参数的函数,如下
1 2 3 4 5 6 7 function make_pow (n ){ return function (x ){ return Math .pow (x,n); } }var pow2 = make_pow (2 );console .log (pow2 (3 ));
7.4 探究闭包原理 接下来让我们看一个经典的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function count ( ){ var arr = []; for (var i=1 ;i<3 ;i++){ arr.push (function ( ){ return i; }) } return arr; }var results = count ();var f1 = results[0 ];var f2 = results[1 ];f1 (); f2 ();
是不是和你期待的有点不同(1,2)。让我们来具体分析一下这段代码执行时的执行上下文变化情况:
创建全局执行上下文,加入执行上下文栈
初始化全局执行上下文的变量对象VO(添加函数声明count、变量声明results、f1、f2)、作用域链Scope、this
1 2 3 4 5 6 7 8 9 10 globalContext = { VO :{ count : reference to function count ( ){}, results : undefined , f1 :undefined , f2 :undefined }, Scope :[globalConetxt.VO ], this :globalConetxt.VO }
建立函数count,赋值所有父级变量对象到函数count
的scope
内部属性中
var results = count();
,执行函数count,创建count的函数执行上下文,加入上下文栈
初始化count的函数执行上下文
1 2 3 4 5 6 7 8 9 10 11 countContext = { AO :{ arguments :{ length :0 }, i :3 , arr :[f ( ){return i},f ( ){return i},f ( ){return i}] }, Scope :[AO ,globalContext.VO ], this :undefined }
count函数执行完毕,count的函数执行上下文退栈
f1()
,执行result数组里的第一个匿名函数f(),创建匿名函数f的执行上下文,加入栈
初始化f的执行上下文
1 2 3 4 5 6 7 8 9 fContext = { AO :{ arguments :{ length :0 } }, Scope : [AO ,countContext.AO ,globalContext.VO ], this :undefined }
执行return
,在当前上下文中,没有找到变量i
,沿着作用域链往上找,在countContext.AO中找到了i:3
,于是返回 3
f1执行完毕,函数上下文退栈
f2()
,与步骤7、8、9同理
所有代码执行完毕,全局上下文退栈。
这里没有输出预期的1、2,是因为i的中间值没有保存到作用域链中,countContext的活动对象中只保存了i的最终值
为了达到保存中间值的目的,可以增加一个作用域来保存每个中间值,具体就是在for循环里面增加一个匿名函数(因为每个函数都会创建一个单独的执行上下文,而变量就保存在执行上下文的活动对象中)
修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 function count ( ){ var arr = []; for (var i=1 ;i<3 ;i++){ arr.push (function (n ){ return function ( ){ return n; } }(i)) } return arr; }
这里的变化在于f1执行时的作用域链
1 2 3 4 5 6 7 8 9 fContext = { AO: { arguments: { length: 0 } } , Scope: [AO,匿名函数Context.AO,countContext.AO,globalContext.VO], this: undefined}
而匿名函数的执行上下文的活动对象AO,通过形参n
,保存到了i的循环中间值。