首页 阅读DAY13 JavaScript高级程序设计 10章上 函数
文章
取消

阅读DAY13 JavaScript高级程序设计 10章上 函数

开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。

JavaScript高级程序设计(第5版)微信读书

红宝书《JavaScript高级程序设计(第5版)》学习大纲 - 大前端全栈开发 - SegmentFault 思否

全文概览:

重要度知识点理由
⭐⭐⭐this:标准函数 vs 箭头函数面试必考,开发中最常见的 this 陷阱源
⭐⭐⭐apply / call / bind控制调用的核心方法,bind 与箭头函数修复 this 是面试高频对比
⭐⭐⭐箭头函数限制(无 arguments / this / prototype / 不能 new)箭头函数日常开发最常用,不知道限制会踩坑
⭐⭐⭐扩展操作符 …(扩展参数 + 收集参数)现代开发必备语法,面试常考 rest 参数和扩展的区别
⭐⭐⭐函数声明提升 vs 函数表达式面试经典题”函数和变量提升的区别”,开发中决定写法
⭐⭐⭐参数按值传递(对象传的是引用的副本)面试高频陷阱题,理解不清会写 bug
⭐⭐默认参数值 + 作用域与 TDZ默认参数日常常用,TDZ 规则是面试区分度题
⭐⭐arguments 对象(同步规则、length 由实参决定)理解旧代码和 arguments 细节的基础,默认参数下映射消失
⭐⭐函数作为值(回调、排序比较函数)函数式编程基础,sort 回调是开发高频场景
⭐⭐new.targetclass 章节抽象基类的前置知识,构造函数安全防护
⭐⭐命名函数表达式解决递归耦合严格模式下递归的正确写法,面试可能追问
⭐⭐没有重载理解 JS 函数机制的基础概念,面试可能问”JS 为什么没有重载”
arguments.callee严格模式已禁用,老代码才出现,知道就行
caller 属性严格模式下受限,实际开发几乎不用
name 属性(bound / get / set 前缀)纯信息标记,调试时偶尔看到,不影响行为
函数 toString() 序列化仅用于调试,格式不可靠,不能依赖

箭头函数:

=> 语法定义函数表达式,行为与普通函数表达式基本相同:

1
2
let arrowSum = (a, b) => { return a + b; };
let funcSum = function(a, b) { return a + b; };

简洁语法适合嵌入场景:

1
2
3
4
5
6
7
let ints = [1, 2, 3];

console.log(ints.map(function(i) { return i + 1; }));  // [2, 3, 4]
console.log(ints.map((i) => { return i + 1 }));        // [2, 3, 4]

 // 单参数时可以有很简略的写法
[1, 2, 3].map(i => i + 1); // [2, 3, 4]

参数规则:

1
2
3
4
5
x => {} // 单参数,括号可省
(x) => {} // 单参数,括号也可
() => {} // 无参数,括号必须
(a, b) => {} // 多参数,括号必须
a, b => {} // ✗ 无效

函数体规则:

1
2
3
(x) => { return 2 * x; }; // 大括号 → 函数体,需手动 return
(x) => 2 * x; // 无大括号 → 隐式返回表达式的值
(a, b) => return a * b; // ✗ 无效,return 必须在大括号内

箭头函数的限制:

  • 不能使用 argumentssupernew.target
  • 缺少 [[Construct]] 内部方法,不能用作构造函数(不能 new
  • 没有 prototype 属性
  • 没有this,只继承外层

虽然是四个原因,但其实是是一个原因,因为箭头函数没有自己的执行上下文,自然也就缺少这些机制了。

函数名:

函数名是指向函数的指针,一个函数可以有多个名称:

1
2
3
4
5
function sum(num1, num2) { return num1 + num2; }
let anotherSum = sum; // 不带括号 → 访问指针,不执行函数
anotherSum(10, 10); // 20,和 sum 指向同一个函数
sum = null; // 切断 sum 与函数的关联
anotherSum(10, 10); // 20,anotherSum 不受影响

name 属性:所有函数对象的只读属性,包含函数名称信息:

1
2
3
4
5
6
7
8
9
function foo() {}
let bar = function() {};
let baz = () => {};

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

带前缀的情况:getter/setter 和 bind() 会加前缀:

1
2
3
4
5
6
7
8
9
function foo() {}
foo.bind(null).name; // "bound foo",加了 "bound" 前缀

let dog = {
	get age() { return 1; },
	set age(v) {}
};
Object.getOwnPropertyDescriptor(dog, 'age').get.name; // "get age",加了 "get" 前缀
Object.getOwnPropertyDescriptor(dog, 'age').set.name; // "set age",加了 "set" 前缀

仅表达函数的来源,它是被 bind 过的,还是个 getter/setter。纯粹是信息标记,不影响函数行为。

理解参数:

ECMAScript 函数不关心参数个数和类型。参数在内部表现为数组,通过 arguments 类数组对象访问:

1
2
3
4
5
6
7
function sayHi(name, message) {
 console.log("Hello " + name + ", " + message);
}
// 等价于
function sayHi() {
 console.log("Hello " + arguments[0] + ", " + arguments[1]);
}

命名参数只是为了方便,不是必须的,JS 不存在验证命名参数的机制。

arguments 的特性:

  • arguments.length 获取实际传入参数个数
  • arguments[i] 与对应命名参数同步(非严格模式下修改一方会影响另一方,但内存独立)
  • 只传入一个参数时 arguments[1] = 10 不会同步到第二个命名参数(length 由实参决定)
  • 未传入的命名参数值为 undefined
1
2
3
4
5
6
function doAdd(num1, num2) {
 arguments[1] = 10;
 console.log(arguments[0] + num2);
 // 非严格模式:num2 被同步为 10
 // 严格模式:num2 保持原值,arguments 赋值不影响命名参数
}

arguments 的长度由实际传入的参数个数决定,不是由定义了几个命名参数决定。只传 1 个参数时,arguments 只有 1 项,arguments[1] 是你手动赋值的,跟 num2 之间没有建立同步关系。传了 2 个参数时,arguments[1]num2 才有映射,修改一方会影响另一方。

严格模式的变化:

  • arguments[i] 赋值不再影响命名参数
  • 重写 arguments 对象本身会报语法错误

箭头函数中的参数:

箭头函数没有 arguments 对象,只能通过命名参数访问:

1
2
let bar = () => { console.log(arguments[0]); };
bar(5); // ReferenceError: arguments is not defined

但可以从外层包装函数中通过闭包访问:

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

参数传递方式: 所有参数都是按值传递,不可能按引用传递。对象作为参数时,传递的值是对象的引用(仍是值传递,只是会在函数上下文新建一个临时变量存储这个值,这个值是一个引用,可以通过它修改对象内部,但不能让它替代原来的变量)。

没有重载:

重载是其他语言(Java、C++、C#)的概念,JS 没有真正的重载。

ECMAScript 没有函数签名(参数由数组表示),因此不支持传统重载。同名函数后定义的覆盖先定义的:

1
2
3
function addSomeNumber(num) { return num + 100; }
function addSomeNumber(num) { return num + 200; }
addSomeNumber(100); // 300,第二个覆盖了第一个

本质和重新赋值变量一样,函数名是指针,第二次定义只是让指针指向了新函数:

1
2
3
let addSomeNumber = function(num) { return num + 100; };
addSomeNumber = function(num) { return num + 200; }; // 指针被重写
addSomeNumber(100); // 300

可通过检查 arguments 的类型和数量模拟重载行为。

默认参数值:

ES6 支持在参数后用 = 定义默认值,替代旧版 typeof 检测写法:

1
2
3
4
5
6
7
8
9
// 旧写法
function makeKing(name) {
  name = (typeof name !== 'undefined') ? name : 'Henry';
}

// 新写法
function makeKing(name = 'Henry') {
  return `King ${name} VIII`;
}

新写法并不是旧写法的语法糖。它改变了函数参数的底层机制:引擎检测到默认参数后,就不建立 arguments 与命名参数的映射了,此时的arguments 在函数内部对命名参数进行改值后也不会进行反映,只会反映实际传入的值。

具体会表现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 旧写法
function foo(name) {
  name = (typeof name !== 'undefined') ? name : 'Henry';
  name = 'Tom'; // 函数内部修改为Tom
  console.log(arguments[0]); 
}
foo('Jake'); // 传入Jake,输出'Tom'

// 默认参数
function foo(name = 'Henry') {
  name = 'Tom';
  console.log(arguments[0]); 
}
foo('Jake'); // 传入Jake,输出'Jake'

实际上,arguments 的语义本来就是调用时传入了什么,不该因为函数内部改了命名参数就变掉。默认参数写法让 arguments 忠实地保持传入值,不受内部赋值干扰,这更符合直觉。

旧写法的映射反而是一个特性,非严格模式下 arguments 和命名参数互相牵制,很容易踩坑,所以 ES5 严格模式就已经取消了映射,默认参数只是延续了同样的设计方向。

undefined 等同于没传,可跳过中间参数利用后面的默认值:

1
2
function makeKing(name = 'Henry', numerals = 'VIII') {}
makeKing(undefined, 'VI'); // name 用默认值,numerals 用传入值

arguments 不反映默认值,只反映实际传入的值,修改命名参数也不影响 arguments

1
2
3
4
5
6
function makeKing(name = 'Henry') {
  name = 'Louis';
  return `King ${arguments[0]}`;
}
makeKing(); // 'King undefined',没传值,arguments[0] 不存在
makeKing('Louis'); // 'King Louis'

默认值不限于原始值,可以是函数返回值,且只在调用时且未传该参数时才求值

1
2
3
4
5
6
7
let ordinality = 0;
function getNumerals() { return ['I','II','III'][ordinality++]; }
function makeKing(name = 'Henry', numerals = getNumerals()) {}

makeKing(); // 'King Henry I'
makeKing('Louis','XVI'); // 不调用 getNumerals
makeKing(); // 'King Henry II',再次调用,递增

箭头函数也支持默认参数,但单参数时括号不能省:

1
let makeKing = (name = 'Henry') => `King ${name}`;

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

默认参数按定义顺序初始化,等同于 let 逐个声明:

1
2
3
function makeKing(name = 'Henry', numerals = 'VIII') {}
// 等价于
function makeKing() { let name = 'Henry'; let numerals = 'VIII'; }

后定义的参数可以引用先定义的,反过来则不行(TDZ):

1
2
function makeKing(name = 'Henry', numerals = name) {} // ✓
function makeKing(name = numerals, numerals = 'VIII') {} // ✗ ReferenceError

参数存在于自己的作用域中,不能引用函数体内的变量

1
2
3
function makeKing(name = 'Henry', numerals = defaultNumeral) {
  let defaultNumeral = 'VIII'; // ✗ ReferenceError,参数作用域访问不到函数体
}
1
2
3
外层作用域
 └─ 参数作用域(name, numerals 在这里)
 └─ 函数体作用域(let/const 声明在这里)

参数扩展与收集:

使用扩展操作符(…)可以非常简洁地操作和收集数据。

扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。

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

扩展参数:

扩展操作符 ... 可将可迭代对象拆开,逐个传入函数,替代 apply()

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

getSum.apply(null, values); // 10,旧写法
getSum(...values); // 10,扩展操作符

apply 是函数的方法,用来指定 this 并传参调用函数,参数以数组形式传入:

1
func.apply(thisArg, [arg1, arg2, ...]);
  • 第一个参数:绑定 this(不需要时传 null
  • 第二个参数:参数数组,会被拆开逐个传入

现在传数组参数用扩展操作符 ... 更简洁,apply 的主要用途剩下了绑定 this

可与其他参数混用,也可多次使用:

1
2
3
4
getSum(-1, ...values); // 9
getSum(...values, 5); // 15
getSum(-1, ...values, 5); // 14
getSum(...values, ...[5, 6, 7]); // 28

函数中的arguments 对象实际上不知道扩展操作符的存在,只按实际传入的值计数:

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

扩展操作符也可用于命名参数,配合默认参数:

1
2
3
4
function getProduct(a, b, c = 1) { return a * b * c; }
getProduct(...[1, 2]); // 2(c 用默认值 1)
getProduct(...[1, 2, 3]); // 6
getProduct(...[1, 2, 3, 4]); // 6(多余参数忽略)

收集参数:

扩展操作符 ... 在函数定义时使用,可将剩余参数收集为数组(Array 实例,非 arguments 类数组):

1
2
3
4
function getSum(...values) {
  return values.reduce((x, y) => x + y, 0);
}
getSum(1, 2, 3); // 6

命名参数在前,收集参数只收剩余的;没有剩余则得到空数组:

1
2
3
4
5
6
7
function ignoreFirst(firstValue, ...values) {
  console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1, 2); // [2]
ignoreFirst(1,2,3); // [2, 3]

收集参数只能是最后一个参数:

1
function getProduct(...values, lastValue) {} // ✗ 语法错误

箭头函数没有 arguments,但支持收集参数,效果相同:

1
let getSum = (...values) => values.reduce((x, y) => x + y, 0);

收集参数不影响 arguments 对象,两者独立存在:

1
2
3
4
5
function getSum(...values) {
  console.log(arguments.length); // 3
  console.log(values); // [1, 2, 3]
}
getSum(1, 2, 3);

函数声明与函数表达式:

函数声明的关键特点是函数声明提升,代码执行前就已定义,可以先调用后声明:

1
2
sayHi(); // ✓ 正常执行
function sayHi() { console.log("Hi!"); }

函数表达式像普通变量赋值,必须先赋值再使用,没有提升。function 后无标识符,称为匿名函数(也叫 lambda 函数):

1
2
sayHi(); // ✗ ReferenceError
let sayHi = function() { console.log("Hi!"); };

不要在条件块中使用函数声明,浏览器纠正方式不一致,行为不可预测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✗ 危险!
if (condition) {
  function sayHi() { console.log('Hi!'); }
} else {
  function sayHi() { console.log('Yo!'); }
}

// ✓ 用函数表达式替代
let sayHi;
if (condition) {
  sayHi = function() { console.log("Hi!"); };
} else {
  sayHi = function() { console.log("Yo!"); };
}

函数表达式也可以作为值返回,任何时候函数被当作值来使用,它就是函数表达式:

1
2
3
4
5
6
7
8
9
function createComparisonFunction(propertyName) {
  return function(object1, object2) {
    let value1 = object1[propertyName];
    let value2 = object2[propertyName];
    if (value1 < value2) return -1;
    else if (value1 > value2) return 1;
    else return 0;
  };
}

函数作为值:

函数名就是变量,所以函数可以用在任何可以使用变量的地方作为参数传入,或者作为返回值传出。

作为参数传入:传函数时不加括号(传的是函数本身,不是调用结果):

1
2
3
4
5
6
7
8
9
function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument);
}

function add10(num) { return num + 10; }
callSomeFunction(add10, 10); // 20

function getGreeting(name) { return "Hello, " + name; }
callSomeFunction(getGreeting, "Alice"); // "Hello, Alice"

作为返回值:典型场景是按任意属性排序对象数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createComparisonFunction(propertyName) {
  return function(object1, object2) {
    let value1 = object1[propertyName];
    let value2 = object2[propertyName];
    if (value1 < value2) return -1;
    else if (value1 > value2) return 1;
    else return 0;
  };
}

let data = [
  { name: "Bob", age: 28 },
  { name: "Alice", age: 29 }
];
data.sort(createComparisonFunction("name")); // Alice 在前
data.sort(createComparisonFunction("age")); // Bob 在前

内部函数通过闭包访问 propertyName,用中括号语法取属性值,返回 sort() 所需的比较结果。

函数内部:

在ECMAScript中,函数内部存在三个特殊的对象:arguments、this和new.target

arguments:

arguments 是类数组对象,只有 function 关键字定义的函数才有(箭头函数没有)。它有一个 callee 属性,是指向 arguments 所在的函数本身的指针。

callee 属性用途是让递归函数与函数名解耦:

1
2
3
4
5
6
7
8
9
10
11
// 硬编码函数名——紧密耦合
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);
}

解耦能让函数名被重写时,callee 仍指向原函数:

1
2
3
4
5
let trueFactorial = factorial; // 如果 factorial 中采用arguments.callee进行递归
factorial = function() { return 0; }; // 重写函数

trueFactorial(5); // 120,callee 指向原函数,正常递归
factorial(5); // 0,已被重写

若不用 calleetrueFactorial(5) 内部调用的是已被重写的 factorial,结果为 0。

这个属性不是很重要,一般只在老代码出现了:

  1. 严格模式下会直接报错,'use strict' 中访问 arguments.callee 抛 TypeError
  2. 而且有更好的替代,命名函数表达式同样能解耦,且不受限制

this:

更完整的 this 绑定规则和陷阱分析,见 [番外 this解析]([番外 this解析Tonite14](https://tonite14.github.io/posts/this-analysis/))

this 在标准函数和箭头函数中行为不同。

标准函数this 引用调用时的上下文对象,运行时才确定:

1
2
3
4
5
6
7
window.color = 'red';
let o = { color: 'blue' };

function sayColor() { console.log(this.color); }
sayColor(); // 'red',this → window
o.sayColor = sayColor; // 在把sayColor()赋值给o之后再调用o.sayColor(),this会指向o
o.sayColor(); // 'blue',this → o

同一个函数,谁调用 this 就指向谁。

箭头函数this 继承定义时的上下文,不会被调用方式改变:

1
2
3
4
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red',this 仍是 window

典型用途:回调中保持正确的 this。定时回调的普通函数 this 会丢失,箭头函数则继承外层:

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

函数名只是保存指针的变量,sayColor()o.sayColor 是同一个函数,区别只在执行上下文。

caller:

函数对象的 caller 属性引用调用当前函数的函数,全局调用时为 null

1
2
3
function outer() { inner(); }
function inner() { console.log(inner.caller); } // 指向 outer
outer();

以上代码会显示outer()函数的源代码。这是因为outer()调用了inner(),inner.caller指向outer()。

可通过 arguments.callee.caller 降低耦合,但严格模式下 arguments.callee 报错。

严格模式限制:

  • arguments.caller 访问报错(非严格模式下始终为 undefined,与函数的 caller 区分)
  • 给函数的 caller 赋值报错

作为对这门语言的安全防护,这些改动也让第三方代码无法检测同一上下文中运行的其他代码。

知道就行,实际开发很少使用。

new.target:

检测函数是否通过 new 调用:普通调用时为 undefinednew 调用时将引用被调用的构造函数。

1
2
3
4
5
6
7
8
9
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"

典型用途是防止构造函数被当作普通函数调用。

new 调用时 new.target 就是 King 这个函数本身:

1
2
3
4
function King() {
  console.log(new.target === King); // true
}
new King();

所以可以用 new.target 判断是哪个构造函数被 new 了,在继承链中也有用,例如子类 newnew.target 指向子类而非父类。

函数属性与方法:

前面提到过,ECMAScript中的函数是对象,因此有属性和方法(两个属性,三个方法)。

每个函数都有两个属性:length和prototype,其中length属性保存函数定义的命名参数的个数,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sayName(name) {
  console.log(name);
}

function sum(num1, num2) {
  return num1 + num2;
}

function sayHi() {
  console.log("hi");
}

console.log(sayName.length);  // 1
console.log(sum.length);      // 2
console.log(sayHi.length);    // 0

prototype属性也许是ECMAScript核心中最有趣的部分。

prototype是保存引用类型所有实例方法的地方,这意味着toString()、valueOf()等方法实际上都保存在prototype上,进而由所有实例共享。这个属性在自定义类型时特别重要。(相关内容已经在第8章详细介绍过了。)

prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。

使用apply()、call()和bind():

三者都用于控制函数调用时的 this 值。

这三个方法能让让没有某个方法的对象借用别人的方法,来处理自己传入的数据,而this可以告诉借用来的方法在谁的身上干活。

可以说call/apply 的本质就是指定 this,让一个方法以为自己是某个对象的方法来执行,而bind也是前两者的变种。

apply():接收 this 值和参数数组(或 arguments 对象):

1
2
3
4
5
function sum(num1, num2) { return num1 + num2; }
function callSum1(num1, num2) { return sum.apply(this, arguments); } // 传 arguments
function callSum2(num1, num2) { return sum.apply(this, [num1, num2]); } // 传数组
callSum1(10, 10); // 20,全局调用时,this就是window
callSum2(10, 10); // 20

call():与 apply() 作用相同,参数逐个传递:

1
2
function callSum(num1, num2) { return sum.call(this, num1, num2); }
callSum(10, 10); // 20

选哪个取决于传参方式:数组/argumentsapply,逐个传用 call。不传参则都一样。

核心用途是控制调用上下文

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

sayColor(); // 'red',this → window
sayColor.call(window); // 'red'
sayColor.call(o); // 'blue',this → o

不需要先把函数赋给对象的属性,直接切换上下文即可。

严格模式下,未指定上下文对象调用函数时 thisundefined,不会指向 window

bind():创建新函数实例,this 永久绑定到传入的对象:

1
2
let objectSayColor = sayColor.bind(o);
objectSayColor(); // 'blue',即使全局调用 this 也是 o

对ECMAScript后来增补的特性,比如箭头函数和新的Array方法而言,apply()、call()和bind()的实用性已经很小了。虽然某些情况下还是有用,但它们在现代JavaScript代码库中出现的机会总体上会明显减少。

用途过去现在
借用方法Array.prototype.forEach.call()[...args]Array.from()
修复回调 thisfn.bind(this)箭头函数
继承Parent.call(this)class + super()
偏函数fn.bind(null, arg)仍常用,箭头函数也能替代但 bind 更语义化

序列化函数:

函数的 toString()toLocaleString() 返回函数的代码字符串,原生函数返回占位符:

1
2
3
4
5
6
7
function foo(value = "foo") { return value; }
foo.toString(); // 'function foo(value = "foo") { return value; }'

const bar = (value = "bar") => value;
bar.toString(); // '(value = "bar") => value'

alert.toString(); // 'function alert() { [native code] }'

返回格式因浏览器而异(是否保留注释、是否被解释器修改),不能在重要功能中依赖,仅用于调试。valueOf() 返回函数本身。

递归:

归函数通过名称调用自身,但函数名被重写后会出错:

1
2
3
4
5
6
7
function factorial(num) {
 if (num <= 1) return 1;
 else return num * factorial(num - 1);
}
let anotherFactorial = factorial;
factorial = null;
anotherFactorial(4); // ✗ 报错,factorial 已不是函数

这里把factorial()函数保存在了另一个变量anotherFactorial中,然后将factorial设置为null,于是只保留了一个对原始函数的引用。

而在调用anotherFactorial()时,要递归调用factorial(),但因为它已经不是函数了,所以会出错。在写递归函数时使用arguments.callee可以避免这个问题。

将指针值赋给了anotherFactorial ,这个af指向原来函数的内存地址,然后factorial赋值为null,指向空,但是在调用af的时候,原来的内存空间里面的代码还是会调用名为factorial的指针,可factorial已经是空了,所以不行。

解决方案 1arguments.callee,指向正在执行的函数本身,与函数名解耦:

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

但严格模式下访问 arguments.callee 报错。

解决方案 2:命名函数表达式,严格模式和非严格模式都可用:

1
2
3
4
const factorial = (function f(num) {
 if (num <= 1) return 1;
 else return num * f(num - 1); // f 只在函数内部可见,不受外部变量影响
});

这里创建了一个命名函数表达式f(),然后将它赋值给了变量factorial。即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题。

本文由作者按照 CC BY 4.0 进行授权

重启DAY9 回溯算法套路①子集型回溯

阅读DAY14 JavaScript高级程序设计 10章下 函数