阅读 509

JavaScript 系列六: 函数

"Code tailor",为前端开发者提供技术相关资讯以及系列基础文章,微信关注“小和山的菜鸟们”公众号,及时获取最新文章。

前言

在开始学习之前,我们想要告诉您的是,本文章是对JavaScript语言知识中 "函数" 部分的总结,如果您已掌握下面知识事项,则可跳过此环节直接进入题目练习

  • 函数简介
  • 函数名称
  • 函数重载
  • 函数声明与函数表达式
  • 函数作为值
  • this
  • 函数的递归

如果您对某些部分有些遗忘,👇🏻 已经为您准备好了!

汇总总结

函数简介

函数是所有编程语言的核心部分,因为它们可以封装语句,且被定义后可以在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字进行声明,后跟一组参数,然后是函数体。

以下是函数的基本语法:

function functionName(arg0, arg1,...,argN) {
 //表达式
}
复制代码

下面是一个例子:

function sayHi(name, message) {
  console.log('Hello ' + name + ', ' + message)
}
复制代码

可以通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)。

下面是调用函数 sayHi() 的示例:

sayHi('xhs', 'do you study today?')
复制代码

调用这个函数的输出结果是 "Hello xhs, do you study today?"。 参数 namemessage 在函数内部作为字符串被拼接在了一起,最终通过 console.log() 输出到控制台。

ECMAScript 中的函数不需要指定是否返回值,也不限制返回值的类型。任何函数在任何时间都可以使用 return 语句来返回函数的值,用法是后跟要返回的值,返回值可以是任何类型。比如:

function sum(num1, num2) {
  return num1 + num2
}
复制代码

函数 sum() 会将两个值相加并返回结果。注意,除了 return 语句之外没有任何特殊声明表明该函数有返回值。然后就可以这样调用它:

const result = sum(5, 10)
复制代码

值得注意的是,只要执行到 return 语句,函数就会立即停止执行并退出。因此,return 语句后面的代码不会被执行。比如:

function sum(num1, num2) {
  return num1 + num2

  console.log('Hello world') //不会执行
}
复制代码

在这个例子中,console.log() 不会执行,因为它在 return 语句后面,解释器在执行到 return 语句后就停止对该函数的执行。

一个函数里可以有多个 return 语句,像这样:

function subtract(num1, num2) {
  if (num1 < num2) {
    return num2 - num1
  } else {
    return num1 - num2
  }
}
复制代码

这个 subtract() 函数用于计算两个数值的差。如果第一个数值小于第二个,则用第二个减第一个;否则,就用第一个减第二个。代码中每个分支都有自己的 return 语句,返回正确的差值。return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined。这种用法最常用于提前终止函数执行,并不是为了返回值。比如在下面的例子中,console.log() 不会执行:

function sayHi(name, message) {
  return

  console.log('Hello ' + name + ', ' + message) // 不会执行
}
复制代码

根据开发经验而言,一个函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。

函数名称

函数名是一个指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个函数名,如下所示:

function sum(num1, num2) {
  return num1 + num2
}

console.log(sum(10, 10)) // 20

let xhsSum = sum

console.log(xhsSum(10, 10)) // 20

sum = null

console.log(xhsSum(10, 10)) // 20
复制代码

以上代码定义了一个名为 sum 的函数,用于求两个数之和。然后又声明了一个变量 xhsSum,并将它的值设置为等于 sum 。注意,使用不带括号的函数名会访问函数指针名,而不会执行函数。此时,xhsSumsum 都指向同一个函数。调用 xhsSum() 也可以返回结果。把 sum 设置为 null 之后,就切断了它与函数之间的关联。而 xhsSum() 还是可以照常调用,没有问题。

函数参数

ECMAScript 函数的参数跟大多数其他语言不同。ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,也可以一个也不传,解释器都不会报错。

之所以会这样,主要是因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。如果数组中什么也没有,那没问题;如果数组的元素超出了要求,那也没问题。

事实上,在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是 arguments[0],第二个参数是 arguments[1])。而要确定传进来多少个参数,可以访问 arguments.length 属性。在下面的例子中,sayHi() 函数的第一个参数叫 name

function sayHi(name, message) {
  console.log('Hello ' + name + ', ' + message)
}
复制代码

可以通过 arguments[0] 取得相同的参数值。因此,把函数重写成不声明参数也可以:

function sayHi() {
  console.log('Hello ' + arguments[0] + ', ' + arguments[1])
}
复制代码

在重写后的代码中,没有命名参数。namemessage 参数都不见了,但函数照样可以调用。这就表明,ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。与其他语言不同,在 ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制。也可以通过 arguments 对象的 length 属性检查传入的参数个数。下面的例子展示了在每调用一个函数时,都会打印出传入的参数个数:

function getArgsNum() {
  console.log(arguments.length)
}

getArgsNum('string', 45) // 2

getArgsNum() // 0

getArgsNum(12) // 1
复制代码

这个例子分别打印出 201 (按顺序)。既然如此,那么开发者可以想传多少参数就传多少参数。

比如:

function getTotle() {
  if (arguments.length === 1) {
    console.log(arguments[0] + 10)
  } else if (arguments.length === 2) {
    console.log(arguments[0] + arguments[1])
  }
}

getTotle(10) // 20

getTotle(30, 20) // 50
复制代码

这个函数 getTotle() 在只传一个参数时会加 10 ,在传两个参数时会将它们相加,然后返回。因此 getTotle(10) 返回 20 ,而 getTotle(30,20) 返回 50 。虽然不像真正的函数重载那么明确,但这已经足以弥补 ECMAScript 在这方面的缺失了。

还有一个必须理解的重要方面,那就是 arguments 对象可以跟命名参数一起使用,比如:

function getTotle(num1, num2) {
  if (arguments.length === 1) {
    console.log(num1 + 10)
  } else if (arguments.length === 2) {
    console.log(arguments[0] + num2)
  }
}
复制代码

在这个 getTotle() 函数中,同时使用了两个命名参数和 arguments 对象。命名参数 num1 保存着与 arugments[0] 一样的值,因此使用谁都无所谓。(同样,num2 也保存着跟 arguments[1] 一样的值。)arguments 对象的另一个有意思的地方就是,它的值始终会与对应的命名参数同步。来看下面的例子:

function getTotle(num1, num2) {
  arguments[1] = 10

  console.log(arguments[0] + num2)
}
复制代码

这个 getTotle() 函数把第二个参数的值重写为 10。因为 arguments 对象的值会自动同步到对应的命名参数,所以修改 arguments[1] 也会修改 num2 的值,因此两者的值都是 10 。但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。

另外还要记住一点:如果只传了一个参数,然后把 arguments[1] 设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。对于命名参数而言,如果调用函数时没有传这个参数,那么它的值就是 undefined。这就类似于定义了变量而没有初始化。

比如,如果只给 getTotle() 传了一个参数,那么 num2 的值就是 undefined。严格模式下,arguments 会有一些变化。首先,像前面那样给 arguments[1] 赋值不会再影响 num2 的值。就算把 arguments[1] 设置为 10num2 的值仍然还是传入的值。其次,在函数中尝试重写 arguments 对象会导致语法错误。(代码也不会执行。)

函数重载

ECMAScript 函数不能像传统编程那样重载。在其他语言比如 Java 中,一个函数可以有两个定义,只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。如果在ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。来看下面的例子:

function addSomeNumber(num) {
  return num + 100
}

function addSomeNumber(num) {
  return num + 200
}
let result = addSomeNumber(100) // 300
复制代码

这里,函数 addSomeNumber() 被定义了两次。第一个版本给参数加 100,第二个版本加 200。最后一行调用这个函数时,返回了 300,因为第二个定义覆盖了第一个定义。

函数声明与函数表达式

我们可以通过 function 构造一个函数,也可以将一个变量赋值成一个函数,这两种方法在大多数情况下都可以正常调用执行。可是在 JavaScript 引擎在加载数据时对函数声明和函数表达式是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。来看下面的例子:

// 没问题
console.log(sum(10, 10))

function sum(num1, num2) {
  return num1 + num2
}
复制代码

以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:

// 会出错
console.log(sum(10, 10))

var sum = function (num1, num2) {
  return num1 + num2
}
复制代码

函数作为值

因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。

function add10(num) {
  return num + 10
}
let result1 = callSomeFunction(add10, 10)
console.log(result1) // 20
function getGreeting(name) {
  return 'Hello, ' + name
}
let result2 = callSomeFunction(getGreeting, 'Nicholas')
console.log(result2) // "Hello, Nicholas"
复制代码
function fun1(x) {
  return function (y) {
    return function (z) {
      console.log(x * y * z)
    }
  }
}
fun1(2)(3)(4) //24
复制代码

this

this 是一个特殊的对象,它在标准函数和箭头函数中有不同的行为。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 window 对象)。来看下面的例子:

window.color = 'red'

let o = {
  color: 'blue',
}

function sayColor() {
  console.log(this.color)
}

sayColor() // 'red'

o.sayColor = sayColor

o.sayColor() // 'blue'
复制代码

定义在全局上下文中的函数 sayColor() 引用了 this 对象。这个 this 到底引用哪个对象必须到函数被调用时才能确定。因此这个值在代码执行的过程中可能会变。如果在全局上下文中调用 sayColor(),这结果会输出 red ,因为 this 指向 window,而 this.color 相当于 window.color 。而在把 sayColor() 赋值给 o 之后再调用 o.sayColor()this 会指向 o ,即 this.color 相当于 o.color ,所以会显示 blue

在箭头函数中,this 引用的是定义箭头函数的上下文。箭头函数的内容我们会在第二部分的函数扩展中详细提及。

函数的递归

递归函数通常的形式是一个函数通过名称调用自己,如下面的例子所示:

function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}
复制代码

这是经典的递归阶乘函数。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题:

let anotherFactorial = factorial

factorial = null

console.log(anotherFactorial(4)) // 报错
复制代码

这里把 factorial()函数保存在了另一个变量 anotherFactorial 中,然后将 factorial 设置为 null,于是只保留了一个对原始函数的引用。而在调用 anotherFactorial() 时,要递归调用 factorial() ,但因为它已经不是函数了,所以会出错。在写递归函数时使用 arguments.callee 可以避免这个问题。arguments.callee 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下所示:

function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * arguments.callee(num - 1)
  }
}
复制代码

把函数名称替换成 arguments.callee ,可以确保无论通过什么变量调用这个函数都不会出问题。因此在编写递归函数时 arguments.callee 是引用当前函数的首选。不过,在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的。比如:

const factorial = function f(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * f(num - 1)
  }
}
复制代码

题目自测

一:以下代码输出什么

function callFunc(func, argument) {
  return func(argument)
}
function func1(argument) {
  console.log(argument + '.')
}
callFunc(func1, 'hello,world')
复制代码
Answer

Answer: 'hello,world.'

callFunc 函数的作用其实就是执行了第一个参数,并把第二个参数传给第一个参数当做参数。


二:以下代码输出什么

function recursion(num) {
  if (num === 0) return 1
  else if (num % 2 === 0) return recursion(num - 1) + 1
  else return recursion(num - 1)
}

console.log(recursion(6))
复制代码
Answer

Answer: 4

recursion 是一个递归函数,如果 num 等于0就返回1,如果 num 是偶数,就在下一个结果前 +1,如果是奇数就返回下一个结果。其作用就是统计从 0num 一共有多少个偶数。所以 recursion(6)4


三:两段代码分别执行各会输出什么

func1(1, 2)
function func2(x, y) {
  console.log(x + y)
}
let func1 = func2
复制代码
func2(1, 2)
function func2(x, y) {
  console.log(x + y)
}
复制代码
Answer

func1(1,2) 输出 ReferenceError,因为在调用时 func1 还未被定义和赋值。 func2(1,2) 输出 3,因为 function 的声明会被提前,可以在之前使用。

JavaScript系列的函数,我们到这里结束啦,谢谢各位对作者的支持!你们的关注和点赞,将会是我们前进的最强动力!谢谢大家!

文章分类
前端
文章标签