# js 函数

函数实际上是对象。每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义。

// 函数声明的方式
function sum (num1, num2) { 
  return num1 + num2; 
} 

// 函数表达式
let sum = function(num1, num2) { 
  return num1 + num2; 
};

// 箭头函数
let sum = (num1, num2) => { 
  return num1 + num2; 
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 1、箭头函数

箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

let arrowSum = (a, b) => { 
  return a + b; 
}; 
1
2
3

# 2、函数名

所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。

function foo() {} 
let bar = function() {}; 
let baz = () => {}; 

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

# 3、参数

可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。

function sayHi() { 
  console.log("Hello " + arguments[0] + ", " + arguments[1]); 
} 
1
2
3

# 4、没有重载

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

function addSomeNumber(num) { 
  return num + 100; 
} 
function addSomeNumber(num) { 
  return num + 200; 
} 
let result = addSomeNumber(100); // 300 
1
2
3
4
5
6
7

# 5、默认参数值

function sayHi(name = 'xiaoming'){
  return name;
}
console.log(sayHi('zhangsan')); // 'zhangsan' 
console.log(sayHi()); // 'xiaoming'
1
2
3
4
5

在使用默认参数时, arguments 对象的值不反映参数的默认值,只反映传给函数的参数。

function makeKing(name = 'Henry') {
  name = 'Louis';
  return `King ${arguments[0]}`;
}
console.log(makeKing()); // 'King undefined'
console.log(makeKing('Louis')); // 'King Louis'
1
2
3
4
5
6

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

function makeKing(name = 'Henry', numerals = name) {
  return `King ${name} ${numerals}`;
}
console.log(makeKing()); // King Henry Henry
1
2
3
4

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。

// 调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') {
  return `King ${name} ${numerals}`;
}
1
2
3
4

参数也存在于自己的作用域中,它们不能引用函数体的作用域

// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals = defaultNumeral) {
  let defaultNumeral = 'VIII';
  return `King ${name} ${numerals}`;
}
1
2
3
4
5

# 6、参数扩展与收集

# 6.1 扩展参数

let values = [1, 2, 3, 4];
function getSum() {
  let sum = 0;
  for (let i = 0; i < arguments.length; ++i) {
    sum += arguments[i];
  }
  return sum;
}

// 方式一
console.log(getSum.apply(null, values)); // 10

// 方式二
console.log(getSum(...values)); // 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.2 收集参数

function getSum(...values) {
  // 顺序累加 values 中的所有值
  // 初始值的总和为 0
  return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3)); // 6
1
2
3
4
5
6

# 7、函数声明与函数表达式

// 没问题
console.log(sum(10, 10));
function sum(num1, num2) {
  return num1 + num2;
}
1
2
3
4
5

以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。在执行代码时,JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。

// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
  return num1 + num2;
};
1
2
3
4
5

# 8、函数作为值

一个函数接收另一个函数作为参数,这种函数就称之为高阶函数

function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument);
}
1
2
3

# 9、函数内部

# 9.1 arguments

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

arguments 对象还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

// 使用 arguments.callee
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 9.2 this

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

在箭头函数中, this 引用的是定义箭头函数的上下文。

# 9.3 caller

这个属性引用的是调用当前函数的函数

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);
}
outer(); // [Function: outer]

// 使用 arguments.callee.caller
function inner() {
  console.log(arguments.callee.caller);
}
outer();
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.4 new.target

检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined ;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。

function King() {
  if (!new.target) {
    throw 'King must be instantiated using "new"'
  }
  console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
1
2
3
4
5
6
7
8

# 10、函数属性与方法

每个函数都有两个属性: length 和 prototype 。

function sayName(name) {
  console.log(name);
}
function sum(num1, num2) {
  return num1 + num2;
}
function sayHi() {
  console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
1
2
3
4
5
6
7
8
9
10
11
12

函数还有两个方法: apply() 和 call() 。

function sum(num1, num2) {
  return num1 + num2;
}
function callSum1(num1, num2) {
  return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2) {
  return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
1
2
3
4
5
6
7
8
9
10
11

通过 call() 向函数传参时,必须将参数一个一个地列出来

function sum(num1, num2) {
  return num1 + num2;
}
function callSum(num1, num2) {
  return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
1
2
3
4
5
6
7

bind() 方法会创建一个新的函数实例,其 this 值会被绑定到传给 bind() 的对象。

window.color = 'red';
var o = {
 color: 'blue'
};
function sayColor() {
 console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
1
2
3
4
5
6
7
8
9

# 11、函数表达式

let functionName = function(arg0, arg1, arg2) {
  // 函数体
};
1
2
3

# 12、递归

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

let anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));

// 使用 arguments.callee
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在严格模式下运行的代码是不能访问 arguments.callee。此时,可以使用命名函数表达式(named function expression)达到目的。

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

# 13、尾调用优化

ECMAScript 6规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。

function outerFunction() {
  return innerFunction(); // 尾调用
}
1
2
3

在 ES6 优化之前,执行这个例子会在内存中发生如下操作。

  • (1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
  • (2) 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction 。
  • (3) 执行到 innerFunction 函数体,第二个栈帧被推到栈上。
  • (4) 执行 innerFunction 函数体,计算其返回值。
  • (5) 将返回值传回 outerFunction ,然后 outerFunction 再返回值。
  • (6) 将栈帧弹出栈外。

在 ES6 优化之后,执行这个例子会在内存中发生如下操作。

  • (1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
  • (2) 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction 。
  • (3) 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction的返回值。
  • (4) 弹出 outerFunction 的栈帧。
  • (5) 执行到 innerFunction 函数体,栈帧被推到栈上。
  • (6) 执行 innerFunction 函数体,计算其返回值。
  • (7) 将 innerFunction 的栈帧弹出栈外。

尾调用优化的条件

  • 代码在严格模式下执行;
  • 外部函数的返回值是对尾调用函数的调用;
  • 尾调用函数返回后不需要执行额外的逻辑;
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包。

# 14、闭包

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

function createComparisonFunction(propertyName) { 
  return function(object1, object2) { 
    let value1 = object1[propertyName]; // propertyName为外部变量形成闭包
    let value2 = object2[propertyName]; 
    if (value1 < value2) { 
      return -1; 
    } else if (value1 > value2) { 
      return 1; 
    } else { 
      return 0; 
    } 
  }; 
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
function compare(value1, value2) { 
  if (value1 < value2) { 
    return -1; 
  } else if (value1 > value2) { 
    return 1; 
  } else { 
    return 0; 
  } 
} 
let result = compare(5, 10);
1
2
3
4
5
6
7
8
9
10

这里定义的 compare() 函数是在全局上下文中调用的。第一次调用 compare() 时,会为它创建一 个包含 arguments、value1 和 value2 的活动对象,这个活动对象是其作用域链上的第一个对象。而全局 上下文的变量对象则是 compare() 作用域链上的第二个对象,其中包含 this、result 和 compare。

作用域链

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

全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。在定义 compare() 函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。在这个例子中,这意味着 compare() 函数执行上下文的作用域链中有两个变量对象:局部变量对象和全局变量对象。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

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

在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。因此,在 createComparisonFunction() 函数中,匿名函数的作用域链中实际上包含 createComparisonFunction() 的活动对象。

let compare = createComparisonFunction('name'); 
let result = compare({ name: 'Nicholas' }, { name: 'Matt' }); 
1
2
闭包

createComparisonFunction() 返回匿名函数后,它的作用域链被初始化为包含 createComparisonFunction() 的活动对象和全局变量对象。这样,匿名函数就可以访问到 createComparisonFunction() 可以访问的所有变量。另一个有意思的副作用就是,createComparisonFunction() 的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction() 执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁:

// 创建比较函数
let compareNames = createComparisonFunction('name'); 
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' }); 
// 解除对函数的引用,这样就可以释放内存了
compareNames = null; 
1
2
3
4
5
6

# 14.1 this 对象

如果在全局函数中调用,则 this 在非严格模式下等于 window,在严格模式下等于 undefined。如果作为某个对象的方法调用,则 this 等于这个对象。

window.identity = 'The Window'; 
let object = { 
  identity: 'My Object', 
  getIdentityFunc() { 
    return function() { 
      return this.identity; 
    }; 
  } 
}; 
console.log(object.getIdentityFunc()()); // 'The Window'
1
2
3
4
5
6
7
8
9
10

# 14.2 内存泄漏

由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制(第 4 章讨论过),所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。

function assignHandler() { 
  let element = document.getElementById('someElement'); 
  element.onclick = () => console.log(element.id); 
}

// 修改后
function assignHandler() { 
  let element = document.getElementById('someElement'); 
  let id = element.id; 
  element.onclick = () => console.log(id);
  element = null; 
} 
1
2
3
4
5
6
7
8
9
10
11
12

# 15、立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。

(function() { 
  // 块级作用域 
})();

let divs = document.querySelectorAll('div'); 
// 达不到目的! 
for (var i = 0; i < divs.length; ++i) { 
  divs[i].addEventListener('click', function() { 
  console.log(i); 
  }); 
} 

// 修改后
for (var i = 0; i < divs.length; ++i) { 
  divs[i].addEventListener('click', (function(frozenCounter) {
    return function() { 
      console.log(frozenCounter); 
    }; 
  })(i)); 
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 16、纯函数

一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。

满足两个条件:

  • 函数的返回结果只依赖于它的参数。
  • 函数执行过程里面没有副作用。
// 纯函数
const foo = (x, y) => x + y;
foo(1, 2); // => 3

// 非纯函数
const a = 1;
const foo = (b) => a + b; // 依赖了外部变量a
foo(2); // => 3

// 函数执行过程没有副作用
const a = 1
const foo = (obj, b) => {
  return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 3
counter.x // => 1

// 修改后有副作用
const a = 1
const foo = (obj, b) => {
  obj.x = 2 // 改变了外部对象的属性值
  return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2
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
27

# 17、总结

  1. 函数就是对象,每个函数都是 Function 类型的实例

  2. 定义函数有三种方式:

  • 函数声明:function test() {}
  • 函数表达式 let test = function() {}
  • 箭头函数 let test = () => {}
  1. 箭头函数:没有 argumentssupernew.targetprototype 属性,也不能作为构造函数

  2. 函数名:使用 name 属性获取

  3. 没有重载:如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的

  4. 默认参数值:在使用默认参数时, arguments 对象的值不反映参数的默认值

  5. 函数声明:在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。而函数表达式不会被提升

  6. 高阶函数:一个函数接收另一个函数作为参数

  7. 函数内部:

  • arguments 对象是一个类数组对象,包含调用函数时传入的所有参数
    • callee 指向 arguments 对象所在函数的指针
  • this
    • 在标准函数中,是函数的上下文对象
    • 在箭头函数中,是箭头函数的上下文对象
  • caller 这个属性引用的是调用当前函数的函数
  • new.target 检测函数是否使用 new 关键字调用的 new.target 属性
    • 如果函数是正常调用的,则 new.target 的值是 undefined
    • 如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数
  1. 函数属性与方法:
  • length 返回函数的参数个数
  • prototype 返回函数的 constuctor 构造函数
  • apply(this,[a,b,c,...]) 调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数
  • call(this,a,b,c,...) 调用一个具有给定 this 值的函数,以及单独给出的一个或多个参数来调用一个函数
  • bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用
  1. 递归:在函数内调用函数自己,可以配合 arguments.callee 使用

  2. 尾调用优化

  • 尾调用:外部函数的返回值是一个内部函数的返回值
  • 优化:引擎发现把外部函数栈帧弹出栈外也没问题,因为内部函数的返回值也是外部函数的返回值
  1. 闭包:在函数里引用了另一个函数作用域中的变量,通常是在嵌套函数中实现的

  2. 立即调用的函数表达式:类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式,例如:(function(){})();

  3. 纯函数:一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用

上次更新: 4/10/2022, 10:26:50 AM