概述

声明方法

  • 普通方法
  • 赋值语句(将 匿名函数 赋值给某个变量)

第一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(x, y) {
return x + y;
}

// 将函数赋值给一个变量
var operator = add;

// 将函数作为参数和返回值
function a(op){
return op;
}
a(operator)(1, 1)
// 2

函数名提升与重复声明

  • 用赋值语句定义的函数 不能 在调用前声明,因为变量提升到前面后并未赋值,相当于undefined
  • 采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。
    • 而一般情况下,重复声明,后面声明的会覆盖前面的

属性和方法

  • name属性:函数的名字/匿名函数声明时赋给的变量名
    • 可以用于获取传入的参数函数的名字
  • length属性:返回 定义 的参数个数
  • toString()方法返回一个字符串,内容是函数的源码。
    • 函数内部的注释也可以返回。利用这一点,可以变相实现多行字符串。[1]
    • 对于那些原生的函数,toString()方法返回function (){[native code]}

函数作用域scope

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6新增)

函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

参数

参数的省略

没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined

传递方式

值传递与引用传递

值传递

方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的原始值得一个copy,此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值得修改,不影响实际参数的值

引用传递

一般也称为地址传递。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的原始值的内存地址

变量p是一个原始类型的值,传入函数f的方式是传值传递。

如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。

如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。(重新对o赋值导致o指向另一个地址,保存在原地址上的值当然不受影响。)

同名参数

如果有同名的参数,则取最后出现的那个值。

arguments对象

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

  • 数组专有的方法(比如sliceforEach),不能在arguments对象上直接使用。

callee 属性

arguments对象带有一个callee属性,返回它所对应的原函数。

可以通过arguments.callee(),达到调用函数自身的目的。( ES5禁止在严格模式下使用此属性 )

闭包

JavaScript 允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。[2]

但是,嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。这给内部函数的变量提供了一定的 安全性

总结如下:

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

由于内部函数可以访问外部函数的作用域,因此当内部函数 生存周期 大于外部函数时,外部函数中定义的变量和函数的生存周期将比内部函数执行时间长。当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。

就是说,一个闭包必须 保存它可见作用域中所有参数和变量 。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。

1
2
3
4
5
6
7
8
9
10
11
function createIncrementor(start) {
return function () {
return start++;
};
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

闭包的用处:

  1. 读取外层函数内部的变量
  2. 让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
  3. 封装对象的私有属性和私有方法

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

JavaScript 规定,如果function关键字出现在行首,一律解释成语句。因此,引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

函数定义后立即调用的解决方法,最简单的处理,就是将其放在一个圆括号里面。(Immediately-Invoked Function Expression),简称 IIFE。(注意分号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SyntaxError: Unexpected token (
function(){ /* code */ }();

// correct
var f = function f(){ return 1}();
f // 1
true && function(){ /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();

// IIFE
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:

  1. 不必为函数命名,避免了污染全局变量;
  2. IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

箭头函数

箭头函数总是匿名的 ref 箭头函数表达式 (mozilla.org)/深度了解ES6:箭头函数 (mozilla.org)

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

预定义函数

函数 - JavaScript | MDN (mozilla.org)

eval

eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。


  1. 函数 - JavaScript 教程 - 网道 (wangdoc.com) ↩︎

  2. 函数 - JavaScript | MDN (mozilla.org) ↩︎