0%

简介

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

Introduction

Reflow and repaint are two important concepts in the browser rendering process. Understanding them can help us optimize the performance of our web pages.

Reflow

Reflow is the process of recalculating the position and size of all elements in the DOM tree. When the layout of the page changes, the browser needs to reflow the page to recalculate the position and size of all elements. The following changes can trigger a reflow:

  • Resizing the window
  • Changing the width or height of an element
  • Changing the padding, margin, or border of an element
  • Changing the font size of an element
  • Changing the position of an element

Repaint

Repaint is the process of updating the pixels on the screen. When the layout of the page changes, the browser needs to repaint the affected elements to update the pixels on the screen. The following changes can trigger a repaint:

  • Changing the background color of an element
  • Changing the text color of an element
  • Changing the visibility of an element
  • Changing the opacity of an element
  • Changing the z-index of an element

How Browser render pages

The browser rendering process consists of several steps:

  1. Parse HTML: The browser parses the HTML code and creates a DOM tree.
  2. Parse CSS: The browser parses the CSS code and creates a CSSOM tree.
  3. Combine DOM and CSSOM: The browser combines the DOM tree and CSSOM tree to create a render tree.
  4. Layout: The browser calculates the position and size of all elements in the render tree.
  5. Paint: The browser paints the pixels on the screen based on the render tree.
  6. Composite: The browser combines the painted pixels to create the final image on the screen.
  7. In the process of rendering a web page, the browser may need to trigger reflows and repaints to update the layout and appearance of the page.
    1. Reflow: Recalculating the position and size of all elements in the DOM tree.
    2. Repaint: Updating the pixels on the screen.
    3. Reflow will trigger repaint, but repaint does not necessarily trigger reflow.

References

  1. https://www.explainthis.io/en/swe/repaint-and-reflow
  2. https://dev.to/gopal1996/understanding-reflow-and-repaint-in-the-browser-1jbg
  3. https://medium.com/sessionstack-blog/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance-7b95553baeda

1. Url changed but page not loaded

Reason: The <router-outlet> is not in the html template.
Solution: Add the <router-outlet> to the template file.

1
2
3
4
5
6
7
8
9
<!-- app.component.html -->
<nav>
<ul>
<li><a routerLink="/home">Home</a></li>
<li><a routerLink="/product">Product</a></li>
<li><a routerLink="/about">About</a></li>
</ul>
</nav>
<router-outlet></router-outlet> <!-- Add this line -->

2. ERROR RuntimeError: NG04002: Cannot match any routes. URL Segment: ‘login’

Reason: Route ‘login’ is not defined in the router configuration.
Solution: Add the route to the router configuration.

1
2
3
4
// app-routes.ts
const routes: Routes = [
{ path: 'login', component: LoginComponent },
]

Reason 2: You use canMatch guard in route configuration and the guard return false.
Solution: Make sure the guard returns true.

1
2
3
4
5
{
path: 'product',
loadComponent: () => import('./product/product.component').then(m => m.ProductComponent),
canMatch: [CanMatchGuard], // <-- CanMatchGuard return false cause this error.
},

3. NG8001: ‘router-outlet’ is not a known element

Reason: The RouterModule is not imported in the module.
Solution: Import the RouterModule in the module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Module based component(app.module.ts)
import { RouterModule } from '@angular/router';
@NgModule({
imports: [RouterModule] // <-- Add this line
})

// Standalone component
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet], // <-- Add this line
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})