RN-《JavaScript忍者秘籍》(上)

第一章 无处不在的 JavaScript

性能分析,可以使用 console.timeconsole.timeEnd 在 node 和 浏览器环境下可以看到运行的时间

1
2
3
4
5
6
7
8
9
console.time("My operation");

for(let i = 0; i < 100000; i ++) {
// execute opeation
}

console.timeEnd("My operation");

// My operation: 2.27099609375ms

这本书集中探讨了核心 JavaScript 的机制,例如函数、函数闭包和原型,还有一些新的 JavaScript 特性,例如生成器、promise、代理、映射、集合和模块。

第二章 运行时的页面构建过程

事件

客户端 Web 应用的两个生命周期是页面构建事件处理

页面构建

当Web应用能被展示或交互之前,其页面必须根据服务器获取的响应(通常是HTML、CSS和JavaScript代码)来构建。页面构建阶段的目标是建立Web应用的UI,其主要包括两个步骤:

  1. 解析HTML代码并构建文档对象模型(DOM);
  2. 执行JavaScript代码。

步骤1会在浏览器处理HTML节点的过程中执行,步骤二会在HTML解析到一种特殊节点——脚本节点(包含或引用JavaScript代码的节点)时执行。页面构建阶段中,只要还有没处理完的HTML元素和没执行完的JavaScript代码,两个步骤都会一直交替执行。

当浏览器处理完所有HTML元素后,页面构建阶段就结束了。随后浏览器就会进入应用生命周期的第二部分:事件处理。

事件处理

客户端 Web 应用是一种 GUI 应用,也就是说这种应用会对不同类型的事件作响应,如鼠标移动、单击和键盘按压等。因此,在页面构建阶段执行的JavaScript代码,除了会影响全局应用状态和修改DOM外,还会注册事件监听器(或处理器)。这类监听器会在事件发生时,由浏览器调用执行。有了这些事件处理器,我们的应用也就有了交互能力。

事件可能会以难以预计的时间和顺序发生,如以下的事件。

  • 网络事件,例如来自服务器的响应(Ajax 事件和服务器端事件)
  • 用户事件,例如鼠标单击、鼠标移动和键盘事件
  • 计时器事件,当 timeout 时间到期或又触发了一次时间间隔

这里就是熟悉的 EventLoop,不再详述,与第十三章结合起来。个人认为第二章是从宏观上看,第十三章是从微观上看。

第三章 函数课

回调函数定义

1
2
3
document.body.addEventListener('click', function () {
console.log(123)
})

上面是一个典型的回调函数,是异步的。但回调函数不是异步函数,是其他代码会在随后的某个合适时间点 “回过来调用” 的函数,下面的代码就是一个同步的回调函数。这里的概念要搞清楚。

1
2
3
4
5
6
7
8
9
function example(cb) {
cb()
}

function cb() {
console.log(123)
}

example(cb)

函数作为对象的乐趣

存储函数

比如下面的代码,利用函数也可以拥有属性,可以存储唯一函数集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var store = {
nextId: 1,
cache: {},
add: function (fn) {
if (!fn.id) { // 检测fn有没有id这个属性
fn.id = this.nextId++;
this.cache[fn.id] = fn;
return true;
}
}
};

function ninja() {}

store.add(ninja)
console.log(store.cache)
// { '1': { [Function: ninja] id: 1 } } node环境下,key为1,value为函数,函数的属性id为1
store.add(ninja)
console.log(store.cache)
// { '1': { [Function: ninja] id: 1 } }

自记忆函数

利用函数也可以拥有属性实现简单的素数判断,如果已经有结果,就不用计算了,直接取出缓存的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {};
}

if (isPrime.answers[value] !== undefined) {
return isPrime.answers[value];
}

var prime = value !== 1;

for (var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}

return isPrime.answers[value] = prime;
}

console.log(isPrime(5)) // true
console.log(isPrime(5)) // true 第二次就不用经过for循环运算了

第四章 函数进阶

构造函数返回值

执行下面的代码,会看到如果将 Ninja 作为一个函数调用,的确会返回 1,但如果通过 new 关键字将其作为构造函数调用,会构造并返回一个新的 ninja 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Ninja() {
this.skulk = function () {
return true;
};

return 1;
}

console.log(Ninja() === 1) // true

var ninja = new Ninja();

console.log(ninja) // Ninja { skulk: [Function] }

console.log(typeof ninja) // object

console.log(typeof ninja.skulk) // function

但如果做一些改变,一个构造函数返回另一个对象,如下面代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var puppet = {
rules: false
};

function Emperor() {
this.rules = true;
return puppet;
}

var emperor = new Emperor();

console.log(emperor === puppet); // true

console.log(emperor.rules); // false

结果表明:puppet 对象最终作为构造函数调用的返回值,在构造函数中对函数上下文的操作都是无效的。最终返回的将是 puppet。

总结

  • 如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this将被丢弃。
  • 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象。

函数的命名

函数和方法的命名通常以描述其行为的动词开头(doSomethingWonderful),且第一个字母小写。而构造函数则通常以描述所构造对象的名词命名,并以大写字母开头:Ninja

箭头函数的this

箭头函数没有单独的 this 值,箭头函数的 this 与声明所在的上下文的相同,在创建时就确定了 this 的指向。

在构造函数内部时

1
2
3
4
5
6
7
8
9
10
11
12
13
<button id="test">Click Me!</button>
<script>
function Button() {
this.clicked = false;
this.click = () => {
this.clicked = true;
console.log(button.clicked) // true
}
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
</script>

如上所示,button.clicked 为 true。

调用箭头函数时,不会隐式传入 this 参数,而是从定义时的函数继承上下文,在上面的代码中,箭头函数在构造函数内部,this 指向新创建的对象本身,因此无论何时调用 click 函数,this 都将指向新创建的 button 对象。

使用对象字面量时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<button id="test">Click Me!</button>
<script type="text/javascript">
console.log(this === window) // true
var button = {
clicked: false,
click: () => {
this.clicked = true;
console.log(button.clicked) // false
console.log(this); // window
console.log(window.clicked) // true
}
}

var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
</script>

click 箭头函数是作为对象字面量的属性定义的,对象字面量在全局代码中定义,因此,箭头函数内部的 this 值与全局代码的 this 值相同。

第五章 闭包

闭包是 JavaScript 作用域规则的副作用。当函数创建时所在的作用域消失后,仍然能够调用函数。

第六章 未来的函数:生成器和promise

对迭代器进行迭代

用 while 循环迭代生成器的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义一个生成器
function* WeaponGenerator() {
yield "Katana";
yield "Wakizashi";
}

const weaponsIterator = WeaponGenerator(); // 新建一个迭代器

var item; // 这个变量来保存生成器产生的值

// 每次循环从生成器中取出一个值,当生成器不再生成值的时候,停止迭代
while (!(item = weaponsIterator.next()).done) {
console.log(item.value)
}

// 依次打印
// Katana
// Wakizashi

上面的代码就是 for of 循环的原理。for of 循环不过是对迭代器进行迭代的语法糖。

1
2
3
for(var item of WeaponGenerator()) {
console.log(item)
}

不同于手动调用迭代器的 next 方法,for of 循环同时还要查看生成器是否完成,在后台自动做了相同的工作。

把执行权交给下一个生成器

for of 循环不会关心 WarriorGenerator 委托到另一个生成器上,它只关心在 done 状态到来之前都一直调用 next 方法。

注意这里的 yield*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* WarriorGenerator() {
yield "Sun Tzu";
yield* NinjaGenerator();
yield "Genghis Khan";
}

function* NinjaGenerator() {
yield "Hatori";
yield "Yoshi";
}

for (let warrior of WarriorGenerator()) {
console.log(warrior)
}

// 依次打印
// Sun Tzu
// Hatori
// Yoshi
// Genghis Khan

用生成器生成ID序列

测试结果结尾 true,id 仅能在生成器中被访问,while 的每次迭代都能生成一个新的 id 值并挂起执行,直到下一次调用 next 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* IdGenerator() {
let id = 0;
while (true) {
yield ++id;
}
}

const idIterator = IdGenerator();

const ninja1 = {
id: idIterator.next().value
};
const ninja2 = {
id: idIterator.next().value
};
const ninja3 = {
id: idIterator.next().value
};

console.log(ninja1.id === 1)
console.log(ninja2.id === 2)
console.log(ninja3.id === 3)

迭代器遍历 DOM 树

一般的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="subTree">
<form>
<input type="text" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>

<script>
function traverseDOM(element, callback) {
callback(element);
element = element.firstElementChild;
while (element) {
traverseDOM(element, callback);
element = element.nextElementSibling;
}
}

const subTree = document.getElementById("subTree");
traverseDOM(subTree, function (element) {
console.log(element.nodeName)
});
</script>

使用生成器写法,不用再写回调函数了,一个 for of 循环就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="subTree">
<form>
<input type="text" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>

<script>
function* DomTraversal(element) {
yield element;
element = element.firstElementChild;
while (element) {
yield* DomTraversal(element);
element = element.nextElementSibling;
}
}

const subTree = document.getElementById("subTree");
for (let element of DomTraversal(subTree)) {
console.log(element.nodeName)
}
</script>

与生成器交互

注意第二个 next 传递了参数,imposter 随之改变,进行了交互,可以想象到和 promise 结合时,next 传递的参数可以改变 imposter,然后进行下一步的请求(这个请求需要用到 imposter)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* NinjaGenerator(action) {
const imposter = yield ("Hatori " + action);

console.log(imposter === "Hanzo") // 这里是关键

yield ("Yoshi (" + imposter + ") " + action);
}

const ninjaIterator = NinjaGenerator("skulk");

const result1 = ninjaIterator.next();
console.log(result1.value === "Hatori skulk")

const result2 = ninjaIterator.next("Hanzo"); // 这里进行了传参
console.log(result2.value === "Yoshi (Hanzo) skulk")

与promise结合

这只是个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
async (function* () {
try {
const ninjas = yield getJSON("data/ninjas.json");
const missions = yield getJSON(ninjas[0].missionsUrl);
const missionDescription = yield getJSON(missions[0].detailsUrl);

console.log(ninjas !== null && missions !== null && missionDescription !== null)
} catch (e) {
console.log(e);
}
});

function async (generator) {
const iterator = generator();

function handle(iteratorResult) {
if (iteratorResult.done) {
return;
}

const iteratorValue = iteratorResult.value;

if (iteratorValue instanceof Promise) {
iteratorValue.then(res => handle(iterator.next(res)))
.catch(err => iterator.throw(err))
}
}

try {
handle(iterator.next());
} catch (e) {
iterator.throw(e);
}
}

第七章 面向对象与原型

理解原型

在 JavaScript 中,对象的原型属性是内置属性(使用标记[[prototype]]),无法直接访问。内置方法 Object.setPrototypeOf 需要传入两个对象作为参数,并将第二个对象设置为第一个对象的原型。

1
2
3
4
5
6
7
8
9
10
const xiaoming = {
speak: true
}
const xiaohong = {
eat: true
}

Object.setPrototypeOf(xiaoming, xiaohong)

console.log("eat" in xiaoming) // true

每个对象都可以有一个原型,每个对象的原型也可以拥有一个原型,以此类推,形成一个原型链,查找特定属性将会被委托在整个原型链上,只有当没有更多的原型可以进行查找时,才会停止查找。

constructor

下面的代码中,我们使用第一个实例对象的 constructor 属性创建第二个实例。验证表明第二个 Ninja 对象被创建成功,并且与第一个是完全不同的两个实例。

有趣的是利用 constructor,我们不需要访问原始构造函数就可以直接创建对象,即使原始构造函数已经不在作用域内。

1
2
3
4
5
6
function Ninja() {}
const ninja = new Ninja();
const ninja2 = new ninja.constructor();

console.log(ninja2 instanceof Ninja) // true
console.log(ninja !== ninja2) // true

JS深入prototype

instanceof

instanceof 操作符的真正语义:检测右边的函数原型是否存在于操作符左边的对象的原型链上

1
2
3
4
5
6
7
8
9
function Ninja() {}

const ninja = new Ninja();

console.log(ninja instanceof Ninja) // true

Ninja.prototype = {};

console.log(ninja instanceof Ninja) // false

使用关键字 class

class 是语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Ninja {
constructor(name) {
this.name = name;
}

swingSword() {
return true;
}
}

const ninja = new Ninja("Yoshi");

console.log(ninja instanceof Ninja) // true
console.log(ninja.name === "Yoshi") // true
console.log(ninja.swingSword()) // true

上面的代码可以转换成下面的ES5代码:

1
2
3
4
5
6
7
function Ninja(name) {
this.name = name
}

Ninja.prototype.swingSword = function () {
return true;
}

静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Ninja {
constructor(name, level) {
this.name = name;
this.level = level;
}

swingSword() {
return true;
}

static compare(ninja1, ninja2) {
return ninja1.level - ninja2.level;
}
}

const ninja1 = new Ninja("Yoshi", 4);
const ninja2 = new Ninja("Hatori", 3);

console.log(ninja1.compare === undefined && ninja2.compare === undefined) // true

console.log(Ninja.compare(ninja1, ninja2) > 0) // true

console.log(Ninja.compare) // [Function: compare]

console.log(Ninja.swingSword === undefined) // true

通过 static 关键字定义了一个静态方法 compare,实例不可访问 compare 方法,而 Ninja 类可以访问 compare 方法

ES6 之前要像下面这样实现 “静态” 方法:

1
2
function Ninja() {}
Ninja.compare = function(ninja1, ninja2) {}

class实现继承

super + extends

console.log 除了注释中的 false,其余全为 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person {
constructor(name) {
this.name = name;
}

dance() {
return true;
}
}

class Ninja extends Person {
constructor(name, weapon) {
super(name);
this.weapon = weapon;
}

wieldWeapon() {
return true;
}
}

const person = new Person("Bob");

console.log(person instanceof Person)
console.log(person.dance())
console.log(person.name === "Bob")
console.log(person instanceof Ninja) // false
console.log(person.wieldWeapon === undefined)

const ninja = new Ninja("Yoshi", "Wakizashi");

console.log(ninja instanceof Ninja)
console.log(ninja.wieldWeapon())
console.log(ninja instanceof Person)
console.log(ninja.name === "Yoshi")
console.log(ninja.dance())

第八章 控制对象的访问

getter 和 setter

定义 getter 和 setter

如果一个属性具有 getter 和 setter 方法,访问该属性时将隐式调用 getter 方法,为该属性赋值时将隐式调用 setter 方法

在对象字面量中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ninjaCollection = {
ninjas: ["Yoshi", "Kuma", "Hattori"],
get firstNinja() {
return this.ninjas[0];
},
set firstNinja(value) {
this.ninjas[0] = value;
}
};

console.log(ninjaCollection.firstNinja === "Yoshi") // true

ninjaCollection.firstNinja = "Hachi";

console.log(ninjaCollection.firstNinja === "Hachi" && ninjaCollection.ninjas[0] === "Hachi") // true

在ES6的class中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NinjaCollection {
constructor() {
this.ninjas = ["Yoshi", "Kuma", "Hattori"];
}
get firstNinja() {
return this.ninjas[0];
}
set firstNinja(value) {
this.ninjas[0] = value;
}
}

const ninjaCollection = new NinjaCollection();

console.log(ninjaCollection.firstNinja === "Yoshi") // true

ninjaCollection.firstNinja = "Hachi";

console.log(ninjaCollection.firstNinja === "Hachi" && ninjaCollection.ninjas[0] === "Hachi") // true

通过Object.defineProperty定义getter和setter

在第五章中我们已经知道 Javascript 没有私有对象属性。我们可以通过闭包模拟私有属性,通过定义变量和指定对象包含这些变量。由于对象字面量与类、getter和setter方法不是在同一个作用域中定义的,因此那些希望作为私有对象属性的变量是无法实现的。幸运的是,可以通过Object.defineProperty方法实现。

在第7章中我们看到Object.defineProperty方法可以用于定义新的属性,传入属性描述对象即可。属性描述对象可以包含get和set来定义getter和setter方法。我们使用这种特性重新编写上面的示例,来实现内置的getter和setter,控制私有对象属性的访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Ninja() {
let _skillLevel = 0;

Object.defineProperty(this, 'skillLevel', {
get: () => {
return _skillLevel;
},
set: value => {
_skillLevel = value;
}
});
}

const ninja = new Ninja();

console.log(ninja._skillLevel === undefined) // true

console.log(ninja.skillLevel === 0) // true

ninja.skillLevel = 10;

console.log(ninja.skillLevel === 10) // true

由于我们希望通过skillLevel属性控制访问私有变量,因此我们定义了set和get方法。

注意,与对象字面量和类中的getter和setter不同,通过Object.defineProperty创建的get和set方法,与私有skillLevel变量处于相同的作用域中。get和set方法分别创建了含有私有变量的闭包,我们只能通过get和set方法访问私有变量。

剩下的代码运行的效果与前面的示例一致。我们创建新的ninja实例,验证无法直接访问私有变量。所有的交互都必须通过getter和setter,与标准对象属性无差异。

正如你所看到的,Object.defineProperty方法比对象字面量或类更为复杂。但是,当我们需要实现私有对象属性时,Object.defineProperty方法派上了用场。

使用 getter 和 setter 校验属性值

下面这段代码展示了如何规避指定属性发生类型错误异常。

无论何时对skillLevel属性赋值,我们都会校验该值是否是整型。如果不是,则抛出异常,并且不会修改属性_skillLevel 的值。如果是整型,则对属性 _skillLevel 赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Ninja() {
let _skillLevel = 0;

Object.defineProperty(this, 'skillLevel', {
get: () => _skillLevel,
set: (value) => {
if (!Number.isInteger(value)) {
throw new TypeError("Skill level should be a number");
}
_skillLevel = value;
}
});
}

const ninja = new Ninja();

ninja.skillLevel = 10;

console.log(ninja.skillLevel === 10) // true

try {
ninja.skillLevel = "Great";
} catch (e) {
console.log(e) // TypeError: Skill level should be a number
}

使用 getter 和 setter 定义计算属性值

下面的测试结果都是 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const shogun = {
name: "Yoshiaki",
clan: "Ashikaga",
get fullTitle() {
return this.name + " " + this.clan;
},
set fullTitle(value) {
const segments = value.split(" ");
this.name = segments[0];
this.clan = segments[1];
}
};

console.log(shogun.name === "Yoshiaki" && shogun.clan === "Ashikaga")
console.log(shogun.fullTitle === "Yoshiaki Ashikaga")

shogun.fullTitle = "Ieyasu Tokugawa";

console.log(shogun.name === "Ieyasu" && shogun.clan === "Tokugawa")
console.log(shogun.fullTitle === "Ieyasu Tokugawa")

代理

使用代理控制访问

下面的测试结果都是 true

通过 emperor 直接访问 name 属性,则返回 Komei。但是,若通过代理对象访问,则隐式调用 get 方法。由于在目标对象上可以找到 name 属性,因此也会返回 Komei。

通过 emperor 直接访问不存在的属性 nickname,返回 undefined。但是如果通过代理对象访问不存在的属性 nickname,将会激活 get,由于目标对象不具有 nickname 属性,get 方法将会返回消息 Don’t bother the emperor!

要点:通过 Proxy 构造器创建代理对象,通过代理对象访问目标对象属性时要执行指定的操作(访问时 get 、赋值时 set)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const emperor = {
name: "Komei"
};
const representative = new Proxy(emperor, {
get: (target, key) => {
return key in target ? target[key] : "Don’t bother the emperor!"
},
set: (target, key, value) => {
target[key] = value;
}
});

console.log(emperor.name === "Komei")
console.log(representative.name === "Komei")
console.log(emperor.nickname === undefined)
console.log(representative.nickname === "Don’t bother the emperor!")

representative.nickname = "Tenno";

console.log(emperor.nickname === "Tenno")
console.log(representative.nickname === "Tenno")

使用代理记录日志

我们创建了 ninja 对象,将其传入 makeLoggable 函数,作为要创建的代理对象的目标对象,将代理对象重新赋值给 ninja 标识符。

这样做的好处就是不需要为每个对象属性添加单独的日志,记录日志的代码只需要写一个函数,解决了如果有很多个对象都要写单独日志的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function makeLoggable(target) {
return new Proxy(target, {
get: (target, property) => {
console.log(`Reading ${property}`);
return target[property];
},

set: (target, property, value) => {
console.log(`Writing value ${value} to ${property}`)
target[property] = value;
}
});
}

let ninja = {
name: "Yoshi"
};
ninja = makeLoggable(ninja);

console.log(ninja.name == "Yoshi")
ninja.weapon = "sword";

// 依次打印:
// Reading name
// true
// Writing value sword to weapon

代理的性能消耗

代理效率不高,所以在需要执行多次的代码中,比如 for 循环很多次,建议谨慎使用代理。

使用代理可以实现 日志记录、性能测量、数据校验、自动填充对象、数组负索引,这里只记录了日志记录

第九章 处理集合

数组

访问

如果试图访问数组长度范围之外的索引,不会抛出异常,而是返回 undefined,这个结果表明,Javascript 的数组是对象。

1
2
3
let arr = [1, 2, 3]

console.log(arr[4]) // undefined

数组排序

array.sort((a, b) => a - b),JavaScript 提供了 sort 方法,我们需要提供回调函数,告诉排序算法相邻的两个数组元素的关系。可能的结果有如下几种。

  • 如果回调函数的返回值 < 0,元素 a 应该出现在元素 b 之前
  • 如果回调函数的返回值 = 0,元素 a 和元素 b 出现在相同位置
  • 如果回调函数的返回值 > 0,元素 a 应该出现在元素 b 之后

reduce

1
2
3
4
5
6
7
8
const numbers = [1, 2, 3, 4];
let sum = 0;

numbers.forEach(number => {
sum = sum + number;
})

console.log(sum)

对于数组的求和可以使用 reduce

1
2
3
4
5
6
const numbers = [1, 2, 3, 4];

const sum = numbers.reduce((aggregated, number) =>
aggregated + number, 0);

console.log(sum)

复用内置数组函数

比如下面的代码,就复用了数组中的 push,find 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<input id="first" />
<input id="second" />
<script>
const elems = {
length: 0,
add: function (elem) {
Array.prototype.push.call(this, elem);
},
gather: function (id) {
this.add(document.getElementById(id));
},
find: function (callback) {
return Array.prototype.find.call(this, callback);
}
};

elems.gather("first");
console.log(elems)

elems.gather("second");
console.log(elems)

const found = elems.find(elem => elem.id === "second");
console.log(found)
</script>

Map

why use Map

为什么要使用 Map,是因为对象字面量存在一些问题,比如下面访问 constructor 属性发现不为空,原因是 {} 的原型上存在 constructor 属性

1
2
3
const dictionary = {}

console.log(dictionary.constructor) // [Function: Object]

比如映射 HTML 节点,因为 {} 的 key 必须是字符串,如果想映射为其他类型,会默默转化为字符串,没有任何提示,比如下面试图使用 HTML 元素作为 key 时,其值被 toString 方法静默转换为字符串类型。HTML 元素转换为字符串后的值为 [object HTMLDivElement]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="firstElement"></div>
<div id="secondElement"></div>
<script>
const firstElement = document.getElementById("firstElement");
const secondElement = document.getElementById("secondElement");

const map = {};

map[firstElement] = {
data: "firstElement"
};

console.log(map[firstElement].data === "firstElement") // true

map[secondElement] = {
data: "secondElement"
};

console.log(map[secondElement].data === "secondElement") // true
console.log(map[firstElement].data === "firstElement") // false
</script>

综上,由于这两个原因:原型继承属性以及key仅支持字符串,所以要用 Map 类型。

示例

firstLink 和 secondLink 的内容相同,但这两个对象仍然不相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const map = new Map();
const currentLocation = location.href;

const firstLink = new URL(currentLocation);
const secondLink = new URL(currentLocation);

console.log(firstLink === secondLink) // false

map.set(firstLink, {
description: "firstLink"
});
map.set(secondLink, {
description: "secondLink"
});

console.log(map.get(firstLink).description === "firstLink") // true
console.log(map.get(secondLink).description === "secondLink") // true
console.log(map.size === 2) // true

遍历 Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const directory = new Map();

directory.set("Yoshi", "+81 26 6462");
directory.set("Kuma", "+81 52 2378 6462");
directory.set("Hiro", "+81 76 277 46");

for (let item of directory) {
console.log(item); // 数组形式 ["Yoshi", "+81 26 6462"]
}

for (let key of directory.keys()) {
console.log(key); // Yoshi
}

for (let value of directory.values()) {
console.log(value) // +81 26 6462
}

Set

并集

1
2
3
4
5
6
7
const ninjas = ["Kuma", "Hattori", "Yagyu"];
const samurai = ["Hattori", "Oda", "Tomoe"];

const warriors = new Set([...ninjas, ...samurai]);

console.log(warriors) // Set { 'Kuma', 'Hattori', 'Yagyu', 'Oda', 'Tomoe' }
console.log(warriors.size) // 5

交集

[…ninjas] 将 Set 转换为数组

1
2
3
4
5
6
7
8
9
const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);

const ninjaSamurais = new Set(
[...ninjas].filter(ninja => samurai.has(ninja))
);

console.log(ninjaSamurais) // Set { 'Hattori' }
console.log(ninjaSamurais.size) // 1

差集

1
2
3
4
5
6
7
8
9
const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);

const pureNinjas = new Set(
[...ninjas].filter(ninja => !samurai.has(ninja))
);

console.log(pureNinjas) // Set { 'Kuma', 'Yagyu' }
console.log(pureNinjas.size) // 2

笔记基本完成,除了第八章代理部分只记录了日志记录