Tiny'Wo | 小窝

网络中的一小块自留地

面试官:你是怎么理解ES6�?Generator的?使用场景�?

一、介�?

Generator 函数�?ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不�?
回顾下上文提到的解决异步的手段:

  • 回调函数
  • promise

那么,上文我们提到promsie已经是一种比较流行的解决异步方案,那么为什么还出现Generator?甚至async/await呢?

该问题我们留在后面再进行分析,下面先认识下Generator

Generator函数

执行 Generator 函数会返回一个遍历器对象,可以依次遍�?Generator 函数内部的每一个状�?
形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function关键字与函数名之间有一个星�?- 函数体内部使用yield表达式,定义不同的内部状�?
    1
    2
    3
    4
    5
    function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
    }

二、使�?

Generator 函数会返回一个遍历器对象,即具有Symbol.iterator属性,并且返回给自�?

1
2
3
4
5
6
7
8
function* gen(){
// some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

通过yield关键字可以暂停generator函数返回的遍历器对象的状�?

1
2
3
4
5
6
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();

上述存在三个状态:helloworldreturn

通过next方法才会遍历到下一个内部状态,其运行逻辑如下�?

  • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值�?- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达�?- 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值�?- 如果该函数没有return语句,则返回的对象的value属性值为undefined
1
2
3
4
5
6
7
8
9
10
11
hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

done用来判断是否存在下个状态,value对应状态�?
yield表达式本身没有返回值,或者说总是返回undefined

通过调用next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

正因为Generator 函数返回Iterator对象,因此我们还可以通过for...of进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}

for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5

原生对象没有遍历接口,通过Generator 函数为它加上这个接口,就能使用for...of进行遍历�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);

for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

三、异步解决方�?

回顾之前展开异步解决的方案:

  • 回调函数
  • Promise 对象
  • generator 函数
  • async/await

这里通过文件读取案例,将几种解决异步的方案进行一个比较:

回调函数

所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,再调用这个函�?

1
2
3
4
5
6
7
8
fs.readFile('/etc/fstab', function (err, data) {
if (err) throw err;
console.log(data);
fs.readFile('/etc/shells', function (err, data) {
if (err) throw err;
console.log(data);
});
});

readFile函数的第三个参数,就是回调函数,等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执�?

Promise

Promise就是为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');

const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};


readFile('/etc/fstab').then(data =>{
console.log(data)
return readFile('/etc/shells')
}).then(data => {
console.log(data)
})

这种链式操作形式,使异步任务的两段执行更清楚了,但是也存在了很明显的问题,代码变得冗杂了,语义化并不�?

generator

yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化

1
2
3
4
5
6
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

async/await

将上面Generator函数改成async/await形式,更为简洁,语义化更强了

1
2
3
4
5
6
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

区别�?

通过上述代码进行分析,将promiseGeneratorasync/await进行比较�?

  • promiseasync/await是专门用于处理异步操作的

  • Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口…�?- promise编写代码相比Generatorasync更为复杂化,且可读性也稍差

  • Generatorasync需要与promise对象搭配处理异步情况

  • async实质是Generator的语法糖,相当于会自动执行Generator函数

  • async使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方�?

四、使用场�?

Generator是异步解决的一种方案,最大特点则是将异步操作同步化表达出�?

1
2
3
4
5
6
7
8
9
10
11
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()

// 卸载UI
loader.next()

包括redux-saga 中间件也充分利用了Generator特�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}

function* mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}

export default mySaga;

还能利用Generator函数,在对象上实现Iterator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}

// foo 3
// bar 7

参考文�?

面试官:你是怎么理解ES6中Module的?使用场景�?

一、介�?

模块,(Module),是能够单独命名并独立地完成一定功能的程序语句�?*集合(即程序代码和数据结构的集合体)**�?
两个基本的特征:外部特征和内部特�?

  • 外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功�?
  • 内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码�?

为什么需要模块化

  • 代码抽象
  • 代码封装
  • 代码复用
  • 依赖管理

如果没有模块化,我们代码会怎样�?

  • 变量和方法不容易维护,容易污染全局作用�?- 加载资源的方式通过script标签从上到下�?- 依赖的环境主观逻辑偏重,代码较多就会比较复杂�?- 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔�?
    因此,需要一种将JavaScript程序模块化的机制,如

  • CommonJs (典型代表:node.js早期)

  • AMD (典型代表:require.js)

  • CMD (典型代表:sea.js)

AMD

Asynchronous ModuleDefinition(AMD),异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行

代表库为require.js

1
2
3
4
5
6
7
8
9
10
11
12
/** main.js 入口文件/主模�?**/
// 首先用config()指定各模块路径和引用�?require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});

CommonJs

CommonJS 是一�?Javascript 模块规范,用于服务端

1
2
3
4
5
// a.js
module.exports={ foo , bar}

// b.js
const { foo,bar } = require('./a.js')

其有如下特点�?

  • 所有代码都运行在模块作用域,不会污染全局作用�?- 模块是同步加载的,即只有加载完成,才能执行后面的操作
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓�?- require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个�?

既然存在了AMD以及CommonJs机制,ES6Module又有什么不一样?

ES6 在语言标准的层面上,实现了Module,即模块功能,完全可以取�?CommonJS �?AMD 规范,成为浏览器和服务器通用的模块解决方�?
CommonJS AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属�?

1
2
3
4
5
6
7
// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同�?let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量

1
2
// ES6模块
import { stat, exists, readFile } from 'fs';

上述代码,只加载3个方法,其他方法不加载,�?ES6 可以在编译时就完成模块加�?
由于编译加载,使得静态分析成为可能。包括现在流行的typeScript也是依靠静态分析实现功�?

二、使�?

ES6模块内部自动采用了严格模式,这里就不展开严格模式的限制,毕竟这是ES5之前就已经规定好

模块功能主要由两个命令构成:

  • export:用于规定模块的对外接口
  • import:用于输入其他模块提供的功能

export

一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量

1
2
3
4
5
6
7
8
9
10
11
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

�?
// 建议使用下面写法,这样能瞬间确定输出了哪些变�?var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

输出函数或类

1
2
3
export function multiply(x, y) {
return x * y;
};

通过as可以进行输出变量的重命名

1
2
3
4
5
6
7
8
function v1() { ... }
function v2() { ... }

export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

import

使用export命令定义了模块的对外接口以后,其�?JS 文件就可以通过import命令加载这个模块

1
2
3
4
5
6
// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
element.textContent = firstName + ' ' + lastName;
}

同样如果想要输入变量起别名,通过as关键�?

1
import { lastName as surname } from './profile.js';

当加载整个模块的时候,需要用到星号*

1
2
3
4
5
6
7
8
9
10
11
12
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}

export function circumference(radius) {
return 2 * Math.PI * radius;
}

// main.js
import * as circle from './circle';
console.log(circle) // {area:area,circumference:circumference}

输入的变量都是只读的,不允许修改,但是如果是对象,允许修改属�?

1
2
3
4
import {a} from './xxx.js'

a.foo = 'hello'; // 合法操作
a = {}; // Syntax Error : 'a' is read-only;

不过建议即使能修改,但我们不建议。因为修改之后,我们很难差错

import后面我们常接着from关键字,from指定模块文件的位置,可以是相对路径,也可以是绝对路径

1
import { a } from './a';

如果只有一个模块名,需要有配置文件,告诉引擎模块的位置

1
import { myMethod } from 'util';

在编译阶段,import会提升到整个模块的头部,首先执行

1
2
3
foo();

import { foo } from 'my_module';

多次重复执行同样的导入,只会执行一�?

1
2
import 'lodash';
import 'lodash';

上面的情况,大家都能看到用户在导入模块的时候,需要知道加载的变量名和函数,否则无法加�?
如果不需要知道变量名或函数就完成加载,就要用到export default命令,为模块指定默认输出

1
2
3
4
// export-default.js
export default function () {
console.log('foo');
}

加载该模块的时候,import命令可以为该函数指定任意名字

1
2
3
// import-default.js
import customName from './export-default';
customName(); // 'foo'

动态加�?

允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势

这个新功能允许您将import()作为函数调用,将其作为参数传递给模块的路径�?它返回一�?promise,它用一个模块对象来实现,让你可以访问该对象的导�?

1
2
3
4
import('/modules/myModule.mjs')
.then((module) => {
// Do something with the module.
});

复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一�?

1
2
3
4
5
export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

同理能够搭配as*搭配使用

三、使用场�?

如今,ES6模块化已经深入我们日常项目开发中,像vuereact项目搭建项目,组件化开发处处可见,其也是依赖模块化实现

vue组件

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="App">
组件化开�?---- 模块�? </div>
</template>

<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>

react组件

1
2
3
4
5
6
7
8
function App() {
return (
<div className="App">
组件化开�?---- 模块�? </div>
);
}

export default App;

包括完成一些复杂应用的时候,我们也可以拆分成各个模块

参考文�?- https://macsalvation.net/the-history-of-js-module/

面试官:对象新增了哪些扩展?

一、属性的简�?

ES6中,当对象键名与对应值名相等的时候,可以进行简�?

1
2
3
const baz = {foo:foo}

// 等同�?const baz = {foo}

方法也能够进行简�?

1
2
3
4
5
6
7
8
9
10
11
12
const o = {
method() {
return "Hello!";
}
};

// 等同�?
const o = {
method: function() {
return "Hello!";
}
}

在函数内作为返回值,也会变得方便很多

1
2
3
4
5
6
7
8
function getPoint() {
const x = 1;
const y = 10;
return {x, y};
}

getPoint()
// {x:1, y:10}

注意:简写的对象方法不能用作构造函数,否则会报�?

1
2
3
4
5
6
7
const obj = {
f() {
this.foo = 'bar';
}
};

new obj.f() // 报错

二、属性名表达�?

ES6 允许字面量定义对象时,将表达式放在括号内

1
2
3
4
5
6
7
8
9
10
let lastWord = 'last word';

const a = {
'first word': 'hello',
[lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

表达式还可以用于定义方法�?

1
2
3
4
5
6
7
let obj = {
['h' + 'ello']() {
return 'hi';
}
};

obj.hello() // hi

注意,属性名表达式与简洁表示法,不能同时使用,会报�?

1
2
3
4
5
6
7
8
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]

1
2
3
4
5
6
7
8
9
const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}

三、super关键�?

this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象

1
2
3
4
5
6
7
8
9
10
11
12
13
const proto = {
foo: 'hello'
};

const obj = {
foo: 'world',
find() {
return super.foo;
}
};

Object.setPrototypeOf(obj, proto); // 为obj设置原型对象
obj.find() // "hello"

四、扩展运算符的应�?

在解构赋值中,未被读取的可遍历的属性,分配到指定的对象上面

1
2
3
4
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

注意:解构赋值必须是最后一个参数,否则会报�?
解构赋值是浅拷�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2; // 修改obj里面a属性中键�?x.a.b // 2,影响到了结构出来x的�?```

对象的扩展运算符等同于使用`Object.assign()`方法



## 五、属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性�?
- for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)

- Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键�?
- Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键�?
- Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所�?Symbol 属性的键名

- Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名�?Symbol 或字符串,也不管是否可枚�?
上述遍历,都遵守同样的属性遍历的次序规则�?
- 首先遍历所有数值键,按照数值升序排�?- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历所�?Symbol 键,按照加入时间升序�?
```js
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

六、对象新增的方法

关于对象新增的方法,分别有以下:

  • Object.is()
  • Object.assign()
  • Object.getOwnPropertyDescriptors()
  • Object.setPrototypeOf(),Object.getPrototypeOf()
  • Object.keys(),Object.values(),Object.entries()
  • Object.fromEntries()

Object.is()

严格判断两个值是否相等,与严格比较运算符�?==)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身

1
2
3
4
5
+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign()

Object.assign()方法用于对象的合并,将源对象source的所有可枚举属性,复制到目标对象target

Object.assign()方法的第一个参数是目标对象,后面的参数都是源对�?

1
2
3
4
5
6
7
const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

注意:Object.assign()方法是浅拷贝,遇到同名属性会进行替换

Object.getOwnPropertyDescriptors()

返回指定对象所有自身属性(非继承属性)的描述对�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
foo: 123,
get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }

Object.setPrototypeOf()

Object.setPrototypeOf方法用来设置一个对象的原型对象

1
2
3
4
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);

Object.getPrototypeOf()

用于读取一个对象的原型对象

1
Object.getPrototypeOf(obj);

Object.keys()

返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数�?

1
2
3
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]

Object.values()

返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组

1
2
3
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]

Object.entries()

返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数�?

1
2
3
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]

Object.fromEntries()

用于将一个键值对数组转为对象

1
2
3
4
5
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }

参考文�?

面试官:你是怎么理解ES6�?Promise的?使用场景�?

一、介�?

Promise ,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强�?
在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代�?

1
2
3
4
5
6
7
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('得到最终结�? ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);

阅读上面代码,是不是很难受,上述形成了经典的回调地狱

现在通过Promise的改写上面的代码

1
2
3
4
5
6
7
8
9
10
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('得到最终结�? ' + finalResult);
})
.catch(failureCallback);

瞬间感受到promise解决异步操作的优点:

  • 链式操作减低了编码难�?- 代码可读性明显增�?

下面我们正式来认识promise�?

状�?

promise对象仅有三种状�?

  • pending(进行中�?- fulfilled(已成功�?- rejected(已失败�?

特点

  • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状�?- 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果

流程

认真阅读下图,我们能够轻松了解promise整个流程

二、用�?

Promise对象是一个构造函数,用来生成Promise实例

1
const promise = new Promise(function(resolve, reject) {});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功�?- reject函数的作用是,将Promise对象的状态从“未完成”变为“失败�?

实例方法

Promise构建出来的实例存在以下方法:

  • then()
  • catch()
  • finally()

then()

then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

1
2
3
4
5
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});

catch

catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数

1
2
3
4
5
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON �?前一个回调函数运行时发生的错�? console.log('发生错误�?, error);
});

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为�?

1
2
3
4
5
6
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错�?});

一般来说,使用catch方法代替then()第二个参�?
Promise 对象抛出的错误不会传递到外层代码,即不会有任何反�?

1
2
3
4
5
6
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};

浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进�?
catch()方法之中,还能再抛出错误,通过后面catch方法捕获�?

finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操�?

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

构造函数方�?

Promise构造函数存在以下方法:

  • all()
  • race()
  • allSettled()
  • resolve()
  • reject()
  • try()

all()

Promise.all()方法用于将多�?Promise 实例,包装成一个新�?Promise 实例

1
const p = Promise.all([p1, p2, p3]);

接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例

实例p的状态由p1p2p3决定,分为两种:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函�?- 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函�?
    注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
throw new Error('报错�?);
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

如果p2没有自己的catch方法,就会调用Promise.all()catch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
throw new Error('报错�?);
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错�?```



### race()

`Promise.race()`方法同样是将多个 Promise 实例,包装成一个新�?Promise 实例

```javascript
const p = Promise.race([p1, p2, p3]);

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变

率先改变�?Promise 实例的返回值则传递给p的回调函�?

1
2
3
4
5
6
7
8
9
10
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);

p
.then(console.log)
.catch(console.error);

allSettled()

Promise.allSettled()方法接受一�?Promise 实例作为参数,包装成一个新�?Promise 实例

只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结�?

1
2
3
4
5
6
7
8
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

resolve()

将现有对象转�?Promise 对象

1
2
Promise.resolve('foo')
// 等价�?new Promise(resolve => resolve('foo'))

参数可以分成四种情况,分别如下:

  • 参数是一�?Promise 实例,promise.resolve将不做任何修改、原封不动地返回这个实例
  • 参数是一个thenable对象,promise.resolve会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法
  • 参数不是具有then()方法的对象,或根本就不是对象,Promise.resolve()会返回一个新�?Promise 对象,状态为resolved
  • 没有参数时,直接返回一个resolved状态的 Promise 对象

reject()

Promise.reject(reason)方法也会返回一个新�?Promise 实例,该实例的状态为rejected

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p = Promise.reject('出错�?);
// 等同�?const p = new Promise((resolve, reject) => reject('出错�?))

p.then(null, function (s) {
console.log(s)
});
// 出错�?```

`Promise.reject()`方法的参数,会原封不动地变成后续方法的参�?
```javascript
Promise.reject('出错�?)
.catch(e => {
console.log(e === '出错�?)
})
// true

三、使用场�?

将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化

1
2
3
4
5
6
7
8
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};

通过链式操作,将多个渲染数据分别给个then,让其各司其职。或当下个异步请求依赖上个请求结果的时候,我们也能够通过链式操作友好解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 各司其职
getInfo().then(res=>{
let { bannerList } = res
//渲染轮播�? console.log(bannerList)
return res
}).then(res=>{

let { storeList } = res
//渲染店铺列表
console.log(storeList)
return res
}).then(res=>{
let { categoryList } = res
console.log(categoryList)
//渲染分类列表
return res
})

通过all()实现多个请求合并在一起,汇总所有请求结果,只需设置一个loading即可

1
2
3
4
5
6
7
8
9
10
11
12
function initLoad(){
// loading.show() //加载loading
Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{
console.log(res)
loading.hide() //关闭loading
}).catch(err=>{
console.log(err)
loading.hide()//关闭loading
})
}
//数据初始�?
initLoad()

通过race可以设置图片请求超时

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
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
//img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg"; 正确�? img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1";
});
return p;
}

//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});

参考文�?

面试官:你是怎么理解ES6中Proxy的?使用场景?

一、介�?

*定义�? 用于定义基本操作的自定义行为

*本质�? 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作

一段代码来理解

1
2
3
4
5
6
7
#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
echo "echo $I" >>program
done
chmod +x program

这段程序每执行一次能帮我们生成一个名为program的文件,文件内容�?024行echo,如果我们手动来�?024行代码,效率显然低效

  • 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译

Proxy 亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等�?

二、用�?

Proxy�?构造函数,用来生成 Proxy 实例

1
var proxy = new Proxy(target, handler)

参数

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)�?
handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行�?

handler解析

关于handler拦截属性,有如下:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔�?- deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔�?- ownKeys(target):拦截Object.keys(proxy)for...in等循环,返回一个数�?- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔�?- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔�?- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对�?- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔�?- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔�?- apply(target, object, args):拦�?Proxy 实例作为函数调用的操�?- construct(target, args):拦�?Proxy 实例作为构造函数调用的操作

Reflect

若需要在Proxy内部调用对象的默认行为,建议使用Reflect,其是ES6中操作对象而提供的�?API

基本特点�?

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false�?- 让Object操作都变成函数行�?

下面我们介绍proxy几种用法�?

get()

get接受三个参数,依次为目标对象、属性名�?proxy 实例本身,最后一个参数可�?

1
2
3
4
5
6
7
8
9
10
11
var person = {
name: "张三"
};

var proxy = new Proxy(person, {
get: function(target, propKey) {
return Reflect.get(target,propKey)
}
});

proxy.name // "张三"

get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};

let target = [];
target.push(...elements);
return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

注意:如果一个属性不可配置(configurable)且不可写(writable),�?Proxy 不能修改该属性,否则会报�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});

const handler = {
get(target, propKey) {
return 'abc';
}
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed

set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身

假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}

// 对于满足条件�?age 属性以及其他属性,直接保存
obj[prop] = value;
}
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable: false,
});

const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = 'baz';
}
};

const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"

注意,严格模式下,set代理如果没有返回true,就会报�?

1
2
3
4
5
6
7
8
9
10
11
'use strict';
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
// 无论有没有下面这一行,都会报错
return false;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
Reflect.deleteProperty(target,key)
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`无法删除私有属性`);
}
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属�?```

注意,目标对象自身的不可配置(configurable)的属性,不能被`deleteProperty`方法删除,否则报�?


### 取消代理

Proxy.revocable(target, handler);

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

## 三、使用场�?
`Proxy`其功能非常类似于设计模式中的代理模式,常用功能如下:

- 拦截和监视外部对对象的访�?- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理



使用 `Proxy` 保障数据类型的准确�?
```js
let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("属性只能是number类型");
}
return Reflect.set(target, key, value, proxy);
}
});

numericDataStore.count = "foo"
// Error: 属性只能是number类型

numericDataStore.count = 333
// 赋值成�?```

声明了一个私有的 `apiKey`,便�?`api` 这个对象内部的方法调用,但不希望从外部也能够访问 `api._apiKey`

```js
let api = {
_apiKey: '123abc456def',
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可访问.`);
} return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`);
} return Reflect.get(target, key, value, proxy);
}
});

console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错�?```

还能通过使用`Proxy`实现观察者模�?
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执�?
`observable`函数返回一个原始对象的 `Proxy` 代理,拦截赋值操作,触发充当观察者的各个函数

```javascript
const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}

观察者函数都放进Set集合,当修改obj的值,在会set函数中拦截,自动执行Set所有的观察�?

参考文�?

面试官:你是怎么理解ES6新增Set、Map两种数据结构的?

如果要用一句来描述,我们可以说

Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构

什么是集合?什么又是字典?

  • 集合
    是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合

  • 字典
    是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同

区别�?

  • 共同点:集合、字典都可以存储不重复的�?- 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存�?

一、Set

Setes6新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集�?
Set本身是一个构造函数,用来生成 Set 数据结构

1
const s = new Set();

增删改查

Set的实例关于增删改查的方法�?

  • add()

  • delete()

  • has()

  • clear()

add()

添加某个值,返回 Set 结构本身

当添加实例中已经存在的元素,set不会进行处理添加

1
2
3
4
5
6
7
8
s.add(1).add(2).add(2); // 2只被添加了一�?```

### delete()

删除某个值,返回一个布尔值,表示删除是否成功

```js
s.delete(1)

has()

返回一个布尔值,判断该值是否为Set的成�?

1
s.has(2)

clear()

清除所有成员,没有返回�?

1
s.clear()

遍历

Set实例遍历的方法有如下�?
关于遍历的方法,有如下:

  • keys():返回键名的遍历�?- values():返回键值的遍历�?- entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成�?
    Set的遍历顺序就是插入顺�?
    keys方法、values方法、entries方法返回的都是遍历器对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

forEach()用于对每个成员执行某种操作,没有返回值,键值、键名都相等,同样的forEach方法有第二个参数,用于绑定处理函数的this

1
2
3
4
5
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

扩展运算符和 Set 结构相结合实现数组或字符串去�?

1
2
3
4
5
6
// 数组
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)]; // [3, 5, 2]

// 字符�?let str = "352255";
let unique = [...new Set(str)].join(""); // "352"

实现并集、交集、和差集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// (a 相对�?b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

二、Map

Map类型是键值对的有序列表,而键和值都可以是任意类�?
Map本身是一个构造函数,用来生成 Map 数据结构

1
const m = new Map()

增删改查

Map 结构的实例针对增删改查有以下属性和操作方法�?

  • size 属�?- set()
  • get()
  • has()
  • delete()
  • clear()

size

size属性返�?Map 结构的成员总数�?

1
2
3
4
5
const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2

set()

设置键名key对应的键值为value,然后返回整�?Map 结构

如果key已经有值,则键值会被更新,否则就新生成该键

同时返回的是当前Map对象,可采用链式写法

1
2
3
4
const m = new Map();

m.set('edition', 6) // 键是字符�?m.set(262, 'standard') // 键是数�?m.set(undefined, 'nah') // 键是 undefined
m.set(1, 'a').set(2, 'b').set(3, 'c') // 链式操作

get()

get方法读取key对应的键值,如果找不到key,返回undefined

1
2
3
4
5
6
const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello) // Hello ES6!

has()

has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中

1
2
3
4
5
6
7
8
9
10
const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // true

delete()

delete方法删除某个键,返回true。如果删除失败,返回false

1
2
3
4
5
6
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true

m.delete(undefined)
m.has(undefined) // false

clear()

clear方法清除所有成员,没有返回�?

1
2
3
4
5
6
7
let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

遍历

Map 结构原生提供三个遍历器生成函数和一个遍历方法:

  • keys():返回键名的遍历�?- values():返回键值的遍历�?- entries():返回所有成员的遍历�?- forEach():遍�?Map 的所有成�?
    遍历顺序就是插入顺序
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
37
38
39
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);

for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或�?for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});

三、WeakSet �?WeakMap

WeakSet

创建WeakSet实例

1
const ws = new WeakSet();

WeakSet 可以接受一个具�?Iterable 接口的对象作为参�?

1
2
3
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

APIWeakSetSet有两个区别:

  • 没有遍历操作的API
  • 没有size属�?
    WeakSet只能成员只能是引用类型,而不能是其他类型的�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let ws=new WeakSet();

    // 成员不是引用类型
    let weakSet=new WeakSet([2,3]);
    console.log(weakSet) // 报错

    // 成员为引用类�?let obj1={name:1}
    let obj2={name:1}
    let ws=new WeakSet([obj1,obj2]);
    console.log(ws) //WeakSet {{…}, {…}}

WeakSet 里面的引用只要在外部消失,它�?WeakSet 里面的引用就会自动消�?

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集�?
APIWeakMapMap有两个区别:

  • 没有遍历操作的API
  • 没有clear清空方法
1
2
3
4
5
6
7
8
9
10
11
12
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键�?

1
2
3
4
5
6
7
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

WeakMap的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

举个场景例子�?
在网页的 DOM 元素上添加数据,就可以使用WeakMap结构,当�?DOM 元素被清除,其所对应的WeakMap记录就会自动被移�?

1
2
3
4
5
6
const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用

下面代码中,键值obj会在WeakMap产生新的引用,当你修改obj不会影响到内�?

1
2
3
4
5
6
7
8
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}

参考文�?

面试官:说说var、let、const之间的区�?

一、var

在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变�?
注意:顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象

1
2
var a = 10;
console.log(window.a) // 10

使用var声明的变量存在变量提升的情况

1
2
console.log(a) // undefined
var a = 20

在编译阶段,编译器会将其变成以下执行

1
2
3
var a
console.log(a)
a = 20

使用var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声�?

1
2
3
var a = 20 
var a = 30
console.log(a) // 30

在函数中使用使用var声明变量时候,该变量是局部的

1
2
3
4
5
6
var a = 20
function change(){
var a = 30
}
change()
console.log(a) // 20

而如果在函数内不使用var,该变量是全局�?

1
2
3
4
5
6
var a = 20
function change(){
a = 30
}
change()
console.log(a) // 30

二、let

letES6新增的命令,用来声明变量

用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

1
2
3
4
{
let a = 20
}
console.log(a) // ReferenceError: a is not defined.

不存在变量提�?

1
2
console.log(a) // 报错ReferenceError
let a = 2

这表示在声明它之前,变量a是不存在的,这时如果用到它,就会抛出一个错�?
只要块级作用域内存在let命令,这个区域就不再受外部影�?

1
2
3
4
5
var a = 123
if (true) {
a = 'abc' // ReferenceError
let a;
}

使用let声明变量前,该变量都不可用,也就是大家常说的“暂时性死区�?
最后,let不允许在相同作用域中重复声明

1
2
3
let a = 20
let a = 30
// Uncaught SyntaxError: Identifier 'a' has already been declared

注意的是相同作用域,下面这种情况是不会报错的

1
2
3
4
let a = 20
{
let a = 30
}

因此,我们不能在函数内部重新声明参数

1
2
3
4
5
function func(arg) {
let arg;
}
func()
// Uncaught SyntaxError: Identifier 'arg' has already been declared

三、const

const声明一个只读的常量,一旦声明,常量的值就不能改变

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

这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋�?

1
2
const a;
// SyntaxError: Missing initializer in const declaration

如果之前用varlet声明过变量,再用const声明同样会报�?

1
2
3
4
5
var a = 20
let b = 20
const a = 30
const b = 30
// 都会报错

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改�?
对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量

对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变

1
2
3
4
5
6
7
8
const foo = {};

// �?foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// �?foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

其它情况,constlet一�?

四、区�?

varletconst三者区别可以围绕下面五点展开�?

  • 变量提升
  • 暂时性死�?- 块级作用�?- 重复声明
  • 修改声明的变�?- 使用

变量提升

var 声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined

letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报�?

1
2
3
4
5
6
7
8
9
10
11
// var
console.log(a) // undefined
var a = 10

// let
console.log(b) // Cannot access 'b' before initialization
let b = 10

// const
console.log(c) // Cannot access 'c' before initialization
const c = 10

暂时性死�?

var不存在暂时性死�?
letconst存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变�?

1
2
3
4
5
6
7
8
9
10
11
// var
console.log(a) // undefined
var a = 10

// let
console.log(b) // Cannot access 'b' before initialization
let b = 10

// const
console.log(c) // Cannot access 'c' before initialization
const c = 10

块级作用�?

var不存在块级作用域

letconst存在块级作用�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// var
{
var a = 20
}
console.log(a) // 20

// let
{
let b = 20
}
console.log(b) // Uncaught ReferenceError: b is not defined

// const
{
const c = 20
}
console.log(c) // Uncaught ReferenceError: c is not defined

重复声明

var允许重复声明变量

letconst在同一作用域不允许重复声明变量

1
2
3
4
5
6
7
8
9
10
11
// var
var a = 10
var a = 20 // 20

// let
let b = 10
let b = 20 // Identifier 'b' has already been declared

// const
const c = 10
const c = 20 // Identifier 'c' has already been declared

修改声明的变�?

varlet可以

const声明一个只读的常量。一旦声明,常量的值就不能改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// var
var a = 10
a = 20
console.log(a) // 20

//let
let b = 10
b = 20
console.log(b) // 20

// const
const c = 10
c = 20
console.log(c) // Uncaught TypeError: Assignment to constant variable

使用

能用const的情况尽量使用const,其他情况下大多数使用let,避免使用var

参考文�?

计算机基础

计算机基础在校招中考核较为常见,尤其是大厂,不可忽视�?
::: tip
如有疑问,可免费 加群 讨论咨询,也可参�?1v1 面试咨询服务�?专业、系统、高效、全流程 准备前端面试
:::

进程和线程的区别

参考答�?
::: details

进程是操作系统中一个正在运行的程序,每个进程都有自己的地址空间、内存、文件描述符等资源�?
线程是进程中的一个执行单元,�?cpu 调度的最小单元,共享进程的资源,但有自己的独立执行流�?
二者的区别

  • 进程拥有独立的堆栈空间和数据段,需要分配独立的地址空间,开销�?- 线程开销小,切换快,没进程安全,从通信机制上看,线程可以共享数据段

举例

  • 浏览器中一�?tab 就是一个进程,进程中某个线程崩了,整个进程就会崩,但是这不会影响其他的进程,因此进程之间相对独�?- Nginx �?Nodejs PM2 开启的多个 instance 是进程,每个 instance 之间不能共享内存数据
  • JS �?WebWorker 是一个线程,它可以和和主线程共享内存数据,独立运行不阻塞 UI

:::

进程间的通信

参考答�?
::: details

进程间通信(Inter-Process Communication,IPC)是指在操作系统中,不同进程之间交换信息和数据的过程,常见的进程通信方式包括�?

  • 管道:用于单向或双向通信
  • 消息队列:允许进程通过信息传递进行通信
  • 共享内存:多个进程可以访问同一块内存区�?- 信号量:用于进程间的同步
  • 套接字:用于网络通信

线程间通信与进程间通信类似,但由于线程之间共享进程的资源,线程间通信通常比进程间通信更高�?
:::

单核 CPU 如何实现并发

参考答�?
::: details

单核CPU 主要是通过时间片轮转和上下文切换来实现并发

时间片轮�?

  • CPU将时间划分为很小的时间片,通常是几十毫�?- 每个进程、线程分配到一个时间片
  • CPU轮流执行每个进程、线程的时间�?- 当一个时间片用完,CPU就会切换到下一个进程、线�?
    上下文切�?
  • 在切换进程、线程时,CPU需要保存当前进程的状态(上下文),包�? - 程序计数器的�? - 寄存器的�? - 内存映射信息
  • 加载下一个要执行的进程、线程的上下�?

    在任意时刻,CPU只能执行一个任务,由于切换速度非常快,给用户的感觉就像是在同时运行多个程序,所以这种机制被称为“伪并发”,若线程过多也不好,频繁的上下文切换会带来一定的性能开销,所以过多的线程反而会带来性能下降的问�?
    :::

CPU 调度算法有哪些?

参考答�?
::: details

  • 先来先服务(First Come First Serve�? - 最简单的调度算法
    • 进程按到达的顺序排队,先到达的先执行
    • 缺点:可能导致长时间的等待,特别是当一个长进程在队列前面时
  • 短作业优先(Shortest Job First�? - 优先执行预计运行时间最短的进程
    • 可以是非抢占式或是抢占式(shortest Remaining Time First,SRTF�? - 可能导致“饥饿”现象,即长作业可能永远得不到执�?- 优先级调度(Priority Scheduling�? - 每个进程分配一个优先级,优先级高的进程先执�? - 也可能导致“饥饿”现象,通常使用老化(aging)技术来解决
  • 轮转调度(Round Robin�? - 每个进程分配一个固定的时间片,时间片用完后,进程被放到队列的末�? - 适用于时间共享系�? - 时间片的大小对系统性能有很大影�?- 多级队列调度(Multilevel Queue Scheduling�? - 将进程分成多个队列,每个队列有不同的优先�? - 不同队列可以使用不同的调度算�?- 多级反馈队列调度(Multilevel Feedback Queue�? - 允许进程在不同的队列之间移动
    • 根据进程的行为动态调整其优先�?
      :::

linux 如何查找你的进程占用的那个端�?

参考答�?
::: details

使用 netstat 命令

1
2
3
# 查看所有端口占用情�?netstat -tunlp
# 查看特定端口,比�?8080
netstat -tunlp | grep 8080
  • t - tcp 协议;u - udp 协议;n - 显示端口号;l - 仅显示监听端口;p - 显示进程信息

使用 lsof 命令

1
2
# 查看特定端口,比�?8080
lsof -i :8080
  • i - 显示网络连接

:::

单核服务器连接数超载了怎么�?

参考答�?
::: details

  • 优化代码和查�?
    确保应用程序代码和数据库查询是高效的,以减少每个连接的资源消�?

  • 使用负载均衡

    将流量分配到多个服务器上,以分散负载

  • 增加连接�?
    使用连接池来管理数据库连接,减少连接的创建和销毁开销

  • 限制连接�?
    配置服务器以限制每个客户端的最大连接数,以防止单个客户端占用过多资�?

  • 使用缓存

    利用缓存机制(如 Redis,Memcached)来减少对数据库的访问次�?

:::

请简述一个编译器的执行过程。前端有哪些常见的编译工具?

参考答�?
::: details

编译器的执行过程

  1. 词法分析
    • 将源代码转换为一系列的标记(tokens),这些标记是编程语言的基本语法单�?2. 语法分析
    • 根据语言的语法规则,将标记序列转换为语法书(parse tree),也称为抽象语法树(AST�?3. 语义分析
    • 检查语法树是否符合语言的语义规则,例如类型检查,作用域检查等
  2. 中间代码生成
    • 将语法树转换为中间代码,这种代码通常独立于机�?5. 代码优化
    • 对中间代码进行优化,以提高程序的执行效率
  3. 目标代码生成
    • 将中间代码转换为目标机器代码
  4. 代码生成后优�?- 对生成的目标代码进行进一步优�? > 编译器的目的是将我们编写的源码转换为机器码(目标代码),以便计算机能够读懂执�?
    前端常见的编译工�?
  • Babel
    • 用于将现�?JavaScript 代码转换为向后兼容的版本
  • TypeScript Compiler
    • �?TypeScript 代码转换�?JavaScript
  • Sass/SCSS
    • �?Sass/SCSS 代码转换�?CSS
  • Webpack
    • 用于打包 JavaScript 模块,并支持多种编译和转换插�?
      :::

什么是编译型语言和解释型语言,他们有什么区别?

参考答�?
::: details

高级编程语言分为解释型语言和编译型语言

编译型语言

  • 常见编译型语言:C,C++,Java,Go,Rust
  • 执行方式:编译型语言的代码在运行期间由编译器一次性翻译成机器码,生成的机器码可以在目标机器上运行
  • 优点:代码在运行前就已经被翻译成机器码,运行速度通常更快
  • 缺点:需要编译步骤,开发和调试过程可能较慢

解释型语言

  • 常见解释性语言:JavaScript,Python,Ruby,PHP
  • 执行方式:解释型语言的代码在运行期间由解释器逐行翻译成机器码并执行。这意味着每次运行程序时,代码都需要被重新解释
  • 优点:由于不需要编译成机器码,开发和调试过程通常更快,更灵活
  • 缺点:运行速度通常比编译型语言慢,因为每次执行都需要进行翻�?
    编译型语言和解释型语言的区�?
  • 执行速度:编译型语言通常比解释型语言快,因为他们直接运行机器�?- 开发灵活性:解释型语言通常更灵活,适合快速开发和迭代
  • 错误检测:编译型语言在编译阶就可以捕获更多的语法和类型错误,而解释型语言通常在运行时才发现错�?

    举个🌰:读一门外文著作,编译型语言就是给你将这本著作翻译成中文,然后你就可以直接看中文了。解释型语言就是给你一个翻译,一边看一边翻译,下次看仍需要翻�?
    JIT(Just-In-Time)编�?
    为了结合编译型和解释型语言的优点,JIT 随之诞生,可以理解为“即时编译�?

  • 执行方式:JIT 编译在程序运行时将部分代码编译成机器码,而不是逐行解释,这种编译方式在代码即将被执行时进行,因此得名“即时编译�?- 应用场景:现�?JavaScript 引擎(如 V8 引擎)通常使用 JIT 编译来提高性能,包�?Java 虚拟机(JVM)也会使�?JIT 编译来提高性能

:::

简�?JS 垃圾回收的过程。用什么算法?

参考答�?
::: details

垃圾回收(Garbage Collection, GC)是自动管理内存的过程。JavaScript 引擎会自动检测不再使用的对象,并释放它们所占用的内存。常用的垃圾回收算法是标�?清除(Mark-and-Sweep)算法�?
垃圾回收过程

  1. 标记阶段
  • 垃圾回收器会从根对象(如全局对象、局部变量)开始,遍历所有对象,标记所有可达的对象
  • 可达对象:从根对象可以通过引用链访问到的对�?
  1. 清除阶段
  • 标记阶段结束后,所有未被标记的对象被视为不可达对象

  • 垃圾回收器会清除这些不可达对象,释放它们所占用的内�?
    常用算法

  • 标记-清除

    • 最常用的垃圾回收算�?- 引用计数
    • 每个对象有一个引用计数器,当对象被引用时,计数器�?,当对象不再被引用时,计数器�?,当引用计数�?0 时,表示该对象不再被使用,可以被回收。无法解决循环引用的问题,如今已很少使用
  • 分代回收

    • 现代 JavaScript 引擎通常使用分代回收策略,将内存分为新生代和老生代。新生代存储生命周期短的对象,老生代存储生命周期长的对象。不同代的对象使用不同的回收策略,以提高效率�?
      :::

参考资�?
::: details

:::

什么是内存泄漏?如何排查?JS 内存泄漏的常见原因?

参考答�?
::: details

内存泄露是指在程序运行过程中,程序未能释放不再使用的内存空间,导致内存资源被浪费�?
排查内存泄露

  1. 使用内存分析工具
    • 浏览器开发者工具:Chrome �?DevTools 提供了内存分析工�?Memory,可以监控内存使用情�? - 也可以结�?setInterval 使用 console.memory 查看内存使用的快�?2. 代码审查
    • 检查代码中是否有未释放的事件监听器,定时器,全局变量,确保不再需要某对象时,及时解除引用
  2. 性能监控
    • 监控应用程序的内存使用情况,观察是否有持续增长的趋势
    • 使用日志记录内存使用情况,帮助识别内存泄露的模式

JS 内存泄露的常见原�?

  1. 意外的全局变量
    • 忘记使用 var,let,const 声明变量时,变量会被挂载到全局对象�?2. 闭包
    • 闭包中引用了不再需要的外部变量,导致这些变量无法被垃圾回收
  2. 未清理的 DOM 引用
    • 删除 DOM 元素时,未能清理相关的事件监听器或引�?4. 定时器和回调
    • 未能清理不再需要的 setInterval �?setTimeout 回调

:::

简�?JS 运行时的堆栈内存模型�?

参考答�?
::: details

�?JS 运行时,内存管理主要依赖于堆(Heap)和栈(Stack)两种数据结�?
栈(Stack�?

  • 特点:栈是一种后进先出(LIFO)的数据结构,用于存储函数调用和原始数据类型
  • 用途:当函数被调用时,相关的执行上下文(包括局部变量,函数参数)会被压入栈中,当函数执行完毕后,栈顶的执行上下文会被弹出栈
  • 限制:栈内存的空间大小通常十分有限,适合存储生命周期短,大小固定的数据(比如无限递归不断创建栈帧,会导致爆栈�?
    堆(Heap�?
  • 特点:堆是一种动态内存分配的,无序的数据结构,用于存储对象和复杂数据类型
  • 用途:堆用于存储动态分配的内存,比如对象,数组,函数等
  • 限制:堆内存的分配和释放速度较慢,容易导致内存碎片化

这里用一�?js 代码画图举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = 1
let obj = {
name: 'Heap',
}

function foo() {
let b = 2
console.log(b)
function bar() {
let c = 3
console.trace()
console.log(c)
}
bar()
}

foo()

�?

  • 调用栈用于跟踪程序执行的活动函数。每当一个函数被调用时,它的执行上下文会被推入调用栈。当函数执行完毕后,它的执行上下文会从调用栈中弹出�?- 在这个例子中,最初的调用栈会包含一个匿名的全局执行上下文(anonymous),然后�?foo 函数的执行上下文,最后是 bar 函数的执行上下文�?- �?bar 函数执行完毕后,它的执行上下文会被弹出调用栈,然�?foo 函数的执行上下文也会被弹出调用栈,最后全局执行上下文也会被弹出调用栈�?
    �?
  • 全局存储�?obj 对象,在栈中仅仅是一个引用地址,真正的对象存储在堆中�?
    :::

冯·诺依曼架构是什么?

参考答�?
::: details

冯·诺依曼架构确定了现代计算机结构中的五大部件�?

  1. 输入设备: 键盘,鼠标,摄像头等
  2. 输出设备: 显示器,打印机,扬声器等
  3. 存储�? 计算机的记忆装置,主要存放数据和程序。分为内部存储器(内�?主存储器)和外部存储器(硬盘,光盘,U盘等�? - 内存:也称之为主存储器,又分为随机存储器(RAM)和只读存储器(ROM�? - RAM 存放的是计算机在通电运行的过程中即时的数据,计算机的内存容量就是指的 RAM 的容量。RAM 可读,可写,断电会数据丢�? - ROM 存放的是每次计算机开机都需要处理的,固定不变的程序和数据,比如 BIOS 程序。ROM 可读,不可写,断电不会丢�? - 外存:外存是硬盘,是计算机的辅助存储器,可以长期保存数据,断电不会丢失。当计算机需要从外存读取数据时,需要将数据从外存读取到内存中,然后才能下一步处理。根据介质不同,外存可分为软盘,硬盘,光盘。硬盘最为常见,硬盘又分为机械硬盘(HD)和固态硬盘(SSD)�?4. 运算�? 算术逻辑单元(ALU),负责执行算术和逻辑运算
  4. 控制�? 控制整个计算机系统的工作流程,包括指令的执行顺序,数据传输等,运算器和控制器通常集成在一起,就是我们熟知�?CPU

图中的箭头流向是数据流向。数据从输入设备进入到计算机,存放在存储器中,控制器负责控制运算器对存储器中的数据进行运算,运算的结果再次放入存储器中,通过控制器将存储器中的计算结果输出到输出设备�?
:::

计算机内部为何使用二进制�?

参考答�?
::: details

  • 硬件实现简单:二进制只需要两个状态,通常用电压的高低来表示(如高电压表示 1,低电压表示 0)。这种简单的状态切换使得硬件电路设计更为简单和可靠
  • 抗干扰能力强:在电路中,二进制的两个状态(0 �?1)可以通过明显的电压差来区分,这使得系统对噪声和干扰的容忍度更高,数据传输更稳�?- 逻辑运算方便:计算机的基本运算是逻辑运算,二进制系统与布尔代数非常契合,能够进行与,或,非等逻辑运算,简化了计算机的设计和操�?- 存储和处理效率高:二进制数据在计算机中可以直接存储和处理,避免了其他进制系统转换带来的复杂性和效率损失
  • 历史和标准化:从计算机发展的早期,二进制就被广泛采用,形成了标准化的设计和技术积累,进一步推动了其普及和应用

:::

二进制如何表示负数和小数�?

参考答�?
::: details

表示负数
下面�?8 位二进制�?-2 举例

  1. 求补�?
    1
    2
    3
    - 2 的原码:0000 0010 // 原码就是最简单的表示方法
    - 2 的反码:1111 1101 // 反码是原码的反向
    - 2 的补码:1111 1110 // 补码是反码加 1

1111 1110 就是 -2 的二进制

  1. 模减
1
2
- 模:2 ^ 8 = 256
- 256 - 2 = 254

254 的二进制就是 -2 的二进制 1111 1110

表示小数
下面�?10.625 转换为二进制举例

  1. 整数部分
  • 将整数部分除�?,记录余数,直到商为0。余数的逆序即为二进制表示�?
    1
    2
    3
    4
    - 10 / 2 = 5 �?0
    - 5 / 2 = 2 �?1
    - 2 / 2 = 1 �?0
    - 1 / 2 = 0 �?1

逆序排列�?010

  1. 小数部分
  • 将小数部分乘�?,记录整数部分,直到小数部分�?或达到所需的精度。整数部分的顺序即为二进制表示�?
    1
    2
    3
    - 0.625 * 2 = 1.25 取整 1
    - 0.25 * 2 = 0.5 取整 0
    - 0.5 * 2 = 1 取整 1

顺序排列�?.101

10.625 的二进制就是 1010.101

:::

什么是虚拟内存,为何要使用虚拟内存�?

参考答�?
::: details

什么是虚拟内存�?
虚拟内存相对物理内存,它是一种计算机内存管理技术,它为每个进程提供了一个连续的地址空间,使得进程可以认为自己拥有一个完整的,连续的内存空间,而实际上,这个空间可能是分散的,并且部分可以存储在外存中

为什么使用虚拟内存?

  1. 扩展内存容量:虚拟内存允许计算机使用硬盘空间来扩展物理内存的容量,即使物理内存不足,程序也可以运行,因为不常用的数据可以被交换到磁盘�?2. 内存保护:每个进程都有自己的虚拟地址空间,防止一个进程访问另一个进程的内存,提供了内存保护
  2. 简化内存管理:程序员不需要担心物理内存的分配和管理,操作系统负责将虚拟地址映射到物理地址
  3. 多任务处理:通过虚拟内存,多个进程可以同时运行,操作系统可以在它们之间切换,提供多任务处理能�?5. 程序加载和执行的灵活性:程序可以被分成多个部分,只有需要的部分才会被加载到内存中,减少了内存的使用

:::

什么是 Unicode 编码?它和常见的 UTF-8 有什么关系?

参考答�?
::: details

  • Unicode 俗称万国码,它为每个字符提供了一个唯一的数字标识,这个数字标识被称为码点。Unicode 的出现就是为了解�?ASCII 编码的局限性,ASCII 编码只能表示 128 个字符,全球各个国家的字符远不止 128 个,所�?Unicode 应运而生�?

  • Unicode 的定义了一个字符集和一系列编码方案(UTF-8,UTF-16,UTF-32),UTF-8 �?Unicode 最常用的编码方案,它是一种变长编码,根据不同的字符,使用不同的字节数来表示。对�?ASCII 字符,使�?1 个字节表示,�?ASCII 编码兼容

  • GBK 编码是一种用于中文字符的编码标准,扩展了 GB2312 编码,支持简体和繁体中文字符。GBK 编码使用 2 个字节表示一个中文字符,适合中文环境,但不支�?Unicode

:::

简述计算机网络�?OSI 模型

参考答�?
::: details

OSI(开放系统互联)模型是一个用于理解和实现网络协议的七层概念框架。每一层都有特定的功能,并与其直接上下的层进行通信�?

  1. 物理�?Physical Layer):这是OSI模型的最低层,负责设备之间的物理连接,包括通过物理介质传输原始比特流。它涉及硬件组件,如电缆、交换机和网络接口卡�?
  2. 数据链路�?Data Link Layer):负责节点到节点的数据传输以及错误检测和纠正,确保数据在物理链路上的可靠传输。它分为两个子层:媒体访问控制(MAC)层和逻辑链路控制(LLC)层�?
  3. 网络�?Network Layer):负责数据的路由、转发和寻址,确定数据到达目的地的最佳物理路径。像IP(互联网协议)这样的协议在这一层运行�?
  4. 传输�?Transport Layer):为应用程序提供端到端的通信服务,负责错误恢复、流量控制和确保完整的数据传输。像TCP(传输控制协议)和UDP(用户数据报协议)这样的协议在这一层运行�?
  5. 会话�?Session Layer):管理应用程序之间的会话,建立、维护和终止应用程序之间的连接,负责会话的检查点和恢复�?
  6. 表示�?Presentation Layer):负责数据的翻译、加密和压缩,确保数据以可用的格式呈现给应用层,充当网络和应用之间的翻译器�?
  7. 应用�?Application Layer):这是OSI模型的最高层,直接为终端用户应用程序提供网络服务,负责电子邮件、文件传输和网页浏览等网络服务。像HTTP、FTP和SMTP这样的协议在这一层运行�?
    OSI 模型

:::

一个域名对应一�?ip �?

参考答�?
::: details

一个域名不一定只对应一�?IP 地址,具体情况如下:

  • 单个域名对应单个 IP 地址:这是最简单的情况,一个域名解析到一个固定的 IP 地址�?- 单个域名对应多个 IP 地址:这种情况通常用于负载均衡和高可用性。通过 DNS 轮询(Round Robin DNS),一个域名可以解析到多个 IP 地址,用户的请求会被分配到不同的服务器上�?- 多个域名对应单个 IP 地址:多个域名可以指向同一�?IP 地址,这在虚拟主机中很常见。通过服务器配置,服务器可以根据请求的域名来提供不同的内容�?- CDN(内容分发网络):CDN 服务会根据用户的地理位置将域名解析到不同�?IP 地址,以提高访问速度和可靠性�?
    因此,域名和 IP 地址之间的关系可以是多对多的,具体取决于网络架构和配置�?
    :::

UDP �?TCP 协议的区别?有什么应用场�?

参考答�?
::: details

UDP(User Datagram Protocol,用户数据报协议�?

  • 无连接协议:UDP 是一种无连接协议,发送数据前不需要建立连接�?- 不提供错误恢复:UDP 不提供错误恢复机制,如果数据包丢失,不会进行重传�?- 传输速度快:由于没有错误检查和连接建立的过程,UDP 传输速度更快,延迟更低�?- 应用场景:适用于对速度要求高且允许偶尔数据丢失的应用,如直播流媒体、在线游戏和语音通话(VoIP)�?
    TCP(Transmission Control Protocol,传输控制协议)

  • 面向连接协议:TCP 在发送数据前需要建立连接,确保通信的可靠性�?- 提供错误恢复:TCP 提供错误检查,确保数据按顺序且无误地传输�?- 传输速度较慢:由于需要建立连接和进行错误检查,TCP 的传输速度相对较慢�?- 应用场景:适用于对数据完整性和顺序有严格要求的应用,如网页浏览、电子邮件和文件传输�?
    :::

数组和链表有什么区别?从内存结构上来说

参考答�?
::: details

  • 数组�? - 连续内存分配:数组在内存中是连续分配的,这意味着数组的所有元素在内存中是紧挨着的�? - 随机访问:由于数组的连续性,可以通过索引直接访问任意元素,访问速度快,时间复杂度为 O(1)�? - 固定大小:数组的大小在创建时就确定了,不能动态调整�?- 链表�? - 非连续内存分配:链表的每个元素(称为节点)在内存中可以是分散的,每个节点通过指针指向下一个节点�? - 顺序访问:访问链表中的元素需要从头节点开始,逐个遍历,访问速度较慢,时间复杂度�?O(n)�? - 动态大小:链表可以动态调整大小,方便插入和删除操作�?
    :::

HTML �?CSS

随着 Vue React 等框架,和各�?CSS UI 组件库的普及,HTML �?CSS 很容易被忽略�?
如果你是实习生、应届生或刚毕业不久,HTML �?CSS 知识一定要认真准备,大厂必考�?
::: tip
如有疑问,可免费 加群 讨论咨询,也可参�?1v1 面试咨询服务�?专业、系统、高效、全流程 准备前端面试
:::

DOCTYPE 是什么,都有哪些属性?

参考答�?
::: details

HTML �?<!DOCTYPE> 声明是文档类型声明,用于告知浏览器当�?HTML 文档使用�?HTML 版本,从而确保文档以正确的模式渲染。它通常出现�?HTML 文档的第一行�?
在现代开发中,推荐使�?HTML5 的简单声�?

1
<!DOCTYPE html>

在之前的 HTML 版本中,�?HTML4 ,会有其他写法,不过现在已经不常用�?

1
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

:::

meta 标签是干什么的,都有什么属性和作用

参考答�?
::: details

HTML 中的 <meta> 标签用于提供页面�?*元信�?*,这些信息不会直接显示在网页内容中,但对浏览器、搜索引擎和其他服务非常重要�?
常见�?meta 信息如下�?

  1. 字符编码。指定网页的字符编码,确保正确显示内容�?

    1
    <meta charset="UTF-8" />
  2. 页面视口设置(响应式设计)。控制页面在移动设备上的显示和缩放行为�?

    1
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  • width=device-width:页面宽度匹配设备屏幕宽�?- initial-scale=1.0:初始缩放比例为 1
  1. 搜索引擎优化(SEO)。提供描述性信息,便于搜索引擎索引�?

    1
    2
    3
    4
    5
    6
    <meta
    name="keywords"
    content="前端, 面试, 前端面试, 面试�? 刷题, 面试流程, 前端面试流程, 面试准备, 简�? 前端简�? Javascript, Typescript, React, Vue, webpack, vite, HTTP, 算法"
    />
    <meta name="description" content="面试派,双越老师整理的前端面试真实流程,大厂面试规范,开源免�? />
    <meta name="robots" content="index, follow" />
  2. 作者信息。提供网页作者信息�?

    1
    <meta name="author" content="双越老师" />

:::

什么是 DOM ,它�?HTML 有什么区别?

参考答�?
::: details

DOM �?Document Object Model 文档对象模型,它是一�?JS 对象。�?HTML 是一种标记语言(和 XML 类似)用于定义网页的内容和结构�?
DOM 的特�?

  • 树形结构,DOM �?- 可编程,可以使用 Javascript 读取和修�?DOM 数据
  • 动态性,通过 DOM API 动态修改结构和数据

HTML �?DOM 的过�?

  • HTML 解析:浏览器解析 HTML 代码,生�?DOM 树�?- CSSOM 生成:解�?CSS,生�?CSSOM(CSS 对象模型)�?- 渲染树:结合 DOM �?CSSOM,生成渲染树�?- 页面渲染:根据渲染树将内容显示在页面上�?
    :::

如何理解 HTML5 语义�??有哪些常见的语义化标签�?

理解 HTML5 语义�?
::: details

HTML5 语义化是指通过使用具有明确含义的标签,使网页的结构和内容更加清晰,方便浏览器、开发者以及搜索引擎理解网页内容�?
语义化的核心在于让标签不仅描述外观,还能表达内容的含义,从而提升网页的可读性、可维护性和可访问性�?

  • 提高代码可读性:开发者无需额外注释即可理解代码结构�?- 增强 SEO(搜索引擎优化):搜索引擎能更好地抓取和理解网页内容�?- 提升可访问性:辅助技术(如屏幕阅读器)可以更准确地解释页面内容�?- 支持更好的浏览器兼容性:现代浏览器能够更高效地渲染语义化结构�?
    :::

常见�?HTML5 语义化标�?
::: details

  • <header> 注意:要区别�?<head>
  • <nav>
  • <main>
  • <article>
  • <section>
  • <aside>
  • <footer>
  • <figure>
  • <figcaption>
  • <mark>
  • <time>
  • <summary>
  • <details>

:::

写一�?HTML5 语义化的例子

::: details

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
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTML5 语义化示�?/title>
</head>
<body>
<header>
<h1>面试�?/h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于</a></li>
<li><a href="#contact">联系</a></li>
</ul>
</nav>
</header>

<main>
<section id="home">
<h2>欢迎访问</h2>
<p>这是一�?HTML5 语义化的示例�?/p>
</section>
<section id="about">
<h2>关于我们</h2>
<article>
<h3>我们的历�?/h3>
<p>这是关于我们历史的介绍�?/p>
</article>
</section>
</main>

<aside>
<h3>相关文章</h3>
<ul>
<li><a href="#">文章 1</a></li>
<li><a href="#">文章 2</a></li>
</ul>
</aside>

<footer>
<p>&copy; 2025 &copy; 面试�?/p>
</footer>
</body>
</html>

:::

DOM 节点�?attr �?property 有何区别

  • attr 指的�?HTML 属性(attribute�?- property 指的�?DOM 对象的属性(property�?
    主要区别

::: details

定义不同

  • attr 定义�?HTML 元素上的初始属性,存储�?DOM 元素的属性列表中,与 HTML 源代码一一对应�?- property �?DOM 对象的属性,是通过浏览器解�?HTML 并生�?DOM 对象时动态创建的,供 JavaScript 操作�?
    存储位置不同

  • attr �?HTML 的一部分,存储在元素�?HTML 标记 中�?- property �?DOM 的一部分,存储在 JavaScript 对象中�?
    行为不同

  • attr 一般是静态的,表示元素初始的值,即从 HTML 源代码中解析的值,通常不会因用户操作或脚本修改而自动更新。除非你手动使用 JS 修改值�?- property 一般是动态的,表示当前状态,可以通过 JavaScript 修改,并反映�?DOM 中�?
    对于一些常用的属性(�?id、value、checked 等),attr �?property 会部分同步:

  • 修改 attr 会影�?property 值�?- 而修�?property 可能不会同步�?attr�?
    总结,一般来说,attr 用于设置元素的初始状态,�?property 用于操作和获取当前状态�?
    :::

如何一次性插入多�?DOM 节点?考虑性能

参考答�?
::: details

直接多次操作 DOM(如多次 appendChild �?innerHTML 更新)会导致性能问题,因为每次操作都会触�?DOM 的重新渲染�?
DocumentFragment 是一个轻量级的文档片段,可以在内存中操作节点,最后一次性插入到 DOM 中,从而减少重绘和回流�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取目标容器
const container = document.getElementById('list')

// 创建 DocumentFragment
const fragment = document.createDocumentFragment()

// 创建多个节点并添加到 fragment �?for (let i = 1; i <= 1000; i++) {
const li = document.createElement('li')
li.textContent = `item ${i}`
fragment.appendChild(li)
}

// 一次性插入到 DOM
container.appendChild(fragment)

:::

offsetHeight scrollHeight clientHeight 有什么区�?

参考答�?
::: details

offsetHeight 元素的总高度,包括内容高度、内边距(padding)、水平滚动条高度(如果存在)、以及边框(border)。不包括外边距(margin)�?
scrollHeight 元素的实际内容高度,包括不可见的溢出部分(scrollable content),大于等于 clientHeight�?
clientHeight 元素的可见内容高度,包括内容高度和内边距(padding),但不包括水平滚动条高度、边框(border)和外边距(margin)�?
:::

HTMLCollection �?NodeList 的区�?

在操�?DOM 时,HTMLCollection �?NodeList 都是用来表示节点集合的对象,它们的区别是�?
::: details

HTMLCollection 只包�?HTML 元素节点。通过 document.getElementsByTagName �?document.getElementsByClassName 返回的结果是 HTMLCollection。

NodeList 包括 *元素节点、文本节点、注释节�? 等,不仅仅是 HTML 元素节点

  • 通过 document.querySelectorAll 返回的是 静�?NodeList
  • 通过 childNodes 返回的是 动�?NodeList

当文档结构发生变化时

  • HTMLCollection �?动�?NodeList 会随着 DOM 的变化自动更�?- 静�?NodeList 不会随着 DOM 的变化自动更�?
    :::

Node �?Element 有什么区别?

�?DOM(文档对象模型)中,HTML Element �?Node 都是表示文档结构中的对象,但它们有不同的定义和用途�?
::: details

Node �?DOM 树中所有类型对象的基类,是一个接口,表示文档树中的一个节点。它有多个子类型,Element 是其中的一个。其他的还有 Text、Comment 等�?
Node 常见属性如 nodeName nodeValue

HTML Element �?Node 的子类,专门表示 HTML 元素节点。它提供了与 HTML 元素相关的更多功能,如属性、样式等。HTML Element 仅表�?HTML 元素节点,通常对应 HTML 标签,如 <div>, <p>, <a> 等�?
Element 常见属性和方法�?innerHTML getAttribute setAttribute

:::

开发一个无限下拉加载图片的页面,如何给每个图片绑定 click 事件�?

参考答�?
::: details

使用 事件委托 实现,避免重复绑定事件,性能高,适合动态加载的场景�?
代码示例

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
<div id="image-container" style="height: 400px; overflow-y: scroll; border: 1px solid #ccc;">
<!-- 加载图片 -->
</div>

<script>
const container = document.getElementById('image-container')

// 模拟 API 请求加载图片
let page = 1 // 当前加载的页�? const loadImages = () => {
for (let i = 1; i <= 10; i++) {
const img = document.createElement('img')
img.src = `https://via.placeholder.com/150?text=Image+${(page - 1) * 10 + i}`
img.style.margin = '10px'
img.alt = `Image ${(page - 1) * 10 + i}`
img.className = 'image-item' // 添加统一的类�? container.appendChild(img)
}
page++
}

// 绑定父容器的 click 事件
container.addEventListener('click', (event) => {
if (event.target.tagName === 'IMG') {
alert(`You clicked on ${event.target.alt}`)
}
})

// 监听滚动事件,实现无限加�? container.addEventListener('scroll', () => {
if (container.scrollTop + container.clientHeight >= container.scrollHeight) {
loadImages() // 加载更多图片
}
})

// 初次加载图片
loadImages()
</script>

以上代码中,我们�?click 事件统一绑定�?container 容器中,然后判断 event.target.tagName === 'IMG' 即触发事件�?
:::

window.onload �?DOMContentLoaded 的区别是什么?

这两个事件都用于检测页面的加载状态,但触发的时机和作用范围有所不同�?
::: details

DOMContentLoaded 是当 **DOM 树构建完�?*(HTML 被解析完成,不等待样式表、图片、iframe 等资源加载)时触发,不依赖于外部资源�?
window.onload 是当 **整个页面及所有资�?*(包括样式表、图片、iframe、脚本等)加载完成时触发,依赖于外部资源�?
DOMContentLoaded 会更早触发�?
使用推荐

  • 如果你的逻辑只依�?DOM 的加载(如操作页面结构、绑定事件),使�?DOMContentLoaded�?- 如果你的逻辑需要依赖页面所有资源加载完成(如获取图片尺寸、执行动画),使�?window.onload�?
    :::

script 标签放在 head 里,怎么解决加载阻塞的问�?

�?HTML 中,<script> 标签通常会阻塞页面的渲染,尤其是当它放在 <head> 部分时,因为浏览器会在执�?JavaScript 代码之前停止解析 HTML�?
可参考的解决方案

::: details

  1. 使用 async 属性。当 async 属性添加到 <script> 标签时,脚本会异步加载,并在加载完成后立即执行,不会阻塞页面的渲染。适用于不依赖其他脚本或页面内容的独立脚本,但多个 JS 文件时无法保证加载和执行顺序�?

    1
    2
    3
    <head>
    <script src="script.js" async></script>
    </head>
  2. 使用 defer 属性。defer 属性使得脚本延迟执行,直到 HTML 文档解析完毕。这意味着脚本不会阻塞 HTML 渲染,且会按照文档中 <script> 标签的顺序执行。适用于依�?DOM 元素的脚本(如操作页面内容)�?

    1
    2
    3
    <head>
    <script src="script.js" defer></script>
    </head>
  3. �?<script> 放在 <body> 最后�?
    :::

常见�?HTML 标签哪些�?inline 元素,哪些是 block 元素,哪些是 inline-block 元素

参考答�?
::: details

  1. inline 元素有:a, span, img, strong, em, b, i, abbr, code, br, q(引用), sub(下标), sup(上标)

  2. block 元素有:div, p, h1, h2, h3, h4, h5, h6, ul, ol, li, form, section, article, footer, header, nav

  3. inline-block 元素有:input button

注意,table 虽然也是独占一行,但它 display: table 不是 block

:::

常见�?CSS 选择器有哪些�?

参考答�?
::: details

选择器类�? 示例 说明
*元素选择�? p 选择所�?<p> 元素
*类选择�? .button 选择所�?class="button" 的元�?
*ID 选择�? #header 选择 id="header" 的元�?
*通用选择�? * 选择页面中的所有元�?
*后代选择�? div p 选择 div 内的所�?<p> 元素
*子元素选择�? div > p 选择 div 的直接子元素 <p>
*相邻兄弟选择�? h1 + p 选择紧接�?<h1> 后面�?<p> 元素
*通用兄弟选择�? h1 ~ p 选择所有紧跟在 <h1> 后面�?<p> 元素
*属性选择�? a[href] 选择具有 href 属性的所�?<a> 元素
:hover a:hover 选择鼠标悬停时的 <a> 元素
:first-child p:first-child 选择父元素中的第一�?<p> 元素
:nth-child(n) li:nth-child(odd) 选择父元素中所有奇数位置的 <li> 元素
::before p::before { content: "Note: "; } 在每�?<p> 元素的前面插�?”Note: “
::after p::after { content: "."; } 在每�?<p> 元素的后面插入一个句�?
:not() p:not(.highlight) 选择所有不具有 highlight 类的 <p> 元素

注意,这里的内容比较全,面试时你也许记不住所有,但只要能说上一半儿�?
:::

CSS 盒子模型,尺寸计�?

如下代码,请�?div1 �?offsetWidth 是多大?

1
2
3
4
5
6
7
8
9
10
11
<!-- 如下代码,请�?div1 �?offsetWidth 是多大? -->
<style>
#div1 {
width: 100px;
padding: 10px;
border: 1px solid #ccc;
margin: 10px;
}
</style>

<div id="div1"></div>

答案

::: details
offsetWidth 是指 元素内容 + 内间�?+ 边框的距离,不包括外间距

所�?offsetWidth �?122px
:::

追问:如果想要让 offsetWidth 等于 100px ,还需要再增加一个什么属性?

答案

::: details
增加 box-sizing: border-box;
:::

margin 纵向重叠

如下代码,AAA �?BBB 之间的距离是多少�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 如下代码,AAA �?BBB 之间的距离是多少�?-->
<style>
p {
font-size: 16px;
line-height: 1;
margin-top: 10px;
margin-bottom: 15px;
}
</style>

<p>AAA</p>
<p></p>
<p></p>
<p></p>
<p>BBB</p>

答案

::: details
AAA �?BBB 之间的距离是 15px
:::

lineHeight 如何继承�?

如下代码,<p> 标签的行高将会是多少�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--如下代码,p 标签的行高将会是多少�?->
<style>
body {
font-size: 20px;
line-height: 200%;
}
p {
font-size: 16px;
}
</style>

<body>
<p>AAA</p>
</body>

答案

::: details
line-height 不同类型的值,继承规则是不一样的

  • 写具体的数值,�?30px,则继承该数�?—�?比较好理�?- 写百分比,如 200% ,则继承当前计算出来的值,如上述题�?—�?重要!!�?- 写比例,�?2 �?1.5 ,则继承比例

所以,该问题的的答案是,继�?40px �?:::

margin 负值问�?

参考答�?
::: details

  • margin-left 负值,元素左移
  • margin-top 负值,元素上移
  • margin-right 负值,自身宽度缩小,右侧元素会跟进,但内容不受影响
  • margin-bottom 负值,自身高度缩小,下方元素会跟进,但内容不受影响

:::

什么是 BFC 如何触发 BFC�?

参考答�?
::: details

BFC (Block formatting context) 直译�?块级格式化上下文”。它是一个独立的渲染区域,与这个区域外部毫不相干。即,BFC 里面的的内容再怎么发生变化,也不会影响�?BFC 外面的布局,这一点是在网页布局中非常有用的。先说,能形�?BFC 的条件有�?

  • 根元�?- float 属性不�?none
  • position �?absolute �?fixed
  • display �?inline-block table-cell table-caption flex inline-flex
  • overflow 不为 visible

BFC 在网页布局中经常用来清除浮动(特别是在使用 float 布局的情况下),最常被用来触发 BFC 的属性是overflow: hidden,例如要实现一个左图右文的布局�?

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
.bfc {
overflow: hidden; /* 触发 BFC */
}
.left {
float: left;
}
</style>

<div class="bfc">
<img src="x.png" class="left" style="margin-right: 10px;" />
<p class="bfc">text...</p>
</div>

:::

使用 CSS 实现居中对齐,有哪几种方式?

实现水平居中对齐

::: details

  1. inline 元素用text-align: center;即可,如下:
1
2
3
.container {
text-align: center;
}
  1. block 元素可使用margin: auto;
1
2
3
4
5
6
7
.container {
text-align: center;
}
.item {
width: 1000px;
margin: auto;
}
  1. 绝对定位元素可结合leftmargin实现,但是必须知道宽�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .container {
    position: relative;
    width: 500px;
    }
    .item {
    width: 300px;
    height: 100px;
    position: absolute;
    left: 50%;
    margin-left: -150px;
    }

:::

实现垂直居中对齐

::: details

  1. inline 元素可设置line-height的值等于height值,如单行文字垂直居中:
1
2
3
4
.container {
height: 50px;
line-height: 50px;
}
  1. 绝对定位元素,可结合topmargin实现,但是必须知道尺�?
  • 优点:兼容性好�?- 缺点:需要提前知道尺寸,
1
2
3
4
5
6
7
8
9
10
11
12
13
.container {
position: relative;
height: 200px;
}
.item {
width: 80px;
height: 40px;
position: absolute;
left: 50%;
top: 50%;
margin-top: -20px;
margin-left: -40px;
}
  1. 绝对定位可结合transform实现居中
  • 优点:不需要提前知道尺寸;
  • 缺点:兼容性不好(现代浏览器都没问题)
1
2
3
4
5
6
7
8
9
10
11
12
.container {
position: relative;
height: 200px;
}
.item {
width: 80px;
height: 40px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
  1. 绝对定位结合margin: auto,不需要提前知道尺寸,兼容性好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.container {
position: relative;
height: 300px;
}
.item {
width: 100px;
height: 50px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}

:::

什么是 CSS 定位上下文?absolute �?relative 分别依据谁来定位�?

参考答�?
::: details

  • relative 是相对于自身定位的(且不会影响其他元素的定位�?- absolute 是相对于上层最近的一个定位元素来定位的,如果没有就依赖于 body 定位�?
    :::

参考资�?
::: details

:::

CSS overflow: hidden display:none �?visibility: hidden 有什么区�?

参考答�?
::: details

  • overflow: hidden 溢出内容不可见,未溢出的部分正常可见
  • display:none 隐藏内容,不占用任何空间,内容变化不会重新渲�?- visibility: hidden 隐藏元素,但保留其占据的空间,内容变化会重新渲染

:::

CSS px % em rem vw/vh 的区�?

参考答�?
::: details

单位 基准 绝对/相对 优点 缺点 适用场景
px 固定像素 绝对 精确,简单易�? 缺乏响应式能�? 固定尺寸元素
% 父元素尺�? 相对 灵活,适合响应式设�? 依赖父元�? 响应式布局,流式设�?
em 当前元素字体大小 相对 动态调整,适合局部相对设�? 嵌套复杂,计算难预测 动态字体、内外边距等
rem 根元素字体大小(html�? 相对 全局一致,计算简�? 需要设置根元素字体 全局比例调整,响应式设计
vw/vh 视口宽度或高�? 相对 基于视口,适合全屏设计 小屏显示可能不理�? 全屏布局,视口动态调�?

使用建议:

  • 响应式设计:结合使用 rem �?%�?- 固定大小:使�?px 定义精确尺寸�?- 全屏布局:使�?vw �?vh�?- 动态比例设计:em �?rem 都是优秀的选择,但推荐 rem 更加简洁统一�?
    :::

如何实现 Retina �?1px 像素边框

参考答�?
::: details

  1. 使用 transform: scale 实现�?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    .retina-border {
    position: relative;
    }

    .retina-border::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px; /* 边框的物理宽�?*/
    background-color: black; /* 边框颜色 */
    transform: scaleY(0.5); /* 缩放�?0.5 */
    transform-origin: 0 0; /* 缩放起点 */
    }
  2. 使用 box-shadow 模拟边框

1
2
3
4
.retina-border {
position: relative;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); /* 通过阴影模拟边框 */
}

:::

使用 CSS 画一个三角形

参考答�?
::: details

使用 CSS “画”一个向上的三角形,重点在于使用透明边框�?

1
2
3
4
5
6
7
8
9
10
<style>
.triangle-up {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 50px solid #000; /* 底部颜色即为三角形颜�?*/
}
</style>
<div class="triangle-up"></div>

:::

如何实现黑白主题变化�?

参考答�?
::: details

可使�?CSS 变量

1
2
3
4
5
6
7
8
9
10
11
12
/* 定义变量 */
:root,
:host {
--color: #333;
--bg-color: #fff;
}

/* 使用变量 */
p {
color: var(--color);
background-color: var(--bg-color);
}

:::

如何实现响应式布局�?

CSS 实现响应式布局可以使页面在不同的设备和屏幕尺寸上有良好的显示效果,以下是几种常见的实现方式�?
::: details

  1. 使用媒体查询(Media Queries)。媒体查询是响应式布局的核心技术,通过检测设备的宽度、高度、分辨率等条件应用不同的样式。可根据屏幕宽度调整字体大小、布局样式等�?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* 默认样式 */
    body {
    font-size: 16px;
    padding: 20px;
    }

    /* 屏幕宽度小于等于768px时的样式 */
    @media (max-width: 768px) {
    body {
    font-size: 14px;
    padding: 10px;
    }
    }

    /* 屏幕宽度大于1200px时的样式 */
    @media (min-width: 1200px) {
    body {
    font-size: 18px;
    padding: 30px;
    }
    }
  2. 使用弹性盒子(Flexbox)。创建水平或垂直方向上的自适应布局,比如导航栏、网格布局�?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .container {
    display: flex;
    flex-wrap: wrap; /* 允许换行 */
    }

    .item {
    flex: 1 1 200px; /* 每个子项占据至少200px,随空间调整 */
    margin: 10px;
    background-color: #f0f0f0;
    }
  3. 使用网格布局(CSS Grid Layout)�?创建复杂的自适应网格布局,比如图片库、商品列表�?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 20px;
    }

    .item {
    background-color: #d4edda;
    padding: 10px;
    }
  4. 使用百分比和单位 vh/vw 实现宽度和高度的自适应。实现宽度和高度随窗口变化而调整�?

    1
    2
    3
    4
    5
    .container {
    width: 80%; /* 占父容器�?0% */
    height: 50vh; /* 占视口高度的50% */
    background-color: #e9ecef;
    }

:::

如何理解 z-index �?

::: details

  • z-index 是一�?CSS 属性,用于控制元素的堆叠顺序(�?Z 轴的显示顺序)。值越大,元素越靠前显示,反之值越小,元素越靠后�?- z-index 只适用�?*定位的元素,需要设�?position 属性为 relativeabsolutefixed �?sticky,否�?z-index 不生效�?- z-index 只在同级**比较,父子元素的 z-index 不会互相影响�?
    :::

使用 flex 设计一个“四合院”布局

如下�?

参考答�?
::: details

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CSS 四合�?/title>
<style>
html * {
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
height: 100%;
}
#header {
height: 50px;
background-color: red;
}
#container {
flex: 1;
display: flex;
}
#left-container {
width: 100px;
background-color: green;
}
#main-container {
flex: 1;
background-color: #ccc;
}
#right-container {
width: 200px;
background-color: yellow;
}
#footer {
height: 50px;
background-color: blue;
}
</style>
</head>
<body>
<!-- HTML5 标签 语义�?-->
<header id="header">header</header>
<section id="container">
<aside id="left-container">left</aside>
<section id="main-container">main</section>
<!-- 或者用 article ,看用�?-->
<aside id="right-container">right</aside>
<!-- 或者用 section ,看用�?-->
</section>
<footer id="footer">footer</footer>
</body>
</html>

:::

你用过哪�?CSS 相关的技术,如库、框架、预处理语言、后处理语言�?

参考答�?
::: details

  • CSS 框架:TailwindCSS BootStrap
  • CSS 预处理语言:Less Sass Stylus
  • CSS 后处理语言:PostCSS Autoprefixer
  • CSS 组件库:ElementUI AntDesign
  • CSS-in-JS:Styled-Components Emotion
  • CSS 工具:Normalize.css Animate.css

:::

HTTP 网络请求

HTTP �?Ajax 是前后端沟通的桥梁,面试重点考察,无论工作经验长短�?
::: tip
如有疑问,可免费 加群 讨论咨询,也可参�?1v1 面试咨询服务�?专业、系统、高效、全流程 准备前端面试
:::

TCP 是如何建立连接的,三次握手,四次挥手

参考答�?
::: details

三次握手

  • 客户端向服务端发送建立连接请求,客户端进�?SYN-SEND 状�?- 服务端收到建立连接请求后,向客户端发送一个应答,服务端进�?SYN-RECEIVED 状�?- 客户端接收到应答后,向服务端发送确认接收到应答,客户端进入 ESTABLISHED 状�?
    四次挥手

  • 客户端向服务端发送断开连接请求

  • 服务端收到断开连接请求后,告诉应用层去释放 tcp 连接

  • 服务端向客户端发送最后一个数据包 FINBIT ,服务端进入 LAST-ACK 状�?- 客户端收到服务端的断开连接请求后,向服务端确认应答

三次握手四次挥手,客户端都是主动方,服务端都是被动方。在状态方面:三次握手的客户端和服务端都是由原来的 closed 变为 established,四次挥手的客户端和服务端都是由原来�?established 变为 closed�?
:::

参考资�?
::: details

HTTP 几个版本的区�?

参考答�?
::: details

HTTP/0.9 - 单行协议

  • 只有 GET 请求行,无请求头和请求体
  • 只能传输 HTML 文件,以 ASCII 字符流返�?- 无响应头

HTTP/1.0 - 多类型支�?

  • 支持多种文件类型传输,不限于 ASCII 编码
  • 引入请求头和响应�? key-value 形式)
  • 每个请求都需要建立新�?TCP 连接

HTTP/1.1 - 持久连接

  • 引入持久连接( keep-alive ):一�?TCP 连接可传输多�?HTTP 请求
  • 默认开�?keep-alive,通常限制 6-8 个并发连�?- 存在队头阻塞问题:前面的请求阻塞会影响后续请�?- 引入 Host 字段,支持虚拟主�?- 引入 Chunk transfer 机制处理动态内容长�?

HTTP/2.0 - 多路复用

  • 一个域名只使用一�?TCP 长连�?- 引入二进制分帧层,实现多路复�?- 可对请求设置优先�?- 引入 HTTPS(HTTP + TLS) 加密

HTTP/3.0 - QUIC 协议

  • 基于 UDP 协议而非 TCP
  • 实现了类�?TCP 的流量控制和可靠传输
  • 集成 TLS 加密
  • 实现多路复用
  • 解决 TCP 队头阻塞问题

:::

参考资�?
::: details

HTTP 常见的状态码

参考答�?
::: details

  • 200 请求成功,请求在服务端被正确处理
  • 204 响应成功,没有数�?- 205 服务器处理成功,浏览器应重置文档视图
  • 206 服务器成功处理了部分get请求
  • 301 资源永久重定�?- 302 资源临时重定�?- 303 让你查看其他地址
  • 304 请求的资源没有修改,服务端不会返回任何资源,协商缓存
  • 400 请求语法错误,服务器看不�?- 401 请求没有携带信息,比�?token 认证失败
  • 403 请求被拒绝、敏感词
  • 404 找不到资�?- 500 服务器内部错误,无法完成请求
  • 501 服务器不支持当前请求所需的功�?- 503 服务器系统维护或者超载,暂时无法处理客户端的请求

:::

HTTP 常见 Header

参考答�?
::: details

请求�?

  • accept: text/html 告诉服务端我期望接收到一个html的文�?- accept-encoding: gzip, deflate, br 告诉服务端以这种方式压缩
  • accept-language: zh-CN 告诉服务端以中文的格式返�?- authorization: 告诉服务端授权信�?- cookie: 告诉服务端客户端存储�?cookie
  • origin: 告诉服务端请求的来源

响应�?

  • content-encoding: br 告诉浏览器压缩方式是br
  • content-type: text/html; charset=utf-8 告诉浏览器以这种方式,编码加�?- cache-control: 告诉浏览器缓存策�?- expires: 告诉浏览器缓存过期时�?- set-cookie: 告诉浏览器设�?cookie
  • access-control-allow-origin: * 告诉浏览器允许跨�?
    :::

URL 包含哪些部分�?

参考答�?
::: details

URL (Uniform Resource Locator) 包含以下部分�?

  1. 协议 (protocol):如 http://https://ftp:// �?

  2. 域名 (domain):如 www.example.com

    • 子域名:www
    • 主域名:example
    • 顶级域名:com
  3. 端口�?(port):如 :80:443(可选,HTTP 默认 80,HTTPS 默认 443�?

  4. 路径 (path):如 /blog/article

  5. 查询参数 (query string):如 ?id=123&name=test

  6. 锚点/片段标识�?(fragment):如 #header

示例:https://www.example.com:80/blog/article?id=123&name=test#header

:::

GET �?POST 请求的区�?

参考答�?
::: details

  • 协议层面:请求行里一定要有请求方法,官方为了统一语义,定义了 GET 表示拿数据,POST 表示上传数据,PUT 表示修改数据,所�?GET,POST 请求这里仅仅是语义上的差别,没有说哪个请求必须做�?- 应用层面:开发者约定俗成的规范,GET 请求的请求体会设空,不是没有请求�?- 浏览器层面:GET 请求会缓存,有历史记�?
    :::

Ajax Fetch Axios 三者有什么区别?

参考答�?
::: details

Ajax、Fetch �?Axios 都是用于发�?HTTP 请求的技术,但有以下区别�?
Ajax (Asynchronous JavaScript and XML)

  • 是一种技术统称,不是具体�?API
  • 最常用的实现是 XMLHttpRequest (XHR)
  • 写法比较繁琐,需要手动处理各种状�?- 回调地狱问题
  • 不支�?Promise

Fetch

  • 浏览器原�?API

  • 基于 Promise

  • 更简洁的写法

  • 不需要额外引�?- 只对网络请求报错,对 400�?00 都当做成功的请求

  • 默认不带 cookie

  • 不支持请求超时控�?- 不支持请求取�?- 不支持请求进度监�?
    Axios

  • 第三方库,需要额外引�?- 基于 Promise

  • 支持浏览器和 Node.js

  • 请求/响应拦截�?- 自动转换 JSON 数据

  • 客户端支持防�?XSRF

  • 支持请求取消

  • 支持请求超时控制

  • 支持请求进度监控

  • 支持并发请求

  • 自动转换请求和响应数�?
    使用建议�?

  • 如果是简单的请求,使�?Fetch 即可

  • 如果需要更多功能,建议使用 Axios

  • 现代项目中已经很少直接使�?XMLHttpRequest

:::

Fetch �?XMLHTTPRequest 有什么区别?

参考答�?
::: details

语法和使�?

  • Fetch 基于 Promise,代码更简洁优�?- XHR 使用回调函数,容易产生回调地�?- Fetch �?API 设计更简单现�?- XHR �?API 设计较老,使用相对复杂

功能特�?

  • Fetch 默认不发�?cookies,需要配�?credentials
  • XHR 默认发�?cookies
  • Fetch 不能监听上传进度
  • XHR 可以监听上传和下载进�?- Fetch 不能直接取消请求(需�?AbortController�?- XHR 可以通过 abort() 直接取消请求

错误处理

  • Fetch 只有网络错误才会 reject,HTTP 错误码不会导�?reject
  • XHR 可以处理所有类型的错误,包�?HTTP 错误�?
    浏览器支�?
  • Fetch 是现代浏览器标准 API
  • XHR 有更好的浏览器兼容性,包括旧版�?
    :::

什么是 Restful API �?

参考答�?
::: details

RESTful API 是一种软件架构风格,用于设计网络应用程序的接口。主要特点:

资源导向

  • 使用 URL 定位资源

  • 每个资源都有唯一�?URL

  • 资源可以有多种表现形式(�?JSON、XML�?
    HTTP 方法对应操作

  • GET:获取资�?- POST:创建资�?- PUT:更新资源(完整更新�?- PATCH:更新资源(部分更新�?- DELETE:删除资�?
    无状�?

  • 服务器不保存客户端状�?- 每个请求包含所需的所有信�?- 有利于横向扩�?
    统一接口

  • 使用标准�?HTTP 方法

  • 使用标准�?HTTP 状态码

  • 返回格式一致(通常�?JSON�?
    :::

什么是 GraphQL �?

参考答�?
::: details

GraphQL 是一种用�?API 的查询语言和运行时,由 Facebook 开发。主要特点:

查询灵活�?

  • 客户端可以精确指定需要哪些数�?- 可以在一个请求中获取多个资源
  • 避免了传�?REST API 的过度获取和获取不足问题

类型系统

  • 强类型的 Schema 定义
  • 自动生成文档
  • 开发时有更好的类型提示

单个端点

  • 只需要一�?API 端点

  • 所有查询都发送到同一个地址

  • 通过查询语句区分不同的操�?
    主要操作类型

  • Query:获取数�?- Mutation:修改数�?- Subscription:实时数据订�?
    优点

  • 减少网络请求

  • 避免版本化问�?- 强类型保�?- 更好的开发体�?
    缺点

  • 学习成本较高

  • 缓存较为复杂

  • 服务端实现复杂度增加

:::

参考答�?
::: details

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据�?
主要特点�?

  • 由服务器生成,浏览器进行存储

  • 每次请求时会自动携带对应域名下的 cookie

  • 可设置过期时�?- 默认情况下随着浏览器关闭而删除(会话 cookie�?
    常用属性:

  • name:cookie 名称

  • value:cookie �?- domain:指�?cookie 所属域�?- path:指�?cookie 所属路�?- expires/max-age:过期时�?- secure:只�?HTTPS 下传�?- httpOnly:禁�?JS 访问

  • sameSite:跨站点请求限制

使用场景�?

  • 会话状态管理(用户登录状态、购物车等)
  • 个性化设置(用户偏好、主题等�?- 浏览器行为跟踪(分析用户行为等)

限制�?

  • 大小限制:通常�?4KB
  • 数量限制:每个域名下�?cookie 数量有限
  • 安全性:明文传输(除非使�?HTTPS�?- 作用域:只能在所属域名下使用

:::

参考答�?
::: details

主要原因是保护用户隐私和安全�?
隐私问题

  • 第三�?Cookie 可以跨站点追踪用户行�?- 广告商可以构建用户画像和浏览历史
  • 用户数据可能被未经授权收集和使用

安全风险

  • 增加 CSRF(跨站请求伪造)攻击风险
  • 可能被用于会话劫�?- 恶意网站可能滥用第三�?Cookie

技术影�?

  • Safari �?Firefox 已默认禁用第三方 Cookie
  • Chrome 计划�?2024 年完全禁用第三方 Cookie
  • 替代方案�? - First-Party Cookie
    • localStorage
    • Privacy Sandbox
    • FLoC (Federated Learning of Cohorts)

:::

如何理解 Session �?

参考答�?
::: details

Session 是服务器端的会话管理机制�?
基本概念

  • 服务器为每个用户创建的临时会话存储空�?- 用于保存用户的会话状�?- 通过 SessionID 来识别不同用�?- SessionID 通常保存�?Cookie �?
    工作流程
  1. 用户首次访问服务器时,服务器创建 Session 并生�?SessionID
  2. 服务器将 SessionID 通过 Cookie 发送给客户�?3. 客户端后续请求会自动携带包含 SessionID �?Cookie
  3. 服务器通过 SessionID 找到对应 Session 并识别用�?
    特点
  • 安全性较高:敏感数据存储在服务器
  • 服务器负载较大:需要存储所有用户的 Session
  • 依赖 Cookie:通常需�?Cookie 来存�?SessionID
  • 集群问题:需要考虑 Session 共享

使用场景

  • 用户登录状态管�?- 购物�?- 权限验证
  • 表单验证

�?Cookie 的区�?

  • 存储位置:Session 在服务器,Cookie 在客户端
  • 安全性:Session 较安全,Cookie 相对不安�?- 存储容量:Session 容量较大,Cookie 通常限制 4KB
  • 性能:Session 消耗服务器资源,Cookie 消耗带宽资�?
    :::

什么是 JWT 描述它的工作过程

参考答�?
::: details

JWT (JSON Web Token) 是一种开放标准,用于在各方之间安全地传输信息�?
组成部分(用 . 分隔的三部分):

  • Header(头部):指定加密算法和令牌类型
  • Payload(负载):包含声明(claims)的实际数据
  • Signature(签名):对前两部分的签名,用于验证消息未被篡改

工作流程�?

  1. 用户登录成功后,服务器创�?JWT

    • 设置 Header �?Payload
    • 使用密钥生成签名
    • 将三部分组合�?token
  2. 服务器将 token 返回给客户端

    • 客户端存储在 localStorage �?cookie �?
  3. 后续请求携带 token

    • 通常放在 Authorization header
    • 格式:Bearer <token>
  4. 服务器验�?token

    • 检查签名是否有�? - 验证是否过期
    • 验证其他声明(claims�?
      特点�?
  • 无状态:服务器不需要存储会话信�?- 可扩展:负载部分可以包含自定义数�?- 跨域友好:可以在不同域名下使�?- 性能好:验证在服务端完成,不需要查询数据库

安全考虑�?

  • 不要�?payload 中存储敏感信�?- 设置合理的过期时�?- 使用 HTTPS 传输
  • 妥善保管签名密钥

:::

参考资�?
::: details

:::

JWT 如何自动更新 token �?

参考答�?
::: details

JWT token 自动更新主要有以下几种方案:

�?token 机制

  • access token:短期令牌,用于接口认证
  • refresh token:长期令牌,用于刷新 access token
  • 优点:安全性高,即�?access token 泄露影响有限
  • 缺点:实现相对复杂,需要额外存�?refresh token

工作流程�?

  1. 用户登录后获�?access token �?refresh token
  2. 使用 access token 访问接口
  3. access token 过期时,使用 refresh token 获取新的 access token
  4. refresh token 过期时,需要重新登�?
    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
    // 前端示例代码
    async function request(url, options) {
    try {
    const res = await fetch(url, {
    ...options,
    headers: {
    Authorization: `Bearer ${getAccessToken()}`,
    },
    })

    if (res.status === 401) {
    // access token 过期,尝试刷�? const newToken = await refreshToken()
    if (newToken) {
    // 使用�?token 重试请求
    return request(url, options)
    } else {
    // refresh token 也过期,跳转登录
    redirectToLogin()
    }
    }

    return res
    } catch (error) {
    console.error(error)
    }
    }

滑动过期机制

  • 每次请求都刷�?token 过期时间

  • 类似于会话超时机�?- 优点:实现简单,用户体验�?- 缺点:安全性相对较�?
    无感刷新机制

  • �?token 即将过期时自动刷�?- 可以通过定时器或请求拦截器实�?- 优点:用户无感知,体验好

  • 缺点:需要处理并发请求的问题

最佳实践:

  • 根据业务安全需求选择合适的方案
  • access token 过期时间不宜过长(如 2 小时�?- refresh token 过期时间可以较长(如 7 天)
  • 重要操作仍需要二次验�?- 考虑 token 注销机制

:::

什么是 SSO 单点登录,描述它的工作过�?

参考答�?
::: details

SSO (Single Sign On) 单点登录是一种身份验证机制,允许用户使用一组凭证访问多个相关但独立的系统�?
基本概念

  • 一次登录,全局通用

  • 多个子系统共享用户会�?- 统一的认证中�?- 提高用户体验和安全�?
    工作流程�?
    用户首次访问系统

  • 用户访问系统 A

  • 系统 A 检查无登录状�?- 重定向到 SSO 认证中心

  • 带上系统 A 的地址作为参数

SSO 认证中心处理

  • 检查用户是否已登录 SSO
  • 未登录则显示登录页面
  • 用户输入账号密码
  • 认证中心验证身份

回到系统 A

  • SSO 生成票据(ticket�?- 重定向回系统 A
  • 带上票据参数
  • 系统 A 验证票据
  • 创建本地会话

访问系统 B

  • 用户访问系统 B
  • 系统 B 检查无登录状�?- 重定向到 SSO 认证中心
  • SSO 发现用户已登�?- 直接生成票据返回
  • 系统 B 验证票据
  • 创建本地会话

实现方式�?

  • 基于 Cookie
  • 基于 Token
  • 基于 SAML
  • 基于 OAuth
  • 基于 CAS

优点�?

  • 提升用户体验
  • 减少密码管理
  • 统一认证流程
  • 提高安全�?
    缺点�?
  • 认证中心单点故障
  • 配置相对复杂
  • 需要额外的安全考虑

:::

参考资�?
::: details

:::

什么是跨域?如何实现跨域通讯�?

参考答�?
::: details

跨域是指浏览器的同源策略限制,当前域名的 JavaScript 代码试图访问其他域名下的资源时会受到限制�?
同源的定义:

  • 协议相同(http/https�?- 域名相同
  • 端口相同

跨域解决方案�?
CORS(跨域资源共享)

  • 服务器设�?Access-Control-Allow-Origin 等响应头

  • 可以配置允许的请求方法、请求头、是否允许携带认证信息等

  • 最常用的跨域解决方�?
    JSONP

  • 利用 <script> 标签不受同源策略限制的特�?- 只支�?GET 请求

  • 需要服务器配合返回 JavaScript 代码

代理服务�?

  • 开发环境:webpack-dev-server、vite 等的 proxy 配置
  • 生产环境:Nginx 反向代理

postMessage

  • HTML5 标准中的 API
  • 用于不同窗口间的跨域通信
  • 可以在父子页面(iframe)或者多窗口间通信

WebSocket

  • 建立�?TCP 之上的协�?- 天然支持跨域

  • 适合需要实时通信的场�?
    document.domain(已废弃�?

  • 仅适用于主域名相同的情�?- 将子域和主域�?document.domain 设为相同的主�?
    最佳实践:

  • 优先使用 CORS,配置得当的情况下最安全

  • 需要兼容旧浏览器时可以考虑 JSONP

  • 开发环境优先使用代理服务器

  • 特殊场景(如页面通信)可以考虑 postMessage

  • 需要实时通信时使�?WebSocket

:::

参考资�?
::: details

:::

HTTP 请求跨域时为何要发�?options 请求

参考答�?
::: details

OPTIONS 请求�?CORS 预检请求(Preflight Request),用于检查实际请求是否可以安全地发送�?
触发条件�?

  • 使用非简单请求方法:�?GET、POST、HEAD 之外的方�?- 使用非简单请求头:除 Accept、Accept-Language、Content-Language、Content-Type 之外的请求头
  • Content-Type 不是以下之一�? - application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

工作流程�?

  1. 浏览器发�?OPTIONS 预检请求,包含:

    • Origin:请求来�? - Access-Control-Request-Method:实际请求使用的方法
    • Access-Control-Request-Headers:实际请求使用的请求�?
  2. 服务器响应预检请求,返回:

    • Access-Control-Allow-Origin:允许的�? - Access-Control-Allow-Methods:允许的方法
    • Access-Control-Allow-Headers:允许的请求�? - Access-Control-Max-Age:预检请求的缓存时�?
  3. 如果预检通过,浏览器才会发送实际请�?
    优化建议�?

  • 尽可能使用简单请求,避免触发预检
  • 合理设置 Access-Control-Max-Age 缓存预检结果
  • 服务端正确配�?CORS 响应�?
    :::

参考资�?
::: details

:::

参考答�?
::: details

OPTIONS 请求通常不会携带 Cookie。它是一个预检请求,用于检查实际请求是否可以安全地发送。浏览器在发�?OPTIONS 请求时,不会自动附带 Cookie �?Authorization 等认证信息,除非明确设置�?credentials 选项�?
如果需要在 OPTIONS 请求中携�?Cookie,可以在请求中设�?credentials: 'include',但通常不推荐这样做,因�?OPTIONS 请求的目的就是检查跨域请求的安全性,而不是进行身份验证�?
:::

简述浏览器的缓存策�?

参考答�?
::: details

浏览器缓存策略主要分为两种:强缓存和协商缓存�?
强缓�?

  • 不需要向服务器发送请求,直接使用本地缓存

  • 通过 HTTP 响应头控制:

    • Cache-Control�? - max-age:缓存有效时间(秒)
      • no-cache:需要和服务器协商验�? - no-store:不使用任何缓存
      • private:仅浏览器可缓存
      • public:中间代�?CDN 等也可缓�? - Expires:过期时间点(已�?Cache-Control 取代�?
        协商缓存
  • 需要向服务器发送请求验证资源是否有�?- 如果有效返回 304,使用本地缓�?- 通过以下响应头实现:

    • Last-Modified/If-Modified-Since:基于文件修改时�? - ETag/If-None-Match:基于文件内容哈希�?
      缓存位置(优先级从高到低):
  1. Service Worker
  2. Memory Cache(内存缓存)
  3. Disk Cache(硬盘缓存)
  4. Push Cache(HTTP/2�?
    最佳实践:
  • HTML:使用协商缓�?- CSS、JS、图片:使用强缓存,文件名带 hash
  • API 请求:根据业务需求设置合适的缓存策略

:::

参考资�?
::: details

:::

什么是图片防盗链,如何实现�?

参考答�?
::: details

图片防盗链是指服务器通过 HTTP 协议中的 Referer 字段来判断请求是否来自合法站点,从而防止其他网站直接引用本站图片资源�?
实现方式�?
服务器端实现

  • 检�?HTTP Referer 字段
  • 判断请求来源是否在白名单�?- 对非法请求返�?403 或替代图�?
    Nginx 配置示例�?
    1
    2
    3
    4
    5
    6
    7
    location ~ .*\.(gif|jpg|jpeg|png|bmp)$ {
    valid_referers none blocked server_names *.example.com;
    if ($invalid_referer) {
    return 403;
    # 或者返回替代图�? # rewrite ^/ /path/to/default.jpg break;
    }
    }

其他防盗链方案:

  • 给图片添加水�?- 使用 Token 验证
  • 使用 CDN 提供的防盗链功能
  • 对图片进行加密处�?
    注意事项�?
  • Referer 可以被伪造,不能作为唯一判断依据
  • 移动�?APP 可能不发�?Referer
  • 部分浏览器可能禁�?Referer
  • 需要考虑用户体验�?SEO 影响

:::

简�?HTTPS 加密过程

参考答�?
::: details

HTTPS 使用 TLS/SSL 协议进行加密,主要包含以下步骤:

客户端发起请�?

  • 发送支持的加密算法列表
  • 发送随机数 Client Random

服务器回�?

  • 选择加密算法
  • 发送数字证书(包含公钥�?- 发送随机数 Server Random

客户端验证证�?

  • 验证证书是否由可�?CA 签发

  • 验证证书域名是否匹配

  • 验证证书是否在有效期�?
    生成会话密钥

  • 客户端生成随机数 Pre-master secret

  • 使用服务器公钥加�?Pre-master secret

  • 客户端和服务器都通过三个随机数生成会话密�? (Client Random + Server Random + Pre-master secret)

开始加密通信

  • 双方使用会话密钥进行对称加密通信
  • 保证通信内容的机密性和完整�?
    特点�?
  • 采用混合加密:非对称加密传输密钥,对称加密传输数�?- 数字证书保证服务器身份可�?- 具有防篡改和不可否认�?
    :::

移动�?H5 如何抓包�?

参考答�?
::: details

移动�?H5 抓包主要有以下几种方法:

Charles/Fiddler

  • 电脑端安装抓包工�?- 手机和电脑连接同一网络
  • 手机设置代理为电�?IP 和端�?- 安装并信任证书(HTTPS 抓包需要)

优点�?

  • 功能强大,可以查看详细请求信�?- 支持请求修改和重�?- 支持断点调试

vConsole

  • 移动端调试面�?- 直接在页面中引入 js 文件
  • 可以查看网络请求、console 日志�?- 适合开发环境使�?
    1
    2
    3
    4
    <script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
    <script>
    var vConsole = new VConsole();
    </script>

Chrome Remote Debug

  • 安卓设备通过 USB 连接电脑
  • 开启开发者模式和 USB 调试
  • Chrome 访问 chrome://inspect
  • 可以使用完整�?Chrome DevTools

Safari Web Inspector

  • iOS 设备通过 USB 连接 Mac
  • 开�?Web 检查器
  • Safari 开发菜单中选择设备
  • 可以使用完整�?Safari 调试工具

注意事项�?

  • HTTPS 抓包需要安装证�?- 部分 App 可能有反抓包机制
  • 生产环境建议移除调试工具
  • 注意数据安全和隐私保�?
    :::

script 标签�?defer �?async 有什么区�?

参考答�?
::: details

script 标签�?defer �?async 属性都是用于控制脚本的加载和执行时机:

普�?script

  • 阻塞 HTML 解析

  • 立即下载并执行脚�?- 按照在文档中的顺序执�?
    defer

  • 异步下载脚本,不阻塞 HTML 解析

  • 等到 HTML 解析完成后,DOMContentLoaded 事件触发前执�?- 多个 defer 脚本按照在文档中的顺序执�?- 适用于需要操�?DOM 的脚�?- 只对外部脚本文件有效

async

  • 异步下载脚本,不阻塞 HTML 解析
  • 下载完成后立即执行,可能�?HTML 解析完成前执�?- 多个 async 脚本的执行顺序不确定,取决于下载完成时间
  • 适用于独立的脚本,如统计和广告代�?- 只对外部脚本文件有效

使用建议�?

  • 需要操�?DOM 或依赖其他脚本的代码使用 defer
  • 独立的、不依赖 DOM 和其他脚本的代码使用 async
  • 如果脚本之间有依赖关系,不要使用 async

示例�?

1
2
3
4
5
6
7
8
<!-- 普通脚�?-->
<script src="script.js"></script>

<!-- defer 脚本 -->
<script defer src="script.js"></script>

<!-- async 脚本 -->
<script async src="script.js"></script>

:::

prefetch �?dns-prefetch 分别是什�?

参考答�?
::: details

prefetch �?dns-prefetch 是两种不同的资源预加载技术:

prefetch

  • 用于预加载将来可能需要的资源
  • 浏览器空闲时才会下载
  • 优先级较低,不影响当前页面加�?- 适用于下一页可能用到的资源
1
2
3
<!-- 预加载资�?-->
<link rel="prefetch" href="/next-page.js" />
<link rel="prefetch" href="/images/large.jpg" />

dns-prefetch

  • 预先解析域名�?DNS 记录
  • 减少 DNS 解析时间
  • 适用于即将请求其他域名的资源
  • 对跨域资源加载特别有�?
    1
    2
    3
    <!-- DNS 预解�?-->
    <link rel="dns-prefetch" href="//example.com" />
    <link rel="dns-prefetch" href="//api.example.com" />

使用建议�?

  • 对确定即将访问的资源使用 prefetch

  • 对跨域资源较多的站点使用 dns-prefetch

  • 不要过度预加载,可能浪费带宽

  • 移动端要谨慎使用,考虑流量消�?
    相关技术:

  • preload:当前页面必需资源的预加载

  • preconnect:预先建立连接(DNS + TCP + TLS�?- prerender:预先渲染整个页�?
    :::

WebSocket �?HTTP 协议有什么区�?

参考答�?
::: details

WebSocket �?HTTP 的主要区别:

连接特�?

  • HTTP 是短连接:每次请求都需要建立新�?TCP 连接(除非使�?keep-alive�?- WebSocket 是持久化的长连接:只需要一次握手,后续可以持续通信

通信方式

  • HTTP 是单向通信:客户端请求,服务器响应

  • WebSocket 是双向通信:客户端和服务器都可以主动发送数�?
    数据格式

  • HTTP 每次请求都要带完整的 HTTP �?- WebSocket 第一次握手完成后,后续数据传输只需要很小的头部

应用场景

  • HTTP 适合一次性的数据交互
  • WebSocket 适合实时性要求高的场景,如:
    • 实时聊天
    • 游戏实时数据
    • 实时协作文档

性能

  • WebSocket 的性能和效率通常优于 HTTP 轮询
  • WebSocket 可以更好地节省服务器资源和带�?
    支持�?
  • HTTP 被所有浏览器支持
  • WebSocket 需要浏览器支持(现代浏览器普遍已支持)

:::

如何上传文件?使�?fetch 或�?axios

参考答�?
::: details

文件上传主要有以下几种方式:

使用 FormData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// HTML
<input type="file" id="file">

// fetch
const file = document.querySelector('#file').files[0]
const formData = new FormData()
formData.append('file', file)

fetch('/upload', {
method: 'POST',
body: formData
})

// axios
const formData = new FormData()
formData.append('file', file)

axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})

使用 Base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 将文件转�?Base64
function fileToBase64(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.readAsDataURL(file)
})
}

// fetch
const base64 = await fileToBase64(file)
fetch('/upload', {
method: 'POST',
body: JSON.stringify({ file: base64 }),
headers: {
'Content-Type': 'application/json',
},
})

// axios
const base64 = await fileToBase64(file)
axios.post('/upload', {
file: base64,
})

多文件上�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// HTML
<input type="file" multiple id="files">

// fetch
const files = document.querySelector('#files').files
const formData = new FormData()
Array.from(files).forEach(file => {
formData.append('files', file)
})

fetch('/upload', {
method: 'POST',
body: formData
})

// axios
const formData = new FormData()
Array.from(files).forEach(file => {
formData.append('files', file)
})

axios.post('/upload', formData)

注意事项�?

  • 设置正确�?Content-Type
  • 考虑文件大小限制
  • 添加上传进度显示
  • 处理上传错误
  • 考虑文件类型限制
  • 添加取消上传功能

:::

如何上传大文件?

参考答�?
::: details

大文件上传主要有以下几种方案�?
切片上传

  • 将大文件分割成小�?- 并发上传多个切片
  • 服务端合并所有切�?- 支持断点续传和进度显�?
    实现步骤�?前端切片
1
2
3
4
5
6
7
8
9
function createFileChunk(file, size = 1 * 1024 * 1024) {
const chunks = []
let cur = 0
while (cur < file.size) {
chunks.push(file.slice(cur, cur + size))
cur += size
}
return chunks
}

上传切片

1
2
3
4
5
6
7
8
9
async function uploadChunks(chunks) {
const requests = chunks.map((chunk, index) => {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('index', index)
return axios.post('/upload', formData)
})
await Promise.all(requests)
}

发送合并请�?

1
2
3
4
await axios.post('/merge', {
filename: file.name,
size: chunks.length,
})

断点续传

  • 记录已上传的切片

  • 重新上传时跳过已上传的部�?- 可以通过 localStorage 存储进度

  • 使用 hash 标识文件和切�?
    秒传

  • 上传前先发送文�?hash

  • 服务端存在相同文件则直接返回

  • 可以使用 spark-md5 计算文件 hash

性能优化

  • 并发控制:限制同时上传的切片�?- 切片大小:根据网络状况动态调�?- 进度显示:计算整体上传进�?- 错误重试:单个切片上传失败后重试

:::

参考资�?
::: details

:::

如何实现图片懒加载?

参考答�?
::: details

IntersectionObserver API

1
2
3
4
5
6
7
8
9
10
11
12
13
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.original
entry.target.removeAttribute('data-original')
io.unobserve(entry.target)
}
})
})
const imgs = document.querySelectorAll('img[data-original]')
imgs.forEach((item) => {
io.observe(item)
})

原生 loading 属�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let viewHeight = window.innerHeight
function lazyLoad() {
let imgs = document.querySelectorAll('img[data-original]')
imgs.forEach((el) => {
let rect = el.getBoundingClientRect()
if (rect.top < viewHeight) {
let image = new Image()
image.src = el.dataset.original
image.onload = function () {
el.src = image.src
}
el.removeAttribute('data-original')
}
})
}
lazyLoad() // 页面初始加载时调用一�?document.addEventListener('scroll', lazyLoad)

:::

参考资�?
::: details

:::

在网络层面可做哪些性能优化�?

参考答�?
::: details

网络性能优化可以从以下几个方面考虑�?
减少请求数量

  • 合并文件(CSS/JS 打包�?- 雪碧图(CSS Sprites�?- 图片懒加�?- 按需加载/异步加载
  • 合理使用缓存

减小资源体积

  • 代码压缩(minify�?- Gzip/Brotli 压缩

  • 图片优化(压缩、webp格式�?- Tree Shaking

  • 代码分割(Code Splitting�?
    CDN 优化

  • 使用 CDN 分发静态资�?- 合理设置 CDN 缓存

  • 选择合适的 CDN 节点

  • 配置 CDN 预热和刷新策�?
    HTTP 优化

  • 使用 HTTP/2 多路复用

  • 开�?Keep-Alive

  • 合理设置缓存策略

  • DNS 预解析(dns-prefetch�?- 预连接(preconnect�?- 预加载(prefetch/preload�?
    资源加载优化

  • 关键资源优先加载

  • 非关键资源延迟加�?- 内联关键 CSS/JS

  • 异步加载非关�?JS(async/defer�?- 优化资源加载顺序

接口优化

  • 接口合并
  • GraphQL 按需查询
  • 数据缓存
  • 避免重复请求
  • 设置合理的超时时�?
    监控和分�?
  • 性能监控
  • 错误监控
  • 用户体验监控
  • 性能数据分析
  • 持续优化

:::

参考资�?
::: details

:::

0%