0%

JavaScript进阶笔记2

3. 函数

3.1 定义

在 JavaScript 中,函数实际上也是一种对象,马哥函数都是 Function类型的实例,能够定义不同类型的属性与方法。函数的定义大体上可以分为三种,分别是函数声明,函数表达式和 Function 构造函数

3.1.1 函数声明

函数声明直接使用 function 关键字接一个函数名,函数名后是接受函数的形参。

1
2
3
4
function sum(x, y) {
return x + y
}
sum(1, 2)
3.1.2 函数表达式

函数表达式的形式类似于变量的初始化,只不过这个变量初始化的值是一个函数。

1
2
3
4
var sum = function(x, y) {
return x + y
}
sum(1, 2)

这个函数表达式没有名称,属于匿名函数表达式。

3.1.3 Function构造函数(可选)

使用new操作符,调用 Function() 构造函数,传入对应的参数,也可以定义一个函数。

1
2
const add = new Function("x", "y", "return x + y")
console.log(add(1, 2)) // 3

其中的参数,除了最后一个参数时执行的函数体,其他参数都是函数的形参。相比于前两种函数的声明方式,构造方式声明函数用的少一些。

  • Function() 构造函数在执行时,都会解析函数的执行主体,并创建一个新的函数对象。
  • 使用 Function() 构造函数,并不遵从典型的作用域,它将抑制作为顶层函数执行。
3.1.4 函数声明与函数表达式的区别
  • 函数名称。

    在使用函数声明时,必须设置函数名,这个函数名相当于一个变量,以后函数调用也会通过这个变量执行。对于函数表达式来说,函数名称是可选的,我们可以定义一个匿名函数表达式,并赋给一个变量,然后通过这个变量进行函数调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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
2
3
4
5
6
7
8
function foo() {
console.log(arguments.length) // 3
function foo2() {
console.log(arguments.length) // 0
}
foo2()
}
foo(1, 2, 3)
3.3.2 可通过索引访问

arguments 对象是一个类数组结构,可以通过索引访问,每一项表示对象传递的实参值,如果该项索引不存在,则会返回 undefined 。

1
2
3
4
5
6
7
8
9
function sum(x, y) {
console.log(arguments[0]) // 1
console.log(arguments[1]) // 2
console.log(arguments[2]) // undefined
console.log(x) // 1
console.log(y) // 2
console.log(arguments) // [1,2]
}
sum(1, 2)

arguments 对象的值由实参决定,而不易由定义的形参决定,形参与 arguments 对象占用对立的内存空间。

  1. arguments 对象的 length 属性在函数调用的时候就已经确定,不会随着寒霜处理而改变
  2. 指定的形参在传递实参的情况下,arguments 对象与形参值相同,并且可以相互改变。
  3. 指定的形参在未传递实参的情况下,arguments 对象对应索引值返回 undefined 。
  4. 指定的形参在未传递实参的情况下,arguments 对象与形参值不能相互改变。
1
2
3
4
5
6
7
8
9
10
11
function sum(x, y) {
console.log(arguments[0]) // undefined
console.log(arguments[1]) // undefined
console.log(x) // undefined
console.log(y) // undefined
arguments[0] = 100
console.log(arguments[0]) // 100
console.log(x) // 注意,依然为 undefined
console.log(arguments) // [100]
}
sum() // 实际没有传递参数
3.3.3 arguments.callee 属性

arguments 对象有一个特殊的属性 callee,表示当前正在执行的函数,在比较时严格相等。注:了解即可

1
2
3
4
function foo() {
console.log(arguments.callee === foo)
}
foo()

通过 arguments.callee属性获取到函数对象后,可以直接传递参数进行调用,这个属性在匿名递归函数中非常有用。

1
2
3
4
5
6
7
8
function create() {
return function(n) {
if(n<=1) return 1;
return arguments.callee(n - 1)
};
}
const result = create()(5);
console.log(result);

3.4 arguments对象的应用

3.4.1 实参个数判断

定义一个函数,明确要求在调用时只能传递3个参数,如果传递的参数个数不等于3个,则直接抛出异常。

1
2
3
4
5
6
7
8
function f(x, y, z) {
console.log(x, y, z);
// 检查传递参数的个数
if (arguments.length !== 3) {
throw new Error('参数个数不对');
}
}
f(1, 2) // Uncaught Error: 参数个数不对

3.4.2 任意个数参数的处理

定义一个函数,该函数只会特定的处理传递的前几个参数,对于后面的参数不论传递多少个都会同意处理,这种场景下我们可以使用 arguments 对象。

3.4.3 模拟函数重载

函数重载表示的是在函数名相同的情况下,通过函数形参的不同参数类型或者不同参数个数来定义不同的参数,但是 JavaScript 中没有函数重载的,主要有以下几个原因导致 JavaScript没有函数重载:

  1. JavaScript 是一门弱类型语言,变量只有在使用时才能确定数据类型,通过形参是无法确定类型的
  2. 无法通过函数参数的个数来指定调用不同的函数,函数的参数的个数实际在函数调用时才确定下来
  3. 使用函数声明定义的具有相同名称的函数,后者会覆盖前者。
1
2
3
4
5
6
7
8
9
10
function sum(num1, num2) {
return num1 + num2
}

function sum(num1, num2, num3) {
return num1 + num2 + num3
}

console.log(sum(1, 2))
console.log(sum(1, 2, 3))

遇到这种情况我们就需要写一个通用的函数,来实现任意个数字相加的结果求和。

1
2
3
4
5
6
7
8
9
10
11
function f(x) {
// 把参数变成数组
const arr = Array.prototype.slice.call(arguments)
let sum
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
f(1, 2)
f(1, 2, 3)

7. 闭包

7.1 闭包概念

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

闭包特点:

  1. 函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
  2. 闭包作为一个函数返回时,其执行上下文环境不会被销毁,扔处于执行上下文环境中。

在 JavaScript 中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

1
2
3
4
5
6
7
8
9
10
function fn() {
let max = 10
return function(x) {
if(x > max) {
console.log(x)
}
}
}
const f1 = fn()
f1(11) // 11

7.2 闭包用途

在了解了什么实际闭包之后,我们可以根据闭包的特点,写出一些更加简洁优美的代码。

7.2.1 结果缓存

在开发过程中,我们可能会遇到这样的场景,假如有一个处理很耗时的函数对象,每次用都会消耗很长时间。我们可以将其处理结果在内存中缓存起来。这样在执行代码时,如果内存中有,则直接返回,如果内存中没有,则调用函数并返回结果。因为闭包不会释放外部变量的引用,所以能够将外部变量值缓存在内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var cacheBox = (function () {
// 缓存的容器
var cache = {}
return {
searchBox: function (id) {
// 如果在内存中,则直接返回
if (id in cache) {
return '查找的结果为:' + cache[id]
}
// 经过一段很耗时的dealFn()函数处理
var result = this.dealFn(id)
// 更新缓存结果
cache[id] = result
// 返回计算的结果
return '查找的结果为:' + result
},
dealFn: function (id) {
console.log("很耗时的操作。。。")
return id
}
}
})()
console.log(cacheBox.searchBox(1)) // 很耗时的操作。。。查找的结果为:1
console.log(cacheBox.searchBox(1)) // 查找的结果为:1
7.2.2 定时器问题

定时器 setTimeOut() 函数和 for 循环在一起使用,总会出现一些意想不到的结果:

1
2
3
4
5
6
const arr = ['one', 'two', 'three']
for (var i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(arr[i])
}, i * 1000)
}

想要间隔一秒输出数组中的数据,但是运行结果却输出 undefined,这是为什么呢?因为 for 循环的结束条件 i 变成 3 所以 arr[3] 找不到,结果就是三次 undefined。

通过闭包可以解决这个问题:

1
2
3
4
5
6
7
8
const arr = ['one', 'two', 'three']
for(let i=0; i < arr.length; i++) {
(function(time){
setTimeout(function(){
console.log(arr[time])
}, time * 1000)
})(i)
}

以前的解决办法,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
2
3
4
5
6
7
8
9
10
let name = "outer"
const obj = {
name: 'inner',
method: function() {
return function() {
return this.name
}
}
}
console.log(obj.method()()) // outer

在调用obj.method()函数时,会返回一个匿名函数,而该匿名函数中返回的是 this.name,因为引用到了 this 属性,在匿名函数中,this 相当于一个外部变量,所以会形成一个闭包。

在 JavaScript 中,this 指向的永远是函数的调用实体,而匿名函数的实体是全局对象 window,因此输出全局变量 name 的值是 outer。

如果想输出 obj 对象自身 name 属性。就要改变 this 的指向,将其指向 obj对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
let name = "outer"
const obj = {
name: 'inner',
method: function() {
// 用 _this 保存 obj 中的 this
var _this = this
return function() {
return _this.name
}
}
}
console.log(obj.method()()) // inner

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 闭包的优点
  1. 保护函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
  2. 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。
7.3.2 闭包的缺点
  1. 消耗内存,通常来说,函数的活动对象会随着执行的上下文环境一起被销毁,但是由于闭包引用的事外部函数活动的对象,由此这个对象无法被销毁,这意味着闭包比普通函数要消耗更多的内存。
  2. 内存泄露,在 IE9 之前,如果闭包作用域链中存在 DOM 对象,则意味着该 DOM 对象无法被销毁,造成内存泄露。
1
2
3
4
5
6
function closure() {
var element = document.getElementId('eId')
element.onclick = function() {
console.log(element.id) // 函数内部存在对外部 element 对象的引用
}
}

需要手动将 element 元素设置为 null

1
2
3
4
5
6
7
8
9
10
function closure() {
var element = document.getElementId('eId')
// 使用临时变量存储
var id = element.id
element.onclick = function() {
console.log(id) // 函数内部存在对外部 element 对象的引用
}
// 手动将元素设置为 null
element = null
}
赞赏是最好的支持