递归和堆栈
- 递归
- 函数会调用 自身。这就是所谓的 递归。
- 两种思考方式
- 迭代思路:使用
for
循环- 递归思路:简化任务,调用自身
- 迭代思路:使用
JavaScriptfunction pow(x, n) { return (n == 1) ? x : (x * pow(x, n - 1)); } //最大的嵌套调用次数(包括首次)被称为 递归深度
- 执行上下文和堆栈
- 有关正在运行的函数的执行过程的相关信息被存储在其 执行上下文 中。
- 执行上下文 是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,
this
的值(此处我们不使用它),以及其它的一些内部细节。
- 出口
- 任何递归都可以用循环来重写。通常循环变体更有效。
- 递归遍历
- 递归结构
- 链表
- value
next
属性引用下一个 链表元素 或者代表末尾的null
。
Rest 参数与 Spread 语法
Math.max(arg1, arg2, ..., argN)
—— 返回参数中的最大值。Object.assign(dest, src1, ..., srcN)
—— 依次将属性从src1..N
复制到dest
。
- Rest 参数 ...
- Rest 参数必须放到参数列表的末尾
- “arguments” 变量
- 有一个名为
arguments
的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。 - 箭头函数没有
"arguments"
- 有一个名为
- Spread 语法
- 复制 array/object
变量作用域,闭包
JavaScript 是一种非常面向函数的语言。 在 JavaScript 中,有三种声明变量的方式:let
,const
(现代方式),var
(过去留下来的方式)
- 代码块
- 如果在代码块
{...}
内声明了一个变量,那么这个变量只在该代码块内可见
- 如果在代码块
- 嵌套函数
- 如果一个函数是在另一个函数中创建的,该函数就被称为“嵌套”函数。
- Step 1. 变量
- 在 JavaScript 中,每个运行的函数,代码块
{...}
以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。 - 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如
this
的值)的对象。 - 对 外部词法环境 的引用,与外部代码相关联。
- 词法环境是一个规范对象
- “词法环境”是一个规范对象(specification object):它只存在于 语言规范 的“理论”层面,用于描述事物是如何工作的。我们无法在代码中获取该对象并直接对其进行操作。
- 在 JavaScript 中,每个运行的函数,代码块
- Step 2. 函数声明
- 不同之处在于函数声明的初始化会被立即完成。
- Step 3. 内部和外部的词法环境
- 当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
- Step 4. 返回函数
- 在变量所在的词法环境中更新变量。
- 闭包
- 闭包 是指一个函数可以记住其外部变量并可以访问这些变量。在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现。但如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。
- 垃圾收集
- 函数调用完成后,会将词法环境和其中的所有变量从内存中删除
- 但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的
[[Environment]]
属性。 - 当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。
- 实际开发中的优化
- 如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
- 在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
JavaScript
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
sayHi(); // 会显示 "Pete"?
老旧的 “var”
- “var” 没有块级作用域
- 用
var
声明的变量,不是函数作用域就是全局作用域
- 用
- “var” 允许重新声明
JavaScript
var user = "Pete";
var user = "John"; // 这个 "var" 无效(因为变量已经声明过了)
// ……不会触发错误
alert(user); // John
- “var” 声明的变量,可以在其声明语句前被使用
- 当函数开始的时候,就会处理
var
声明(脚本启动对应全局变量) - 与它在代码中定义的位置无关(这里不考虑定义在嵌套函数中的情况)。
- 当函数开始的时候,就会处理
- 声明会被提升,但是赋值不会。
JavaScript
function sayHi() {
alert(phrase); //undefined
var phrase = "Hello";
}
sayHi();
赋值操作始终是在它出现的地方才起作用
- IIFE
- JavaScript 中只有
var
这一种声明变量的方式,并且这种方式声明的变量没有块级作用域,程序员们就发明了一种模仿块级作用域的方法。这种方法被称为“立即调用函数表达式”(immediately-invoked function expressions,IIFE)。 - 如今不再使用
- JavaScript 中只有
全局对象
在浏览器中,它的名字是 “window”,对 Node.js 而言,它的名字是 “global”,
- 全局对象
- 全局对象的所有属性都可以被直接访问
- 使用
var
(而不是let/const
!)声明的全局函数和变量会成为全局对象的属性
- 使用 polyfills
函数对象,NFE
- 属性 “name”
- 函数名.name ,返回函数名字
- 属性 “length”
- 返回函数的参数个数
- rest 参数不参与计数。
- 自定义属性
- 属性不是变量
JavaScript
function sayHi() {
alert("Hi");
// 计算调用次数
sayHi.counter++;
}
sayHi.counter = 0; // 初始值
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times
- 命名函数表达式
- 取一个函数自己的别名,在函数内部调用
- 没有函数声明这样的说法
“new Function”语法
- 语法
- let func = new Function ([arg1, arg2, ...argN], functionBody);
- 使用
new Function
创建函数的应用场景非常特殊,比如在复杂的 Web 应用程序中,我们需要从服务器获取代码或者动态地从模板编译函数时才会使用。 - 使用
new Function
创建一个函数,那么该函数的[[Environment]]
并不指向当前的词法环境,而是指向全局环境。
- 闭包
- 闭包是指使用一个特殊的属性
[[Environment]]
来记录函数自身的创建时的环境的函数。
- 闭包是指使用一个特殊的属性
- 即使我们可以在
new Function
中访问外部词法环境,我们也会受挫于压缩程序。 - 在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩 —— 一个特殊的程序,通过删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量。
调度:setTimeout 和 setInterval
有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行。这就是所谓的“计划调用(scheduling a call)”。
setTimeout
允许我们将函数推迟到一段时间间隔之后再执行。setInterval
允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。- 参数都是毫秒单位
- setTimeout
- let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
func|code
:想要执行的函数或代码字符串。 一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做。使用函数名,而不是函数名( ),因为这里是引用函数,不是执行函数delay
:执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0;arg1
,arg2
…:要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持)
- let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
- 用 clearTimeout 来取消调度 1.
setTimeout
在调用时会返回一个“定时器标识符(timer identifier)”,在我们的例子中是timerId
,我们可以使用 clearTimeout 来取消执行。
JavaScript
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // 定时器标识符
clearTimeout(timerId);
alert(timerId); // 还是这个标识符(并没有因为调度被取消了而变成 null)
- setInterval
setInterval
方法和setTimeout
的语法相同:- let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
- 与
setTimeout
只执行一次不同,setInterval
是每间隔给定的时间周期性执行。
- 嵌套的 setTimeout
- 周期性调度有两种方式:一种是使用
setInterval
,另外一种就是嵌套的setTimeout
, - 嵌套的
setTimeout
相较于setInterval
能够更精确地设置两次执行之间的延时。 - 使用
setInterval
时,func
函数的实际调用间隔要比代码中设定的时间间隔要短! - 嵌套的
setTimeout
就能确保延时的固定 - 垃圾回收和 setInterval/setTimeout 回调(callback):当一个函数传入
setInterval/setTimeout
时,将为其创建一个内部引用,并保存在调度程序中。这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收。
- 周期性调度有两种方式:一种是使用
- 零延时的 setTimeout
- 特殊的用法:
setTimeout(func, 0)
,或者仅仅是setTimeout(func)
。 - 这样调度可以让
func
尽快执行。但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它。也就是说,该函数被调度在当前脚本执行完成“之后”立即执行。 - 零延时实际上不为零(在浏览器中)
- 根据 HTML5 标准 所讲:“经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒”。
- 特殊的用法:
JavaScript
setTimeout(() => alert("World"));
alert("Hello");
//会先输出 “Hello”,然后立即输出 “World”
装饰器模式和转发 call,apply
JavaScript 在处理函数时提供了非凡的灵活性。它们可以被传递,用作对象,现在我们将看到如何在它们之间 转发(forward) 调用并 装饰(decorate) 它们。
- 透明缓存
- 函数的结果是稳定的,创建一个包装器(wrapper)函数,该函数增加了缓存功能
cachingDecorator
是一个 装饰器(decorator):一个特殊的函数,它接受另一个函数并改变它的行为。
JavaScript
function slow(x) {
// 这里可能会有重负载的 CPU 密集型工作
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // 如果缓存中有对应的结果
return cache.get(x); // 从缓存中读取结果
}
let result = func(x); // 否则就调用 func
cache.set(x, result); // 然后将结果缓存(记住)下来
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) 被缓存下来了,并返回结果
alert( "Again: " + slow(1) ); // 返回缓存中的 slow(1) 的结果
alert( slow(2) ); // slow(2) 被缓存下来了,并返回结果
alert( "Again: " + slow(2) ); // 返回缓存中的 slow(2) 的结果
- 使用 “func.call” 设定上下文
JavaScript
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // 现在 "this" 被正确地传递了
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存
alert( worker.slow(2) ); // 工作正常
alert( worker.slow(2) ); // 工作正常,没有调用原始函数(使用的缓存)
- 传递多个参数
JavaScript
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
- func.apply
- 我们可以使用
func.apply(this, arguments)
代替func.call(this, ...arguments)
。 - func.apply(context, args)
call
和apply
之间唯一的语法区别是,call
期望一个参数列表,而apply
期望一个包含这些参数的类数组对象。- Spread 语法
...
允许将 可迭代对象args
作为列表传递给call
。
- Spread 语法
- apply 只接受 类数组 args。
- 我们可以使用
- 借用一种方法
JavaScript
function hash(args) {
return args[0] + ',' + args[1];
}
它仅适用于两个参数。如果它可以适用于任何数量的 `args` 就更好了
自然的解决方案是使用 arr.join 方法:
function hash(args) {
return args.join();
}
//另外一种方法
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
- 装饰器和函数属性
- 通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西。如果原始函数有属性,例如
func.calledCount
或其他,则装饰后的函数将不再提供这些属性。因为这是装饰器。因此,如果有人使用它们,那么就需要小心。 - 例如,在上面的示例中,如果
slow
函数具有任何属性,而cachingDecorator(slow)
则是一个没有这些属性的包装器。 - 一些包装器可能会提供自己的属性。例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(expose)这些信息。
- 存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的
Proxy
对象来包装函数。我们将在后面的 Proxy 和 Reflect 中学习它。
- 通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西。如果原始函数有属性,例如
函数绑定
- 丢失 “this”
- 一旦方法被传递到与对象分开的某个地方 ——
this
就丢失。
- 一旦方法被传递到与对象分开的某个地方 ——
JavaScript
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
- 解决方案 1:包装器
JavaScript
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
- 解决方案 2:bind
JavaScript
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// 可以在没有对象(译注:与对象分离)的情况下运行它
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// 即使 user 的值在不到 1 秒内发生了改变
// sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
- 部分(应用)函数(Partial functions)
- bind: let bound = func.bind(context, [arg1], [arg2], ...);
- 在没有上下文情况下的 partial
JavaScript
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// 用法:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// 添加一个带有绑定时间的 partial 方法
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// 类似于这样的一些内容:
// [10:00] John: Hello!
深入理解箭头函数
arr.forEach(func)
——forEach
对每个数组元素都执行func
。setTimeout(func)
——func
由内建调度器执行。
- 箭头函数没有 “this”
- 不能对箭头函数进行
new
操作 - :箭头函数不能用作构造器(constructor)。不能用
new
调用它们。
- 不能对箭头函数进行
- 箭头函数没有 “arguments”