开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。
红宝书《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.target | class 章节抽象基类的前置知识,构造函数安全防护 |
| ⭐⭐ | 命名函数表达式解决递归耦合 | 严格模式下递归的正确写法,面试可能追问 |
| ⭐⭐ | 没有重载 | 理解 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 必须在大括号内
箭头函数的限制:
- 不能使用
arguments、super、new.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,已被重写
若不用 callee,trueFactorial(5) 内部调用的是已被重写的 factorial,结果为 0。
这个属性不是很重要,一般只在老代码出现了:
- 严格模式下会直接报错,
'use strict'中访问arguments.callee抛 TypeError- 而且有更好的替代,命名函数表达式同样能解耦,且不受限制
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 调用:普通调用时为 undefined,new 调用时将引用被调用的构造函数。
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 了,在继承链中也有用,例如子类 new 时 new.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
选哪个取决于传参方式:数组/arguments 用 apply,逐个传用 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
不需要先把函数赋给对象的属性,直接切换上下文即可。
严格模式下,未指定上下文对象调用函数时
this为undefined,不会指向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()修复回调 this fn.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已经是空了,所以不行。
解决方案 1:arguments.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也不变,因此递归调用不会有问题。