本章将深入J高级,诸如Spread语法、闭包、垃圾回收机制、函数对象、SetTimeout和SetInterval、装饰器模式和转发call/apply以及箭头函数。
递归和堆栈
递归
一个函数在其内部调用自身,就是所谓的递归
递归和迭代(即循环) — 两种思考方式
- 递归通常是将函数调用简化为一个更简单的函数调用,然而再将其简化为一个更简单的函数调用,如此反复,直到基础的递归(结果显而易见的)
- 递归深度:最大的嵌套调用次数。在JS中,最大递归深度受限于JS引擎,引擎在最大递归深度小于等于10000时是可靠的。
- 任何递归都可以用迭代(即循环)来重写。同时,循环算法更节省内存。但对于大多数任务来说,递归方法足够快,并且容易编写和维护。
执行上下文和堆栈 — 递归调用如何工作
定义:执行上下文是一个内部数据结构,包含函数执行的详细细节(当前控制流所在位置,当前变量,this的值以及其他)
当一个函数进行嵌套调用时:
- 当前函数被暂停
- 与它相关的执行上下文被执行上下文堆栈的特殊数据结构保存
- 执行嵌套调用
- 嵌套调用结束,从堆栈恢复之前的执行上下文,并从停止的位置继续执行
递归遍历 — 递归的应用之一
递归结构
链表
Rest参数与Spread语法
- rest参数作用:传入任意数量的参数
- Spread作用:数组作为参数传入给支持任意数量参数的函数
Rest参数 — ...
首先,在JavaScript中,无论函数是如何定义的,都可以传入任意数量的参数,并且不会报错,但只会将前n各定义的参数进行调用,而忽略多余的参数。
用法
...剩余参数数组名
例1:
1 | function sumAll(...args) { |
例2:
1 | function members(m1, m2, ...others) { |
注意:...Rest
必须放在参数列表的最后,否则会报错
arguments
变量
arguments
变量是一个特殊的类数组对象,该对象按参数索引包含所有参数。
1 | function showName() { |
注:箭头函数中没有arguments
, 如果其嵌套在另一个“普通”函数中,则其访问到的arguments
属于外部函数
Spread — ...
用法
...
+可迭代对象,即可将可迭代对象展开到参数列表中
用例:
传递可迭代参数
1
2
3
4
5Math.max(1,3,5,29,0); // 29
let arr = [1,3,5,29,0];
Math.max(...arr); // 29
let arr1 = [8,2,1,-9];
Math.max(...arr, ...arr1); // 29合并数组
1
2
3
4
5let arr1 = [1,2,3];
let arr2 = [4,5,6];
let arr = [0,...arr1,...arr2];
arr; // [0,1,2,3,4,5,6]字符串 — 因为Spread语法内部使用迭代器来收集元素,与
for...of
方式相同1
2
3
4
5let str1 = 'Hello';
[...str1]; // ['H','e','l','l','o']
Array.from(str1); // ['H','e','l','l','o']Array.from(obj)
和[...obj]
的差别:Array.from(obj)
的适用范围更广,不仅适用于类数组对象也适用于可迭代对象- Spread语法只适用于可迭代对象
获取一个array/object的副本
即spread语法也可以实现类似Object.assign()
的功能——即浅拷贝
用例
1 | let arr = [1,2,3]; |
变量作用域及闭包
代码块 {...}
如果在代码块{...}
中声明了一个变量。那么它只在该代码块内可见
1 | { |
注:在同一个代码块中,如果对已存在的变量,使用let
或const
进行重复声明,则会报错
if
, for
, while
对于if
、for
、while
等,在{...}
中声明的变量也仅在内部可见
1 | for (let i=0; i<3; i++) { |
表面上看,let i
也位于{...}
之外,但for
构造十分特殊,在()
中声明的变量也被视作代码块的一部分
嵌套函数
1 | function speak(name, age) { |
词法环境
Step1. 变量
在JS中,每个运行的函数,代码块{...}
,以及整个脚本,都有一个被称为词法环境的隐藏的关联对象。该词法环境对象由两部分组成:
- 环境记录——一个将所有局部变量作为其属性的对象
- 对外部词法环境的引用,与外部代码相关联
例如:
![image-20211004201352993](/Users/arieskoo/Library/Application Support/typora-user-images/image-20211004201352993.png)
如上图,矩形表示环境记录,箭头表示外部引用,由于全局词法环境没有外部引用,所以箭头指向了null
注:词法环境是一个规范对象,它仅仅是理论上存在,但我们无法在代码中获取该对象
Step2. 函数声明
注:函数声明的初始化会被立即执行完成,所以不同于变量声明,函数可以在其被定义前使用。但是不适用于将函数分配给变量的函数表达式,如let say = function(){...}
Step3. 内部和外部的词法环境
在一个函数运行时,在其调用刚开始时,会自动创建一个新的词法环境来存储这个调用的局部变量和参数
![image-20211004230247163](/Users/arieskoo/Library/Application Support/typora-user-images/image-20211004230247163.png)
如图所示,内部词法环境存储一个单独的属性name
,外部词法环境即全局词法环境存储phrase
变量和函数本身。并且当代码要访问一个变量时,首先会搜索内部词法环境,然后搜索外部词法环境,然后搜索更外部的,直至全局词法环境。因此内部词法环境引用了outer
去访问变量phrase
。
Step4. 返回函数
![image-20211004233226300](/Users/arieskoo/Library/Application Support/typora-user-images/image-20211004233226300.png)
如果要更新变量的值,变量会在其所在的词法环境中更新其自身的值(即变量将在同一位置更新值)
Plus: 闭包
定义:闭包是指内部函数总是可以访问在外部函数中声明的变量和参数。JavaScript中的函数会自动通过隐藏的词法环境存储了创建它们的位置,所以他们都可以访问外部的变量。
垃圾收集
词法环境仅在可达到时才会被保留在内存中,否则词法环境及其中的所有变量都会被从内存中删除。
全局对象
全局对象提供任何地方都可以使用的变量和函数。在浏览器中,它叫window
,在Node.js中,它叫global
。但更通用的事gloablThis
,几乎所有环境都支持该名称。
全局对象的所有属性都可以直接被访问
1
2
3alert('Hello World!');
// 等价于
window.alert('Hello World!');使用
var
声明的变量会成为全局对象的属性(不建议使用)1
2
3var rank = 1;
window.rank; // 1如果要想在全局范围使用,可以直接将其作为属性写入
1
2
3
4
5
6
7window.rank = 1;
window.admin = {
name: "Will"
};
rank; // 1
admin.name; // 'Will'使用ployfills
使用全局对象测试对现代JS语言的支持情况
1 | if (!window.Promise) { |
函数对象,NFE
在JS中,函数就是对象,即我们不仅可以调用它们,还可以把它们当作对象,增删属性,按引用传递。
属性 “name”
1 | function sayHi() { |
属性 ”length“
1 | function f1(a) {} |
可知,rest参数不参与计数
自定义属性
注: 属性不是变量,定义函数的属性,并不会在函数内部定义一个局部变量,即属性和变量是两个毫不相关的东西。
1 | function sayHi() { |
命名函数表达式(NFE, Named Function Expression)
定义:指带有名字的函数表达式
普通函数表达式
1
2
3let sayHi = function(who) {
console.log(who);
}命名函数表达式
1
2
3let sayHi = function func(who) {
console.log(who);
}命名函数表达式有两个特殊地方:
- 允许函数内部引用自身
- 在函数外部不可见
1
2
3
4
5
6
7
8
9
10let sayHi = function func(who) {
if (who) {
console.log(who);
} else {
func("Guest");
}
}
sayHi(); // 'Guest'
func(); // Error, func is not definednew Function(很少用)
语法:
1 | let func = new Function ([arg1, agr2, ...], functionBody); |
例如:
1 | let sumNum = new Function ('a', 'z', 'return a + z'); |
注:new Function
创建的函数的[[Environment]]
不指向当前的词法环境,而是指向全局环境。
调度:setTimeout和setInterval
有时,我们并不想立即执行一个函数,而是等待一段时间之后再执行,我们称之为“计划调用”(scheduling a call)
目前有两种实现:
setTimeout
允许我们函数在一定时间间隔之后再执行,且仅执行一次setInterval
允许我们一定的时间间隔重复运行该函数,从一定时间间隔之后开始运行
注:这两个方法并不在JS规范中,但大多数运行环境都有内建的调度程序,并提供了这些方法。目前,所有浏览器及Node.js都支持这两个方法
setTimeout
语法:
1 | let timerId = setTimeout(func|codeString, delay, arg1, arg2, ...) |
参数说明:
func|codeString
: 要执行的函数或代码字符串(不推荐)delay
: 执行前的延迟,以毫秒为单位(1000ms = 1s),默认值为0arg1
,arg2, ...
: 被执行函数的参数timerId
: 接收setTimeout
返回的“定时器标识符(timer identifier)”
例1:
1 | function sayHi() { |
例2:
1 | function sayHi(name, age) { |
注:setTimeout
接收的是一个对函数的引用,因此通常情况不要传入一个函数的执行,如sayHi()
,因此通常它们不会返回任何结果,setTimeout
将接收到一个undefined
用clearTimeout
来取消调度
setTimeout
在调用时会返回一个“定时器标识符(timer identifier)”,我们可以用变量来接收它,并使用它来取消执行
1 | let timerId = setTimeout(...); |
例如:
1 | let timerId = setTimeout(() => alert('Hello World!'), 1000); |
setInterval
语法与setTimeout
完全相同
它们的区别是:
setTimeout
只执行一次,但**setInterval
是每间隔给定时间,周期性执行**- 对于
setInterval
来说,内部调用程序会每隔一定时间执行一次function,但是function执行也需要时间,且其消耗时间算在“每隔一定时间”内,因此在使用setInterval
时,function实际调用的间隔要比代码中设定的“每隔一定时间”要短
1 | let timerId = setInterval(() => console.log('tick'), 2000); // 每两秒打印一次'tick' |
嵌套的setTimeout
周期性调度有两种实现方式
setInterval
- 嵌套的
setTimeout
1 | let timerId = setTimeout(function func1() { |
嵌套的setTimeout
比setInterval
具有更多的灵活性,因此它可以根据当前执行结果来调度下一次调用。例如,如果一开始每5秒向服务器请求一次数据,但如果服务器过载了,那么就必须降低请求频率,如增加间隔时间
1 | let delay = 5000; |
零延时的setTimeout
语法:setTimeout(func, 0)
或setTimeout(func)
但func
仍然要等到当前正在执行的脚本执行完成后,调度程序才会调用它。
1 | setTimeout(() => console.log("World!")); |
控制台的结果为:
'Hello, '
'World!'
装饰器模式和转发 — call/apply
在JavaScript中,函数不仅可以被传递和用作对象,而且还可以在它们之间进行转发(forward)调用并装饰(decorate)它们。
透明缓存
假设有一个CPU负载很重的函数,但它的结果非常稳定(即对于相同的输入值,它总返回相同的结果)。因此,我们希望将结果缓存下来,以节省重新计算的额外花费
实现方式:创建一个包装器(wrapper)函数,同时该函数增加了缓存的功能。
1 | function slow(x) { |
cachingDecorator
是一个装饰器(decorator)即它是一个特殊的函数,它接受另一个函数并改变其行为。
使用”func.call”设定上下文
上述的缓存装饰器不适用于对象方法
1 | let worker = { |
错误原因是,当调用(*)
行时,使用的this
是来自包装器在(**)
行调用原始函数的this=undefined
。
使用内置的函数方法function.call(context, arg1, arg2, ...)
可以显式设置this
上述cachingDecarator
代码第(**)
应为let result = func.call(this, x)
。此时this=worker
。
其他用例:
1 | function say(phrase) { |
传递多个参数
例如,多参数worker.slow
:
1 | let worker = { |
解决方法:
- 实现一个类似map的多个键的数据结构
- 使用嵌套map:即
cache.set(min)
存储键值对(max, result)
。然后使用cache.get(min).get(max)
来获取result
- 使用类似哈希函数:将两个值合并为一个
1 | function cachingDecorator(func, hash) { |
function.apply
语法:
1 | function.apply(context, args) |
function.apply
与function.call
类似,参数context
用于设定this
,唯一的区别在于传入apply
的args
必须为一个类数组对象,而传入call
的arg1, arg2, ...
必须为一个参数列表。
Tips:
- 使用Spread语法
...
可以将**可迭代对象args
**展开为参数列表传递给call
apply
仅接受类数组对象args
例如:
1 | func.call(context, ...args); |
其中args
为数组
方法借用
如上类哈希函数只适用于2个参数
1 | function hash(args) { |
简单并理想的方法是使用.join()
方法,但是尽管args
对象是可迭代和类数组对象,但是它不是真正的数组。因此,使用args.join()
会报错。
使用方法借用可以解决这个问题:
1 | function hash() { |
装饰器和函数属性
通常,用装饰的函数/方法去替换原函数/方法是安全的,但是如果原函数有属性,如func.calledCount
,则装饰后的函数不会具有该属性。
函数绑定
问题:当将对象方法作为回调进行传递,经常会出现“丢失this
“的问题
丢失this
例子:
1 | let user = { |
其中,浏览器中的setTimeout
方法有些特殊,它会为函数调用设定this=window
,因为它其实是在获取window.firstName
。在其他类似情况下,this
通常会被设定为undefined
。
那么想将一个对象方法传递到别的地方,然后在该位置进行调用,如何才能确保在正确的上下文中调用它呢?
解决方案1—包装器
1 | let user = { |
也可以使用箭头函数,但更简洁
1 | setTimeout(() => user.sayHi(), 1000); |
但是,如果user的内容在setTimeout中的函数执行前改变了,那么就会发生错误,**使用bind
可以避免这样的问题
解决方案2—bind
内建方法bind
可以帮助函数绑定this
基本语法如下:
1 | let boundFunc = func.bind(context); |
例如:
1 | let user = { |
偏函数—Partial functions
bind
的完整语法:
1 | let boundFunc = func.bind(context, arg1, arg2, ...); |
bind
允许将上下文绑定为this
,以及绑定函数的起始参数(即设定某个参数的输入值且不可修改)
例如:
1 | function mul(a, b) { |
深入理解箭头函数
JS的精髓之一是在于创建一个函数并将其传递到某个地方再执行。在这种情况下,我们通常不想离开当前的上下文,此时箭头函数就显得很方便了
箭头函数没有”this”
箭头函数没有this
,如果要访问this
,则会从外部获取。
1 | let group = { |
其中,forEach
种使用了箭头函数,因此会从外部获取this
,该this
为group
,所以就是group.title
。
箭头函数没有”arguments”
同时,箭头函数也没有arguments
变量。因此当我们(常见,在装饰器中)需要使用当前的this
和arguments
转发一个调用时,可以使用如下写法:
其中:defer
获得一个函数,然后返回一个包装器。
箭头函数版:
1 | function defer(f, ms) { |
非箭头函数版:
1 | function def(f, ms) { |
此时,需要创建ctx
和args
,以便setTimeout
内部的函数可以获取它们。
补充之前
for loop
- for/in - loops through the properties of an object
- for/of- loops through the values of an iterable object