函数

  • 函数也是对象
  • 每个函数都是Function类型的实例,而 Function 也有属性和方法
  • 函数名是指向函数对象的指针,而且不一定与函数本身紧密绑定

定义

  • 函数声明的方式
  1. function sum (num1, num2) {
  2. return num1 + num2;
  3. }
  • 函数表达式
  1. let sum = function(num1, num2) {
  2. return num1 + num2;
  3. };
  • 箭头函数
  1. let sum = (num1, num2) => {
  2. return num1 + num2;
  3. };
  • Function 构造函数(不推荐)
    接受任意多个字符串参数
    最后一个参数是函数体
  1. let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐

箭头函数

箭头函数与正式的函数表达式创建的函数对象行为是相同的

  1. let arrowSum = (a, b) => {
  2. return a + b;
  3. };
  4. let functionExpressionSum = function(a, b) {
  5. return a + b;
  6. };
  7. console.log(arrowSum(5, 8)); // 13
  8. console.log(functionExpressionSum(5, 8)); // 13
  • 如果只有一个参数,可以不用括号
    以下两种写法都有效
  1. let double = (x) => { return 2 * x; };
  2. let triple = x => { return 3 * x; };
  • 只有没有参数,或者多个参数的情况下,才需要使用括号
  1. // 没有参数需要括号
  2. let getRandom = () => { return Math.random(); };
  3. // 多个参数需要括号
  4. let sum = (a, b) => { return a + b; };
  5. // 无效的写法:
  6. let multiply = a, b => { return a * b; };
  • 可以不使用大括号,但是只能写一行代码,省略大括号会隐式返回这行代码的值
  1. // 以下两种写法都有效,而且返回相应的值
  2. let double = (x) => { return 2 * x; };
  3. let triple = (x) => 3 * x;
  4. // 可以赋值
  5. let value = {};
  6. let setName = (x) => x.name = "Matt";
  7. setName(value);
  8. console.log(value.name); // "Matt"
  9. // 无效的写法:
  10. let multiply = (a, b) => return a * b;

箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 arguments、super 和new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

函数名

函数名就是指向函数的指针
意味着一个函数可以有多个名称

  1. function sum(num1, num2) {
  2. return num1 + num2;
  3. }
  4. console.log(sum(10, 10)); // 20
  5. let anotherSum = sum;
  6. console.log(anotherSum(10, 10)); // 20
  7. sum = null;
  8. console.log(anotherSum(10, 10)); // 20

上面代码sum=null只是切短了sum和函数的联系,不影响anothersum

函数包含一个只读的name属性,包含函数标识符(函数名称),字符串类型

  1. function foo() {}
  2. let bar = function() {};
  3. let baz = () => {};
  4. console.log(foo.name); // foo
  5. console.log(bar.name); // bar
  6. console.log(baz.name); // baz
  7. console.log((() => {}).name); //(空字符串)
  8. console.log((new Function()).name); // anonymous

理解参数

ES不会关心传入参数的个数数据类型,无论你定义多少个

arguments对象

  • 在非箭头函数内部可以访问arguments对象
  • arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素
  • length属性检查传入的参数个数
  • arguments 对象可以跟命名参数一起使用
    虽然改变arguments的值能改变命名参数所代替的值
    但是两个所对应的内存是不一样的
  1. function doAdd(num1, num2) {
  2. arguments[1] = 10;
  3. console.log(arguments[0] + num2);
  4. }

比较传入两个参数和传入一个参数的区别

箭头函数中的参数

箭头函数中不能使用arguments对象

可以在包装函数中把arguments提供给箭头函数

  1. function foo() {
  2. let bar = () => {
  3. console.log(arguments[0]); // 5
  4. };
  5. bar();
  6. }
  7. foo(5);

没有重载

如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。

  1. function addSomeNumber(num) {
  2. return num + 100;
  3. }
  4. function addSomeNumber(num) {
  5. return num + 200;
  6. }
  7. let result = addSomeNumber(100); // 300

默认参数值

  • ES5.1及以前,实现默认参数主要检测是否等于undefined
  1. function makeKing(name) {
  2. name = (typeof name !== 'undefined') ? name : 'Henry';
  3. return `King ${name} VIII`;
  4. }
  5. console.log(makeKing()); // 'King Henry VIII'
  6. console.log(makeKing('Louis')); // 'King Louis VIII'
  • ES6之后,直接在定义出加一个=就可以定义默认参数。
  1. function makeKing(name = 'Henry') {
  2. return `King ${name} VIII`;
  3. }
  4. console.log(makeKing('Louis')); // 'King Louis VIII'
  5. console.log(makeKing()); // 'King Henry VIII'
  • 而给参数传undefined相当于没传值,函数会调用默认值
  1. function makeKing(name = 'Henry', numerals = 'VIII') {
  2. return `King ${name} ${numerals}`;
  3. }
  4. console.log(makeKing()); // 'King Henry VIII'
  5. console.log(makeKing('Louis')); // 'King Louis VIII'
  6. console.log(makeKing(undefined, 'VI')); // 'King Henry VI'
  • arguments 对象的值不反映参数的默认值,只反应传给函数的参数
  1. function makeKing(name = 'Henry') {
  2. name = 'Louis';
  3. return `King ${arguments[0]}`;
  4. }
  5. console.log(makeKing()); // 'King undefined'
  6. console.log(makeKing('Louis')); // 'King Louis'
  • 默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值
  1. let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI'];
  2. let ordinality = 0;
  3. function getNumerals() {
  4. // 每次调用后递增
  5. return romanNumerals[ordinality++];
  6. }
  7. function makeKing(name = 'Henry', numerals = getNumerals()) {
  8. return `King ${name} ${numerals}`;
  9. }
  10. console.log(makeKing()); // 'King Henry I'
  11. console.log(makeKing('Louis', 'XVI')); // 'King Louis XVI'
  12. console.log(makeKing()); // 'King Henry II'
  13. console.log(makeKing()); // 'King Henry III'

注意:

  • 默认参数只有在函数被调用时才会求值
  • 计算默认值的函数只有在未传相应参数时才会被调用
  • 箭头函数也可以使用默认参数,在只有一个参数时,括号不能省略
  1. let makeKing = (name = 'Henry') => `King ${name}`;
  2. console.log(makeKing()); // King Henry

默认参数作用域与暂时性死区

  1. function makeKing(name = 'Henry', numerals = 'VIII') {
  2. return `King ${name} ${numerals}`;
  3. }
  4. console.log(makeKing()); // King Henry VIII

可以想象成下面的代码

  1. function makeKing() {
  2. let name = 'Henry';
  3. let numerals = 'VIII';
  4. return `King ${name} ${numerals}`;
  5. }

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误:

  1. // 调用时不传第一个参数会报错
  2. function makeKing(name = numerals, numerals = 'VIII') {
  3. return `King ${name} ${numerals}`;
  4. }

参数也存在于自己的作用域中,它们不能引用函数体的作用域:

  1. // 调用时不传第二个参数会报错
  2. function makeKing(name = 'Henry', numerals = defaultNumeral) {
  3. let defaultNumeral = 'VIII';
  4. return `King ${name} ${numerals}`;
  5. }

参数扩展与收集

扩展参数

有时候可能不需要传一个数组,而是要分别传入数组的元素

  1. console.log(getSum(...values)); // 10

对函数中的 arguments 对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值:

  1. let values = [1,2,3,4]
  2. function countArguments() {
  3. console.log(arguments.length);
  4. }
  5. countArguments(-1, ...values); // 5
  6. countArguments(...values, 5); // 5
  7. countArguments(-1, ...values, 5); // 6
  8. countArguments(...values, ...[5,6,7]); // 7

收集参数

收集参数的结果会得到一个 Array 实例,区别于arguments 对象

  1. function getSum(...values) {
  2. // 顺序累加 values 中的所有值
  3. // 初始值的总和为 0
  4. return values.reduce((x, y) => x + y, 0);
  5. }
  6. console.log(getSum(1,2,3)); // 6

多余的参数要放在它前边

  1. // 不可以
  2. function getProduct(...values, lastValue) {}
  3. // 可以
  4. function ignoreFirst(firstValue, ...values) {
  5. console.log(values);
  6. }
  7. ignoreFirst(); // []
  8. ignoreFirst(1); // []
  9. ignoreFirst(1,2); // [2]
  10. ignoreFirst(1,2,3); // [2, 3]

箭头函数支持收集参数,虽然不支持aarguments,但能利用收集函数实现同样的功能。

函数声明与函数表达式

函数声明会得到函数声明提升

  1. // 没问题
  2. console.log(sum(10, 10));
  3. function sum(num1, num2) {
  4. return num1 + num2;
  5. }

函数表达式会出错

  1. // 会出错
  2. console.log(sum(10, 10));
  3. let sum = function(num1, num2) {
  4. return num1 + num2;
  5. };

1. 函数内部

函数内部存在两个特殊的对象:arguments 和 this。ES6新增了new.target 属性。

1.1 arguments

一个类数组对象,包含调用函数时传入的所有参数。
之前已经多次提到,arguments对象还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。

  1. function factorial(num) {
  2. if (num <= 1) {
  3. return 1;
  4. } else {
  5. return num * arguments.callee(num - 1);
  6. }
  7. }

上面代码中的arguments.callee可以代替函数名factorial实现递归。

1.2 this

1.2.1 在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值。

  1. window.color = 'red';
  2. let o = {
  3. color: 'blue'
  4. };
  5. function sayColor() {
  6. console.log(this.color);
  7. }
  8. sayColor(); // 'red'
  9. o.sayColor = sayColor;
  10. o.sayColor(); // 'blue'

1.2.2 在箭头函数中,this引用的是定义箭头函数的上下文

  1. window.color = 'red';
  2. let o = {
  3. color: 'blue'
  4. };
  5. let sayColor = () => console.log(this.color);
  6. sayColor(); // 'red'
  7. o.sayColor = sayColor;
  8. o.sayColor(); // 'red'

将回调函数写成箭头函数可以避免一些问题。
避免this指向的不是想要的对象。

  1. function King() {
  2. this.royaltyName = 'Henry';
  3. // this 引用 King 的实例
  4. setTimeout(() => console.log(this.royaltyName), 1000);
  5. }
  6. function Queen() {
  7. this.royaltyName = 'Elizabeth';
  8. // this 引用 window 对象
  9. setTimeout(function() { console.log(this.royaltyName); }, 1000);
  10. }
  11. new King(); // Henry
  12. new Queen(); // undefined

1.3 caller

这个属性引用的是调用当前函数的函数
如果是在全局作用域中调用的则为 null。

  1. function outer() {
  2. inner();
  3. }
  4. function inner() {
  5. console.log(inner.caller);
  6. }
  7. outer();

以上代码会显示 outer()函数的源代码。
降低耦合度可以用arguments.callee.caller

  1. function outer() {
  2. inner();
  3. }
  4. function inner() {
  5. console.log(arguments.callee.caller);
  6. }
  7. outer();

在严格模式下访问 arguments.callee 会报错

new.target

ES中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。
new.target是为了区分到底使用哪种方式调用的

  1. function King() {
  2. if (!new.target) {
  3. throw 'King must be instantiated using "new"'
  4. }
  5. console.log('King instantiated using "new"');
  6. }
  7. new King(); // King instantiated using "new"
  8. King(); // Error: King must be instantiated using "new"

2.函数属性与方法

两个属性:length和 prototype
两个方法:apply()和 call()。

2.1 函数的length属性

length 属性保存函数定义的命名参数的个数

  1. function sayName(name) {
  2. console.log(name);
  3. }
  4. function sum(num1, num2) {
  5. return num1 + num2;
  6. }
  7. function sayHi() {
  8. console.log("hi");
  9. }
  10. console.log(sayName.length); // 1
  11. console.log(sum.length); // 2
  12. console.log(sayHi.length); // 0

2.2 函数的prototype属性

prototype 是保存引用类型所有实例方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上。
相关内容已经在第 8 章详细介绍。

2.3 apply()方法和 call()方法

主要作用是传入函数体内 this值的能力。
apply()方法接收两个参数:

  • 函数内 this 的值。
  • 第二个参数可以是 Array 的实例,但也可以是 arguments 对象。
    call()方法和apply()方法差不多,只是把数组的接收变成了一个个分散开的。
  1. function sum(num1, num2) {
  2. return num1 + num2;
  3. }
  4. function callSum(num1, num2) {
  5. return sum.call(this, num1, num2);
  6. }
  7. console.log(callSum(10, 10)); // 20

下面这个例子可以看出这两个方法的用法

  1. window.color = 'red';
  2. let o = {
  3. color: 'blue'
  4. };
  5. function sayColor() {
  6. console.log(this.color);
  7. }
  8. sayColor(); // red
  9. sayColor.call(this); // red
  10. sayColor.call(window); // red
  11. sayColor.call(o); // blue

闭包

单独开一章节