JavaScript深入总结之函数为什么是一等公民

444 阅读8分钟

什么是函数

函数是用function关键字定义的一组用来执行特定功能的语句。定义函数有三种方式:函数声明函数表达式,构造函数

函数声明

这是我们工作中用到最多的一种方式。

function sum(num1,num2){
    return sum1+sum2;
}

函数表达式

var sum=function(num1,num2){
    return num1+num2;
}

函数表达式与函数声明的区别是,函数表达式会将函数当做数据赋值给一个变量。

构造函数

var sum=new Function('num1','num2','return a+b')

Function 构造函数创建一个新的 Function 对象。直接调用此构造函数可用动态创建函数,但会遇到和 eval 类似的的安全问题和(相对较小的)性能问题。然而,与 eval 不同的是,Function 创建的函数只能在全局作用域中运行。其参数必须是字符串。因为定义比较复杂,所以一般用得少。

函数没有重载

function addNumber(num){
    return num+10;
}

function addNumber(count){
    return count+20;
}
addNumber(10);//30

即在JavaScript中如果定义了两个相同的函数,则该名字只属于后定义的函数。函数没有重载的好处在于能重写之前的代码功能,坏处就在于一不小心把别人的代码重写了,导致难以察觉的bug,慎用。

函数声明与函数表达式的区别

  1. 函数声明是函数名跟在关键字function后面,函数表达式是把函数赋值给一个变量;
  2. 函数声明可以在执行环境的任何位置,调用不受限制,而函数表达式的定义必须在函数调用之前。

函数声明提升

前面我们提到变量和函数都有声明提升,即变量和函数的定义会被提升到程序的最顶端。举个例子:

sum(1,2);//3
function sum(num1,num2){
    return num1+num2;
}

上面代码函数调用在函数声明之前,但是能正常输出结果。变量声明提升虽然可以在变量声明前使用变量,但是值是undefined,而函数声明提升是函数的调用和声明可以在执行环境的任何位置。

函数的参数是按值传递

把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。 要理解上面这段话有点费劲。既然是复制,不是基本类型复制后互不影响,引用类型复制后会互相影响吗,为什么是按值传递不是按引用传递?

在函数传递一个基本类型参数时,被传递的值会复制给函数内一个局部变量(arguments中的一个),传递一个引用类型的值时,会把该引用类型的指针地址复制给函数内一个局部变量,当局部变量发生变化时会反映在函数的外部。举个例子:

函数参数传递一个数字,函数内部修改后:

function addNumber(num){
    num=num+10;
    console.log(num);//20
}
var num=10;
addNumber(num);
console.log(num);//10

如果传递的是一个object,在函数内部修改对象属性时,外部对象也发生变化了。

function setName(obj){
   obj.name="李四"
   console.log(obj.name);//李四
}
var obj={
    name:"张三"
};
setName(obj);
console.log(obj);//obj.name="李四"

这不是与上面说的还值传递自相矛盾?我们修改几行代码再看:

function setName(obj){
   obj.name="李四"
   obj=new Object();
   obj.name="王麻子";
   console.log(obj.name);//王麻子
}
var obj={
    name:"张三"
};
setName(obj);
console.log(obj);//obj.name="李四"

这段代码与上面的区别是obj传递给函数后,其属性name被重新赋值,创建一个新对象赋值给obj,再修改obj.name。如果obj是按引用传递,那么修改obj后应该也反映到函数外部,但原始引用并没有发生变化。原因是在函数内部重写obj后实际上创建了一个新的对象引用,该变量引用是局部变量,函数执行完后即被销毁。

总结一下:函数的参数都是按值传递,参数都是局部变量。对于基本数据类型的参数来说,这个值是变量的副本,在函数体内修改参数不会影响外面,对于引用数据类型来说,这个值是指针地址,在函数体内修改数据会反应到外部,但是如果重写了这个引用类型数据,则不会反应到外面。

为什么说JavaScript函数是一等公民

在编程语言中,一等公民需要具备几个条件:可以作为函数的参数,可以作为函数返回值,可以赋值给变量。 按照这个标准,其实JavaScript任何数据都可以是一等公民,一般说JavaScript函数是一等公民是相对其他编程语言而言的,不是所有的编程语言中函数都能满足上述条件~

函数作为函数的参数

这就是我们常说的回调函数:

function doSomething(callback){
    callback(1,2);//3
}
function getResult(num1,num2){
    console.log(num1+num2);
}
doSomething(getResult)

作为函数的返回值

function doSomething(){
    var name="沉默术士";
    return function(){
        return name;
    }
}
var getName=doSomething();
getName();

作为数据变量

function doSomething(){
    return "hello world";
}
var getValue=doSomething;
getValue();

尾调优化

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

但是以下情况就不属于尾调优化

// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

// 情况三
function f(x){
  g(x);
}

尾调用不一定出现在函数尾部,只要是最后一步操作即可。

函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

如果改写成尾递归,只保留一个调用记录:

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

ES6函数拓展

1.默认参数

ES5我们定义函数默认参数的时候通常这样写:

function doSomething(value){
    value=value||'world';
    retrun "hello"+value;
}

ES6中我们定义默认参数可以这样写:

function doSomething(value=world){
    retrun "hello"+value;
}

注意1:参数变量是默认声明的,所以不能用let或者const再次声明。默认参数应该在参数的末尾

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

注意2:使用默认参数时,函数参数不能有同名参数

// 不报错
function foo(x, x, y) {
  // ...
}

// 报错
function foo(x, x, y = 1) {
  // ...
}

注意3:参数默认值是惰性求值的

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。

2. 参数解构

function foo({x, y = 5}) {
  console.log(x, y);
}

3. rest参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function sum(...values){
    let sum=0;
    for(let i of values){
        sum+=i
    }
    return sum
}
sum(1,2,3);//6

4. 箭头函数

var f = () => 5;
// 等同于
var f = function () { return 5 };

箭头函数使用注意点:
1 . 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
2 . 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
3 . 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
4 . 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

总结:

1、创建函数的方式有:函数声明、函数表达式,构造函数;
2、函数没有重载;
3、函数声明提升指的是函数可以在执行环境任何地方声明和调用;
4、函数的参数都是按值传递;
5、函数也是数据,可以作为函数的参数,赋值给变量,作为函数的返回值; 6、ES6拓展了函数非常丰富有效的能力,比如默认参数,rest参数,参数解构,箭头函数等等。