函数

原文链接

1 概述

1.1 声明

function print(s) {
  console.log(s);
}

除了 function 命令外,还可以采用变量赋值的写法,声明一个匿名函数。这种写法又称函数表达式。

var print = function(s) {
  console.log(s);
};

如果在 function 后加上函数名,那么该函数名只在函数体内有效。这种写法的用处有两个,一是在函数体内调用自身,二是在函数调用栈中显示名称。

var f = function f(s) {
  console.log(s);
};

注意,函数表达式要在结尾加上分号,表示语句结束,而函数声明的大括号后面不用加。

第三种方式是构造函数。构造函数可以接收任意数量的参数,只有最后一个参数会被当作函数体。如果只有一个参数,该参数就是函数体。

var add = new Function(
  'x',
  'y',
  'return x + y'
);
// 等同于
function add(x, y) {
  return x + y;
}

Function 构造函数甚至可以不使用 new 命令,结果完全一样。总的来说,这种非常不直观的声明函数方式几乎无人使用。

1.2 第一等公民

由于函数与其他数据类型地位平等,所以又称函数为第一等公民。

1.3 函数名提升

JavaScript 引擎将函数名视同变量名,与变量一样,函数会被提升到代码头部。如果一个函数被多次声明,后者覆盖前者。

但是采用函数表达式定义函数时, JavaScript 就会报错。

f();
var f = function () {};
// TypeError: undefined is not a function
// 等同于
var f;
f();
f = function () {};

2 属性和方法

2.1 name

function f1() {}
f1.name // "f1"

var f2 = function () {};
f2.name // "f2"

var f3 = function myName() {};
f3.name // 'myName'

注意,真正的函数名还是 f3 ,而 myName 这个名字只在函数体内部可用。

name 常用于获取参数的函数名。

var myFunc = function () {};

function test(f) {
  console.log(f.name);
}

test(myFunc) // myFunc

2.2 length

无论调用时输入了多少参数,它只返回函数定义时的参数个数。

2.3 toString()

返回函数源码,包括注释。利用这一点,可以变相实现多行字符串。

var multiline = function (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};

function f() {/*
  这是一个
  多行注释
*/}

multiline(f);
// " 这是一个
//   多行注释"

3 作用域

作用域 scope 指的是变量的有效范围。在 ES5 的规范中, JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。 ES6 新增了区块作用域,本教程不涉及。

函数内部定义的局部变量会在它的作用域内覆盖同名全局变量。

var 命令声明的变量都会被提升到函数体的头部。

函数本身也是一个值,也有自己的作用域。函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

var x = function () {
  console.log(a);
};

function y(f) {
  var a = 2;
  f();
}

y(x)
// ReferenceError: a is not defined

4 参数

4.1 省略

运行时无论提供多少个参数都不会报错,省略的参数值为 undefined 。不能省略靠前的参数,如果一定要省略,必须显式地传入 undefined 。

再次提醒,函数的 length 属性与实际传入的参数无关个数,只反映预期传入的参数个数。

4.2 传递

原始类型参数的传递方式是值传递。复合类型参数的传递方式是地址传递,也就是说,在函数内修改参数将影响原值。注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

var obj = [1, 2, 3];

function f(o) {
  o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]

4.3 同名

函数定义时有同名参数时取后者。即使后者没有值或被省略,也以其为准。

function f(a, a) {
  console.log(a);
}

f(1) // undefined

4.4 arguments 对象

arguments 对象包含了函数运行时的所有参数, arguments[0] 就是第一个参数, arguments[1] 就是第二个参数,以此类推。该对象只能在函数体内部使用。通过 arguments 对象的 length 属性,可以判断函数调用时到底传入了几个参数。

正常模式下, arguments 对象修改有效。

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 5

严格模式下, arguments 对象修改无效。

var f = function(a, b) {
  'use strict'; // 开启严格模式
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 2

注意,虽然 arguments 很像数组,但它是一个对象,不能直接使用数组的方法!

arguments 对象还有一个 callee 属性,返回原函数,该属性在严格模式下禁用。

5 闭包

正常情况下,函数外部无法读取函数内部声明的变量。

function f1() {
  var n = 999;
}

console.log(n) // Uncaught ReferenceError: n is not defined(

于是在函数内部再定义一个函数,并将它作为返回值。因为返回值是一个函数,它可以运行!

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

函数 f2 就是闭包,即能读取其他函数内部变量的函数,变量始终存活在内存中。

6 立即调用的函数表达

原理略。

// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 写法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

写法二比写法一更好,避免了对全局变量的污染。

7 eval

eval 命令将字符串参数当作语句执行。

eval('var a = 1;');
a // 1

eval 没有自己的作用域,可能会修改当前作用域下变量的值。

var a = 1;
eval('a = 2');
a // 2

严格模式虽然可以控制 eval 内声明的变量,但对于在 eval 内修改的变量依旧无能为力。更麻烦的是,别名调用五花八门,难以确认。总之,不推荐使用。