函数

在 JavaScript 中,函数实际上是一个对象。每个函数都是 Function 类型的实例,而且与其它引用类型一样具有属性和方法。因此,你可以把函数名想象成指向函数对象的指针。

在 JavaScript中,函数是对象,更具体地说,函数是 Function 构造函数的实例。这意味着函数本质上是一个特殊的对象,拥有一些额外的特性。

1function fn() {
2	console.log("hello, function!")
3}
4
5console.log(typeof fn) // function
6console.log(fn instanceof Function) // true

首先,每个函数都有一个内置的属性 [[Call]],这使得函数可以被调用。当你调用一个函数时,JavaScript 引擎实际上是在调用这个内置的 [[Call]] 属性。

其次,函数对象可以有属性和方法,就像任何其它的 JavaScript 对象一样。例如,函数有个 length 属性,表示函数期望接收的参数数量,还有一个 name 属性,表示函数的名称。

函数对象还有一些内置的方法,如 call()apply()bind(),这些方法提供了不同的方式来调用函数。

为什么需要函数

函数可以实现代码复用,提高开发效率。

构造函数

构造函数以大写字母开头,并且以它们创建的对象类型命名

构造函数只是使用 new 关键字调用的函数。当你调用构造函数时,它将:

  • 创建一个新对象
  • this 绑定到新对象,以便你可以在构造函数代码中引用 this
  • 运行构造函数中的代码
  • 返回新对象
1function Person(name) {
2  this.name = name;
3  this.introduceSelf = function () {
4    console.log(`你好!我是 ${this.name}`);
5  };
6}
7
8const salva = new Person("Salva");
9salva.name;
10salva.introduceSelf();
11// "你好!我是 Salva。"
12
13const frankie = new Person("Frankie");
14frankie.name;
15frankie.introduceSelf();
16// "你好!我是 Frankie。"

函数介绍

  • 函数:function,是被设计在特定时机可以重复执行特定任务的代码段

  • 说明:

    • 函数可以把具有相同或相似逻辑的代码“包裹”起来,通过函数调用执行这些被“包裹”的代码逻辑,这么做的优势是有利于精简代码方便复用。

函数使用

建议按照先声明后调用的顺序

  • 函数的声明语法

    1function 函数名(参数){
    2	函数体:你希望能重复使用的代码
    3}
    4
    5注意:
    6	参数可以省略
  • 函数的调用语法

    1函数名()
    2
    3注意:
    4	声明(定义)的函数必须调用才会真正被执行,使用函数名()调用函数
  • 函数名命名规范:

    • 和变量命名基本一致

    • 尽量小驼峰式命名法

    • 前缀应该为动词

    • 命名建议:常用动词约定

      动词 含义
      can 判断是否可以执行某个动作
      has 判断是否含有某个值
      is 判断是否为某个值
      get 获取某个值
      set 设置某个值
      load 加载某些数据
  • 循环和函数的区别

    • 循环:写完之后,立即执行
    • 函数:只有调用,才会执行,并且只要声明的函数,可以在任意位置调用。
  • 函数的总结

    • 函数在声明之后,是不会执行里面的代码,需要手动的调用才行

    • 函数一旦声明之后,就可以在任意位置调用

    • 函数的调用没有次数限制,没有上限,完全取决于你的需求

带参函数

函数传参的好处是可以极大的提高函数的灵活性,功能更加强大

如果在重复使用代码的时候,可能有一些会发生变化的数据,就要使用参数。

  • 声明语法:

    1function 函数名(形参){
    2	函数体
    3}
  • 调用语法:

    1函数名(实参)
  • 形参与实参

    • 形参:声明函数时写在函数名右边小括号里的叫形参(形式上的参数)
    • 实参:调用函数时写在函数名右边小括号里的叫实参(实际上的参数)

    形参可以理解为是在这个函数内声明的变量,实参可以理解为是给这个变量赋值(比如num1=10)

    实参<形参的数量时,不会报错,但会给未传值的数据补一个undefined

    实参>形参的数量时,啥事没有,只会保留对应的个数,多的不赋值

    • 如何获取全部实参,可以使用函数内部提供的方法:arguments 功能就是帮助我们获取函数在调用的时候,全部的实参

      • arguments是一个伪数组,有索引和长度

      • 注意事项

        • 函数自带的,不需要声明
        • 只能在函数的内部使用
        • 可以得到函数在实际调用时全部的实参
        1//当实参数量大于形参的数量时,赋值一一对应,多出的不赋值,通过函数内部自带的arguments实现获取全部实参
        2function getSum(num1,num2){
        3  console.log(arguments)
        4  let sum=0
        5  for(let i=0;i<arguments.length;i++){
        6    sum+=arguments[i]
        7  }
        8  alert(sum)
        9}
        10
        11getSum(1,3,4,5,6,8,9)  //36
        12
        13//当实参数量小于形参的数量时,结果为NaN
        14function getSum(a,b,c){
        15  /* 逻辑或 添加默认值*/
        16  a = a || 0
        17  b = b || 0
        18  c = c || 0
        19  return a + b + c
        20}
        21
        22console.log(getSum(1,3))  //4

作用域

1. 变量污染

  • 作用域就是变量和函数的有效范围,作用域可以防止变量污染
    • 如果在全局作用域中,一个出现同名的变量或者函数,后面就会覆盖前面的变量
    • 学名叫做:变量污染 同名变量 后面的覆盖前面的
    • 为了防止变量污染,规定一个范围,把它们隔绝起来,那么这个范围就被称为作用域

2. 函数作用域

通常来说,一段程序代码中所用到的名字并不总是有效和有用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。

  • 全局作用域:全局有效,作用于所有代码执行的环境(整个script标签内部)或者一个独立的js文件

  • 局部作用域:局部有效,作用于函数内的代码环境,就是局部作用域。因为跟函数有关系,所以也称为函数作用域。

  • 块级作用域内有效,块作用域由包括,if语句和for语句里面的

3. 变量的作用域

在JavaScript中,根据作用域的不同,变量分为

  • 全局变量:函数外部let的变量,全局变量在任何区域都可以访问和修改

  • 局部变量:函数内部let的变量,局部变量只能在当前函数内部访问和修改

  • 块级变量:内部的let变量,let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问

  • 变量有一个坑,特殊情况:

    • 如果函数内部或者块级作用域内部,变量没有声明,直接赋值,也当全局变量看,但是强烈不推荐。
    • 但是有一种情况,函数内部的形参可以看做是局部变量,出了函数就无效了,不能使用。

4. 变量的访问原则-作用域链

  • 只要是代码,就至少有一个作用域
  • 写在函数内部的局部作用域
  • 如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
  • 根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,就称作作用域链。
    • 作用域链:采取就近原则的方式来查找变量最终的值

匿名函数

  • 具名函数:

    1//声明:
    2function fn(){}
    3
    4//调用:
    5fn()
  • 匿名函数:

    1function(){}
    2
    3//将匿名函数赋值给一个变量,并且通过变量名称进行调用,我们将这个称为函数表达式。
    4语法:	
    5	let fn=function(){
    6		函数体
    7	}
    8
    9调用:
    10	fn()
  • 立即执行函数(自调用函数)

    1//使用场景:避免全局变量之间的污染
    2语法1:
    3	(function (){  })();
    4
    5语法2:
    6	(function (){  } ());
    7	
    8不需要调用,立即执行
    9注意:多个立即执行函数要用;隔开,要不然会报错。
    • 推导过程:

      1声明:
      2	let fn=function(形参){}
      3
      4调用:
      5	fn(实参)
      6
      7推导:
      8	function(形参){}(实参)	 	
      9	(function(形参){})(实参)	 
      10	(function(形参){}(实参))
  • 函数传值赋值小技巧

    1//为形参添加默认值,防止NaN
    2//形参不赋值,系统默认补一个undefined
    3//数字+undefined  会报NaN
    4声明:
    5	let fn=function(x,y){
    6		x=x||0
    7		y=y||0
    8		console.log(x+y)
    9	}
    10
    11或者
    12	let fn=function(x=0,y=0){
    13		console.log(x+y)
    14	}
    15
    16调用:
    17	fn()
    18	fn(3,5)

副作用函数

在开发过程中,我们经常需要写一些函数来完成特定的任务。这些函数可能会接收一些输入(我们称之为形参),然后根据这些输入计算出一个结果并返回,这就是函数的基本作用。

然而,有些函数除了返回结果之外,还会做一些额外的事情,比如改变一些在函数外部也可以访问的变量的值,或者读写文件,或者在屏幕上打印一些内容等等,这些额外的事情,我们称之为副作用

举个例子,假设我们有一个函数,它的任务是计算两个数的和:

1function sum(x, y) {
2  return x + y;
3}

这个函数就是一个没有副作用的函数,应为它只做了一件事:计算两个数的和并返回结果。它没有改变任何外部的东西,也没有做其他的事情。

但是,我们稍微改变一下这个函数:

1let total = 0;
2function sum(x, y) {
3	total = total + x + y;
4  return total;
5}

这个新的函数就有了副作用了,因为除了返回了两个数的和之外,它还改变了一个叫做 total 的变量的值。这个 total 变量是在函数外部定义的,所以这个函数的副作用就是改变了 total 的值。

这就是副作用函数的概念。在开发中,有时候我们需要使用副作用函数,但是过多的副作用函数会使得代码变得难以理解和维护,所以我们通常会尽量减少副作用函数的使用。

创建函数的方式

函数声明

JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义;而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

函数声明:函数声明会在任何代码执行之前先被读取并添加到执行上下文。在执行代码时,JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此,即使函数定义出现在调用它的代码之后,引擎也会把函数声明提升到顶部。

函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后。JavaScript引擎会先读取函数声明,然后再执行代码

1console.log(sum(1, 2))
2function sum(num1, num2) {
3	return num1 + num2
4}

函数表达式

1console.log(sum(1, 2))
2const sum = function(num1, num2) {
3	return num1 + num2
4}

当函数定义包含在一个变量初始化语句中,这意味着代码如果没有执行到该行,那么执行上下文中就没有函数的定义,所以上面的代码会报错。这并不是因为使用 constlet 导致的,使用 var 关键字也会存在同样的问题。

除了函数什么时候真正有定义这个区别外,两种语法是等价的

函数表达式看起来就像一个普通的变量定义和赋值,即创建了一个函数再把它赋值给一个变量。这样创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。匿名函数有时候也被称为兰姆达函数

在任何时候,只要函数被当作值来使用,它就是一个函数表达式

函数声明和函数表达式在语法和行为上的一些重要的区别:

  • 函数声明会在任何代码被执行前先解析和定义,这种称为函数声明的提升(hoisting)。这意味着你可以在声明函数之前调用函数。而函数表达式只有在运行到定义它的代码行时,才会被解析和定义

  • 函数表达式可以是匿名的,而函数声明必须要有函数名

  • 由于函数表达式是在运行时进行赋值,因此它们可以用在条件语句(如 if...else)中,而函数声明不能

箭头函数

ES6新增了使用胖箭头(=>)语法定义函数表达式的能力。

1const sum = (num1, num2) => num1 + num2

箭头函数的简洁语法非常适合嵌入函数(回调函数)的场景

1const arr = [1, 2, 3]
2console.log(arr.map((i)=> i + 1))

如果只有一个参数也可以不用括号,只有没有参数,或多个参数的情况下,才需要使用括号

箭头函数在 JavaScript 中是一种特殊类型的函数,它有一些特别的限制和特性:

  1. 不能使用arguments

在普通函数中,arguments 是一个类数组对象,它包含了函数调用时传递的所有参数。但在箭头函数中,arguments 对象不可用,如果你需要类似的功能,可以使用rest参数。

1// 普通函数
2function fn1() {
3	console.log(arguments) // [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }
4}
5fn1(1, 2, 3, 4, 5)
6
7// 箭头函数
8const fn2 = () => {
9	console.log(arguments) // ReferenceError: arguments is not defined
10}
11fn2(1, 2, 3, 4, 5)
12
13// rest参数版
14const fn3 = (...args) => {
15	console.log(args) // [ 1, 2, 3, 4, 5 ]
16}
17fn3(1, 2, 3, 4, 5)
  1. 不能使用 super

super 关键字用于调用对象的父对象上的函数。箭头函数不能使用 super,因此它们不能用在那些需要使用 super 的场景中,如类的构造函数

1class MyParentClass {
2	constructor() {
3		this.value = 5
4	}
5}
6
7class MyChildClass extends MyParentClass {
8	constructor() {
9		super()
10		this.arrowFunction = () => {
11			console.log(super.value) // 错误:super关键字在这里无效
12		}
13	}
14}
  1. 不能使用 new.target

在箭头函数中,new.target 是不可用的。

new.target 是一个元属性(meta property),在 JavaScript 中专门用于检测函数或构造方法是否是通过 new 运算符被调用的。它主要用于构造方法中,以确定如何执行函数。

  • 如果函数或构造方法是通过 new 运算符被调用的,new.target 返回一个指向构造方法或函数的引用。
  • 如果函数是正常调用的(即没有使用 new 运算符),new.target 的值为 undefined
1function MyFunc() {
2	if (new.target === MyFunc) {
3		console.log("yes")
4	} else {
5		console.log("no")
6	}
7}
8
9const instance = new MyFunc() // yes
10
11MyFunc() // no
  1. 不能用作构造函数:由于箭头函数没有自己的 this,并且也没有 constructor,所以你不能使用 new 运算符来调用箭头函数,也就是说,箭头函数不能用作构造函数。
1let MyFunc = () => {
2  this.value = 5
3}
4let obj = new MyFunc() // Uncaught TypeError: MyFunc is not a constructor
  1. 没有 prototype 属性

只有通过 constructor 函数定义的函数才有 prototype 属性。由于箭头函数不能用作构造函数,所以它们没有 prototype 属性。

1let MyFunc = () => {
2  console.log('>>>')
3}
4console.log(MyFunc.prototype) // undefined

构造函数

JavaScript 的 Function 构造函数可以接收任意多个字符串参数,最后一个参数始终会被当成函数体,这个函数体是一个字符串,其中包含了 JavaScript 代码。

1const sum = new Function('num1', 'num2', 'return num1 + num2')

不推荐使用这种语法来定义函数

  1. 因为这段代码会被解释两次,会影响性能

    • 首次解析是在 JavaScript 引擎解析整个脚本时完成的
      • 当 JavaScript 引擎首次解析这段代码时,它会创建一个新的 Function对象,并将字符串参数 num1,num2,return num1+num2 传递给 Function 构造函数。然而这个时候,这些字符串参数并没有被解析为实际的JavaScript代码,它们仍然只是字符串。
    • 第二次解析时在 Function 构造函数被调用时完成的
      • 只有当我们实际调用 sum 函数时,这些字符串才会被解析并执行。例如,当我们执行 sum(1,2) 时,字符串 return num1+num2 会被解析为 JavaScript 代码并执行,计算出 1+2 的结果。
  2. 因为函数体是通过字符串来指定的,所以它不会在编写代码时进行语法检查,只有在运行时才会进行解析和执行。这可能会导致潜在的错误更难以发现。此外,这种方法也可能有安全问题,因为它相当于动态执行了一段 JavaScript 代码。

函数名

在 JavaScript 中,当我们说函数名是指向函数的“指针”,我们实际上是在比喻说明函数名的行为和性质。这并不是说 JavaScript 真的有 C 或 C++ 那样的指针概念。

在 JavaScript 中,函数(和所有其他对象)是通过引用来访问的。当你创建一个函数并给它一个名字时,这个名字实际上是一个变量,它存储了对函数对象的引用。你可以把这个引用看作是一个“指针”,它“指向”函数对象。

例如:

1function greet() {
2  console.log('Hello, world!');
3}
4
5// 这里,greet 是一个“指针”,它指向函数对象

在这个例子中,greet 是一个变量,它存储了对函数对象的引用。当我们调用 greet() 时,JavaScript 会跟踪这个引用,找到对应的函数对象,并执行它。

因此,当我们说“函数名是指向函数的指针”时,我们实际上是在描述 JavaScript 中的引用行为。

函数名就是指向函数的指针,这意味着一个函数可以有多个名称

1function sum(num1, num2) {
2  return num1 + num2;
3}
4console.log(sum(1, 2)); // 3
5
6let anotherSum = sum
7console.log(anotherSum(1, 2)); // 3
8
9sum = null
10console.log(anotherSum(1, 2)); // 3

使用不带括号的函数名会访问函数指针,而不会执行函数

ES6的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,即使函数没有名称,也会如实显示成空字符串。如果是使用Function构造函数创建的,则会标识成“anonymous”

1function foo() {}
2const bar = function () {}
3const baz = () => {}
4
5console.log(foo.name) // foo
6console.log(bar.name) // bar
7console.log(baz.name) // baz
8
9console.log((() => {}).name) // 空字符串
10console.log(new Function().name) // anonymous

如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前会加上一个前缀

1// bind
2function foo() {}
3console.log(foo.bind(null).name) // bound foo
4
5// get 和 set
6let dog = {
7  years: 1,
8  get age() {
9    return this.years
10  },
11  set age(newAge) {
12    this.years = newAge
13  }
14}
15// 返回 dog 对象上 age 属性的描述符。
16let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age')
17// 返回 age 属性的 getter 函数的名称
18console.log(propertyDescriptor.get.name) // get age 
19// 返回 age 属性的 setter 函数的名称
20console.log(propertyDescriptor.set.name); // set age

参数

ECMAScript函数的参数在内部表现为一个数组,在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

1function sum() {
2  return arguments[0] + arguments[1]
3}
4
5console.log(sum(1,2));

arguments对象是一个类数组对象,可以使用 arguments.length来获取传进来多少个参数。

arguments的值始终会与对应的命名参数同步,但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。

arguments的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。

ECMAScript函数的参数只是为了方便才写出来的,并不是必须写出来的。

在严格模式下,在函数中重写arguments对象会导致语法错误,直接赋值也不会再影响对应的命名参数的值。

箭头函数中的参数

如果函数是使用箭头函数定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问

1const bar = () => {
2  console.log(arguments[0]);
3}
4
5bar(5); // ReferenceError: arguments is not defined

虽然箭头函数中没有arguments对象,但可以在包装函数中把它提供给箭头函数:

1function foo() {
2  const bar = () => {
3    console.log(arguments[0]); // 5
4  }
5  bar()
6}
7
8foo(5)

注意:ECMAScript中所有的参数都按值传递的,不能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用。

函数返回值

当函数需要返回数据出去时,用return关键字

1//return返回一个值
2return 返回值
3
4//return返回多个值
5return [返回值1,返回值2]
6
7细节:
8	在函数体中使用return关键字能将内部的执行结果交给函数外部使用
9	函数内部只能出现1return,并且return后面代码不会再被执行,所以return后面的数据不要换行写
10	return会立即结束当前函数,后面的代码不在执行
11	函数没有return,这种情况函数默认返回值为undefined
  • return的作用
    • 修改函数的返回值

    • 终止函数的运行

没有重载

ECMAScript 函数不能像传统编程那样重载。

ECMAScript函数没有签名,因为参数是由包含零个或多个值的数组表示。没有函数签名,自然也就没有重载。

如果在ECMAScript中定义了两个同名函数,则后定义的会覆盖先定义的。

把函数名当成指针也有助于理解为什么ECMAScript没有函数重载

默认参数值

ES6之前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传这个参数,那么就给它赋一个值

1function foo(name) {
2  name = (typeof name === 'undefined') ? name : 'dancy';
3  return `name: ${name}`
4}

ES6之后支持显示定义默认参数了,只要在函数定义中的参数后面用 = 就可以为参数赋一个默认值

1function foo(name='dancy') {
2  return `name: ${name}`
3}

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。

1const person = (name = 'dancy', age = getAge()) => `name: ${name},age: ${age}`

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但为传相应参数时才会被调用。

箭头函数也同样可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了

1const name = (name = 'dancy') => `name: ${name}`

默认参数作用域与暂时性死区

给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样

1function foo(name = 'dancy', age = 18) {
2  return `name: ${name},age: ${age}`
3}

这里的默认参数会按照定义它们的顺序依次被初始化。可以按照如下示例想象一个这个过程:

1function foo() {
2  let name = 'dancy';
3  let age = 18;
4  return `name: ${name},age: ${age}`
5}

在 JavaScript 中,函数参数的默认值是在函数调用时计算的,而不是在函数定义时计算。这意味着参数的默认值可以引用在其之前定义的参数(因为参数是按顺序初始化的),但不能引用在其之后定义的参数。这就是所谓的“暂时性死区”规则。

1// 后定义的参数可以引用先定义的参数
2function foo(name = 'dancy', aa = name) {
3  return `name: ${name},age: ${age}`
4}
1// 前面定义的参数不能引用后面定义的,否则会抛出错误
2function example(first = second, second = 5) {
3  console.log(first, second);
4}
5
6example(); // 抛出错误:second is not defined

此外,函数参数的作用域是独立于函数体的。这意味着函数参数不能引用函数体内的变量。

1function example(first = x) {
2  var x = 5;
3  console.log(first);
4}
5
6example(); // 抛出错误:x is not defined

在这个例子中,我们试图在 first 参数的默认值中引用函数体内的 x 变量,但是这是不允许的,所以会抛出一个错误。

参数扩展与收集

ES6新增了扩展运算符,使用它可以非常简洁地操作和组合集合数据。

扩展运算符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。

扩展运算符既可以用于调用函数时传参,也可以用于定义函数参数

1// 用于调用函数时传参
2function sum(num1, num2) {
3  return num1 + num2;
4}
5console.log(sum(...[1,2]))
6
7// 用于定义函数参数

扩展参数

再给函数传参时,有可能不需要传一个数组,而是分别传入数组的元素

1const arr = [1, 2, 3, 4, 5];
2
3console.log(sum(...arr));

因为数组的长度已知,所以在使用扩展运算符传参的时候,并不妨碍在其前面或后面再传其他值,包括使用扩展操作符传其他参数

1console.log(sum(0, ...arr));
2console.log(sum(...arr, 6));
3console.log(sum(0, ...arr, 6));
4console.log(sum(...arr, ...[6, 7, 8]));

对函数中的 arguments 对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值

1const arr = [1, 2, 3, 4, 5];
2
3function sum() {
4  console.log(arguments.length);
5}
6
7sum(0, ...arr);
8sum(...arr, 6);
9sum(0, ...arr, 6);
10sum(...arr, ...[6, 7, 8]);

在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数

1function getProduct(a, b, c = 1) {
2      return a * b * c;
3}
4    let getSum = (a, b, c = 0) => {
5      return a + b + c;
6}
7console.log(getProduct(...[1,2])); //2 
8console.log(getProduct(...[1,2,3])); //6 
9console.log(getProduct(...[1,2,3,4])); // 6
10console.log(getSum(...[0,1])); //1 
11console.log(getSum(...[0,1,2])); //3 
12console.log(getSum(...[0,1,2,3])); //3

收集参数

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。这有点类似arguments对象的构造机制,只不过收集参数的结果会得到一个Array实例

1function sum(...values) {
2  return values.reduce((x, y) => x + y, 0)
3}
4
5console.log(sum(1, 2, 3))

收集参数的前面如果还有命名参数,则只会收集其余参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数

1function sum(a, b, ...values) {
2  return values.reduce((x, y) => x + y, 0)
3}

箭头函数虽然不支持 arguments对象,但支持收集参数的定义方式,因此也可以实现与使用 arguments一样的逻辑

1function sum(...values) {
2  return values.reduce((x, y) => x + y, 0)
3}

另外,使用收集参数并不影响 arguments 对象,它仍然反映调用时传给函数的参数

1function sum(...values) {
2  console.log(arguments.length);
3  console.log(arguments);
4  console.log(values);
5}
6
7sum(1,2,3)

函数作为值

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

注意:如果是访问函数韩式还不是调用函数,那就必须不带括号。

函数内部

在ES5中,函数内部存在两个特殊的对象:arguments 和 this。

ES6中又新增了 new.target

arguments

arguments是一个类数组对象,包含调用函数时传入的所有参数。

这个对象只有以 function 关键字定义函数时才有(箭头函数没有)。

虽然主要用于包含函数参数,但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。用途,解耦递归调用。

this

当你在任何函数外部定义变量或函数时,它们会被添加到全局上下文中。在浏览器环境中,全局上下文就是 window 对象。

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

  • 标准函数
    • this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值(简记:this指向方法调用者)
    • 注意:这个this到底引用哪个对象必须到函数被调用时才能确定
  • 箭头函数
    • 箭头函数的this上下文是静态的,由它们的包围函数(或全局作用域)在定义时决定,而不是在运行时决定。
    • this引用的是定义箭头函数的上下文
    • this会保留定义该函数时的上下文
1function King() {
2  this.royaltyName = 'Henry';  // 这里的 'this' 是新创建的 King 实例
3
4  // 这个箭头函数继承了 'this' 值,所以它也引用了新创建的 King 实例
5  setTimeout(() => console.log(this.royaltyName), 1000);
6}
7
8var king = new King();  // 1秒后输出: "Henry"
1function Queen() {
2  this.royaltyName = 'Elizabeth';  // 这里的 'this' 是新创建的 Queen 实例
3
4  // 这是一个普通函数,所以它有自己的 'this' 上下文,这里的 'this' 指向全局 window 对象
5  setTimeout(function() { console.log(this.royaltyName); }, 1000);
6}
1var obj = {
2  value: 'Hello, world!',
3  createArrowFunction: function() {
4    return () => console.log(this.value);
5  }
6};
7
8var arrowFunction = obj.createArrowFunction();
9arrowFunction();  // 输出: 'Hello, world!'

createArrowFunction 是一个普通函数,所以它有自己的 this 上下文。当你调用 obj.createArrowFunction() 时,thiscreateArrowFunction 函数内部引用了 obj 对象。

然后,createArrowFunction 返回了一个箭头函数。箭头函数不创建自己的 this 上下文,而是从它的包围函数(在这种情况下是 createArrowFunction)那里继承 this。因此,箭头函数内部的 this 引用的也是 obj 对象。

所以,当你调用 arrowFunction() 时,它能正确地访问并打印 obj 对象的 value 属性,即 'Hello, world!'。

caller

ES5也会给函数对象添加一个属性:caller。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null,如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值。

1function outer() {
2  inner()
3}
4function inner() {
5  console.log(inner.caller)
6  console.log(arguments.callee.caller)
7}
8outer()
9inner()

在严格模式下访问 arguments.callee 会报错。

严格模式下还有一个限制,就是不能给函数的 caller 属性赋值,否则会导致错误。

new.target

ECMAScript中的函数始终可以作为构造函数去实例化一个新对象,也可以作为普通函数被调用。

ES6新增了检测函数是否使用new关键字调用的 new.target 属性。

  • 如果函数是正常调用的,则 new.target 的值是 undefined
  • 如果函数是使用new关键字调用的,则 new.target 将引用被调用的构造函数
1function King() {
2  console.log(new.target)
3}
4new King() // [function: King]
5King() // undefined

函数属性和方法

ECMAScript中的函数是对象,因此有属性和方法。

每个函数都有两个实例属性:

  • length:保存函数定义的命名参数的个数
  • prototype:保存引用类型所有实例方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享。
    • 在ES5中 prototype属性是不可枚举的,因此使用 for-in 循环不会返回这个属性

每个函数都有两个方法,这两个方法都会以指定的this值来调用函数,即会设置调用函数时函数体内this对象的值。

  • apply():接收两个参数(this的值和一个参数数组/Array实例/arguments)
  • call():接收多个参数(this的值和剩下的要传给函数的参数)

在严格模式下,调用函数时如果没有指定上下文对象,则 this 值不会指向 window。 除非使用 apply()或 call()把函数指定给一个对象,否则 this 的值会变成 undefined。

到底是用 apply() 还是 call(),完全取决于怎么给要调用的函数传参方便。如果都不给被调用的函数传参,则使用哪个方法都一样。

apply()和 call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内 this 值的能力。

  • bind():ES5出于同样的目的定义了一个新方法:bind(),bind() 方法会创建一个新的函数实例,其this的值会被绑定到传给bind()的对象

继承的方法 toLocaleString() 和 toString() 始终放回函数的代码

继承的方法 valueOf() 返回函数本身

递归

递归函数通常的形式是一个函数通过名称调用自己。

注意:如果把这个函数赋值给其他变量,就会出现问题。在写递归函数时使用 arguments.callee 可以避免这个问题。

arguments.callee 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用

1function factorial(num) {
2  if (num <= 1) {
3    return 1
4  } else {
5    // arguments.callee 可以确保无论通过什么变量 调用这个函数都不会出问题
6    return num * arguments.callee(num - 1)
7  }
8}

不过在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错。此时可以使用命名函数表达式达到目的。这个模式在严格模式和非严格模式 下都可以使用。

1const factorial = function f(num) {
2  if (num <= 1) {
3    return 1
4  } else {
5    return num * f(num - 1)
6  }
7}

尾调用优化

闭包

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

  • 在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。

  • 然后用arguments和其他命名参数来初始化这个函数的活动对象。这个对象是其作用域链上的第一个对象

  • 外部函数的活动对象是内部函数作用域链上的第二个对象。

  • 这个作用域链一直向外串起了所有包含函数的活动对象,知道全局执行上下文才终止。

在函数执行时,要从作用域链中查找变量,以便读、写值。

1function compare(value1, value2) {
2  if (value1 < value2) {
3    return -1
4  } else if (value1 > value2) {
5    return 1
6  } else {
7    return 0
8  }
9}
10
11// 这里定义的compare函数是在全局上下文中调用的
12// 第一次调用 compare() 时,会为它创建一个包含 arguments、value1 和 value2的活动对象,这个对象是其作用域链上的第一个对象
13// 而全局上下文的变量对象则是 compare() 作用域链上的第二个对象,其中包含this、result、compare
14let result = compare(5, 10)

函数执行时,每个执行上下文中都会有一个包含其中变量的对象。

  • 全局上下文中的叫变量对象,它会在代码执行期间始终存在
  • 函数局部上下文中的叫活动对象,只在函数执行期间存在。

在定义 compare() 函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的 [[Scope]] 中。

在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的 [[Scope]] 来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

在 JavaScript 中,当一个函数被调用时,会创建一个新的执行上下文和一个对应的活动对象(也称为变量对象) 这个活动对象包含了函数的参数和局部变量,这个活动对象会被添加到函数的作用域链的开始,作用域链是一个对象列表,这些对象定义了函数可以访问的变量。 函数可以访问它自己的活动对象以及包含它的上下文的活动对象。全局上下文的变量对象包含了全局变量和全局函数,在浏览器环境中还包含了window对象和一些其他的全局对象和函数,总是位于作用域链的末尾。 作用域链的开始总是当前函数的活动对象,接着是包含函数的上下文的活动对象,一直到全局上下文对象。所以,全局上下文对象总是位于作用域链的末尾。

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。

在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。

1function createComparisonFunction(propertyName) {
2  return function (object1, object2) {
3    let value1 = object1[propertyName]
4    let value2 = object2[propertyName]
5    if (value1 < value2) {
6      return -1
7    } else if (value1 > value2) {
8      return 1
9    } else {
10      return 0
11    }
12  }
13}
14let compare = createComparisonFunction('name')
15let result = compare({ name: 'Nicholas' }, { name: 'Matt' })

createComparisonFunction() 的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它活动对象仍然会保留在内存中,直到匿名函数被销毁才会被销毁

1// 这里,创建的比较函数被保存在变量 compare 中。
2// 把 compare 设置为 null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉。作用域链也会被销毁,其作用域(除全局作用域之外)也可以销毁。
3compare = null

因为闭包会保留它们包含函数的作用域,所以比其他函数更占内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用。

this对象

在闭包中使用this会让代码变复杂

  • 如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。
  • 如果在全局函数中调用,则this在非严格模式下等于window,在严格模式下等于undefined
  • 如果作为某个对象的方法调用,则this等于这个对象
  • 匿名函数在这种场景下不会绑定到某个对象,这就意味着this会指向window,除非在严格模式下this是undefined。

不过,由于闭包的写法所致,这个事实有时候没有那么容易看出来

1window.identity = 'The Window'
2let object = {
3  identity: 'My Object',
4  getIdentityFunc() {
5    return function () {
6      return this.identity
7    }
8  }
9}
10
11// object.getIdentityFunc() 返回了一个函数,然后你立即调用了这个返回的函数。这个返回的函数是一个普通函数,不是箭头函数,所以它有自己的 this 上下文。
12// 当你调用 object.getIdentityFunc()() 时,返回的函数是在全局上下文中被调用的,所以它的 this 指向 window 对象。
13// 在 JavaScript 中,当一个函数独立于任何对象被调用(即它不是作为对象的方法被调用),this 默认指向全局对象。在浏览器环境中,全局对象就是 window 对象。
14console.log(object.getIdentityFunc()()) // 'The Window'

每个对象被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可以访问的另一个变量中,则是行的通的。比如:

1window.identity = 'The Window'
2let object = {
3  identity: 'My Object',
4  getIdentityFunc() {
5    // 在闭包内部保存this的引用
6    let that = this
7    return function () {
8      return that.identity
9    }
10  }
11}
12console.log(object.getIdentityFunc()()) // 'My Object'

注意:this和arguments都是不能直接在内部函数中访问的。如果想访问包含作用域中的 arguments 对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。

1window.identity = 'The Window'
2let object = {
3  identity: 'My Object',
4  getIdentity() {
5    return this.identity
6  }
7}
8
9// getIdentity作为对象的方法被调用,this指向调用者即object
10object.getIdentity(); // 'My Object'
11
12// 在 JavaScript 中,括号不会改变函数调用的上下文。当你写 (object.getIdentity)() 时,你仍然是在 object 上下文中调用 getIdentity 方法,所以 this 在 getIdentity 方法内部引用的仍然是 object 对象。
13// 括号在这里只是改变了表达式的优先级,使得函数调用有更高的优先级。但无论是否有括号,只要函数是作为对象的方法被调用的,this 都会指向调用它的对象。
14(object.getIdentity)(); // 'My Object'
15
16
17// 先执行了一次赋值,然后再调用赋值后的结果
18// 在 JavaScript 中,赋值操作符 (=) 的返回值是赋值的右侧的值。所以,表达式 (object.getIdentity = object.getIdentity) 的值是 object.getIdentity 函数本身。
19// 然后,你立即调用了这个函数。因为这个函数是独立于任何对象被调用的(即它不是作为对象的方法被调用的),所以 this 在函数内部引用的是全局对象。在浏览器环境中,全局对象就是 window 对象。
20(object.getIdentity = object.getIdentity)(); // 'The Window'

立即调用的函数表达式

立即调用的匿名函数又被称为立即调用的函数表达式(IIFE,Immediately Invlked Function Expression)

1(function(){
2  // 块级作用域
3})()

使用IIFE可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。只要函数执行完毕,其作用域链就可以被销毁。

ES5及以前,为了防止变量定义外泄,IIFE是个非常有效的方式。

ES6以后,出现了块级作用域就无须用IIFE实现同样的隔离了。

1// 块级作用域
2{
3  let i
4}
1let divs = document.querySelectorAll('div')
2// 这里使用 var 关键字声明了循环迭代变量i,但这个变量并不会被限制在 for 循环的块级作用域内。
3// var 关键字定义的变量在它们的包围函数中是共享的(函数作用域),如果在全局作用域中定义,那么它们就在全局作用域中是共享的。
4for (var i = 0; i < divs.length; ++i) {
5  // 当你在 for 循环中为每个 div 元素添加事件监听器时,这些监听器都引用了同一个 i 变量。然后,for 循环继续执行,i 的值增加,直到 i 等于 divs.length,循环结束。
6  
7  // 在执行单机处理程序时,迭代变量的值是循环结束时的最终值,即元素的个数。
8  // 当你点击一个 div 元素时,事件处理函数会执行,它会打印 i 的当前值,这个值是 divs.length,因为这是在循环结束后 i 的值。
9  divs[i].addEventListener('click', function () {
10    console.log(i)
11  })
12}
13
14// 并且,这个变量 i 存在于循环体外部,随时可访问
15console.log(i); // 10

在ES6中,如果对 for 循环使用块级作用域变量关键字let,那么循环就会为每个循环创建独立的变量,从而让每个单机处理程序都能引用特定的索引。

1let divs = document.querySelectorAll('div');
2
3// 这里的 i 是在 for 循环的块级作用域中声明的。
4for (let i = 0; i < divs.length; ++i) {
5  // 每次循环迭代,都会创建一个新的 i 变量。
6  // 这个新的 i 变量是这次迭代的 i 值。第一个 i 变量,其值为0,第二个为1,以此类推。
7
8  // 这个函数是一个闭包,它可以访问并记住它被创建时的环境。
9  // 在这个环境中,有一个 i 变量,这个 i 变量的值是这次迭代的 i 值。
10  let listener = function () {
11    // 当这个函数被调用时,它会打印出它被创建时的 i 值。
12    console.log(i);
13  };
14
15  // 将这个闭包函数添加为 div 元素的点击事件监听器。
16  divs[i].addEventListener('click', listener);
17}
18
19// 这里会抛出一个 ReferenceError,因为在这个作用域中没有定义 i。
20console.log(i);

但是注意,如果把变量声明拿到for循环外部,那就不行了,会遇到通var一样的问题。

1let i
2for (i = 0; i < divs.length; ++i) {
3  divs[i].addEventListener('click', function () {
4    console.log(i)
5  })
6}

私有变量

严格来讲,JavaScript没有私有成员的概念,所有对象属性都公有的。但是有私有变量的概念。

任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。

私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。

1function add (num1, num2) {
2  let sum = num1 + num2;
3  return sum
4}

在这个函数中,函数add() 有3个私有变量:num1、num2 和 sum。这几个变量只能在函数内部使用,不能在函数外部访问。

如果这个函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部的这3个变量。基于这一点,就可以创建能够访问私有变量的公有方法。

特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。

在对象上有两种方式创建特权方法:

  • 在构造函数中实现,这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。

静态私有变量

特权方法也可以通过使用私有作用域定义私有变量和函数来实现。

1(function () {
2  // 私有变量和私有函数
3  let privateVariable = 10;
4  
5  function privateFunction() {
6    return false
7  }
8  
9  // 构造函数
10  MyObject = function (){}
11  
12  // 公有和特权方法
13  MyObject.prototype.publicMethod = function() {
14    privateVariable++;
15    return privateFunction();
16  }
17})()

注意:使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多。

模块模式

单例对象就是只有一个实例的对象。

JavaScript是通过对象字面量来创建单例对象的

1let singleton = {
2  name: value,
3  method() {
4    // 方法代码
5  }
6}

模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。

1let singleton = function () {
2  // 私有变量和私有函数
3  let privateVariable = 10;
4  
5  function privateFunction() {
6    return false
7  }
8  
9  // 特权/公有方法和属性
10  return {
11    privateProperty: true,
12    publicMethod() {
13      privateVariable++;
14      return privateFunction();
15    }
16  }
17}()

模块模式使用匿名函数返回一个对象。在匿名函数内部

  • 首先定义私有变量和私有函数。
  • 创建一个要通过匿名函数返回的对象字面量,这个对象字面量中只包含可以公开访问的属性和方法

因为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一作用域的私有变量和私有函数。

本质上,对象字面量定义了单例对象的公共接口。如果单例对象需要进行某种初始化,并且需要访问私有变量时,那就可以采用这个模式:

1let application = function() {
2  // 私有变量和私有函数
3  let components = new Array()
4  // 初始化
5  components.push(new Basecomponent())
6  // 公共接口
7  return {
8    getComponentCount() {
9      return components.length
10		},
11    registerComponent(component) {
12      components.push(component)
13    }
14  }
15}()

在web开发中,经常需要使用单例对象管理应用程序级的信息。

在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是Object实例,因为最终单例都由一个对象字面量来表示。

模块增强模式

另一个利用模块模式的做法是在返回对象之前先对其进行增强。

这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。

1let singleton = function () {
2  // 私有变量和私有函数
3  let privateVariable = 10;
4  
5  function privateFunction() {
6    return false
7  }
8  
9  // 创建局部变量保存实例
10  let object = new CustomType()
11  
12  // 添加特权/公有属性和方法
13  object.privateProperty = true;
14  object.publicMethod = function () {
15    privateVariable++;
16    return privateFunction()
17  }
18  
19  // 返回对象
20  return object
21}()

小结

  • 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。
  • ES6新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别
  • JavaScript中函数定义与调用时的参数极其灵活。arguments对象,以及ES6新增的扩展操作符,可以实现函数定义和调用的完全动态化
  • 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了什么参数等信息
  • JavaScript引擎可以优化符合尾调用条件的函数,可以节省栈空间
  • 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象
  • 通常,函数作用域及其其中的所有变量在函数执行完毕后都会被销毁
  • 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁
  • 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用
  • 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都会被销毁
  • 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量
  • 可以访问私有变量的公共方法叫做特权方法
  • 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增强模式在单例对象上实现