函数

定义:函数实际上是对象。每个函数都是Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。

函数定义

1.函数声明

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

2.函数表达式

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

3.箭头函数

1
let sum = (num1, num2) => {  return num1 + num2;  };

4.Function 构造函数

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

箭头函数

任何可以使用函数表达式的地方,都可以使用箭头函数

如果只有一个参数,那也可以不用括号。

只有没有参数,或者多个参数的情况下,才需要使用括号:

以下两种写法都有效

1
2
3
let double = (x) => { return 2 * x; }; 

let triple = x => { return 3 * x; };

没有参数需要括号

1
let getRandom = () => { return Math.random(); }; 

箭头函数不能使用 arguments(js的arguments到底是什么?-CSDN博客)、super([js中的super_js super-CSDN博客](https://blog.csdn.net/qq_35087256/article/details/82669618?ops_request_misc=%7B%22request%5Fid%22%3A%22169927501416800188547081%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=169927501416800188547081&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-82669618-null-null.142^v96^pc_search_result_base6&utm_term=js super&spm=1018.2226.3001.4187)) 和 new.target

函数名

一个函数可以有多个名称

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

即使函数没有名称, 也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成”anonymous“。

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

1

参数

ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。

定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。

arguments

arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是 arguments[0],第二个参数是 arguments[1])。而要确定传进来多少个参数, 可以访问 arguments.length 属性。

在下面的例子中,sayHi()函数的第一个参数叫 name:

1
2
3
function sayHi(name, message) {  

console.log("Hello " + name + ", " + message); }

可以通过 arguments[0]取得相同的参数值。

因此,把函数重写成不声明参数也可以:

1
2
3
function sayHi() {  

console.log("Hello " + arguments[0] + ", " + arguments[1]); }

在重写后的代码中,没有命名参数。name 和 message 参数都不见了,但函数照样可以调用。这就表明,ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。与其他语言不同,在 ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制。

也可以通过 arguments 对象的 length 属性检查传入的参数个数。

下面的例子展示了在每调用一 个函数时,都会打印出传入的参数个数:

1
2
3
4
5
6
7
8
9
function howManyArgs() {  

console.log(arguments.length); }

howManyArgs("string", 45); // 2

howManyArgs(); // 0

howManyArgs(12); // 1

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

1
2
3
4
5
6
7
8
9
10
11
12
function doAdd(num1, num2) {  

if (arguments.length === 1) {

console.log(num1 + 10);}

else if
(arguments.length === 2) {

console.log(arguments[0] + num2);
}
}

在这个 doAdd()函数中,同时使用了两个命名参数和 arguments 对象。命名参数 num1 保存着与 arugments[0]一样的值,因此使用谁都无所谓

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

没有重载

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

参数定义默认值和暂时性死区

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

来看下面的例子:

1
2
3
4
5
function makeKing(name = 'Henry', numerals = 'VIII') {  

return `King ${name} ${numerals}`; }

console.log(makeKing()); // King Henry VIII

这里的默认参数会按照定义它们的顺序依次被初始化。

可以依照如下示例想象一下这个过程:

1
2
3
4
5
function makeKing() { 

let name = 'Henry'; let numerals = 'VIII';

return `King ${name} ${numerals}`; }

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

看下面这个例子:

1
2
3
4
5
function makeKing(name = 'Henry', numerals = name) {  

return `King ${name} ${numerals}`; }

console.log(makeKing()); // King Henry Henry

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

像这样就会抛出错误

1
2
3
function makeKing(name = numerals, numerals = 'VIII') {  

return `King ${name} ${numerals}`; }

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

1
2
3
4
5
function makeKing(name = 'Henry', numerals = defaultNumeral) {  

let defaultNumeral = 'VIII';

return `King ${name} ${numerals}`; }

参数扩展与收集

通过扩展操作符极为简洁地实现这种操作。

对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。

比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:

2

1
console.log(getSum(...values)); // 10  

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

1
2
3
4
5
6
7
console.log(getSum(-1, ...values)); // 9  

console.log(getSum(...values, 5)); // 15

console.log(getSum(-1, ...values, 5)); // 14

console.log(getSum(...values, ...[5,6,7])); // 28

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。

函数声明与函数表达式

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

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

1
2
3
4
5
6
7
8
// 会出错 

console.log(sum(10, 10));

let sum = function(num1, num2) //这样创建的函数叫作匿名函数

{ return num1 + num2; };

函数内部

在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增了 new.target 属性

arguments 对象前面讨论过多次了,它是一个类数组对象,包含调用函数时传入的所有参数。这 个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。虽然主要用于包含函数参数,但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针

callee

来看下面这个经典的阶乘函数:

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

阶乘函数一般定义成递归调用的,就像上面这个例子一样。只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。

但是,这个函数要正确执行就必须保证函数名是 factorial,从而导致了紧密耦合。

使用 arguments.callee 就可以让函数逻辑与函数名解耦

1
2
3
4
5
6
function factorial(num) {  
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1); }
}

这个重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。 这意味着无论函数叫什么名称,都可以引用正确的函数

this

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

来看下面的例子:

1
2
3
4
5
6
7
8
9
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引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor() 的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的

1
2
3
4
5
6
7
8
window.color = 'red';  
let o = { color: 'blue' };

let sayColor = () => console.log(this.color);

sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'

在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文:

当函数不是作为对象的方法被调用时,this 默认指向全局对象,在浏览器环境中通常是 window 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

new.target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fn(name) {
console.log('fn:',new.target)
}
fn('nanjiu') // undefined
new fn('nanjiu')
//输出结果:
/*fn: ƒ fn(name) {
console.log('fn:',new.target)
}
fn 被作为构造函数使用,因此 new.target 的值为指向 fn 函数本身的函数定义。*/
let fn2 = (name) => {
console.log('fn2',new.target)
}
fn2('nan')
// 箭头函数没有自己的 new.target 绑定,因此在箭头函数内部访问 new.target 会导致语法错误。
// 报错 Uncaught SyntaxError: new.target expression is not allowed here

函数属性与方法

ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length 和 prototype。

length

length 属性保存函数定义的命名参数的个数,

1
2
3
4
5
6
7
8
9
10
11
12
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

prototype

prototype属性是保存引用类型所有实例方法的地方,这意味着 toString()valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享。

apply

apply()方法接收两个参数:函数内 this 的值和一个参数数组。第二个参数可以是 Array 的实例,但也可以是 arguments 对象。

1
2
3
4
5
6
7
8
9
10
11
12
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

在这个例子中,callSum1()会调用sum()函数,将 this 作为函数体内的 this 值(这里等于 window,因为是在全局作用域中调用的)传入,同时还传入了 arguments 对象。callSum2()也会调用 sum()函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。

call

call()方法与 apply()的作用一样,只是传参的形式不同。第一个参数跟 apply()一样,也是 this 值,而剩下的要传给被调用函数的参数则是逐个传递的。

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

总结:apply()和 call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内 this 值的能力。使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域

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

递归

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

1
2
3
4
5
6
7
8
9
10
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 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下所示

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

不过,在严格模式strict下运行的代码是不能访问 arguments.callee 的

立即调用的函数表达式(IIFE)

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

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

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。

为了防止变量定义外泄,IIFE 是个非常有效的方式。 这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。