函数

函数定义

函数定义,也称为函数声明或函数语句,由function关键字组成。其后为:

  • 函数的名称
  • 函数参数列表
  • 定义函数的JavaScript语句,用大括号{}括起来

例子:

1
2
3
4
5
6
7
8
function myFunc(theObject) {
theObject.make = 'Toyota';
}
var mycar = {make: 'Honda', model: 'Accord', year: 1998};
var x, y;
x = mycar.make; // Honda
myFunc(mycar);
y = mycar.make; // Toyota

原始类型的参数将值传给函数,非原始类型的参数(比如对象、数组)将引用传给函数。

除了上面的函数声明方式,还可以通过函数表达式来创建函数。使用这种方法创建的函数可以是匿名的(即不需要函数名),例如:

1
var square = function(number) { return number * number; };

当然,函数表达式也可以提供函数名,并且可以用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数:

1
var factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};

还有第三种创建函数的方法,即使用Function构造函数来创建一个Function对象。

1
2
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6)); // 8

当函数是一个对象的属性时,它也叫方法。

调用函数

函数被定义后不会被自动执行,函数定义仅仅是赋予函数以名称并明确函数被调用时该做什么。调用函数才会真正执行定义函数时的动作,可以使用函数名加小括号()的方式调用它,例:

1
2
3
4
function test () {
console.log('function test');
}
test();

调用函数时,他们必须在作用域内(但会有函数声明提升,即声明在调用下方)。例:

1
2
3
4
test();
function test () {
console.log('function test');
}

作用域指函数声明时所处的地方,当函数在顶层被声明时就是整个程序。

函数作用域

函数内定义的变量不能再函数之外的任何地方访问,因为变量仅仅在该函数内部有定义。例:

1
2
3
4
function() {
var test = '1';
}
console.log(test); // undefined

作用域和函数堆栈

  • 递归

    调用自身的函数称为递归函数,一个函数可以指向调用自身。有三种方法可以达到这个目的:

    1. 函数名
    2. arguments.callee
    3. 作用域下的一个指向该函数的变量名

    例如:

    1
    let foo = function bar() {}

    在该函数体内,下面语句是等价的

    1. bar()
    2. arguments.callee()
    3. foo()

    在某种意义上递归近似于循环:两者都重复执行相同的代码,并且两者都需要一个终止条件。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 循环
    let x = 0;
    while (x < 10) {
    x++;
    }
    // 上面循环可以转化成下面的递归
    function loop() {
    if (x >= 10) return;
    loop(x + 1);
    }
    loop(0);
  • 嵌套函数和闭包

    你可以在一个函数里面嵌套另外一个函数,内部函数对内部函数来说是私有的。它自身也形成了一个闭包。闭包是一个表达式(最常见的是一个函数),可以拥有自身变量以及绑定这些变量的环境。这意味着内部函数可以访问外部函数的参数和变量。

    总结如下:

    • 内部函数只可以在外部函数中访问
    • 内部函数形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的函数和变量

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function out(x) {
    function inside(y) {
    return x + y;
    }
    return inside;
    }
    let fn_inside = out(3); // 返回一个函数
    let result = fn_inside(5); // 8

    let result1 = out(3)(5); // 8

    闭包必须在其引用的所有范围内保留参数和变量,由于每次调用都可能传入不同的参数,每一次外部函数的调用实际上重新创建了一遍这歌闭包。仅当返回的函数没有再被引用时,才释放内存。

    函数可以被多层嵌套,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function A(x) {
    function B(y) {
    function C(z) {
    console.log(x + y +z);
    }
    C(3);
    }
    B(1);
    }
    A(1); // 6

    函数B和C都形成了闭包,所以B可以访问A,C可以访问B和A。所以闭包可以包含多个作用域,它们递归式的包含了所有包含它的作用域,这被称为作用域链。

    当同一个闭包作用域下两个参数或者变量同名时会产生命名冲突,最近的作用域拥有更高的优先级,这就是作用域链。链的第一个元素就是最里面的元素,最后一个元素便是最外层的作用域。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    function outside() {
    let x = 5;
    function inside(x) {
    return x * 2;
    }
    return inside;
    }
    outside()(10); // 20

闭包

闭包是JavaScript最强大的特性之一,JavaScript允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。但是外部函数不能访问·定义在内部函数中的变量和函数。这给内部函数的变量提供了一定的安全性。此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生命周期大于外部函数时,外部函数中定义的变量和函数的生命周期将比内部函数执行时间长。当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let createPet = function(name) {
let sex;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
},
getSex: function() {
retuen sex;
},
setSex: function(newSex) {
if(typeof newSex == 'string' && (newSex.toLowerCase() == 'male' || newSex.toLowerCase() == 'female')) sex = newSex;
}
}
}

let pet = create('Pet');
pet.getName(); // Pet
pet.setName('Ppet');
pet.setSex('male');
pet.getName(); // ppet
pet.getSex(); // male

如果一个闭包的函数定义了一个和外部函数的某个变量名称相同的变量,那么这个闭包将无法引用外部函数的这个变量。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
let createPet = function(name) {
return {
setName: function(name) {
name = name;
},
getName: function() {
return name;
}
}
}
let pet = createPet('Pet');
pet.setName('Ppet');
pet.getName('); // Pet

arguments对象

函数的实际参数会被保存在一个类似数组的arguments对象中,在函数内,可以使用arguments[i]的形式获取传入的参数。例如:

1
2
3
4
5
6
7
8
9
function myConcat(separator) {
let result = '';
let i;
// 遍历获取所有参数
for (i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}

argumnets只是类数组对象,并不是一个数组,并不拥有全部的Array对象的操作方法。

函数参数

从ES6开始,有两个新类型的参数:默认参数剩余参数

默认参数

在JavaScript中,函数参数的默认值是undefined。在ES6之前,设置默认参数一般是在函数体中判断函数值是否为undefined,如果是则赋给这个参数一个默认值。例如:

1
2
3
4
5
6
7
8
9
10
11
function multiply(a, b) {
b = (typeof b !== 'undefined') ? b : 1;
return a * b;
}
multiply(5); // 5

// ES6中可以使用以下写法
function multiply(a, b = 1) {
return a * b;
}
multiply(5); // 5

剩余参数

rest语法允许将不确定数量的参数表示成一个数组。例如:

1
2
3
4
5
function multiply(a, ...theArgs) {
return theArgs.map(x => a * x);
}
let arr = multiply(1, 2, 3, 4);
console.log(arr); // [2, 3, 4]

箭头函数

箭头函数表达式相比函数表达式具有较短的语法并以词法的形式绑定this。箭头函数总是匿名的。

更简洁的函数
1
2
3
4
5
6
7
8
9
10
11
let arrs = [
"Hydrogen",
"Helium",
"Lithium",
"Beryllium"
]

let arr2 = arrs.map(function(s){ return s.length });
console.log(arr2); // logs [ 8, 6, 7, 9 ]
let arr3 = arrs.map( s => s.length );
console.log(arr3); // logs [ 8, 6, 7, 9 ]

this词法

在箭头函数之前,每一个函数都重新定义了自己的this值(在构造函数中是一个新的对象,在严格模式下是undefined,在作为对象方法调用的函数中指向这个对象)。例如:

1
2
3
4
5
6
7
function Person() {
this.age = 0;
setTimeout(function() {
console.log(this.age); // undefined
}, 1000)
}
let p = new Person();

在ES3或ES5中,通过把this的值赋值给一个变量赋值给一个变量可以修复这个问题。

1
2
3
4
5
6
7
8
function Person() {
let self = this;
self.age = 0;
setTimeout(function() {
console.log(self.age); // 0
}, 1000)
}
let p = new Person();

使用箭头函数可以捕捉闭包上下文的this值

1
2
3
4
5
6
7
8
function Person() {
let self = this;
self.age = 0;
setTimeout(() => {
console.log(self.age); // 0
}, 1000)
}
let p = new Person();

本文参考资料

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • © 2019-2021 musi

请我喝杯咖啡吧~

支付宝
微信