0%

JavaScript函数进阶---学习笔记

本章将深入J高级,诸如Spread语法、闭包、垃圾回收机制、函数对象、SetTimeout和SetInterval、装饰器模式和转发call/apply以及箭头函数。

递归和堆栈

递归

一个函数在其内部调用自身,就是所谓的递归

递归和迭代(即循环) — 两种思考方式

  1. 递归通常是将函数调用简化为一个更简单的函数调用,然而再将其简化为一个更简单的函数调用,如此反复,直到基础的递归(结果显而易见的)
  2. 递归深度:最大的嵌套调用次数。在JS中,最大递归深度受限于JS引擎,引擎在最大递归深度小于等于10000时是可靠的。
  3. 任何递归都可以用迭代(即循环)来重写。同时,循环算法更节省内存。但对于大多数任务来说,递归方法足够快,并且容易编写和维护。

执行上下文和堆栈 — 递归调用如何工作

定义:执行上下文是一个内部数据结构,包含函数执行的详细细节(当前控制流所在位置,当前变量,this的值以及其他)

当一个函数进行嵌套调用时:

  1. 当前函数被暂停
  2. 与它相关的执行上下文被执行上下文堆栈的特殊数据结构保存
  3. 执行嵌套调用
  4. 嵌套调用结束,从堆栈恢复之前的执行上下文,并从停止的位置继续执行

递归遍历 — 递归的应用之一

递归结构

链表

Rest参数与Spread语法

  • rest参数作用:传入任意数量的参数
  • Spread作用:数组作为参数传入给支持任意数量参数的函数

Rest参数 — ...

首先,在JavaScript中,无论函数是如何定义的,都可以传入任意数量的参数,并且不会报错,但只会将前n各定义的参数进行调用,而忽略多余的参数。

用法

...剩余参数数组名

例1:

1
2
3
4
5
6
7
8
9
10
11
function sumAll(...args) {
let sum = 0;

for (let item of args) {
sum += item;
}

return sum;
}

console.log(sumAll(1,2,3,4)); // 10

例2:

1
2
3
4
5
6
7
function members(m1, m2, ...others) {
console.log(m1);
console.log(m2);
console.log(others.length);
}

members('Will', 'Dion', 'Akshay', 'Sam'); // 'Will', 'Dion', 2

注意:...Rest必须放在参数列表的最后,否则会报错

arguments变量

arguments变量是一个特殊的类数组对象,该对象按参数索引包含所有参数。

1
2
3
4
5
6
7
8
9
10
function showName() {
console.log(arguments.length);
console.log(arguments[0]);
console.log(arguments[1]);

let sum = 0;
for (let person of arguments) sum += 1; // 可遍历的
}

showName('Will'); // 1, 'Will', 'undefined'

注:箭头函数中没有arguments, 如果其嵌套在另一个“普通”函数中,则其访问到的arguments属于外部函数

Spread — ...

用法

...+可迭代对象,即可将可迭代对象展开到参数列表中

用例:

  • 传递可迭代参数

    1
    2
    3
    4
    5
    Math.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
    5
    let 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
    5
    let 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
2
3
4
5
6
7
8
9
let arr = [1,2,3];
let arrCopy = [...arr];

JSON.stringify(arr) === JSON.stringify(arrCopy); // true
arr === arrCopy; // false

arr.push(4);
arr; // [1,2,3,4]
arrCopy; // [1,2,3]

变量作用域及闭包

代码块 {...}

如果在代码块{...}中声明了一个变量。那么它只在该代码块内可见

1
2
3
4
5
6
{
let only = 'hello';

only; // 'hello'
}
only; // Error: only is not defined

注:在同一个代码块中,如果对已存在的变量,使用letconst进行重复声明,则会报错

if, for, while

对于ifforwhile等,在{...}中声明的变量也仅在内部可见

1
2
3
4
5
for (let i=0; i<3; i++) {
console.log(i); // 0,1,2
}

console.log(i); // Error, no such variable

表面上看,let i也位于{...}之外,但for构造十分特殊,在()中声明的变量也被视作代码块的一部分

嵌套函数

1
2
3
4
5
6
7
8
9
function speak(name, age) {
function infoMan() {
return name+": "+age;
}

console.log(infoMan());
}

speak("Will", 24);

词法环境

Step1. 变量

在JS中,每个运行的函数,代码块{...},以及整个脚本,都有一个被称为词法环境的隐藏的关联对象。该词法环境对象由两部分组成:

  1. 环境记录——一个将所有局部变量作为其属性的对象
  2. 外部词法环境的引用,与外部代码相关联

例如:

image-20211004201352993

如上图,矩形表示环境记录,箭头表示外部引用,由于全局词法环境没有外部引用,所以箭头指向了null

注:词法环境是一个规范对象,它仅仅是理论上存在,但我们无法在代码中获取该对象

Step2. 函数声明

注:函数声明的初始化会被立即执行完成,所以不同于变量声明,函数可以在其被定义前使用。但是不适用于将函数分配给变量的函数表达式,如let say = function(){...}

Step3. 内部和外部的词法环境

在一个函数运行时,在其调用刚开始时,会自动创建一个新的词法环境来存储这个调用的局部变量和参数

image-20211004230247163

如图所示,内部词法环境存储一个单独的属性name,外部词法环境即全局词法环境存储phrase变量和函数本身。并且当代码要访问一个变量时,首先会搜索内部词法环境,然后搜索外部词法环境,然后搜索更外部的,直至全局词法环境。因此内部词法环境引用了outer去访问变量phrase

Step4. 返回函数

image-20211004233226300

如果要更新变量的值,变量会在其所在的词法环境中更新其自身的值(即变量将在同一位置更新值)

Plus: 闭包

定义:闭包是指内部函数总是可以访问在外部函数中声明的变量和参数。JavaScript中的函数会自动通过隐藏的词法环境存储了创建它们的位置,所以他们都可以访问外部的变量。

垃圾收集

词法环境仅在可达到时才会被保留在内存中,否则词法环境及其中的所有变量都会被从内存中删除。

全局对象

全局对象提供任何地方都可以使用的变量和函数。在浏览器中,它叫window,在Node.js中,它叫global。但更通用的事gloablThis,几乎所有环境都支持该名称。

  • 全局对象的所有属性都可以直接被访问

    1
    2
    3
    alert('Hello World!');
    // 等价于
    window.alert('Hello World!');
  • 使用var声明的变量会成为全局对象的属性(不建议使用)

    1
    2
    3
    var rank = 1;

    window.rank; // 1
  • 如果要想在全局范围使用,可以直接将其作为属性写入

    1
    2
    3
    4
    5
    6
    7
    window.rank = 1;
    window.admin = {
    name: "Will"
    };

    rank; // 1
    admin.name; // 'Will'

    使用ployfills

使用全局对象测试对现代JS语言的支持情况

1
2
3
if (!window.Promise) {
window.Promise = ... // 该旧版本浏览器不支持Promise对象,因此手动实现该现代语言功能
}

函数对象,NFE

在JS中,函数就是对象,即我们不仅可以调用它们,还可以把它们当作对象,增删属性,按引用传递。

属性 “name”

1
2
3
4
5
function sayHi() {
alert("Hello World!");
}

sayHi.name; // 'sayHi'

属性 ”length“

1
2
3
4
5
6
7
function f1(a) {}
function f2(a,b) {}
function f3(a,b,..) {}

f1.length; // 1
f2.length; // 2
f3.length; // 2

可知,rest参数不参与计数

自定义属性

注: 属性不是变量,定义函数的属性,并不会在函数内部定义一个局部变量,即属性和变量是两个毫不相关的东西。

1
2
3
4
5
6
7
8
function sayHi() {
sayHi.counter++;
}

sayHi.counter = 0;
sayHi();
sayHi();
sayHi,counter; // 2

命名函数表达式(NFE, Named Function Expression)

定义:指带有名字的函数表达式

  1. 普通函数表达式

    1
    2
    3
    let sayHi = function(who) {
    console.log(who);
    }
  2. 命名函数表达式

    1
    2
    3
    let sayHi = function func(who) {
    console.log(who);
    }

    命名函数表达式有两个特殊地方:

    1. 允许函数内部引用自身
    2. 在函数外部不可见
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let sayHi = function func(who) {
    if (who) {
    console.log(who);
    } else {
    func("Guest");
    }
    }

    sayHi(); // 'Guest'
    func(); // Error, func is not defined

    new Function(很少用)

语法:

1
let func = new Function ([arg1, agr2, ...], functionBody);

例如:

1
2
let sumNum = new Function ('a', 'z', 'return a + z');
sum(1,3); // 4

注:new Function创建的函数的[[Environment]]不指向当前的词法环境,而是指向全局环境

调度:setTimeout和setInterval

有时,我们并不想立即执行一个函数,而是等待一段时间之后再执行,我们称之为“计划调用”(scheduling a call)

目前有两种实现:

  • setTimeout允许我们函数在一定时间间隔之后再执行,且仅执行一次
  • setInterval允许我们一定的时间间隔重复运行该函数,从一定时间间隔之后开始运行

注:这两个方法并不在JS规范中,但大多数运行环境都有内建的调度程序,并提供了这些方法。目前,所有浏览器及Node.js都支持这两个方法

setTimeout

语法:

1
let timerId = setTimeout(func|codeString, delay, arg1, arg2, ...)

参数说明:

  1. func|codeString: 要执行的函数或代码字符串(不推荐)
  2. delay: 执行前的延迟,以毫秒为单位(1000ms = 1s),默认值为0
  3. arg1, arg2, ...: 被执行函数的参数
  4. timerId: 接收setTimeout返回的“定时器标识符(timer identifier)”

例1:

1
2
3
4
5
function sayHi() {
console.log("Hello");
}

steTimeout(sayHi, 1000);

例2:

1
2
3
4
5
function sayHi(name, age) {
console.log(`${name}: ${age}`);
}

setTimeout(sayHi, 1000, 'Will', 24); // Will: 24

注:setTimeout接收的是一个对函数的引用,因此通常情况不要传入一个函数的执行,如sayHi(),因此通常它们不会返回任何结果,setTimeout将接收到一个undefined

clearTimeout来取消调度

setTimeout在调用时会返回一个“定时器标识符(timer identifier)”,我们可以用变量来接收它,并使用它来取消执行

1
2
let timerId = setTimeout(...);
clearTimeout(timerId);

例如:

1
2
3
4
5
let timerId = setTimeout(() => alert('Hello World!'), 1000);
console.log(timerId); // 定时器标识符

clearTimeout(timerId); // 取消调度
console.log(timerId); // 定时器标识符还在(并不会因为调度被取消而变成null)

setInterval

语法与setTimeout完全相同

它们的区别是:

  • setTimeout只执行一次,但**setInterval是每间隔给定时间,周期性执行**
  • 对于setInterval来说,内部调用程序会每隔一定时间执行一次function,但是function执行也需要时间,且其消耗时间算在“每隔一定时间”内,因此在使用setInterval时,function实际调用的间隔要比代码中设定的“每隔一定时间”要短
1
2
3
let timerId = setInterval(() => console.log('tick'), 2000); // 每两秒打印一次'tick'

setTimeout(() => clearInterval(timerId), 5000); // 5秒后停止

嵌套的setTimeout

周期性调度有两种实现方式

  • setInterval
  • 嵌套的setTimeout
1
2
3
4
let timerId = setTimeout(function func1() {
console.log('tick');
timerId = setTimeout(func1, 1000); //嵌套
}, 1000)

嵌套的setTimeoutsetInterval具有更多的灵活性,因此它可以根据当前执行结果来调度下一次调用。例如,如果一开始每5秒向服务器请求一次数据,但如果服务器过载了,那么就必须降低请求频率,如增加间隔时间

1
2
3
4
5
6
7
8
9
10
let delay = 5000;

let timerId = setTimeout(function request() {
...发送请求...
if (由于服务器过载请求失败) {
delay =* 2;
}

timerId = setTimeout(request, delay)
}, delay)

零延时的setTimeout

语法:setTimeout(func, 0)setTimeout(func)

func仍然要等到当前正在执行的脚本执行完成后,调度程序才会调用它。

1
2
setTimeout(() => console.log("World!"));
console.log("Hello, ");

控制台的结果为:

'Hello, '

'World!'

装饰器模式和转发 — call/apply

在JavaScript中,函数不仅可以被传递和用作对象,而且还可以在它们之间进行转发(forward)调用并装饰(decorate)它们。

透明缓存

假设有一个CPU负载很重的函数,但它的结果非常稳定(即对于相同的输入值,它总返回相同的结果)。因此,我们希望将结果缓存下来,以节省重新计算的额外花费

实现方式:创建一个包装器(wrapper)函数,同时该函数增加了缓存的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function slow(x) {
console.log(x);
return x;
}

function cachingDecorator(func) {
let cache = new Map();

return function(x) { // wrapper
if (cache.has(x)) {
return cache.get(x);
}

let result = func(x); // (**)

cache.set(x, result);
return result;
}
}

slow = cachingDecorator(slow);

console.log(slow(1)); // 1
console.log(slow(2)); // 2

cachingDecorator是一个装饰器(decorator)即它是一个特殊的函数,它接受另一个函数并改变其行为。

使用”func.call”设定上下文

上述的缓存装饰器不适用于对象方法

1
2
3
4
5
6
7
8
9
10
11
12
let worker = {
someMethod() {
return 1;
},

slow(x) {
return x * this.someMethod(); // (*)
}
};

worker.slow = cachingDecorator(worker.slow);
worker.slow(2); // Error: Cannot read property 'someMethod' of undefined

错误原因是,当调用(*)行时,使用的this是来自包装器在(**)行调用原始函数的this=undefined

使用内置的函数方法function.call(context, arg1, arg2, ...)可以显式设置this

上述cachingDecarator代码第(**)应为let result = func.call(this, x)。此时this=worker

其他用例:

1
2
3
4
5
6
7
function say(phrase) {
console.log(this.name+': '+phrase);
}

let user = {name:"John"};

say.call(user, 'Hello'); // 'John: Hello'

传递多个参数

例如,多参数worker.slow:

1
2
3
4
5
let worker = {
slow(min, max) {
return min + max;
}
}

解决方法:

  1. 实现一个类似map的多个键的数据结构
  2. 使用嵌套map:cache.set(min)存储键值对(max, result)。然后使用cache.get(min).get(max)来获取result
  3. 使用类似哈希函数:将两个值合并为一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
console.log(worker.slow(1,2)); // 3

function.apply

语法:

1
function.apply(context, args)

function.applyfunction.call类似,参数context用于设定this唯一的区别在于传入applyargs必须为一个类数组对象,而传入callarg1, arg2, ...必须为一个参数列表。

Tips:

  • 使用Spread语法...可以将**可迭代对象args**展开为参数列表传递给call
  • apply仅接受类数组对象args

例如:

1
2
func.call(context, ...args);
func.apply(context, args);

其中args为数组

方法借用

如上类哈希函数只适用于2个参数

1
2
3
function hash(args) {
return args[0] + ', ' + args[1];
}

简单并理想的方法是使用.join()方法,但是尽管args对象是可迭代和类数组对象,但是它不是真正的数组。因此,使用args.join()会报错。

使用方法借用可以解决这个问题:

1
2
3
function hash() {
return [].join.call(arguments);
}

装饰器和函数属性

通常,用装饰的函数/方法去替换原函数/方法是安全的,但是如果原函数有属性,如func.calledCount,则装饰后的函数不会具有该属性。

函数绑定

问题:当将对象方法作为回调进行传递,经常会出现“丢失this“的问题

丢失this

例子:

1
2
3
4
5
6
7
8
let user = {
firstName: "John",
sayHi() {
console.log(`Hello, ${this.firstName}!`);
}
};

setTimeout(user.sayHi, 1000); // 'Hello, undefined'

其中,浏览器中的setTimeout方法有些特殊,它会为函数调用设定this=window,因为它其实是在获取window.firstName。在其他类似情况下,this通常会被设定为undefined

那么想将一个对象方法传递到别的地方,然后在该位置进行调用,如何才能确保在正确的上下文中调用它呢?

解决方案1—包装器

1
2
3
4
5
6
7
8
9
10
let user = {
firstName: "John",
sayHi() {
console.log(`Hello, ${this.firstName}!`);
}
};

setTimeout(function() {
user.sayHi(); // 'Hello, John!'
}, 1000);

也可以使用箭头函数,但更简洁

1
setTimeout(() => user.sayHi(), 1000);

但是,如果user的内容在setTimeout中的函数执行前改变了,那么就会发生错误,**使用bind可以避免这样的问题

解决方案2—bind

内建方法bind可以帮助函数绑定this

基本语法如下:

1
let boundFunc = func.bind(context);

例如:

1
2
3
4
5
6
7
8
9
10
11
12
let user = {
firstName: 'John',
sayHi() {
console.log(`Hello, ${this.firstName}!`);
}
};

let sayHi = user.sayHi.bind(user);

setTimeout(sayHi, 1000); // 'Hello, John!'

user = 1;

偏函数—Partial functions

bind的完整语法:

1
let boundFunc = func.bind(context, arg1, arg2, ...);

bind允许将上下文绑定为this,以及绑定函数的起始参数(即设定某个参数的输入值且不可修改)

例如:

1
2
3
4
5
6
7
8
function mul(a, b) {
return a*b;
}

let double = mul.bind(null, 2);

console.log(double(3)); // 3*2=6
console.log(double(4)); // 4*2=8

深入理解箭头函数

JS的精髓之一是在于创建一个函数并将其传递到某个地方再执行。在这种情况下,我们通常不想离开当前的上下文,此时箭头函数就显得很方便了

箭头函数没有”this”

箭头函数没有this,如果要访问this,则会从外部获取。

1
2
3
4
5
6
7
8
9
10
11
let group = {
title: 'JS-Squad',
students: ['Will', 'Dion', 'Akshay'],

showList() {
this.students.forEach(
student => console.log(this.title + ': ' + student))
}
};

group.showList(); // 'JS-Squad: Will' ...

其中,forEach种使用了箭头函数,因此会从外部获取this,该thisgroup,所以就是group.title

箭头函数没有”arguments”

同时,箭头函数也没有arguments变量。因此当我们(常见,在装饰器中)需要使用当前的thisarguments转发一个调用时,可以使用如下写法:

其中:defer获得一个函数,然后返回一个包装器。

箭头函数版:

1
2
3
4
5
6
7
8
9
10
11
12
function defer(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms)
};
}

function sayHi(who) {
console.log(`Hello, ${who}!`);
}

let sayHiDefer = defer(sayHi, 2000);
sayHiDefer('John');

非箭头函数版:

1
2
3
4
5
6
7
8
function def(f, ms) {
return function(...args) {
let ctx = this;
setTimeout(function() {
return f.apply(ctx, args)
}, ms);
};
}

此时,需要创建ctxargs,以便setTimeout内部的函数可以获取它们。

补充之前

for loop

  • for/in - loops through the properties of an object
  • for/of- loops through the values of an iterable object