0%

var in for loop.

现在是2025年,ES6已经发布快10年了,除了一些基础前端框架外,在常规的业务代码中,我们不应该再使用var了,因为 var有很多的问题:比如变量提升,比如没有块级作用域。我们从一道面试题来分析var的缺点。

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log(100);

Answer:

1
2
3
4
5
6
100
5
5
5
5
5

分析:

  1. 最后一行代码console.log(100);是同步代码,所以会先输出100。
  2. for循环一共执行5次,每次都会调用setTimeout,但是setTimeout是异步代码,JS引擎会将setTimeout的回调函数放到宏任务队列中,等待执行。
  3. 当前Event loop执行完毕后,会去执行宏任务队列中的任务,这时候setTimeout的回调函数才会被执行,但是这时候i已经变成了5,所以会输出5个5。

所以导致这段代码的问题有二,解决其中任何一个都可以让它输出0, 1, 2, 3, 4。

  1. 使用了var, var是函数作用域,所以所有的循环都共享一个i
  2. 使用了setTimeoutsetTimeout是整个循环结束后才开始执行的!如果我们把setTimeout删除就不会有问题了。以下代码works well!
    1
    2
    3
    for (var i = 0; i < 5; i++) {
    console.log(i);
    }

那么是不是不用setTimeout就没有问题了呢?不是的,我们来看这道题的一个变种:

1
2
3
4
5
6
7
8
9
10
const func = [];

for (var i = 0; i < 5; i++) {
func.push(() => {
console.log(i);
});
}

func.forEach((f) => f());
console.log(100);

这个变种没有使用setTimeout,但是结果依然是5个5。为什么呢?观察这个变种和原始题目我们可以看到,他们共同的特点都是在for循环结束后才执行代码,此时i的值已经是5了。这才是根本原因。

好了,现在我们来看看如何解决这个问题。

方法一,使用let代替var

由于let有块级作用域,所以每次循环都会创建一个新的变量i,而不是像var那样所有的循环都共享一个i。

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log(100);

方法二:将循环变量i参数化

说得再通俗一点,将setTimeout的逻辑单独抽离成一个函数print。这样每次循环的时候,都会将当前的i传递给print函数,这样setTimeout的代码加入宏任务队列时会记住当前的i值。

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
print(i);
}

function print(i) {
setTimeout(() => console.log(i), 1000);
}

如果觉得单独抽离函数不够优雅,可以使用IIFE(立即执行函数表达式)。注意,这里和闭包没有半毛钱关系,IIFE这里只是为了传递参数,也就是上面抽离函数的简化版。

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
});
})(i);
}
console.log(100);

方法三:使用setTimeout的第三个参数

这个方法可能很多同学都没有见过,其实setTimeout方法是有第三个参数的,这个参数是用来传递给回调函数的参数的。所以我们可以将当前的i值传递给setTimeout的回调函数。

1
2
3
4
for (var i = 0; i < 5; i++) {
setTimeout((j) => console.log(j), 1000, i);
}
console.log(100);

为了做区分,我把回到函数中的i改成了j,这样就不会混淆了。循环变量i通过setTimeout的第三个参数传递给回调函数。

你学会了吗?

JS中的箭头函数有哪些特点?

特性 优点 缺点
语法简洁 箭头函数语法更短、更简洁,适合简单的回调函数或匿名函数。 复杂逻辑中使用隐式返回或多层嵌套时,可能降低代码可读性和可维护性。
词法作用域绑定 this 箭头函数继承定义时的 this,避免了传统函数中 this 动态绑定的问题。 无法动态绑定 this,在需要动态绑定 this 的场景(如对象方法)中不适合使用。
没有 arguments 对象 避免与普通函数中的 arguments 冲突,可以使用剩余参数(...args)代替。 缺乏 arguments 对象,在需要访问动态参数列表的场景下可能带来不便。
不能用作构造函数 避免误用为构造函数的风险(箭头函数不能通过 new 调用)。 在需要定义构造函数的场景中无法使用箭头函数。
隐式返回 单行箭头函数支持隐式返回,无需使用 return 关键字。 隐式返回在复杂逻辑中可能导致代码难以理解。
调试困难 - 箭头函数的简洁性可能导致代码可读性下降,调试和维护可能会变得困难。
不能用作生成器函数 - 箭头函数不能用作生成器函数(无法与 function* 结合使用)。
适用场景 回调函数、不需要动态绑定 this 的场景、简短的函数逻辑。 需要动态绑定 this 的场景、需要访问 arguments 对象的场景、复杂的逻辑、生成器函数场景。

JS中apply, call, bind的作用和区别是什么?

特性 apply call bind
调用方式 立即调用函数 立即调用函数 返回一个新函数
参数传递 参数以数组或类数组形式传递 参数逐个传递 参数逐个传递,可预填部分参数
适用场景 参数数量不确定时(如动态参数) 参数数量固定时 需要延迟调用或复用绑定函数时
是否修改原函数 否(返回新函数,不修改原函数)

JS中mapweakMap的区别是什么?

特性 Map WeakMap
键的类型 可以是任何类型(包括对象、原始值) 只能是对象或者non-registered symbols
值的类型 可以是任何类型 可以是任何类型
键的引用方式 强引用 弱引用
垃圾回收 键对象不会被垃圾回收 如果键对象没有其他引用,则会被回收
可迭代性 支持(可通过 keys()values() 等方法遍历) 不支持(无法直接遍历或获取大小)
获取大小 支持(通过 size 属性) 不支持
典型应用场景 通用键值对存储 私有数据存储、避免内存泄漏

简单描述一下JS中的事件循环机制?

简单描述一下JS中的Promise

以下代码输出什么?

1
[1, 2, 3].map(parseInt);

答案:[1, NaN, NaN]

解析:先看一下MDN上关于Array.prototype.map的定义,这里我们没有传递第二个参数,所以使用的是下面第一种调用方式。

1
2
map(callbackFn)
map(callbackFn, thisArg)

callbackFn接受三个参数

  • element:当前正在处理的元素
  • index:当前正在处理的元素的索引
  • array:调用map方法的数组

parseInt函数有如下两种形式,由于map传递过来的参数有三个,所以这里会调用第二种形式。

1
2
parseInt(string)
parseInt(string, radix)

所以上面的代码就变成了如下形式

1
[1, 2, 3].map((element, index, array) => parseInt(element, index));

展开之后相当于三次parseInt调用

1
2
3
parseInt(1, 0); // 1
parseInt(2, 1); // NaN
parseInt(3, 2); // NaN

注意:parseInt函数的第二个参数是进制,这个参数有如下限制

  • 如果传递的进制不在2-36之间,那么parseInt返回NaN
  • 如果不传递该参数,或者传递0,那么parseInt会根据第一个参数推断:
    • 如果第一个参数以0x或者0X开头,那么会被解析为十六进制。
    • 否则会被解析为十进制。

所以:

  • parseInt(1, 0); radix为0,根据第一个参数判断,而第一个参数1并非以0x0X开头,所以会被解析为十进制,返回1。
  • parseInt(2, 1); radix为1,不在2-36之间,返回NaN
  • parseInt(3, 2); radix为2,但是3不是二进制数,返回NaN

需要注意的是:parseInt会将第一个参数转换为字符串,然后再解析。所以parseInt(3, 2);等价于parseInt('3', 2);。可是二进制数中不可能有3这个数字,所以返回NaN。一个合法的例子是parseInt('11', 2);,这个会返回3。

大数相加

实现一个函数,输入两个字符串,返回它们的和。这两个字符串表示的数字不会以0开头,且不会以空格开头。返回的结果也不会以0开头。
这个题考察的点有以下几个

  1. 字符串如何转换为整数?
  2. 进位处理
  3. 字符串对齐处理,如何填充前导0?
  4. 如何取整。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function bigNumberAdd(a, b) {
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, '0');
b = b.padStart(maxLength, '0');

let result = '';
let carry = 0;

for (let i = maxLength - 1; i >= 0; i--) {
const sum = Number(a[i]) + Number(b[i]) + carry;
// This is string concatenation, not numeric addition.
result = (sum % 10) + result; // if sum > 10, we only need the last digit.
carry = Math.floor(sum / 10); // the carry is the first digit when sum > 10.
}

// Don't forget the last carry.
if (carry > 0) {
result = carry + result;
}

return result;
}
console.log(bigNumberAdd('999', '999')); // 1998

JS中如何判断Primitive类型?

可以使用Object()函数。

1
2
3
4
5
6
7
8
function isPrimitive(value) {
return value !== Object(value);
}

// or
function isPrimitive(value) {
return value === null || (typeof value !== 'object' && typeof value !== 'function');
}

JS中有哪些方法可以判断数组类型?

  1. Array.isArray([])
  2. [] instanceof Array
  3. Object.prototype.toString.call([]) === '[object Array]'
  4. Array.prototype.isPrototypeOf([])
  5. [].__proto__ === Array.prototype
  6. [].constructor === Array

深拷贝要注意哪些问题?

  1. 循环引用
  2. 特殊对象,比如Date, RegExp, Map, Set, Function等。

以下是一个简单的实现。

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
function deepCopy(obj, hash = new WeakMap()) {
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
if (obj === null || typeof obj !== 'object') return obj; // primitives or function
if (hash.has(obj)) return hash.get(obj); // circular reference

let newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj);

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key], hash);
}
}

return newObj;
}

// 示例对象
const obj = {
a: 1,
b: [2, 3],
c: { d: 4 },
e: new Date(),
f: /abc/,
g() {
console.log('g');
},
};

// 深拷贝
const copy = deepCopy(obj);
console.log(copy);
copy.g(); // g

类型转换,以下代码输出什么?

详情请参考+的运算规则:

1
2
3
4
console.log([] + []);
console.log([] + {});
console.log({} + []);
console.log({} + {});

输出结果是什么?

1
2
3
4
const numbers = [1, 2, 3];
numbers[4] = 4;
console.log(numbers); // 1, 2, 3, <1 empty item>, 4
console.log(numbers[3]); // undefined

以下代码输出什么?

1
2
3
4
5
6
const a = {};
const b = { key: 'b' };
const c = { key: 'c' };
a[b] = 123;
a[c] = 456;
console.log(a[b]);

答案:456
本题考察的点是:对象的键名只能是字符串或者Symbol类型,如果不是,会被转换为字符串。所以a[b]a[c]都会把键名转换为[object Object],所以a[b]a[c]实际上是同一个键,最后赋值会覆盖前面的值。

Introduction

HTTP has two types of messages, request message and response message. Both messages consist of a start line, headers, and an optional body.

Note that, HTTP messages, as defined in HTTP/1.1 and earlier, are human-readable. In HTTP/2, these messages are embedded into a binary structure, a frame, allowing optimizations like compression of headers and multiplexing.

Request Message

http-request-message
Http Request message consists of the following elements:

  1. An HTTP method.
  2. The path of the resource to fetch.
  3. The version of the HTTP protocol.
  4. Optional headers.
  5. An optional body.

Response Message

http-response-message
Http Response message consists of the following elements:

  1. The version of the HTTP protocol.
  2. A status code indicating the success or failure of the request.
  3. A status message.
  4. Optional headers.
  5. An optional body.

Reference

  1. MDN - HTTP Messages

The version history of HTTP

Version Year introduced Current status Usage in August 2024 Support in August 2024
HTTP/0.9 1991 Obsolete 0 100%
HTTP/1.0 1996 Obsolete 0 100%
HTTP/1.1 1997 Standard 33.8% 100%
HTTP/2 2015 Standard 35.3% 66.2%
HTTP/3 2022 Standard 30.9% 30.9%

HTTP/0.9

HTTP/0.9 is the first version of the HTTP protocol. It was a simple protocol that allowed clients to request a file from a server. The server would then send the file back to the client. The protocol did not support headers or status codes, and the client and server communicated using plain text.

HTTP/1.0

HTTP/1.0 was released in 1996 and introduced several new features to the protocol. These features included support for headers, status codes, and the ability to send multiple files in a single request. HTTP/1.0 also introduced the concept of persistent connections, which allowed clients to reuse a single connection to send multiple requests.

HTTP/1.1

HTTP/1.1 was released in 1999 and is the most widely used version of the HTTP protocol today. It introduced several new features, including support for chunked transfer encoding, which allows servers to send data in chunks, and the ability to reuse connections for multiple requests. HTTP/1.1 also introduced several new status codes, such as 100 Continue and 206 Partial Content.

Details about HTTP/1.1:

  1. HTTP/1.1 is a text-based protocol.

HTTP/2

HTTP/2 was released in 2015 and introduced several new features to the protocol. These features included support for multiplexing, which allows clients to send multiple requests over a single connection, and server push, which allows servers to send resources to clients before they are requested. HTTP/2 also introduced several new status codes, such as 103 Early Hints and 421 Misdirected Request.

Details about HTTP/2:

  1. HTTP/2 Message is binary, instead of textual.

HTTP/3

HTTP/3 is the latest version of the HTTP protocol and is currently in development. It is based on the QUIC protocol, which is a new transport protocol that is designed to improve the performance of web applications. HTTP/3 introduces several new features, including support for multiplexing, which allows clients to send multiple requests over a single connection, and server push, which allows servers to send resources to clients before they are requested. HTTP/3 also introduces several new status codes, such as 103 Early Hints and 421 Misdirected Request.

Version Release Date Feature Comments
Http 0.9 1991 只支持Get请求,没有Head,没有Status Code 只支持Plain Text,不支持图片,视频,语音等
Http 1.0 1996 无状态、短连接,队头阻塞 每个请求都要建立一个TCP连接,请求处理完TCP就关闭。下一个请求必须在前一个请求完成时才能开始,可能导致队头阻塞问题
Http 1.1 1999
  • 持久连接(keep alive)
  • 请求管道化(pipeling)
  • 增加缓存处理(新增Cache-Control字段)
  • 增加Host字段、支持断点续传
TCP连接可以复用。客户端可同时发送多个请求,但是服务端仍是一个一个处理,仍然存在队头阻塞问题。
Http 2.0 2015
  • 二进制分帧
  • 多路复用(或连接共享)
  • 头部压缩
  • 服务器推送
仍然基于TCP,虽然没有Http队头阻塞,但是有TCP队头阻塞
Http 3.0 2020 不再基于TCP,而是基于UDP,采用由Google在UDP协议上进行封装而形成的QUIC协议,保证传输的可靠性,并且一路传输失败不影响其他路。

Introduction

SOLID是五个面向对象设计原则的首字母缩写,分别是:

  • Single Responsibility Principle (SRP) - 单一职责原则
  • Open/Closed Principle (OCP) - 开闭原则
  • Liskov Substitution Principle (LSP) - 里氏替换原则
  • Interface Segregation Principle (ISP) - 接口隔离原则
  • Dependency Inversion Principle (DIP) - 依赖倒置原则

Single Responsibility Principle (SRP)

单一职责原则是指一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。这不意味着一个类只能有一个功能 - 这是不准确的说法。确切的说,一个类或者模块应该对且仅对一类行为主体负责!

Open/Closed Principle (OCP)

开闭原则是指一个软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着一个实体应该通过扩展来实现新的功能,而不是通过修改已有的代码来实现。

这通常通过使用抽象类和接口来实现。抽象类和接口定义了一组规范,具体的实现类来实现这些规范。如果将来要添加新的功能,只需要添加新的实现类,而不需要修改已有的代码。

Liskov Substitution Principle (LSP)

里氏替换原则是指一个软件实体(类、模块、函数等)应该可以被它的子类替换,而不影响程序的正确性。这意味着一个子类应该可以替换它的父类,而不需要修改父类的代码。

Interface Segregation Principle (ISP)

接口隔离原则是指一个类对另一个类的依赖应该建立在最小的接口上。换句话说,一个类不应该依赖它不需要的接口。

Dependency Inversion Principle (DIP)

依赖倒置原则是指高层模块不应该依赖于低层模块,两者都应该依赖于抽象。换句话说,模块之间的依赖关系应该是通过抽象发生的,而不是通过具体实现发生的。

Introduction

缓存是前端开发中非常重要的概念,它可以提高网站的性能,减少服务器的负担。本文主要介绍Http和浏览器缓存的相关知识。

不知道各位读者是否有这样的体验:

  1. 某个网站第一次打开的时候特别慢,再次打开的时候就快很多。
  2. 登录某个网站后,下次再访问的时候自动就是登录状态了。

其实以上现象都是缓存的功劳。

Http Cache

缓存相关的头部字段

Expires

Expires是一个HTTP响应头部字段,用于指定资源的过期时间。它的值是一个GMT格式的时间字符串。以下代码表示资源的过期时间是2024年11月16日14点10分23秒。

1
expires:Sat, 16 Nov 2024 14:10:23 GMT

Expires是HTTP/1.0的标准,它有一个问题,就是我们判断缓存过期时,浏览器会使用当前系统时间与Expires字段的时间进行比较。如果系统时间大于Expires字段的时间,那么缓存就会被认为过期了。但是当前系统时间是可以人为修改的,这就会导致缓存过期时间不准确。

Cache-Control

Cache-Control字段的取值有如下几种:

max-age

max-age的值是一个整数,单位是秒,表示缓存会在xxx秒后过期。以下代码表示资源会在3600秒后过期。

1
cache-control:max-age=3600

那么max-age中指定的3600秒参照的是什么时间呢?它参照的是服务器生成Response的时间,而不是浏览器收到Response的时间。那么服务器生成Response的时间在响应头中有吗?有的,就是Date字段。Date字段表示服务器生成Response的时间。所以缓存的的过期时间是Date字段的时间加上max-age的值。

ExpireTime = Date + max-age

注意:如果max-ageExpires同时存在,那么max-age的优先级更高,Expires会被忽略。

no-cache

注意:这个字段的意思不是不使用缓存!
这个字段的意思是可以使用缓存,但是使用前需要向服务器验证缓存是否过期。也就是协商缓存。

no-store

不使用缓存,每次都需要向服务器请求资源。

must-revalidate

在缓存过期前,直接使用缓存,但是过期后需要向服务器验证缓存是否过期。

Last-Modified

Last-Modified是一个HTTP响应头部字段,用于指定资源的最后修改时间。它的值是一个GMT格式的时间字符串。以下代码表示资源的最后修改时间是2024年11月16日14点10分23秒。

1
last-modified:Sat, 16 Nov 2024 14:10:23 GMT

当客户端再次请求资源时,会将上次请求时服务器返回的Last-Modified值放在请求头中的If-Modified-Since字段中,服务器会根据这个值判断资源是否发生了变化。如果资源没有发生变化,服务器会返回304 Not Modified状态码,表示资源没有发生变化,客户端可以使用缓存。

ETag

ETag是一个HTTP响应头部字段,用于指定资源的唯一标识。它的值是一个字符串。服务器会根据资源的内容生成一个唯一的字符串(注意,生成算法因服务器而异),然后将这个字符串作为ETag的值。当客户端再次请求资源时,会将上次请求时服务器返回的ETag值放在请求头中的If-None-Match字段中,服务器会根据这个值判断资源是否发生了变化。如果资源没有发生变化,服务器会返回304 Not Modified状态码,表示资源没有发生变化,客户端可以使用缓存。

ETag相比Last-Modified的优点

精确性:

  • ETag 可以提供比 Last-Modified 更精确的资源版本标识。Last-Modified 是基于时间戳的,而时间戳的精度通常是秒级,这意味着如果一个资源在一秒内发生变化多次,Last-Modified 就无法区分这些变化。
  • ETag 是一个可以由服务器自由定义的字符串,它可以唯一标识一个资源的特定版本,甚至可以包括内容的哈希值,从而确保即使是微小的变化也能被准确识别。

灵活性:

  • ETag 允许更灵活的缓存控制策略。它可以是强验证器(保证内容完全一致)或弱验证器(允许一定程度上的差异,如压缩、格式转换等),这取决于服务器如何生成 ETag。
  • 例如,当资源的内容没有变化,但是某些元数据发生了变化(比如编码方式),服务器可以选择更新 ETag 或者不更新,这取决于是否希望客户端重新获取资源。

避免时间戳问题:

  • 由于 Last-Modified 基于时间,它可能会遇到时钟不同步的问题。如果客户端和服务器之间的系统时钟存在较大偏差,那么基于 Last-Modified 的缓存验证可能会出错。
  • ETag 不依赖于时间戳,因此不受时钟同步问题的影响。

支持非文件资源:

  • 对于动态生成的内容(例如数据库查询结果),可能没有明确的最后修改时间,此时使用 ETag 更为合适。服务器可以根据内容的状态生成一个唯一的 ETag。

总结

  1. expiresmax-age控制的是强缓存。
  2. Last-ModifiedETag控制的是协商缓存。
  3. 浏览器请求时会先检查强缓存,然后再检查协商缓存。

一道面试题

如果一次Http请求返回的状态码是200,那么这个请求走缓存了吗?
答案:可能走,也可能没走。
解析:我们大多数人都知道状态码304表示资源没有改变,那么就使用本地缓存,这个属于协商缓存。还有一种是强缓存,就是直接使用本地缓存,不需要向服务器验证。这种情况下,状态码是200。

References

  1. https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
  3. https://juejin.cn/book/6994678547826606095/section/6997029635766616077?scrollMenuIndex=1

简介

var, let and const是JavaScript中三种定义变量的方式,它们之间有什么区别呢?这是前端面试中常见的一道题,今天我们来一文说透它。
letconst区别不大,主要是const声明的变量是常量,不可修改,而let声明的变量是可修改的。所以我们重点放在varlet上。

变量初始化

声明变量的同时为其赋值叫做初始化。

  • varlet声明的变量都可以不赋值,此时变量的值为undefined
  • const声明的变量必须赋值,否则会报错。
1
2
3
4
5
6
// `var`和`let`声明的变量可以不赋值,此时变量的值为`undefined`。
var num; // num的值是undefined
num = 1; // num的值是1

let str; // str的值是undefined
str = 'hello'; // str的值是'hello'
1
2
// `const`声明的变量必须赋值,否则会报错。
const a; // SyntaxError: Missing initializer in const declaration

变量提升 - Hoisting

Hoisting这个词中文译为提升,就是将变量的声明提升到其作用域的顶部,注意提升的是声明,而不是赋值。

  • var声明的变量会被提升至其作用域顶部。
  • letconst声明的变量不会被提升。(注意这个说法有争议,详见MDN
  • 提升只针对变量声明,不包括赋值。

如果var是在全局作用域声明的,那么它会被提升到全局作用域的顶部。

1
2
console.log(name); // undefined
var name = 'Philip';

以上代码等价于:

1
2
3
var name; // `var`声明的变量会被提升到其作用域顶部。
console.log(name); // undefined
name = 'Philip';

如果var是在函数作用域声明的,那么它会被提升到函数作用域的顶部。

1
2
3
4
5
function printName() {
console.log(name); // undefined
var name = 'Philip';
}
printName();

以上代码等价于:

1
2
3
4
5
6
function printName() {
var name; // `var`声明的变量会被提升到其作用域顶部。
console.log(name); // undefined
name = 'Philip';
}
printName();

letconst声明的变量不会被提升。

对于letconst,它们不会被提升,所以下面代码会报错。

1
2
console.log(num); // ReferenceError: Cannot access 'num' before initialization
const num = 1;

前面说过,关于letconst是否被提升有争议。

  • 一种说法是letconst不会被提升,所以在声明之前访问会报错。
  • 另一种说法是letconst会被提升,但是在声明之前访问会抛出Temporal Dead Zone错误。

比如下面的代码:

1
2
3
4
5
const x = 1;
{
console.log(x); // ReferenceError: Cannot access 'x' before initialization
const x = 2;
}

这段代码会报错,但是如果我们把{}内的const x = 2;注释掉,那么代码就不会报错。如果const x = 2没有被提升的话,那么console.log(x)应该可以访问到全局的const x = 1,而不会报错。换句话说:因为const x = 2被提升了,所以console.log(x)访问的是提升后的x,而此时x还没有被初始化,所以报错。

提升只针对变量声明,不包括赋值。

下面的代码会报错,因为x = 1是赋值,并不是声明,所以不会提升。(注意:如果变量声明前没有加var, letconst,那么其实产生的是一个意外的全局变量。)

1
2
console.log(x); // ReferenceError: x is not defined
x = 1;

如果有同名函数和变量,那么提升后,变量位于函数之前(或者说函数会覆盖变量)。

以下代码中有一个同名的函数和变量。

1
2
3
4
5
6
7
console.log(foo); // [Function: foo], not undefined.

function foo() {
console.log('function foo');
}

var foo = 1;

提升后代码如下:

1
2
3
4
5
6
var foo;
function foo() {
console.log('function foo');
}
console.log(foo);
foo = 1;

面试题

看几道面试题,以下几段代码输出什么?

  1. 第一题
1
2
3
a = 2;
var a;
console.log(a); // 2

解决var提升的问题很简单,就是按照提升规则将代码重写一下,上面的代码等价于如下代码,结果一目了然。

1
2
3
var a;
a = 2;
console.log(a); // 2
  1. 第二题
1
2
3
4
5
6
7
8
9
var a = true;
foo();

function foo() {
if (a) {
var a = 10;
}
console.log(a);
}

只要函数内部有var声明的变量,那么所有全局声明的var变量都会被忽略,以上代码提升后等价于如下代码(注意function也有提升),函数内部的var永远会覆盖全局的var

1
2
3
4
5
6
7
8
9
10
11
var a = true;

function foo() {
var a; // value of a is `undefined`
if (a) {
a = 10; // never executed.
}
console.log(a);
}

foo();
  1. 第三题
1
2
3
4
5
6
7
8
9
10
11
12
function fn() {
console.log(typeof foo);
var foo = 'variable';

function foo() {
return 'function';
}

console.log(typeof foo);
}

fn();

还是那句话,此类题目的解法就是按照提升规则把代码重新写一遍,以上代码提升后等价于如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn() {
var foo;

function foo() {
return 'function';
}

console.log(typeof foo);

foo = 'variable';

console.log(typeof foo);
}

fn();

所以输出结果是functionstring

变量的作用域

  • var声明的变量有只两种作用域:全局作用域和函数作用域。(没有块级作用域)
  • letconst声明的变量有三种作用域:全局作用域,函数作用域和块级作用域。
  • var声明的全局变量会挂载到window对象上,而letconst不会。
  • letconst有临时性死区,而var没有。

面试题

第一题

以下代码输出什么?

1
2
3
4
5
6
let x = 1;
{
let x = 2;
}

console.log(x);

答案:1,因为let有块级作用域,所以let x = 2只在{}内有效。

第二题

以下代码输出什么?

1
2
3
4
5
6
var x = 1;
{
var x = 2;
}

console.log(x);

答案:2,因为var没有块级作用域,所以var x = 2会覆盖外部的var x = 1

第三题

以下代码输出什么?

1
2
3
4
5
let name = 'zdd';
{
console.log(name);
let name = 'Philip';
}

答案:ReferenceError: Cannot access ‘name’ before initialization。因为let有块级作用域,所以console.log(name);访问的是let name = 'Philip';之前的name,而此时name还没有被初始化,处于暂时性死区中,所以报错。

第四题

以下代码输出什么?

1
2
3
4
5
6
7
8
9
'use strict';

{
function foo() {
console.log('foo');
}
}

foo();

答案:ReferenceError: foo is not defined。因为foo是在块级作用域内声明的,所以在外部无法访问。但是如果我们把'use strict';去掉,那么代码就可以正常运行。因为在非严格模式下,函数声明会被提升到全局作用域。

第五题

以下代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
(() => {
let x;
let y;
try {
throw new Error();
} catch (x) {
x = 1;
y = 2;
console.log(x);
}
console.log(x);
console.log(y);
})();

答案:1 undefined 2。因为catch中的x是一个新的变量,不是外部的x,所以x = 1只会改变catch中的x,而不会改变外部的x。而y = 2不是catch的参数,只是在catch中赋值的,所以会改变外部的y

暂时性死区 - Temporal Dead Zone

TDZ即Temporal Dead Zone - 中文名暂时性死区,是指letconst声明的变量在其作用域开始到变量声明之间的这段区域。在暂时性死区内无法访问变量,访问会报错。

1
2
3
4
5
6
7
function foo() {
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let a = 1;
const b = 2;
}

foo();

对于以上代码,常量b的暂时性死区开始于函数的第一行,终止于b的声明,而console.log(b);这句恰恰在暂时性死区内访问了b,所以会报错。

面试题

以下代码输出什么?

1
2
3
4
5
6
function foo() {
console.log(typeof bar);
const bar = 1;
}

foo();

答案:
ReferenceError: Cannot access 'bar' before initialization
因为console.log(typeof bar);这句在bar的暂时性死区内访问了bar,所以会报错。可以看到,即使强如typeof这种几乎不会报错的操作符也无法规避暂时性死区。

如果我们把const bar = 1;去掉,那么代码就不会报错。typeof操作符对于没有声明的变量不会报错,而是返回undefined

1
2
3
function foo() {
console.log(typeof bar); // 输出undefined
}

重新声明- Redeclaration

  • var声明的变量可以被重复声明,后声明的覆盖先声明的。
  • letconst声明的变量不可以被重复声明。

面试题

看几道面试题,以下几段代码输出什么?

  1. 第一题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 1;

function foo() {
var a = 2;
{
var a = 3;
console.log(a);
}
console.log(a);
}

foo();

console.log(a);

答案:3 3 1, 这个题主要考察两个知识点:

  1. var声明的变量没有块级作用域。
  2. var声明的变量可以被重复声明,后声明的会覆盖先声明的。
    所以var a = 3会覆盖外部的var a = 2,但是var a = 2不会覆盖最外面的var a = 1。因为var有函数作用域。

以上代码提升后等价于如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a;
a = 1;
function foo() {
var a;
var a; // redeclaration
a = 2;
{
a = 3;
console.log(a);
}
console.log(a);
}
foo();
console.log(a);

注意:面试题中凡事用{}包裹var的都是障眼法,var没有块级作用域。

第二题

这道题比较简单,考察的是let的块级作用域,代码输出2, 1。因为let有块级作用域。let a = 2只在{}内有效。

1
2
3
4
5
6
7
8
9
10
function foo() {
let a = 1;
{
let a = 2;
console.log(a);
}
console.log(a);
}

foo();

意外的全局变量

如果我们声明变量的时候忘记了写var, let或者const,那么这个变量就是所谓的Accidental Global Variables,意思是意外的全局变量

1
2
3
4
5
function f1() {
b = 2; // accident global variable
}
f1();
console.log(b); // 2

面试题

以下代码输出什么?

1
2
3
4
5
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
})
}

答案:3 3 3
因为var没有块级作用域,所以setTimeout内的i都是指向同一个i,而setTimeout是异步的,其回调函数代码需要先进入宏任务队列,待for循环结束后才能执行,此时i已经是3了。关于这道题的详细解释,请看这篇

最佳实践

  1. 如今ES6已经普及,对于业务代码来说,基本不需要使用var了,var目前只有JS框架或者底层工具库才会使用。

  2. 对于letconst,优先使用const,只有在需要修改变量的情况下才使用let

  3. 经典for循环使用let,因为循环变量会被修改。

    1
    2
    3
    for (let i = 0; i < 5; i++) {
    console.log(i);
    }
  4. for...infor...of使用const,因为循环变量不会被修改。

    1
    2
    3
    4
    const arr = [1, 2, 3];
    for (const item of arr) {
    console.log(item);
    }
    1
    2
    3
    4
    const obj = {a: 1, b: 2};
    for (const key in obj) {
    console.log(key);
    }

祝大家编程愉快,如果觉得有用就点个关注,每篇文章都是纯古法手打,在AI大行其道的当下,认认真真写文章的人不多了,您的点赞转发评论就是对我最大的支持!

Introduction

There is a very famous question, “What happened when you input a URL and press Enter in the browser?”

Answer

1. URL parsing

When you input a URL in the browser, the browser will parse the URL into several parts:

  • Protocol: http or https
  • Host/Domain: www.example.com
  • Port: 80 or 443
  • Path: /page.html
  • Query: ?name=Philip
  • Fragment: #section1

2. DNS lookup

The browser will check the cache to see if the DNS lookup result is cached. If not, it will send a DNS query to the DNS server to get the IP address of the host.

  1. Check browser cache.
  2. Check OS cache.
  3. Check router cache.
  4. Check ISP(Internet Service Provider) cache.
  5. Make request to the DNS server.(Only if all cache above failed!)

3. TCP connection

The browser will establish a TCP connection with the server using the IP address and port number. This including the TCP handshake process. 关于TCP的连接与关闭,可以看这篇

4. HTTP request

The browser will send an HTTP request to the server. The request includes the following information:

  • Request method: GET, POST, PUT, DELETE, etc.
  • Request headers: User-Agent, Accept, Cookie, etc.
  • Request body: for POST and PUT requests.
  • URL: http://www.example.com/page.html?name=Philip#section1

5. Server processing

The server will process the request and generate a response. The response includes the following information:

  • Status code: 200, 404, 500, etc.
  • Response headers: Content-Type, Content-Length, etc.
  • Response body: HTML, CSS, JavaScript, etc.
  • Cookies: Set-Cookie header.

6. Browser rendering

The browser will render the response HTML, CSS, and JavaScript to display the web page to the user. When browser parse html page, it may download js file embed in html, this process might block the parsing, see here for details.

  1. Parse HTML: The browser will parse the HTML to create the DOM tree.
    1. The file was transferred from the server to the browser as binary data.
    2. The browser will parse the binary data to the .html file.
    3. The browser constructs the DOM tree based on the parsed HTML.
    4. If the html contains external CSS files, the browser will download the css in parallel, this won’t block the DOM construction.
    5. If the html contains external JavaScript files, the browser will download the js in parallel, this will/won’t block the DOM construction. see here for details.
  2. Parse CSS: The browser will parse the CSS to create the CSSOM tree.
  3. Render tree: The browser will combine the DOM tree and CSSOM tree to create the render tree.
  4. Layout: The browser will calculate the position and size of each element in the render tree.
  5. Paint: The browser will paint the pixels on the screen.
  6. Composite: The browser will composite the layers to display the final web page.
  7. JavaScript execution: The browser will execute the JavaScript code to add interactivity to the web page.
  8. Event handling: The browser will handle user events like click, scroll, etc.

Conclusion

  1. External CSS doesn’t block the DOM construction, but might block the render tree construction since the browser needs to wait for the CSSOM tree to be constructed.
  2. External JavaScript might block the DOM construction if and only if:
    1. The Scripts is in <head> tag, it will block the DOM construction.
    2. And the script doesn’t set async or defer.
  3. If the script is at the end of the <body> tag, it won’t block the DOM construction even when it doesn’t set async or defer.

What’s the difference between Angular and React?

  1. Angular是一个框架,React是一个UI库
    1. Angular是一个完整的框架,提供了很多开箱即用的功能,比如路由,表单,HTTP Client等。
    2. React只是一个UI库,只提供了构建用户界面的功能,其他功能需要通过第三方库来实现。React-Router用于路由,Axios用于网络请求,Redux用于状态管理等。
  2. Angular使用html/TypeScript/css三者分离的方式书写组件,而React使用JSX,将html和js混合在一起。
  3. 数据绑定,Angular原生支持双向数据绑定,而React不支持,React支持单向数据流,即数据只能由父组件传递给子组件,子组件不能直接修改父组件的数据(但可以通过回调函数实现)。
  4. 变更检测:
    1. Angular使用zone.js来实现变更检测,它会监控所有的异步操作,当异步操作完成后,会触发变更检测。
    2. Angular的变更检测基于脏检查,会检查所有的绑定,如果有变化,就会更新视图。
    3. Angular也提供ChangeDetectorRef类,手动触发变更检测。
    4. React使用虚拟DOM来实现变更检测,当状态发生变化时,React会重新构建虚拟DOM,然后和上一次的虚拟DOM进行对比,找出差异,然后更新真实DOM。
    5. React中,状态变化通过setState方法来触发,每次调用setState都会触发重新渲染。
  5. 使用体验
    1. Angular入门比较难,因为它有很多概念,比如组件,指令,管道,服务,依赖注入等。但是一旦入门,写起来就比较顺手,因为大家都是在同一个框架下工作,所以代码风格比较统一。水平高的和水平低的写出来的代码gap更小。Angular上手之后,写代码心智成本低。
    2. React则不然,它概念很少,甚至连类似于ngIf这种指令都没有,就是纯纯的写js,这就造成了一个问题,不同水平的人写出的代码差异比较大,而且我觉得用React心智成本更高,经常由于忘了写useEffect中的依赖条件导致页面无法更新,或者忘了写key导致列表无法正常渲染等等。

暂时先写这些吧,以后想到了再补充,写得比较泛泛,很多细节还需要填充。

Introduction

reduce() is a very import method in JavaScript, it executes a reducer function (that you provide) on each element of the array, resulting in a single output value.

Here is the syntax of reduce() method:

1
2
reduce(callbackFn)
reduce(callbackFn, initialValue)

Most of the time, reduce() is used to sum up the values in an array, but it can be used for many other things as well.

1
2
3
const nums = [1, 2, 3];
const sum = nums.reduce((a, c) => a + c);
console.log(sum);

Why we use a and c for the parameters of the reducer function, because a = accumulator, c = currentValue.

Each call to the reducer produces a new value, and this value is passed to the next call of the reducer as the accumulator(first argument). Otherwise, a will become undefined in the next call.

Note that you must return value from the callbackFn, otherwise the result will be undefined.

1
2
3
const nums = [1, 2, 3];
const sum = nums.reduce((a, c) => {a + c}); // won't work since we didn't return the value
console.log(sum); // undefined.

I found this when I working on the following work, group the following inventory by type property:

1
2
3
4
5
6
7
const inventory = [
{ name: 'asparagus', type: 'vegetables', quantity: 5 },
{ name: 'bananas', type: 'fruit', quantity: 0 },
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'cherries', type: 'fruit', quantity: 5 },
{ name: 'fish', type: 'meat', quantity: 22 },
];

The output should look like this:

1
2
3
4
5
6
7
8
9
10
11
{
vegetables: [ { name: 'asparagus', type: 'vegetables', quantity: 5 } ],
fruit: [
{ name: 'bananas', type: 'fruit', quantity: 0 },
{ name: 'cherries', type: 'fruit', quantity: 5 }
],
meat: [
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'fish', type: 'meat', quantity: 22 }
]
}

I write the following code, but got and error: TypeError: Cannot read properties of undefined (reading 'fruit'), do you know why?

1
2
3
4
5
6
7
8
9
10
11
12
function groupByType(inventory) {
return inventory.reduce((a, c) => {
const type = c.type;
if (a[type]) {
a[type].push(c);
} else {
a[type] = [c];
}
}, {});
}
const result = groupByType(inventory);
console.log(result);

I forgot the return a inside the callback function, so the a will be undefined in the next call, and undefined doesn’t have the property fruit.

To fix it, add return a at the end of the callback function.