【红宝书精简笔记】第十章 函数

目录

10.1 箭头函数

10.2 函数名

10.3 理解参数

10.4 没有重载

10.5 默认参数值

10.6 参数扩展与收集

10.7 函数声明与函数表达式

10.8 函数作为值

10.9 函数内部

10.10 函数属性与方法

10.11 函数表达式

10.12 递归

10.13 尾调用优化

10.14 闭包

10.15 立即调用的函数表达式

10.16 私有变量

10.17 小结


10.1 箭头函数

ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例 化的函数对象与正式的函数表达式创建的函数对象行为是相同的。

// 以下两种写法都有效
let double = (x) => { return 2 * x; };
let triple = x => { return 3 * x; };
// 没有参数需要括号
let getRandom = () => { return Math.random(); };
// 多个参数需要括号
let sum = (a, b) => { return a + b; };
// 无效的写法:
let multiply = a, b => { return a * b; }; 
// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; };
let triple = (x) => 3 * x;
// 可以赋值
let value = {};
let setName = (x) => x.name = "Matt";
setName(value);
console.log(value.name); // "Matt"
// 无效的写法:
let multiply = (a, b) => return a * b; 

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

10.2 函数名

ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情 况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称, 也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成"anonymous":

function foo() {}
let bar = function() {};
let baz = () => {};
console.log(foo.name); // foo
console.log(bar.name); // bar
console.log(baz.name); // baz
console.log((() => {}).name); //(空字符串)
console.log((new Function()).name); // anonymous

10.3 理解参数

ECMAScript 函数的参数跟大多数其他语言不同。ECMAScript 函数既不关心传入的参数个数,也不 关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一 个、三个,甚至一个也不传,解释器都不会报错。

之所以会这样,主要是因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接 收一个数组,但函数并不关心这个数组中包含什么。如果数组中什么也没有,那没问题;如果数组的元 素超出了要求,那也没问题。事实上,在使用 function 关键字定义(非箭头)函数时,可以在函数内 部访问 arguments 对象,从中取得传进来的每个参数值.

 箭头函数中的参数

 如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只 能通过定义的命名参数访问。

10.4 没有重载

ECMAScript 函数不能像传统编程那样重载。在其他语言比如 Java 中,一个函数可以有两个定义, 只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript 函数没有签名,因为参数是由 包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。

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

function addSomeNumber(num) {
 return num + 100;
}
function addSomeNumber(num) {
 return num + 200;
}
let result = addSomeNumber(100); // 300

10.5 默认参数值

在 ECMAScript5.1 及以前,实现默认参数的一种常用方式就是检测某个参数是否等于 undefined, 如果是则意味着没有传这个参数,那就给它赋一个值。

ECMAScript 6 之后就不用这么麻烦了,因为它支持显式定义默认参数了。下面就是与前面代码等价 的 ES6 写法,只要在函数定义中的参数后面用=就可以为参数赋一个默认值:

function makeKing(name = 'Henry') {
 return `King ${name} VIII`;
}
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing()); // 'King Henry VIII'

function makeKing(name = 'Henry', numerals = 'VIII') {
 return `King ${name} ${numerals}`;
}
console.log(makeKing()); // 'King Henry VIII'
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing(undefined, 'VI')); // 'King Henry VI' 

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

给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。默认参数会按照定义它们的顺序依次被初始化

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

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

10.6 参数扩展与收集

扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。

10.6.1 扩展参数

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

假设有如下函数定义,它会将所有传入的参数累加起来:

let values = [1, 2, 3, 4];
function getSum() {
 let sum = 0;
 for (let i = 0; i < arguments.length; ++i) {
 sum += arguments[i];
 }
 return sum;
} 

在 ECMAScript 6 中,可以通过扩展操作符极为简洁地实现这种操作。对可迭代对象应用扩展操 作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。

比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:

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

因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值, 包括使用扩展操作符传其他参数:

console.log(getSum(-1, ...values)); // 9
console.log(getSum(...values, 5)); // 15
console.log(getSum(-1, ...values, 5)); // 14
console.log(getSum(...values, ...[5,6,7])); // 28 

10.6.2 收集参数

收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集 参数的结果可变,所以只能把它作为最后一个参数:

// 不可以
function getProduct(...values, lastValue) {}
// 可以
function ignoreFirst(firstValue, ...values) {
 console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1,2); // [2]
ignoreFirst(1,2,3); // [2, 3] 

10.7 函数声明与函数表达式

事实上,JavaScript 引擎在加载数据 时对它们是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中 生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

在执行代码时,JavaScript 引擎会先执行一遍扫描, 把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把 函数声明提升到顶部。

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

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

上面的代码之所以会出错,是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到"let sum = function(num1, num2)"的那一行,那么执行上下文中就没有函数的定义。。这并不是因为使用 let 而导致的,使用 var 关键字也会碰到同样的问题。

10.8 函数作为值

因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不 仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。

function callSomeFunction(someFunction, someArgument) {
 return someFunction(someArgument);
} 

例子:

function add10(num) {
 return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function getGreeting(name) {
 return "Hello, " + name;
}
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hello, Nicholas" 

要注意的是,如果是访问函数而不是调用函数,那就必须不带括号, 所以传给callSomeFunction()的必须是 add10 和 getGreeting,而不能是它们的执行结果。

10.9 函数内部

ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增 了 new.target 属性。

10.9.1 arguments

arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的 指针。来看下面这个经典的阶乘函数

function factorial(num) {
 if (num <= 1) {
 return 1;
 } else {
 return num * factorial(num - 1);
 }
} 

 // 紧密耦合。使用 arguments.callee 就可以让函数逻辑与函数名解耦:
function factorial(num) {
 if (num <= 1) {
 return 1;
 } else {
 return num * arguments.callee(num - 1);
 }
} 

// 这个重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。
// 这意味着无论函数叫什么名称,都可以引用正确的函数
let trueFactorial = factorial;
factorial = function() {
 return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0

通过将函数与 名称解耦,trueFactorial()就可以正确计算阶乘,而 factorial()则只能返回 0。 

10.9.2 this

另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。在标准函数中,this 引用的是把函数当成方法调用的上下文对象这时候通常称其为 this 值(在 网页的全局上下文中调用函数时,this 指向 windows)。在箭头函数中,this引用的是定义箭头函数的上下文

在标准函数中,this 引用的是把函数当成方法调用的上下文对象这个 this 到底引用哪个对象必须到 函数被调用时才能确定。因此这个值在代码执行的过程中可能会变 :

window.color = 'red';
let o = {
 color: 'blue'
};
function sayColor() {
 console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'

 在箭头函数中,this引用的是定义箭头函数的上下文,在对sayColor() 的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:

window.color = 'red';
let o = {
 color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red' 

10.9.3 caller

10.9.4 new.target

ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的 构造函数。

function King() {
 if (!new.target) {
 throw 'King must be instantiated using "new"'
 }
 console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new" 

10.10 函数属性与方法

ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length 和 prototype。其中,length 属性保存函数定义的命名参数的个数。

函数还有两个方法:apply()和 call()。这两个方法都会以指定的 this 值来调用函数,即会设置调用函数时函数体内 this 对象的值。apply()方法接收两个参数:函数内 this 的值和一个参数数 组。第二个参数可以是 Array 的实例,但也可以是 arguments 对象

call()方法与 apply()的作用一样,只是传参的形式不同。第一个参数跟 apply()一样,也是 this 值,而剩下的要传给被调用函数的参数则是逐个传递的。换句话说,通过 call()向函数传参时,必须 将参数一个一个地列出来。

function callSum1(num1, num2) {
 return sum.apply(this, arguments); // 传入 arguments 对象
} 

function callSum2(num1, num2) {
 return sum.apply(this, [num1, num2]); // 传入数组
} 

function callSum(num1, num2) {
 return sum.call(this, num1, num2);
}

apply()和 call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内 this 值的能力。

window.color = 'red';
let o = {
 color: 'blue'
};
function sayColor() {
 console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue

使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法。在前面例子最初的版本中,为切换上下文需要先把 sayColor()直接赋值为 o 的属性,然后再 调用。而在这个修改后的版本中,就不需要这一步操作了。

10.11 函数表达式

定义函数有两种方式:函数声明和函数表达式。

函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。这意味着函数声明 可以出现在调用它的代码之后。第二种创建函数的方式就是函数表达式。函数表达式跟 JavaScript 中的其他表达式一样,需要先赋值再使用。

理解函数声明与函数表达式之间的区别,关键是理解提升。

// 函数声明
sayHi(); // Hi!
function sayHi() {
 console.log("Hi!");
} 

// 函数表达式
sayHi(); // Error! function doesn't exist yet
let sayHi = function() {
 console.log("Hi!");
};

10.12 递归

递归函数通常的形式是一个函数通过名称调用自己。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题。解决办法:使用arguments.callee。把函数名称替换成 arguments.callee,可以确保无论通过什么变量 调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee 是引用当前函数的首选。

10.13 尾调用优化

10.13.1 尾调用优化的条件

// 下面展示了几个违反上述条件的函数,因此都不符号尾调用优化的要求:
"use strict";
// 无优化:尾调用没有返回
function outerFunction() {
 innerFunction();
}
// 无优化:尾调用没有直接返回
function outerFunction() {
 let innerFunctionResult = innerFunction();
 return innerFunctionResult;
}
// 无优化:尾调用返回后必须转型为字符串
function outerFunction() {
 return innerFunction().toString();
}
// 无优化:尾调用是一个闭包
function outerFunction() {
 let foo = 'bar';
 function innerFunction() { return foo; }
 return innerFunction();
}
// 下面是几个符合尾调用优化条件的例子:
"use strict";
// 有优化:栈帧销毁前执行参数计算
function outerFunction(a, b) {
 return innerFunction(a + b);
}
// 有优化:初始返回值不涉及栈帧
function outerFunction(a, b) {
 if (a < b) {
 return a;
 }
 return innerFunction(a + b);
}
// 有优化:两个内部函数都在尾部
function outerFunction(condition) {
 return condition ? innerFunctionA() : innerFunctionB();
} 

10.13.2 尾调用优化的代码

// 计算斐波纳契数列的函数:
function fib(n) {
 if (n < 2) {
 return n;
 }
 return fib(n - 1) + fib(n - 2);
}
console.log(fib(0)); // 0
console.log(fib(1)); // 1
console.log(fib(2)); // 1
console.log(fib(3)); // 2
console.log(fib(4)); // 3
console.log(fib(5)); // 5
console.log(fib(6)); // 8

显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。结果,fib(n)的栈 帧数的内存复杂度是 O(2^n )。因此,即使这么一个简单的调用也可以给浏览器带来麻烦: fib(1000);当然,解决这个问题也有不同的策略,比如把递归改写成迭代循环形式。不过,也可以保持递归实 现,但将其重构为满足优化条件的形式。

"use strict";
// 基础框架
function fib(n) {
 return fibImpl(0, 1, n);
}
// 执行递归
function fibImpl(a, b, n) {
 if (n === 0) {
 return a;
 }
 return fibImpl(b, a + b, n - 1);
} 

10.14 闭包

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

注意 因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭 包可能导致内存过度占用,因此建议仅在十分必要时使用。V8 等优化的 JavaScript 引擎会 努力回收被闭包困住的内存,不过我们还是建议在使用闭包时要谨慎。

10.14.1 this对象

在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运 行时绑定到执行函数的上下文。

window.identity = 'The Window';
let object = {
 identity: 'My Object',
 getIdentityFunc() {
 return function() {
 return this.identity;
 };
 }
};
console.log(object.getIdentityFunc()()); // 'The Window' 

每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments。内部函数永 远不可能直接访问外部函数的这两个变量。但是,如果把 this 保存到闭包可以访问的另一个变量中, 则是行得通的 

window.identity = 'The Window';
let object = {
 identity: 'My Object',
 getIdentityFunc() {
 let that = this;
 return function() {
 return that.identity;
 };
 }
};
console.log(object.getIdentityFunc()()); // 'My Object' 

 注意 this 和 arguments 都是不能直接在内部函数中访问的。如果想访问包含作用域中 的 arguments 对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。

10.14.2 内存泄漏

由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制(第 4 章讨论过),所以 闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域 中,就相当于宣布该元素不能被销毁。

10.15 立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组 括号后面的第二组括号会立即调用前面的函数表达式。

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这 样位于函数体作用域的变量就像是在块级作用域中一样。ECMAScript 5 尚未支持块级作用域,使用 IIFE 模拟块级作用域是相当普遍的。

在 ECMAScript 6 以后,IIFE 就没有那么必要了,因为块级作用域中的变量无须 IIFE 就可以实现同 样的隔离

10.16 私有变量

严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。不过,倒是有私有变量的概 念。任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的 变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。

10.16.1 静态私有变量

10.16.2 模块模式

模块模式的样板:

let singleton = function() {
 // 私有变量和私有函数
 let privateVariable = 10;
 function privateFunction() {
 return false;
 }
 // 特权/公有方法和属性
 return {
 publicProperty: true,
 publicMethod() {
 privateVariable++;
 return privateFunction();
 }
 };
}(); 

10.16.3 模块增强模式

10.17 小结

函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而 让开发者可以更有效地使用函数。

 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没 有名称的函数表达式也被称为匿名函数。

 ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。

 JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符, 可以实现函数定义和调用的完全动态化。

 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了 什么参数等信息。

 JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。

 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的 变量对象。  通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。

 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。  函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。

 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都 会被销毁。

 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域 中定义的变量。

 可以访问私有变量的公共方法叫作特权方法。

 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增 强模式在单例对象上实现。

猜你喜欢

转载自blog.csdn.net/weixin_41950078/article/details/121786483