JavaScript 系列 - 函数

73 阅读6分钟

函数是 JavaScript 中的基本组件之一。一个函数是 JavaScript 过程 — 一组执行任务或计算值的语句。要使用一个函数,你必须将其定义在你希望调用它的作用域内。

定义函数

函数声明和函数表达式同时存在,函数表达式覆盖函数声明

函数声明

参数:原始参数和引用参数

  • 原始参数, 如果被调用函数改变了这个参数的值,这样的改变不会影响到全局或调用函数。
  • 引用参数,函数改变了这个对象的属性,这样的改变对函数外部是可见的
function myFunc(theObject) {
  theObject.make = "Toyota";
}

var mycar = { make: "Honda", model: "Accord", year: 1998 };
var x, y;

x = mycar.make; // x 获取的值为 "Honda"

myFunc(mycar);
y = mycar.make; // y 获取的值为 "Toyota"
// (make 属性被函数改变了)

函数表达式

  • 函数表达式可以是匿名

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

    const factorial = function fac(n) {
      return n < 2 ? 1 : n * fac(n - 1);
    };
    
    console.log(factorial(3));
    
  • 当将函数作为参数传递给另一个函数时,函数表达式很方便

  • 根据条件来定义一个函数

  • 使用用 Function 构造器由一个字符串来创建一个函数

  • 当一个函数是一个对象的属性时,称之为方法

调用函数

  • 调用函数才会以给定的参数真正执行这些动作
  • 函数一定要处于调用它们的域中,但是函数的声明可以被提升
    • 函数提升仅适用于函数声明,而不适用于函数表达式
    • 函数域是指函数声明时的所在的地方,或者函数在顶层被声明时指整个程序。
  • 函数可以被递归,就是说函数可以调用其本身
  • 动态调用函数,或者函数的实参数量是变化的,或者调用函数的上下文需要指定为在运行时确定的特定对象。
  • 函数本身就是对象也有方法
function Foo() {
  getName = function () {
    console.log(1);
  };
  return this;
}
Foo.getName = function () {
  console.log(2);
};
Foo.prototype.getName = function () {
  console.log(3);
};
var getName = function () {
  console.log(4);
};
function getName() {
  console.log(5);
}

//请写出以下输出结果:
// 调用静态方法
Foo.getName(); // 2
// 函数声明提升
// 函数表达式覆盖函数声明
getName(); // 4
// Foo方法里面的getName是全局变量
Foo().getName(); // 1
// 调用全局变量
getName(); // 1
// 成员访问 new(参数)
// var a = Foo.getName;
// new a();
new Foo.getName(); // 2
// var a = new Foo();
// var b = a.getName;
// b();
new Foo().getName(); // 3
// var a = new Foo();
// var b = a.getName;
// new b();
new new Foo().getName(); // 3

函数作用域

  • 在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。
  • 一个函数可以访问定义在其范围内的任何变量和函数。
  • 定义在全局域中的函数可以访问所有定义在全局域中的变量。
  • 在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量
// 下面的变量定义在全局作用域 (global scope) 中
var num1 = 20,
  num2 = 3,
  name = "Chamahk";

// 本函数定义在全局作用域
function multiply() {
  return num1 * num2;
}

multiply(); // 返回 60

// 嵌套函数的例子
function getScore() {
  var num1 = 2,
    num2 = 3;

  function add() {
    return name + " scored " + (num1 + num2);
  }

  return add();
}

getScore(); // 返回 "Chamahk scored 5"

作用域和函数堆栈

递归

一个函数可以指向并调用自身

  • 函数名
  • arguments.callee
  • 作用域下的一个指向该函数的变量名
var foo = function bar() {
  console.log(bar, arguments.callee, foo);
};
foo();

调用自身的函数我们称之为递归函数

递归可以和循环相互转化,将递归算法转换为循环可能逻辑上通常会更加复杂,而且需要使用堆栈。

var x = 0;
while (x < 10) {
  // "x < 10" 是循环条件
  // do stuff
  x++;
}
function loop(x) {
  if (x >= 10)
    // "x >= 10" 是退出条件(等同于 "!(x < 10)")
    return;
  // 做些什么
  loop(x + 1); // 递归调用
}
loop(0);

嵌套函数和闭包

你可以在一个函数里面嵌套另外一个函数

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

由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数:

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加 3
result = fn_inside(5); // returns 8

result1 = outside(3)(5); // returns 8

保存变量

注意到上例中 inside 被返回时 x 是怎么被保留下来的。一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的 inside 没有再被引用时,内存才会被释放。

多层嵌套函数

函数可以被多层嵌套。例如,函数 A 可以包含函数 B,函数 B 可以再包含函数 C。B 和 C 都形成了闭包,所以 B 可以访问 A,C 可以访问 B 和 A。因此,闭包可以包含多个作用域;他们递归式的包含了所有包含它的函数作用域。这个称之为作用域链

function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // logs 6 (1 + 2 + 3)

在这个例子里面,C 可以访问 B 的 y 和 A 的 x。这是因为:

  1. B 形成了一个包含 A 的闭包,B 可以访问 A 的参数和变量
  2. C 形成了一个包含 B 的闭包
  3. B 包含 A,所以 C 也包含 A,C 可以访问 B 和 A 的参数和变量。换言之,C 用这个顺序链接了 B 和 A 的作用域

命名冲突

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

function outside() {
  var x = 5;
  function inside(x) {
    return x * 2;
  }
  return inside;
}

outside()(10); // returns 20 instead of 10

命名冲突发生在return x上,inside的参数xoutside变量x发生了冲突。这里的作用链域是{insideoutside, 全局对象}。因此insidex具有最高优先权,返回了 20(insidex)而不是 10(outsidex)。

闭包

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

返回了一个包含可以操作外部函数的内部变量方法的对象。

var createPet = function (name) {
  var sex;

  return {
    getName: function () {
      return name;
    },
    setName: function (newName) {
      name = newName;
    },

    getSex: function () {
      return sex;
    },

    setSex: function (newSex) {
      if (
        typeof newSex == "string" &&
        (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")
      ) {
        sex = newSex;
      }
    },
  };
};

var pet = createPet("Vivie");
pet.getName(); // Vivie

pet.setName("Oliver");
pet.setSex("male");
pet.getSex(); // male
pet.getName(); // Oliver

内嵌函数可以不会被分配给一个变量,或者不必一定要有名字。

var getCode = (function () {
  var secureCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify...

  return function () {
    return secureCode;
  };
})();

getCode(); // Returns the secret code

使用 arguments 对象

函数的实际参数会被保存在一个类似数组的 arguments 对象中

使用 arguments 对象,你可以处理比声明的更多的参数来调用函数。这在你事先不知道会需要将多少参数传递给函数时十分有用。

function myConcat(separator) {
  var result = ""; // 把值初始化成一个字符串,这样就可以用来保存字符串了!!
  var i;
  // iterate through arguments
  for (i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}
// returns "red, orange, blue, "
myConcat(", ", "red", "orange", "blue");

// returns "elephant; giraffe; lion; cheetah; "
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");

// returns "sage. basil. oregano. pepper. parsley. "
myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");

函数参数

参数类型:默认参数,剩余参数

默认参数

在 JavaScript 中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值是有用的。这时默认参数可以提供帮助。

function multiply(a, b) {
  b = typeof b !== "undefined" ? b : 1;

  return a * b;
}

multiply(5); // 5
function multiply(a, b = 1) {
  return a * b;
}

multiply(5); // 5

剩余参数

剩余参数语法允许将不确定数量的参数表示为数组

function multiply(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier * x);
}

var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

箭头函数

箭头函数表达式(也称胖箭头函数)相比函数表达式具有较短的语法并以词法的方式绑定 this。箭头函数总是匿名的。

更简洁的函数

var a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];

var a2 = a.map(function (s) {
  return s.length;
});

console.log(a2); // logs [ 8, 6, 7, 9 ]

var a3 = a.map((s) => s.length);

console.log(a3); // logs [ 8, 6, 7, 9 ]

this 的词法

在箭头函数出现之前,每一个新函数都重新定义了自己的 this 值

  • 构造函数中是一个新的对象
  • 在严格模式下是 undefined
  • 在作为对象方法调用的函数中指向这个对象
function Person() {
  // 构造函数 Person() 将`this`定义为自身
  this.age = 0;

  setInterval(function growUp() {
    // 在非严格模式下,growUp() 函数将`this`定义为“全局对象”,
    // 这与 Person() 定义的`this`不同,
    // 所以下面的语句不会起到预期的效果。
    this.age++;
  }, 1000);
}

var p = new Person();
  • 通过把this的值赋值给一个变量可以修复这个问题
function Person() {
  var self = this; // 有的人习惯用`that`而不是`self`,
  // 无论你选择哪一种方式,请保持前后代码的一致性
  self.age = 0;

  setInterval(function growUp() {
    // 以下语句可以实现预期的功能
    self.age++;
  }, 1000);
}
  • 使用 bind
function Person() {
  // 构造函数 Person() 将`this`定义为自身
  this.age = 0;

  setInterval(
    function growUp() {
      // 在非严格模式下,growUp() 函数将`this`定义为“全局对象”,
      // 这与 Person() 定义的`this`不同,
      // 所以下面的语句不会起到预期的效果。
      this.age++;
      console.log(this.age);
    }.bind(this),
    1000
  );
}

var p = new Person();
  • 箭头函数捕捉闭包上下文的this
function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // 这里的`this`正确地指向 person 对象
  }, 1000);
}

var p = new Person();

预定义函数

  • eval()

    方法会对一串字符串形式的 JavaScript 代码字符求值

  • isFinite()

    函数判断传入的值是否是有限的数值。如果需要的话,其参数首先被转换为一个数值

  • isNaN()

    函数判断一个值是否是 NaN

  • parseFloat()

    函数解析字符串参数,并返回一个浮点数

  • parseInt()

    函数解析字符串参数,并返回指定的基数(基础数学中的数制)的整数

  • decodeURI()

    函数对先前经过 encodeURI 函数或者其他类似方法编码过的字符串进行解码

  • decodeURIComponent()

    方法对先前经过 encodeURIComponent 函数或者其他类似方法编码过的字符串进行解码

  • encodeURI()

    方法通过用以一个、两个、三个或四个转义序列表示字符的 UTF-8 编码替换统一资源标识符(URI)的某些字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)

  • encodeURIComponent()

    方法通过用以一个,两个,三个或四个转义序列表示字符的 UTF-8 编码替换统一资源标识符(URI)的每个字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)

实例属性

Function.prototype.displayName

获取函数的显示名称

该特性是非标准的,请尽量不要在生产环境中使用它

var object = {
  someMethod: function () {},
};

object.someMethod.displayName = "someMethod";

console.log(object.someMethod.displayName); // logs "someMethod"

try {
  someMethod;
} catch (e) {
  console.log(e);
}
// ReferenceError: someMethod is not defined

Function.prototype.length

指明函数的形参个数

  • 形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数
  • 不可写、不可枚举、可配置
  • arguments.length 是函数被调用时实际传参的个数
  • Function 构造器本身也是个 Function。它的 length 属性值为 1
  • Function.prototype 对象的 length 属性值为 0

Function.prototype.name

返回函数实例的名称

  • 不可写、不可枚举、可配置
  • 函数的 name 属性可用于在调试工具或错误消息中标识该函数。它对语言本身没有任何意义。

绑定函数的名称

Function.bind() 所创建的函数将会在函数的名称前加上"bound " 。

function foo() {}
foo.bind({}).name; // "bound foo"

getters 和 setters 的函数名

当通过 get 和 set 访问器来存取属性时,"get" 或 "set" 会出现在函数名称前。

var o = {
  get foo() {},
  set foo(x) {},
};

var descriptor = Object.getOwnPropertyDescriptor(o, "foo");
descriptor.get.name; // "get foo"
descriptor.set.name; // "set foo";

类中的函数名称

你可以使用obj.constructor.name来检查对象的“类”

function Foo() {} // ES2015 Syntax: class Foo {}

var fooInstance = new Foo();
console.log(fooInstance.constructor.name); // logs "Foo"

脚本解释器只有在函数没有名为 name 的属性时才会设置内置的Function.name属性

我们无法获取静态方法属性name()的几乎任何类的类名称

class Foo {
  constructor() {}
  static name() {}
}
// 相当于
function Foo() {}
Object.defineProperty(Foo, "name", { writable: true });
Foo.name = function () {};

使用static name()方法Foo.name不再保存实际的类名称,而是引用name()函数对象。

通过fooInstance.constructor.name获取fooInstance类不会给我们所有的类名,

var fooInstance = new Foo();
console.log(fooInstance.constructor.name); // function name()

静态定义的Foo.name 可以写入

Foo.name = "Hello";
console.log(Foo.name);
//如果 Foo 具有静态 name() 属性,则输出“Hello”,否则为“Foo”

Symbol 作为函数名称

如果 Symbol 被用于函数名称,并且这个 symbol 具有相应的描述符,那么方法的名字就是方括号中的描述符

var sym1 = Symbol("foo");
var sym2 = Symbol();
var o = {
  [sym1]: function () {},
  [sym2]: function () {},
};

o[sym1].name; // "[foo]"
o[sym2].name; // ""

JavaScript 压缩和 minifiers

转换通常会在构建时更改函数的名称

实例方法

Function.prototype.apply(thisArg [, argsArray])

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组或一个类数组对象的形式提供的参数。

Function.prototype.bind(thisArg[, arg1[, arg2[, ...argN]]])

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

Function.prototype.call(thisArg[, arg1, arg2, ...argN])

使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

Function.prototype.toString()

返回表示函数源码的字符串。覆盖了 Object.prototype.toString 方法。