函数
函数
定义:函数实际上是对象。每个函数都是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 | let double = (x) => { return 2 * 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()实例化,那么标识符前面会加上一个前缀:
参数
ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。
定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。
arguments
arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是 arguments[0],第二个参数是 arguments[1])。而要确定传进来多少个参数, 可以访问 arguments.length 属性。
在下面的例子中,sayHi()函数的第一个参数叫 name:
1 | function sayHi(name, message) { |
可以通过 arguments[0]取得相同的参数值。
因此,把函数重写成不声明参数也可以:
1 | function sayHi() { |
在重写后的代码中,没有命名参数。name 和 message 参数都不见了,但函数照样可以调用。这就表明,ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。与其他语言不同,在 ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制。
也可以通过 arguments 对象的 length 属性检查传入的参数个数。
下面的例子展示了在每调用一 个函数时,都会打印出传入的参数个数:
1 | function howManyArgs() { |
还有一个必须理解的重要方面,那就是 arguments 对象可以跟命名参数一起使用,比如:
1 | function doAdd(num1, num2) { |
在这个 doAdd()函数中,同时使用了两个命名参数和 arguments 对象。命名参数 num1 保存着与 arugments[0]一样的值,因此使用谁都无所谓
ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作 为参数传递,那么传递的值就是这个对象的引用
没有重载
ECMAScript 函数不能像传统编程那样重载。
参数定义默认值和暂时性死区
给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。
来看下面的例子:
1 | function makeKing(name = 'Henry', numerals = 'VIII') { |
这里的默认参数会按照定义它们的顺序依次被初始化。
可以依照如下示例想象一下这个过程:
1 | function makeKing() { |
因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。
看下面这个例子:
1 | function makeKing(name = 'Henry', numerals = name) { |
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。
像这样就会抛出错误
1 | function makeKing(name = numerals, numerals = 'VIII') { |
参数也存在于自己的作用域中,它们不能引用函数体的作用域:
1 | function makeKing(name = 'Henry', numerals = defaultNumeral) { |
参数扩展与收集
通过扩展操作符极为简洁地实现这种操作。
对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。
比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:
1 | console.log(getSum(...values)); // 10 |
因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:
1 | console.log(getSum(-1, ...values)); // 9 |
在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。
函数声明与函数表达式
JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个 过程叫作函数声明提升。在执行代码时,JavaScript 引擎会先执行一遍扫描, 把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:
1 | // 会出错 |
函数内部
在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增了 new.target 属性
arguments 对象前面讨论过多次了,它是一个类数组对象,包含调用函数时传入的所有参数。这 个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。虽然主要用于包含函数参数,但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。
callee
来看下面这个经典的阶乘函数:
1 | function factorial(num) { |
阶乘函数一般定义成递归调用的,就像上面这个例子一样。只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。
但是,这个函数要正确执行就必须保证函数名是 factorial,从而导致了紧密耦合。
使用 arguments.callee 就可以让函数逻辑与函数名解耦:
1 | function factorial(num) { |
这个重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。 这意味着无论函数叫什么名称,都可以引用正确的函数。
this
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在 网页的全局上下文中调用函数时,this 指向 windows)。
来看下面的例子:
1 | window.color = 'red'; |
定义在全局上下文中的函数 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 | window.color = 'red'; |
在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文:
当函数不是作为对象的方法被调用时,this
默认指向全局对象,在浏览器环境中通常是 window
对象。
1 | function King() { |
new.target
1 | function fn(name) { |
函数属性与方法
ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length 和 prototype。
length
length 属性保存函数定义的命名参数的个数,
1 | function sayName(name) { |
prototype
prototype属性是保存引用类型所有实例方法的地方,这意味着 toString()
、valueOf()
等方法实际上都保存在 prototype 上,进而由所有实例共享。
apply
apply()方法接收两个参数:函数内 this 的值和一个参数数组。第二个参数可以是 Array 的实例,但也可以是 arguments 对象。
1 | function sum(num1, num2) { |
在这个例子中,callSum1()会调用sum()函数,将 this 作为函数体内的 this 值(这里等于 window,因为是在全局作用域中调用的)传入,同时还传入了 arguments 对象。callSum2()也会调用 sum()函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。
call
call()方法与 apply()的作用一样,只是传参的形式不同。第一个参数跟 apply()一样,也是 this 值,而剩下的要传给被调用函数的参数则是逐个传递的。
1 | function sum(num1, num2) { |
总结:apply()和 call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内 this 值的能力。使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域
1 | window.color = 'red'; |
递归
这是经典的递归阶乘函数。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题:
1 | function factorial(num) { |
这里把factorial()函数保存在了另一个变量 anotherFactorial 中,然后将 factorial 设置 为 null,于是只保留了一个对原始函数的引用。而在调用 anotherFactorial()时,要递归调用 factorial(),但因为它已经不是函数了,所以会出错。在写递归函数时使用 arguments.callee 可以避免这个问题。
arguments.callee 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下所示
1 | function factorial(num) { |
不过,在严格模式strict下运行的代码是不能访问 arguments.callee 的
立即调用的函数表达式(IIFE)
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。下面是一个简单的例子:
1 | (function() {//块级作用域 })(); |
使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。
为了防止变量定义外泄,IIFE 是个非常有效的方式。 这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。