JavaScript(五)函数

本文最后更新于:8 个月前

函数定义、参数、属性、返回值,箭头函数,递归,闭包

JavaScript(五)函数

一、定义函数

函数名,就是指向函数的指针。这个性质决定了后面很多特性,例如:不能重载、函数可以作为变量赋值,等等

1
2
3
4
5
6
7
8
function add(num){return num+1;}
function add(num){return num+2;}
add(1); //3,add实际上函数的指针,所以第2行代码实际上是赋予了add一个新的函数指针

function one(){return 'hello';}
let two = one
one = null
two() //'hello'

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); //10,arguments的值始终与命名参数同步

function doAdd2(num1,num2){
num2 = 10;
console.log(arguments[1]);
}
doAdd2(1,2); //10,arguments的值始终与命名参数同步

2.2 默认参数

只要在函数定义中的参数后面用=就可以为参数赋一个默认值。默认参数放到最后面

1
2
3
4
5
6
function add(num1,num2=10){return num1+num2;}
add(1); //11

function add(num1=10,num2){return num1+num2;}
add(undefined,1); //11,如果默认参数写在前面,而且想使用它,那么可以在调用的时候在对应位置传入undefined
add(1); //NaN

默认参数作用域

后面的参数,可以引用先定义的参数,但是不能反过来。

1
2
3
4
5
function add(num1=10,num2=num1){return num1+num2;}
add(); //20

function add(num1=num2,num2=10){return num1+num2;}
add(); //Error

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); //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(); //red
sayColor.apply(o); //blue

利用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); // [1, 2, 3]

var result1 = addArguments(1, 2); // 3

// 创建一个函数,它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37);

// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]

var result2 = addThirtySeven(5);
// 37 + 5 = 42

var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略

参考: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); //11

function callFunction(fun1,fun2,argu1,argu2){
return fun1(argu1)+fun2(argu2);
}
function add(num){
return num+10;
}
callFunction(add,add,1,2); //23

五、箭头函数

5.1 定义箭头函数

1
(参数) => {函数体}

5.1.1 参数

1
2
3
x => {...}		//参数如果只有一个,可以省略括号
(x,y) => {...} //参数如果没有、或者有多个,需要括号
(x=1) => {...} //参数如果要设置默认值,需要括号

5.1.2 函数体

1
2
(x,y) => x+y	//如果只有一行代码,可以省略大括号,默认会返回这行代码的值
(x,y) => {...} //如果不止一行,需要带上大括号。带了大括号之后,需要return语句才会返回值

5.2 注意事项

  • 不能使用argumentssupernew.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(); //VM375:3 Uncaught TypeError: factorial is not a function

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'}); //1

调用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获得的是一个已经传入了参数,但是还没有执行的函数
f(); // 调用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(); //9
f2(); //9

期望的结果应该是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(); //1
c1.inc(); //2
1
2
3
4
5
6
7
8
9
10
11
function count(){
var x = 0;
return function(){
return x++;
}
}
var f = count();
f(); //0
f(); //1,因为使用的是同一个变量
var f1 = count();
f1(); //0,重新定义后,生成了一组新的变量

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(); //3
f2(); //3

是不是和你期待的有点不同(1,2)。让我们来具体分析一下这段代码执行时的执行上下文变化情况:

  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
    }
  3. 建立函数count,赋值所有父级变量对象到函数countscope内部属性中

  4. var results = count();,执行函数count,创建count的函数执行上下文,加入上下文栈

  5. 初始化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
    }
  6. count函数执行完毕,count的函数执行上下文退栈

  7. f1(),执行result数组里的第一个匿名函数f(),创建匿名函数f的执行上下文,加入栈

  8. 初始化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

  9. f1执行完毕,函数上下文退栈

  10. f2(),与步骤7、8、9同理

  11. 所有代码执行完毕,全局上下文退栈。

这里没有输出预期的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的循环中间值。


JavaScript(五)函数
http://timegogo.top/2022/05/13/JavaScript/JavaScript(五)函数/
作者
丘智聪
发布于
2022年5月13日
更新于
2023年7月16日
许可协议