3. 函数
3.1 定义
在 JavaScript 中,函数实际上也是一种对象,马哥函数都是 Function
类型的实例,能够定义不同类型的属性与方法。函数的定义大体上可以分为三种,分别是函数声明,函数表达式和 Function 构造函数
3.1.1 函数声明
函数声明直接使用 function
关键字接一个函数名,函数名后是接受函数的形参。
1 | function sum(x, y) { |
3.1.2 函数表达式
函数表达式的形式类似于变量的初始化,只不过这个变量初始化的值是一个函数。
1 | var sum = function(x, y) { |
这个函数表达式没有名称,属于匿名函数表达式。
3.1.3 Function构造函数(可选)
使用new
操作符,调用 Function() 构造函数,传入对应的参数,也可以定义一个函数。
1 | const add = new Function("x", "y", "return x + y") |
其中的参数,除了最后一个参数时执行的函数体,其他参数都是函数的形参。相比于前两种函数的声明方式,构造方式声明函数用的少一些。
- Function() 构造函数在执行时,都会解析函数的执行主体,并创建一个新的函数对象。
- 使用 Function() 构造函数,并不遵从典型的作用域,它将抑制作为顶层函数执行。
3.1.4 函数声明与函数表达式的区别
函数名称。
在使用函数声明时,必须设置函数名,这个函数名相当于一个变量,以后函数调用也会通过这个变量执行。对于函数表达式来说,函数名称是可选的,我们可以定义一个匿名函数表达式,并赋给一个变量,然后通过这个变量进行函数调用。
1
2
3
4
5
6
7
8
9
10
11function sum(x, y) {
return x + y
}
var sum = function(x, y) {
return x + y
}
var sum = function foo(x, y) {
return x + y
}函数提升
对于函数声明,存在函数提升,也就是我们可以在函数声明之前调用函数。
对于函数表达式,不存在函数提升,所有在定义函数前不能对其进行调用。
3.2 函数参数
3.2.1 形参和实参
函数的参数分两种,一种是形参另外一种是实参。形参全称是形式参数,是在定义函数名称与函数体时使用的参数,目的使用接收调用该函数时传入的参数。实参全称是实际参数,是在调用是传递给函数的参数,实参可以是常量、变量、表达式、函数等类型。
3.3 arguments 对象的性质
arguments
对象是所有函数都具有的一个内置局部变量,表示的是函数实际接收的参数,是一个类数组结构,之所以说 arguments 对象是一个类数组结构,是因为它除了具有 length 属性之外,不具有数组的一些常用方法。
3.3.1 函数外部无法访问
arguments 对象只能在函数内部访问,无法在函数外部访问到 arguments 对象,同时 arguments 对象存在于作用域中,一个函数无法直接获取另一个函数的 arguments 对象。
1 | function foo() { |
3.3.2 可通过索引访问
arguments 对象是一个类数组结构,可以通过索引访问,每一项表示对象传递的实参值,如果该项索引不存在,则会返回 undefined 。
1 | function sum(x, y) { |
arguments 对象的值由实参决定,而不易由定义的形参决定,形参与 arguments 对象占用对立的内存空间。
- arguments 对象的 length 属性在函数调用的时候就已经确定,不会随着寒霜处理而改变
- 指定的形参在传递实参的情况下,arguments 对象与形参值相同,并且可以相互改变。
- 指定的形参在未传递实参的情况下,arguments 对象对应索引值返回 undefined 。
- 指定的形参在未传递实参的情况下,arguments 对象与形参值不能相互改变。
1 | function sum(x, y) { |
3.3.3 arguments.callee 属性
arguments 对象有一个特殊的属性 callee
,表示当前正在执行的函数,在比较时严格相等。注:了解即可
1 | function foo() { |
通过 arguments.callee
属性获取到函数对象后,可以直接传递参数进行调用,这个属性在匿名递归函数中非常有用。
1 | function create() { |
3.4 arguments对象的应用
3.4.1 实参个数判断
定义一个函数,明确要求在调用时只能传递3个参数,如果传递的参数个数不等于3个,则直接抛出异常。
1 | function f(x, y, z) { |
3.4.2 任意个数参数的处理
定义一个函数,该函数只会特定的处理传递的前几个参数,对于后面的参数不论传递多少个都会同意处理,这种场景下我们可以使用 arguments 对象。
3.4.3 模拟函数重载
函数重载表示的是在函数名相同的情况下,通过函数形参的不同参数类型或者不同参数个数来定义不同的参数,但是 JavaScript 中没有函数重载的,主要有以下几个原因导致 JavaScript没有函数重载:
- JavaScript 是一门弱类型语言,变量只有在使用时才能确定数据类型,通过形参是无法确定类型的
- 无法通过函数参数的个数来指定调用不同的函数,函数的参数的个数实际在函数调用时才确定下来
- 使用函数声明定义的具有相同名称的函数,后者会覆盖前者。
1 | function sum(num1, num2) { |
遇到这种情况我们就需要写一个通用的函数,来实现任意个数字相加的结果求和。
1 | function f(x) { |
7. 闭包
7.1 闭包概念
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包特点:
- 函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
- 闭包作为一个函数返回时,其执行上下文环境不会被销毁,扔处于执行上下文环境中。
在 JavaScript 中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
1 | function fn() { |
7.2 闭包用途
在了解了什么实际闭包之后,我们可以根据闭包的特点,写出一些更加简洁优美的代码。
7.2.1 结果缓存
在开发过程中,我们可能会遇到这样的场景,假如有一个处理很耗时的函数对象,每次用都会消耗很长时间。我们可以将其处理结果在内存中缓存起来。这样在执行代码时,如果内存中有,则直接返回,如果内存中没有,则调用函数并返回结果。因为闭包不会释放外部变量的引用,所以能够将外部变量值缓存在内存中。
1 | var cacheBox = (function () { |
7.2.2 定时器问题
定时器 setTimeOut() 函数和 for 循环在一起使用,总会出现一些意想不到的结果:
1 | const arr = ['one', 'two', 'three'] |
想要间隔一秒输出数组中的数据,但是运行结果却输出 undefined,这是为什么呢?因为 for 循环的结束条件 i 变成 3 所以 arr[3] 找不到,结果就是三次 undefined。
通过闭包可以解决这个问题:
1 | const arr = ['one', 'two', 'three'] |
以前的解决办法,ES6 之后,使用 let 替换 var 即可。
1
2
3
4
5
6 const arr = ['one', 'two', 'three']
for (let i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(arr[i])
}, i * 1000)
}
7.2.3 作用域链问题
闭包往往会设计到作用域链的问题,尤其是包含 this
属性时。
1 | let name = "outer" |
在调用obj.method()
函数时,会返回一个匿名函数,而该匿名函数中返回的是 this.name,因为引用到了 this 属性,在匿名函数中,this 相当于一个外部变量,所以会形成一个闭包。
在 JavaScript 中,this 指向的永远是函数的调用实体,而匿名函数的实体是全局对象 window,因此输出全局变量 name 的值是 outer。
如果想输出 obj 对象自身 name 属性。就要改变 this 的指向,将其指向 obj对象本身。
1 | let name = "outer" |
ES6 之后,使用箭头函数
()=>{}
代替内部的匿名函数,修复 this 指向的问题。
1
2
3
4
5
6
7
8
9
10 let name = "outer"
const obj = {
name: 'inner',
method: function () {
return () => { // 箭头函数
return this.name
}
}
}
console.log(obj.method()()) // inner
7.3 闭包总结
闭包如果使用合理,在一定程度上能够提高代码的执行效率,如果使用不合理,则会造成内存浪费,性能下降。
7.3.1 闭包的优点
- 保护函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
- 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。
7.3.2 闭包的缺点
- 消耗内存,通常来说,函数的活动对象会随着执行的上下文环境一起被销毁,但是由于闭包引用的事外部函数活动的对象,由此这个对象无法被销毁,这意味着闭包比普通函数要消耗更多的内存。
- 内存泄露,在 IE9 之前,如果闭包作用域链中存在 DOM 对象,则意味着该 DOM 对象无法被销毁,造成内存泄露。
1 | function closure() { |
需要手动将 element 元素设置为 null
。
1 | function closure() { |