# 函数

# 1.定义方式

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

在 JavaScript 中,函数是一种特殊的对象,它和对象一样可以拥 有属性和值,但是函数和普通对象不同的是,函数可以被调用。

函数除了可以拥有常用类型的属性值之外,还拥有两个隐藏属性,分别是 name 属性和 code 属性。

# 函数声明

函数通常以函数声明的方式定义,比如:

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

# 函数表达式

另一种定义函数的语法是函数表达式。函数表达式与函数声明几乎是等价的:

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

# 构造函数

几乎不适用的方式是 Function 构造函数。这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。来看下面的例子:

let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
1

# 箭头函数

箭头函数简洁的语法非常适合嵌入函数的场景:

console.log(ints.map(function(i) { return i + 1; })); // [2, 3, 4] 

console.log(ints.map((i) => { return i + 1 })); // [2, 3, 4]
1
2
3

箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 arguments 、 super 和 new.target ,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

# 函数声明提升

// 函数声明,没问题
console.log(sum(10, 10)); 
function sum(num1, num2) { 
  return num1 + num2; 
}

// 函数表达式,会出错
console.log(sum(10, 10)); 
let sum = function(num1, num2) { 
  return num1 + num2; 
};
1
2
3
4
5
6
7
8
9
10
11
  • 函数声明提升(function declaration hoisting)。在执行代码时,JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错。

  • 如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。

# 2.函数特性

# 函数名

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

function sum(num1, num2) { 
  return num1 + num2; 
}
console.log(sum(10, 10)); // 20 

let anotherSum = sum; 
console.log(anotherSum(10, 10)); // 20 

sum = null; 
console.log(anotherSum(10, 10)); // 20
1
2
3
4
5
6
7
8
9
10

# 没有重载

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

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

# 函数作为值

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

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

# 函数的属性和方法

  • 函数的name属性返回函数的名字
  • 函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数
  • toString()方法返回一个字符串,内容是函数的源码。

# 默认参数值

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

function makeKing(name) { 
  name = (typeof name !== 'undefined') ? name : 'Henry'; 
  return `King ${name} VIII`; 
}
console.log(makeKing()); // 'King Henry VIII' 
console.log(makeKing('Louis')); // 'King Louis VIII'
1
2
3
4
5
6

ECMAScript 6之后就不用这么麻烦了,因为它支持显式定义默认参数了。下面就是与前面代码等价的ES6写法,只要在函数定义中的参数后面用 = 就可以为参数赋一个默认值:

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

# 扩展操作符

调用:

let values = [1,2,3,4] 
function countArguments() { 
  console.log(arguments.length); 
}
countArguments(-1, ...values); // 5
1
2
3
4
5

定义:

function getSum(...values) { 
  console.log(arguments.length); // 3 
  console.log(arguments); // [1, 2, 3] 
  console.log(values); // [1, 2, 3] 
}
console.log(getSum(1,2,3));
1
2
3
4
5
6

# 3.函数内部

# arguments

  • arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0] ,第二个参数是 arguments[1] )
  • 要确定传进来多少个参数,可以访问arguments.length 属性。
  • arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。在严格模式下运行的代码是不能访问arguments.callee 的,因为访问会出错。
function factorial(num) { 
  if (num <= 1) { return 1;} 
  else { return num * arguments.callee(num - 1);} //arguments.callee=factorial
}
1
2
3
4

如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。

var args = Array.prototype.slice.call(arguments);

var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}
1
2
3
4
5
6

# this

  • 标准函数中, this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时, this 指向 windows )
•window.color = 'red'; 
let o = { 
	color: 'blue' 
};
function sayColor() { 
	console.log(this.color); 
}
sayColor(); // 'red'
o.sayColor = sayColor; 
o.sayColor(); // 'blue'
1
2
3
4
5
6
7
8
9
10
  • 箭头函数中的 this 会保留定义该函数时的上下文。
function King() { 
	this.royaltyName = 'Henry'; 
	// this引用King的实例
	setTimeout(() => 
		console.log(this.royaltyName), 1000); 
}
function Queen() { 
	this.royaltyName = 'Elizabeth'; 
	// this引用window对象
	setTimeout(function() { 
		console.log(this.royaltyName); }, 1000); 
}
new King(); // Henry 
new Queen(); // undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# caller

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

function outer() { 
	inner(); 
}
function inner() { 
	console.log(inner.caller); //=arguments.callee.caller
}
outer();//显示 outer() 函数的源代码
1
2
3
4
5
6
7

# new.target

ECMAScript 6新增了检测函数是否使用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
9

# 4.立即调用函数表达式

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

// IIFE 
(function () { 
	for (var i = 0; i < count; i++) { 
	console.log(i); 
} 
})(); 
console.log(i); // 抛出错误	
1
2
3
4
5
6
7
  • ECMAScript 5.1及以前,为了防止变量定义外泄,IIFE是个非常有效的方式。这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。为此,只要函数执行完毕,其作用域链就可以被销毁。
  • 在ECMAScript 6以后,IIFE就没有那么必要了,因为块级作用域中的变量无须IIFE就可以实现同样的隔离。
// JavaScript 中有一个圆括号运算符,圆括号里面可以放一个表达式,比如下面的代码:
(a=3) // 括号里面是一个表达式,整个语句也是一个表达式,最终输出 3。

// 如果在小括号里面放上一段函数的定义,V8 就会把这个函数看成是函数表达式
(function () {
//statements
})

// 存放在括号里面的函数便是一个函数表达式,它会返回一个函数,如果加上调用的括号
(function () {
//statements
})()

//也可以这也写 
;(function () {
//statements
})()
// 分号是为了断句


//只要把函数声明变成表达式就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

和普通函数传参一样,立即执行函数也可以传递参数

//IIFE 
(function(name){
  var greeting = 'Hello';
  console.log(greeting+ ' ' +name);
})("world");

//Hello world
1
2
3
4
5
6
7