0%

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.

Background

今天在练习正则表达式的时候,发现了一个有趣的输出,之前未见过(也许曾经见过,但没有留意),索性研究一下。

代码如下:

1
2
3
4
const str = `name: zdd, age: 18, gender: male`;
const regex = /name: (?<name>\w+), age: (?<age>\d+), gender: (?<gender>\w+)/;
const match = str.match(regex);
console.log(match.groups);

简单解释一下这个正则表达式:

  1. /.../ - 这是js的正则表达式语法,用//包裹。
  2. name: - 匹配字符串name:
  3. (?<name>...) - 这是一个命名捕获,表示匹配后的值放到name这变量中,可以用match.groups.name来输出捕获的值。
  4. \w+ - 表示匹配任意字母、数字或下划线(等价于 [a-zA-Z0-9_]), + - 表示一个或者多个。

输出结果如下:

1
[Object: null prototype] { name: 'zdd', age: '18', gender: 'male' }

这个输出结果里面的[Object: null prototype]是什么意思呢?问了一下通义千问,发现这是一个没有原型的对象。

那么,啥是没有原型的对象呢?可以简单的理解为,这个对象没有继承任何Object的属性和方法,是一个干净的对象。它就是它自己!
此类对象没有toStringvalueOf等方法,也没有__proto__属性,因此无法通过原型链访问到Object的属性和方法。我们可以通过Object.create(null)来创建一个没有原型的对象。

以下代码会产生一个TypeError,因为person对象没有原型,所以无法访问toString, hasOwnProperty等方法。

1
2
3
4
5
6
7
const person = Object.create(null);
person.name = 'Philip';
person.age = 18;

console.log(person.toString()); // TypeError: person.toString is not a function
console.log(person.hasOwnProperty('name')); // TypeError: person.hasOwnProperty is not a function
console.log('name' in person); // true

总结

没有原型的对象有哪些好处呢?我们常说,存在即有理,既然这种对象,那么必然有它存在的道理:

优点

  1. 避免原型污染:在JavaScript中,我们经常会遇到原型污染的问题,通过创建没有原型的对象,可以避免这种问题。
  2. 速度快:没有原型的对象,不需要查找原型链,因此访问属性和方法的速度更快。
  3. 纯粹的数据容器:没有原型的对象,可以作为一个纯粹的数据容器。

缺点

  1. 无法使用Object.prototype上的方法,如toStringvalueOf等。
  2. 检查属性是否存在只能使用in操作符,不能使用hasOwnProperty方法。

TypeError: Cannot read properties of undefined (reading ‘xxx’)

This error occurs when you try to access a property of an object that is undefined.

1
2
let person = undefined;
console.log(person.age); // Uncaught TypeError: Cannot read properties of undefined (reading 'age')

ReferenceError: a is not defined

This is the most common error in JavaScript. It means that you are trying to use a variable that has not been declared.

1
console.log(a); // Uncaught ReferenceError: a is not defined

ReferenceError: Cannot access ‘a’ before initialization

This error occurs when you try to access a variable before it is declared. After ES6, this means you touch the (TDZ)temporal dead zone. temporal dead zone is the zone between the start of the block scope and the actual declaration of the variable.

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

TypeError: Assignment to constant variable.

This error occurs when you try to reassign a value to a constant variable.

1
2
const a = 1;
a = 2; // Uncaught TypeError: Assignment to constant variable.

RangeError: Maximum call stack size exceeded

See the following code, why it cause the RangeError: Maximum call stack size exceeded error?

1
2
3
4
5
6
7
8
9
10
11
class Person {
get name() {
return this.name;
}
set name(value) {
this.name = value;
}
}
const person = new Person();
person.name = 'Philip';
console.log(person.name); // undefined

The error is caused by the recursive call to the setter of name.

  1. person.name = 'Philip'; will call the setter set name(value).
  2. Inside the setter, this.name = value; will call the setter again. that’s an infinite loop.

How to fix it?
We can just use another name for the property, like _name.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
get name() {
return this._name;
}

set name(value) {
this._name = value;
}
}

const person = new Person('zdd');
person.name = 'Philip';
console.log(person.name); // undefined

In this way,

  1. person.name = 'Philip'; will call the setter set name(value)
  2. Inside the setter function, we set the value to _name property, not the name property. So it will not cause the infinite loop.

You can find the details of getter and setter from here.

Introduction

This指向问题是JavaScript面试中必考的问题,今天我们就来将this有关的面试题一网打尽!解答此类面试题的关键是 函数是以何种方式调用的,拿到题目后,先确定函数的调用方式,再根据调用方式来判断this的指向。

根据调用方式的不同,this的指向有以下几种情况:

  1. 作为普通函数调用, this指向全局对象。
  2. 作为对象方法调用, this指向调用方法的对象。
  3. 作为构造函数调用, this指向新创建的对象。
  4. 使用apply、call、bind调用, this指向指定的对象。
  5. 箭头函数中的this, this指向其父级作用域的this。
  6. setTimeout函数中的this。
  7. DOM环境中的this。

Conclusion

This的指向只和函数的调用方式相关,与函数的定义位置无关。在面试中,如果遇到this指向的问题,先确定函数的调用方式,再根据调用方式来判断this的指向。

Introduction

What is a closure in JavaScript? This is really a hard question, and I see lots of different answers on the internet. I list some of them below:

  1. A closure is a function that has access to the outer function’s variables.
  2. A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time. - from MDN

Lexical scoping

在JavaScript中,词法作用域(Lexical Scoping)是一种确定变量作用域的规则。词法作用域基于变量在代码中声明的位置来决定其可见性和生命周期。具体来说,词法作用域是在编写代码时就确定下来的,它取决于函数和块(如if语句或循环)的嵌套结构,而不是在运行时动态确定的。

词法作用域的特点
静态确定:词法作用域在代码编写阶段就已经确定。编译器在解析代码时会创建一个作用域链,这个链决定了在何处查找变量。
基于嵌套:作用域是根据函数和块的嵌套关系来定义的。内层作用域可以访问外层作用域中的变量,但外层作用域不能访问内层作用域中的变量。
函数优先:在JavaScript中,函数创建了自己的作用域,即使这个函数没有立即执行,它的作用域也是在定义时就确定的。
词法作用域的例子
下面是一个简单的例子来说明词法作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
function outerFunction(outerArg) {
var outerVariable = 'I am from the outer function';

function innerFunction(innerArg) {
console.log(outerArg); // 可以访问 outerFunction 的参数
console.log(outerVariable); // 可以访问 outerFunction 的变量
console.log(innerArg); // 可以访问 innerFunction 的参数
}

innerFunction('Hello from innerFunction');
}

outerFunction('Hello from outerFunction');

词法作用域是闭包的基础,因为闭包是在词法作用域的基础上实现的。

闭包的特性

  1. 数据封装性 - 可以用来封装私有变量,模拟模块化。
  2. 保持状态 - 保持函数的状态,比如计数器。防抖和节流使用的都是这个特性。

Use case

Data encapsulation(private variables)

In the follow example, the inner function increase has access to the outer function’s variable count. The variable count is private and cannot be accessed from outside the function createCounter. But you can still change count by invoking the increase function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createCounter() {
let count = 0;

function increase() {
count++;
return count;
}

return {
increase,
};
}

const counter = createCounter();
console.log(counter.increase()); // 1

Note that

1
2
3
return {
increase,
};

is shorthand for

1
2
3
return {
increase: increase,
};

We can also return the function increase directly.(This is not work when you have multiple functions to return, you must use the object literal in that case.)

1
2
3
4
5
6
7
8
function createCounter() {
let count = 0;

return function () {
count++;
return count;
}
}

And you should also change the way to invoke the function since you return an anonymous function. so there is no increase property in the counter object.

1
2
const counter = createCounter();
console.log(counter()); // 1

Simulate module(Modernization)

In the following example, we can simulate a module by using closure. The private variables and methods are defined inside the outer function MyModule. The public variables and methods are returned as an object literal. So the private variables and methods are encapsulated and cannot be accessed from outside the function. The public variables and methods can be accessed by invoking the returned object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function MyModule() {
const privateVariable = 'private variable';
const publicVariable = 'public variable';

function privateMethod() {
console.log(privateVariable);
}

function publicMethod() {
console.log(publicVariable);
}

// If you want to expose the variables/methods, return them.
return {
publicVariable,
publicMethod,
};
}

const myModule = MyModule();
console.log(myModule.publicVariable); // public variable
myModule.publicMethod(); // public method

Event handler(or callback)

Some event handler or callback functions use closure to access the outer function’s variables.
In the following example, the event handler for click event is an anymous function. The function has access to the outer function’s variable count.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<button>Click Me</button>
<p>Click count: <span>0</span></p>
<script>
let count = 0;
const button = document.querySelector("button");
const span = document.querySelector("span");
button.addEventListener("click", function () {
count++;
span.textContent = count;
});
</script>
</body>
</html>

Design patter - Singleton

In the following code, we use a local variable instance to check whether the instance has been created or not. The instance variable must memorize its state during different calls to the getInstance function. So we use closure to achieve this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Singleton = (function () {
let instance = null;

function createInstance(name) {
return { name: name };
}

function getInstance(name) {
if (!instance) {
instance = createInstance(name);
}
return instance;
}

return {
getInstance,
};
})();

const instance1 = Singleton.getInstance('Philip');
const instance2 = Singleton.getInstance('zdd');
console.log(instance1); // Philip
console.log(instance2); // Philip

Debounce

防抖和节流都是为了解决在短段时间内大量触发某个函数执行而导致的性能问题。

防抖的原理
事件处理函数在事件触发一段时间后(比如500ms)再执行,如果在此时间内(比如500ms)事件再次触发,则重新计时。

防抖的应用
输入框实时搜索,如果不加防抖处理的话,用户每输入一个字符就会调用一次接口,极大的浪费带宽和后端服务器资源。加入防抖后,用户停止输入一段时间后,才调用接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function debounce(func, wait) {
let timeout;
return function () {
const args = arguments;
const context = this;
clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args);
}, wait);
};
}

document.querySelector("#input").addEventListener(
"input",
debounce(function (e) {
document.querySelector("#output").innerHTML =
"You have input: " + e.target.value;
// You can call and server side api here, and debounce make sure the api is not called very often.
}, 500)
);

注意,这个实现比较简单,没有考虑到immediate参数,即是否立即执行函数。如果要实现immediate参数,可以参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function debounce(func, wait, immediate = false) {
let timeout;

return function () {
const args = arguments;
const context = this;
const callNow = immediate && !timeout;

if (callNow) {
func.apply(context, args);
}

clearTimeout(timeout);

timeout = setTimeout(function () {
if (!immediate) {
func.apply(context, args);
}
timeout = null;
}, wait);
};
}

Throttle

节流是控制某个事件在一段时间内只触发一次函数调用,经常用在处理用户频繁触发的事件中,比如resize、scroll、mousemove等。此类事件如果不加控制,每秒会触发上百次,极大浪费资源。

以下代码是一个节流函数的实现,它确保在每300ms响应一次resize事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function throttle(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
let count = 0;
window.addEventListener('resize', throttle(function() {
count++;
document.querySelector('#counter').innerHTML = 'resize triggered: ' + count;
}, 300));

注意,节流函数的另一个实现方式是不是用setTimeout函数,如下:这个版本的好处是,当事件第一次发生时,立即执行处理函数,而上面使用setTimeout函数的版本则要等待wait时间后才执行处理函数。

1
2
3
4
5
6
7
8
9
10
11
function throttle(func, wait) {
let lastTime = 0;
return function (...args) {
const now = new Date().getTime();
const context = this;
if (now - lastTime >= wait) {
func.apply(context, args);
lastTime = now;
}
};
}

Currying

在JavaScript中,柯里化(Currying)是一种将使用多个参数的函数转换成一系列使用单一参数的函数的技术。通过柯里化,我们可以将一个多参数的函数逐步转化为一系列单参数的函数。每个函数都返回一个新的函数,直到所有参数都被提供,最终返回结果。

1
add(a, b) ===> curriedAdd(a)(b)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
}
return function (...args2) {
return curried.apply(this, [...args, ...args2]);
};
};
}

function sum(a, b, c) {
return a + b + c;
}

const currySum = curry(sum);
console.log(currySum(1)(2)(3)); // 6
console.log(currySum(1, 2)(3)); // 6
console.log(currySum(1)(2, 3)); // 6
console.log(currySum(1, 2, 3)); // 6

实际应用
参数复用:通过柯里化,可以预先设置好某些参数,然后得到一个预设了部分参数的新函数。这对于创建特定配置的函数非常有用。

1
2
const add5 = curriedAdd(5);
console.log(add5(2)(3)); // 输出 10
  • 延迟执行:柯里化允许你在需要的时候才完成函数的执行,这在处理异步操作或事件处理时特别有用。
  • 简化高阶函数:在处理高阶函数时,柯里化可以帮助简化代码,使得函数更易于理解和测试。
  • 函数组合:柯里化可以与其他函数式编程概念如函数组合结合使用,以构建复杂的逻辑链路,同时保持代码清晰简洁。
  • 部分应用:柯里化有时也被称作部分应用的一种形式,尽管严格来说二者有所不同。部分应用是指固定一个或多个参数的值,而柯里化则是逐步接收参数的过程。
    柯里化是一种强大的技术,尤其是在函数式编程范式中,它能够帮助开发者写出更加模块化和可重用的代码。

References

  1. https://en.wikipedia.org/wiki/Closure_(computer_programming)
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures