likes
comments
collection
share

JavaScript开发:函数在实际开发中的使用总结(1)

作者站长头像
站长
· 阅读数 27

本文以《JavaScript高级程序设计》第4版作为基础参考,整理使用JavaScript开发过程中,函数使用相关的知识点。

本文是开发知识点系列第六篇。

函数在JavaScript开发中视为一等公民,足见其重要性,不可不察。

终于写到函数了,前面的五篇可以说是JavaScript内置规则,也可以说是JavaScript语言的基础,给予开发者发挥的空间并不多。一直到函数,开发者可以发挥的余地增大,开发变得异彩纷呈,变得有意思;当然能够与函数在这方面旗鼓相当的还有后面的对象。

什么是函数?函数是具备一定功能的可以被重复调用的代码块。函数可以接收输入(称为参数),并返回一个结果(称为返回值)

函数的基本性质

函数声明&定义

函数可以通过多种方式定义:

  1. 函数声明(Function Declaration):
function myFunction(a, b) {
  return a + b;
}
  1. 函数表达式(Function Expression):
const myFunction = function(a, b) {
  return a + b;
};
  1. 箭头函数(Arrow Function):
const myFunction = (a, b) => {
  return a + b;
};

如果箭头函数函数体只有一条语句,可以简写为:

const myFunction = (a, b) => a + b;
  1. 使用Function构造函数:
const myFunction = new Function('a', 'b', 'return a + b');

这种因为会影响性能,所以不被推荐。

需要注意的是,普通函数声明会被JavaScript引擎提升(hoisted),这意味着你可以在声明之前调用函数。而函数表达式和箭头函数则不会被提升,必须在调用函数之前定义函数。

此外,箭头函数和其他两种方式定义的函数在行为上也有一些差异,例如箭头函数没有自己的thisthis的值在箭头函数被定义时就已经确定,等于箭头函数定义时所在的上下文。

另外,箭头函数不能使用argumenssupernew.target,也不能作为构造函数,也没有prototype属性。

函数名

ECMAScript 6的所有函数对象都会暴露一个只读的name属性。多数情况下这个属性保存的是函数标识符,或者是一个字符串化的变量名。如果没有名称即匿名函数,则显示成空字符串。如果函数是使用Function构造函数创建的,则会标识成“anonymous”。

如果函数是一个获取函数、设置函数或者使用bind()实例化了,则标识符前面会加上一个前缀:对于获取函数(getter)和设置函数(setter),name属性返回 "get " 或 "set " 加上函数的名称。对于使用bind()方法创建的函数,name属性返回 "bound " 加上原函数的名称。

  1. 普通函数:
function myFunction() {}
console.log(myFunction.name); // 输出:"myFunction"
  1. 获取函数和设置函数:
var obj = {
  get value() {},
  set value(v) {}
};

var descriptor = Object.getOwnPropertyDescriptor(obj, 'value');

console.log(descriptor.get.name); // 输出:"get value"
console.log(descriptor.set.name); // 输出:"set value"
  1. 使用bind()创建的函数:
function myFunction() {}
var boundFunction = myFunction.bind(null);

console.log(boundFunction.name); // 输出:"bound myFunction"

此外,name属性的值不一定能反映函数的当前名称,因为函数的名称可以在运行时被改变。

参数

JavaScript中的函数参数不是必须写的,提前写只是为了方便。

非箭头函数可以用arguments获取到参数,其本身就是一个类数组。可通过arguments[0]arguments[1]arguments[2]……获取到参数。

箭头函数因为不能使用arguments,也就不能通过arguments获取参数。但是箭头函数可以通过使用剩余参数的方式,获取参数,后面会说到。

关于函数的默认值,ECMAScript 6可以显式的定义参数的默认值了。不仅限于普通函数,箭头函数也可以。

另外因为参数是按照顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。

function greet(name = 'World', message = `Hello, ${name}!`) {
  console.log(message);
}

扩展运算符:扩展运算符可以将数组展开,数组成员直接作为参数传递给函数,不仅限于普通函数。

let numbers = [1, 2, 3];

function sum(a, b, c) {
  return a + b + c;
}

console.log(sum(...numbers));  // 输出 6

收集参数/剩余参数:在ES6中,你可以使用剩余参数(rest parameters)语法来表示任意数量的参数。剩余参数在参数列表中的最后一个参数前面加上...,在函数内部,剩余参数表现为一个数组。

function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3));  // 输出 6

没有重载

所谓重载在传统编程中,比如Java开发中,一个函数名相同的函数可以有两个甚至以上的定义,只要参数的类型或者数量不同就可以。但是JavaScript不可以,只要函数名相同后面定义的会覆盖前面定义的。

函数的属性和方法

在JavaScript中,函数是一种特殊的对象,除了上面提到的name属性,它还有一些其它的属性和方法。

  1. length:返回函数的参数个数,即在函数定义时给出的形参个数。

  2. prototype:非箭头函数都有一个prototype属性,它指向函数的原型对象。当使用new操作符创建一个新对象时,新对象会从它的构造函数的原型对象上继承属性和方法。

下面则是一些函数对象的内置方法:

  1. call():调用一个函数,并将函数的this值设置为提供的值。所有的参数都会作为函数的参数传递。

  2. apply():调用一个函数,并将函数的this值设置为提供的值。接受一个数组或类数组对象,其中的元素会作为函数的参数传递。

  3. bind():创建一个新的函数,新函数的this值和参数被预设为bind()的参数。

  4. toString():返回一个表示函数源代码的字符串。

需要注意的是,虽然函数是对象,但它们和普通的对象有一些重要的区别。最重要的区别是函数可以被调用执行,而普通的对象不能。

还有箭头函数this因为一开始就确定好了,所以不可以通过callapplybind改变。

函数内部

函数内部有一些特殊的对象和变量,它们在函数执行时被创建,并在函数执行完毕后被销毁。除了上面提到的arguments,还有以下特殊对象和变量:

  1. this:这是一个特殊的变量,它引用了函数被调用时的上下文对象。this的值在函数被调用时确定,取决于函数的调用方式。例如,当一个函数作为对象的方法被调用时,this引用了那个对象;当一个函数直接被调用(即不通过对象或其他函数调用)时,this引用了全局对象(在非严格模式下)或undefined(在严格模式下)。当然箭头函数因为this被提前确定了,所以不具备这样的特性。

  2. return:这是一个关键字,用于指定函数的返回值。当执行到return语句时,函数会立即停止执行,并返回return后面的表达式的值。如果函数没有return语句,或者return后面没有表达式,那么函数会返回undefined

  3. 局部变量:在函数内部声明的变量是局部变量,它们只在函数内部可见。每次函数被调用时,都会创建新的局部变量。

  4. 内部函数:在函数内部可以声明其他的函数,这些函数被称为内部函数或嵌套函数。内部函数可以访问外部函数的变量和参数,这称为词法作用域或静态作用域。

函数的种类

除了上面函数声明提到的三种函数:function声明式函数,函数表达式以及箭头函数,还有以下几种函数类型:

  1. 立即调用的函数表达式(Immediately Invoked Function Expression,IIFE):这种函数在定义后立即调用。
(function() {
  console.log('Hello, world!');
})();
  1. 生成器函数(Generator Function):生成器函数是ES6引入的新特性,它可以返回一个生成器对象。生成器对象可以按需产生一系列的值。
function* idGenerator() {
  let id = 0;
  while (true) {
    yield id++;
  }
}
  1. 构造函数(Constructor Function):构造函数用于创建新的对象。构造函数通常首字母大写,使用new关键字调用。
function Person(name) {
  this.name = name;
}
let john = new Person('John');
  1. 方法(Method):方法是定义在对象或类中的函数。
var obj = {
  greet: function() {
    console.log('Hello, world!');
  }
};

递归函数和尾调用优化

递归函数

递归函数是一种调用自身的函数。

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

console.log(factorial(5)); // 输出:120

factorial函数接受一个参数n,并返回n的阶乘。如果n等于0,那么函数返回1;否则,函数返回n乘以factorial(n - 1)的结果。这里,factorial(n - 1)是一个递归调用,它计算(n - 1)的阶乘。

递归函数必须有一个终止条件,否则函数会无限递归,导致栈溢出错误。

尾调用优化

尾调用优化是一种编程语言的特性,它允许在函数的最后一步调用另一个函数时,不增加调用栈。这意味着,即使在无限递归的情况下,也不会出现栈溢出错误。

以下是一个使用尾调用的例子:

function factorial(n, acc = 1) {
  if (n === 0) {
    return acc;
  } else {
    return factorial(n - 1, n * acc);
  }
}

console.log(factorial(5)); // 输出:120

在这个例子中,factorial函数在最后一步调用了自身,这是一个尾调用。这里使用了一个累积器acc来保存中间结果,这样在每次递归调用时,都只需要计算一次乘法,而不需要保存中间的乘法结果。这使得函数可以被尾调用优化。

尾调用优化失败的一些例子:

function foo(x) {
  return bar(x) + 1;  // 尾调用后还有加法操作,无法优化
}

function foo(x) {
  bar(x);  // 尾调用的结果不是函数的返回值,无法优化
}

function foo(x) {
  let y = x * 2;
  return function() { return y; }  // 尾调用函数是一个闭包,无法优化
}

function foo(x) {
  return new bar(x);  // 尾调用函数是一个构造函数,无法优化
}

function foo(x) {
  return obj.bar(x);  // 尾调用函数是一个方法,无法优化
}

function factorial(n) {
  if (n === 0) {
    return 1;
  } else {
    // 尾调用自身,但是调用后还有乘法操作,所以不能被尾调用优化
    return n * factorial(n - 1);
  }
}

需要注意的是,只有在严格模式下,JavaScript引擎才会进行尾调用优化。通过在文件或函数的开始处添加'use strict';来开启严格模式。

总结一下

虽然说到了函数就有意思了,但这篇主要写的还是函数的基本内置规则。好吧,总结一下要点:

  1. 函数是一个具有一定功能的代码块,可以有输入值参数,也可以有返回值,默认返回undefined
  2. 函数声明有几种,注意区别,根据实际开发需要使用
  3. 函数参数可以写也可以不写,写是为了方便,推荐写
  4. 非箭头函数可以通过arguments获得参数
  5. 函数参数可以通过剩余参数收集参数
  6. JavaScript函数没有重载
  7. 注意非箭头函数中的this指向,这个需要总结,一句话总结的话:决定于调用环境
  8. 箭头函数this提前确定,不可以被bindcallapply改变,没有property属性,没有arguments,也不能作为构造函数
  9. 函数的种类有立即调用函数、构造函数还有生成器函数
  10. 递归和尾调用函数学会使用

本文完。