前端面试题总结-2021
内存管理
https://juejin.cn/post/6844903869525262349
模块化(type = "module")
https://zh.javascript.info/modules-intro
- 始终使用严格模式
- 每个模块具有作用域
- 多次导入的话只会导入一次并共享
this
为undefined
- HTML 文档准备就绪后才会运行(可以用 async 属性解决)
- 用
nomodule
来解决兼容性问题 - 每个文件最多只能有一个默认的导出
- 动态导入使用
import()
,返回一个promise
Promise
https://zh.javascript.info/promise-basics
Promise 链式调用
let p = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});
p.then(res => {
console.log(res); //1
return new Promise((resolve, reject) => {
setTimeout(() => resolve(res * 2), 1000);
});
}).then(res => {
console.log(res); //2
});
then 中返回的结果会传递给下一个 then 中
Promise.all([...promises...])
- 等所有 promise 完成后才会 settled,结果的到所有
promise
resolve
后的数组结果,且数组的顺序和所有 promise 完成的顺序无关。 - 如果有一个 promise 被 rejected,那整个 promise 将会立刻rejected,并带有的就是这个被 rejected 的 promise 的 error,并且其他正在执行的 promise 将会被忽略(但是不会被取消)。
- 等所有 promise 完成后才会 settled,结果的到所有
Promise.allSettled([...promises...])
(ES2020 新增)- 解决了 Promise.all([...promises...])中第二条特征中的问题。Promise.allSettled 会等待所有 promise 都 settled,无论结果如何。
- 返回一个数组,数据类型为
{status:"fulfilled"/"rejected", value:result/error}
Promise.race([...promises...])
- 返回第一个 settled 的 promise
Promise.resolve(value)
等同于let promise = new Promise(resolve => resolve(value));
Promise.reject(reject)
async/await
- async 包裹的函数总是返回一个 promise
面试题
不引用第三个变量的情况下 2 个变量交换值
例如达到一下效果
let a = 1;
let b = 2;
最终
a = 2
b = 1
方法一、异或 a = a ^ b b = a ^ b a = a ^ b
具体推断过程
a = 0010
b = 0001
||||
vvvv
a = 0011 = 3
b = 0001
||||
vvvv
b = 0010 = 2
a = 0011
||||
vvvv
a = 0001 = 1
方法二、ES6 解构
[a, b] = [b, a]
js 基础点
+
运算符,只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。
可选链 ?.
前面的部分是 undefined
或者 null
,它会停止运算并返回,且有短路效应
可以用来读取和删除变量,当时不能用来写入(赋值)
let user = {}; // user 没有 address 属性
alert(user?.address?.street); // undefined(不报错)
let user = null;
alert(user?.address); // undefined
alert(user?.address.street); // undefined
alert(user.address?.street); // 报错
也可用于调用函数
let userAdmin = {
admin() {
alert('I am admin');
}
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没有(没有这样的方法)
也可使用 []
let user1 = {
firstName: 'John'
};
let user2 = null; // 假设,我们不能授权此用户
let key = 'firstName';
alert(user1?.[key]); // John
alert(user2?.[key]); // undefined
alert(user1?.[key]?.something?.not?.existing); // undefined
delete user1?.name; // 如果 user1 存在,则删除 user1.name
不同类型的比较
当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。null/undefined 有特殊的规则
alert('2' > 1); // true,字符串 '2' 会被转化为数字 2
alert('01' == 1); // true,字符串 '01' 会被转化为数字 1
alert(true == 1); // true
alert(false == 0); // true
js 中的特殊规则
// 死记
alert(null == undefined); // true
当使用数学式或其他比较方法 < > <= >=
时:
null/undefined
会被转化为数字:null
被转化为 0,undefined
被转化为 NaN
alert(null > 0); // (1) false 0>0
alert(null == 0); // (2) false
alert(null >= 0); // (3) true 0>=0
Symbol
Symbol([描述]);
“Symbol” 值表示唯一的标识符,即使它们名字相同。
for in
和 Object.keys()
会忽略对象属性键位 Symbol 的类型
let id = Symbol('id');
let user = {
name: 'John',
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age
// 使用 Symbol 任务直接访问
alert('Direct: ' + user[id]); // 123
相反,Object.assign 会同时复制字符串和 symbol 属性:
let id = Symbol('id');
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert(clone[id]); // 123
全局 Symbol
// 从全局注册表中读取
let id = Symbol.for('id'); // 如果该 Symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for('id');
// 相同的 Symbol
alert(id === idAgain); // true
// 通过 name 获取 Symbol
let sym = Symbol.for('name');
let sym2 = Symbol.for('id');
// 通过 Symbol 获取 name
alert(Symbol.keyFor(sym)); // name
alert(Symbol.keyFor(sym2)); // id
let globalSymbol = Symbol.for('name');
let localSymbol = Symbol('name');
alert(Symbol.keyFor(globalSymbol)); // name,全局 Symbol
alert(Symbol.keyFor(localSymbol)); // undefined,非全局
alert(localSymbol.description); // name
对象转原始类型
let user = {
name: 'John',
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == 'string' ? `{name: "${this.name}"}` : this.money;
}
};
// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
如果没有 Symbol.toPrimitive 和 valueOf,toString 将处理所有原始转换
原始类型方法
- “对象包装器”对于每种原始类型都是不同的,它们被称为 String、Number、Boolean 和 Symbol。因此,它们提供了不同的方法。
- 在访问其属性时,会创建一个包含字符串字面值的特殊对象,使用完后特殊对象被销毁。
- 所以原始类型可以提供方法,但它们依然是轻量级的。 JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。
- 除 null 和 undefined 以外的原始类型都提供了许多有用的方法。
数字类型
0.1 + 0.2 !== 0.3
由于二进制无法精确存储小数 2
以 2 的整数次幂为分母的小数在二进制数字系统中可以被精确地表示,例如 0.5(1/$2^1$)、0.25(1/$2^2$)
alert((0.1).toFixed(20)); // 0.10000000000000000555
parseInt(str,base) 将字符串 str 解析为在给定的 base 数字系统中的整数,2 ≤ base ≤ 36,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。
isFinite(value) 将其参数转换为数字,如果是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity:
alert(isFinite('15')); // true
alert(isFinite('str')); // false,因为是一个特殊的值:NaN
alert(isFinite(Infinity)); // false,因为是一个特殊的值:InfinityisNaN(value) 将其参数转换为数字,然后测试它是否为 NaN
字符串
函数 | 规则 |
---|---|
slice(start, end) * | 从 start 到 end(不含 end) 允许 |
substring(start, end) | start 与 end 之间(包括 start,但不包括 end) 负值代表 0 |
substr(start, length) | 从 start 开始获取长为 length 的字符串 允许 start 为负数 |
- 字符串比较(不同语言) str.localeCompare(str2) 如果 str 排在 str2 前面,则返回负数。 如果 str 排在 str2 后面,则返回正数。 如果它们在相同位置,则返回 0。
数组
arr.splice(start[, deleteCount, elem1, ..., elemN]) [原数组]
arr.slice([start], [end]) [新数组]
arr.concat(arg1, arg2...)
let arr = [1, 2];
let arrayLike = {
0: 'something',
length: 1
};
alert(arr.concat(arrayLike)); // 1,2,[object Object]let arr = [1, 2];
let arrayLike = {
0: 'something',
1: 'else',
[Symbol.isConcatSpreadable]: true,
length: 2
};
alert(arr.concat(arrayLike)); // 1,2,something,elseArray.isArray
reduce(acc, current, index, source)
结构赋值
// 不需要第二个元素
let [firstName, , title] = [
'Julius',
'Caesar',
'Consul',
'of the Roman Republic'
];
alert(title); // Consul
let user = {};
[user.name, user.surname] = 'Ilya Kantor'.split(' ');
alert(user.name); // Ilya
let guest = 'Jane';
let admin = 'Pete';
// 交换值:让 guest=Pete, admin=Jane
[guest, admin] = [admin, guest];
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
// 默认值
let [name = 'Guest', surname = 'Anonymous'] = ['Julius'];
alert(name); // Julius(来自数组的值)
alert(surname); // Anonymous(默认值被使用了)
let options = {
title: 'Menu'
};
let { width = 100, height = 200, title } = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
let options = {
title: 'Menu'
};
let { width: w = 100, height: h = 200, title } = options;
alert(title); // Menu
alert(w); // 100
alert(h); // 200
Symbol.iterator
为了让一个对象变得可迭代,需要为对象添加一个 Symbol.iterator
方法,这个方法必须返回一个迭代器 —— 一个有 next 方法的对象。
next()
方法返回的结果的格式必须是 {done: Boolean, value: any}
,当 done=true
时,表示迭代结束,否则 value 是下一个值。
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function () {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与此迭代器一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
显式调用迭代器
let str = 'Hello';
// 和 for..of 做相同的事
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // 一个接一个地输出字符
}
for of 应用于可迭代的对象
Iterable (可迭代),是实现了 Symbol.iterator 方法的对象。
Array-like (类数组) 是有索引和 length 属性的对象,所以它们看起来很像数组。
// 下面的是类数组但却不可迭代
let arrayLike = {
// 有索引和 length 属性 => 类数组对象
0: 'Hello',
1: 'World',
length: 2
};
// Error (no Symbol.iterator)
for (let item of arrayLike) {
}Array.from 接受一个可迭代或类数组的值,并从中获取一个“真正的”数组
let arrayLike = {
0: 'Hello',
1: 'World',
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)
WeakMap 和 Map 的不同
关键词:垃圾回收、弱引用、内存泄漏
- WeakMap 和 Map 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, 'ok'); // 正常工作(以对象作为键)
// 不能使用字符串作为键
weakMap.set('test', 'Whoops'); // Error,因为 "test" 不是一个对象
WeakMap 不支持迭代以及 keys(),values() 和 entries() 方法。所以没有办法获取 WeakMap 的所有键或值。
对象作为键被引用是,对象清除后,weakMap 中对应的引用也会清除
let john = { name: 'John' };
let weakMap = new WeakMap();
weakMap.set(john, '...');
john = null; // 覆盖引用
// john 被从内存中删除了!
Map 和 Set
- Map 存储键值对
- Set 存储值且唯一
Set 和 WeakSet
- WeakSet 仅存储对象
手写题
手写 Promise
new
涉及知识点:原型链、proto、解构赋值、arguments
- 创建一个对象
- 从 arguments 中获取构造函数,并通过构造函数获取原型
- 将新建的对象通过
__proto__
链接到原型上 - 绑定 this 并执行构造函数
function create() {
// 创建一个空的对象
let obj = {};
// 获得构造函数,shift删除第一个元素并返回该元素
let Con = [].shift.call(arguments);
// 链接到原型
obj.__proto__ = Con.prototype;
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments);
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj;
}
instance of
涉及知识点:原型链查找
function myInstanceOf(obj, cons) {
if (!obj || typeof obj !== 'object') {
return false;
}
let objPrototype = obj.__proto__;
let consPrototype = cons.prototype;
while (true) {
if (objPrototype === null) {
return false;
}
if (objPrototype === consPrototype) {
return true;
}
// 向上查找原型链
objPrototype = objPrototype.__proto__;
}
}
console.log(myInstanceOf({}, Object)); // true
call 和 apply
涉及知识点:
- 第一个参数不传时,非严格模式下为 window
- 思路:给新对象添加该函数,并在新对象的上下文中执行该函数,执行完后删除即可
Function.prototype.myCall = function (...args) {
const context = args[0] || window;
// this为调用它的函数
console.log(this);
context.excuteFunc = this;
const result = context.excuteFunc(...args.slice(1));
delete context.excuteFunc;
return result;
};
Function.prototype.myApply = function (...args) {
const context = args[0] || window;
// this为调用它的函数
console.log(this);
context.excuteFunc = this;
let result;
if (args[1]) {
result = context.excuteFunc(...args[1]);
} else {
result = context.excuteFunc();
}
delete context.excuteFunc;
return result;
};
const foo = {
name: 'alan'
};
function sayName(...args) {
console.log(this.name, args);
}
sayName.myCall(foo, 'args1', 'args2');
sayName.myApply(foo, ['args1', 'args2']);
bind
涉及知识:
- 函数柯里化
返回一个函数,可调用,可当作构造函数 new
Function.prototype.myBind = function (thisArg, ...restArgs) {
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
const binder = this;
return function F() {
console.log(this);
// 处理new的情况
if (this instanceof F) {
return new binder(...restArgs, ...arguments);
}
// 函数柯里化处理
return binder.apply(thisArg, restArgs.concat(...arguments));
};
};
const a = {
outer: 'outer'
};
function say(name, age) {
this.name = name;
this.age = age;
console.log(this.outer, name, age);
}
const curry = say.myBind(a, 'alan');
const c = new curry(23);
console.log(c);
// outer alan 21
curry(21);
柯里化
function curry(fn) {
return function curr(...args) {
// fn.length是fn的形参个数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...args2) => curr.apply(this, args.concat(args2));
};
}
function add(a, b, c) {
return a + b + c;
}
const curried = curry(add);
console.log(curried(1, 2, 3), curried(1)(3)(3), curried(1, 1)(1));
- compose
类型判断
function isObject(x) {
return Object.prototype.toString.call(x) === '[object Object]';
}
function isArray(x) {
return Object.prototype.toString.call(x) === '[object Array]';
}
function isDate(x) {
return Object.prototype.toString.call(x) === '[object Date]';
}
垃圾回收
- 种类
- 标记清除(变量离开上下文、作用域,会被加上标记)
- 引用计数(记录每个值得引用次数,存在循环引用问题,需手动解除->null)
- 避免创建全局变量,导致变量无法被回收
- 闭包造成的内存泄漏
ES6
var 和 const/let
- var
- 函数作用域
- 非严格模式下,省略
var
声明变量时,变量会变成全局变量 - 变量会自动提升到函数作用域顶端
- 可以重复声明
- 全局作用域中声明会成为全局对象(window)的属性
- const/let
- 块级作用域
- 不能重复声明
- 暂存性死区:在声明之前的执行瞬间
- 全局作用域中声明不会成为全局对象(window)的属性
文件上传
- 文件上传/大文件上传/断点续传(根据文件名字生成 hash(缺陷文件名修改了就失效了),可以根据文件内容(spark-md5)来生成 hash 值)
看题写答案
题目一
https://segmentfault.com/a/1190000008475665
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a.x);
console.log(b.x);
console.log(b.x === a);
undefined {n:2} true
题目二
const arr1 = ['A1', 'A2', 'B1', 'B2'];
const arr2 = ['A', 'B'];
const c = [...arr1, ...arr2].sort(
(a, b) =>
a.charCodeAt(0) - b.charCodeAt(0) ||
a.length - b.length ||
a.charCodeAt(1) - b.charCodeAt(1)
);
// ['A', 'A1', 'A2', 'B', 'B1', 'B2']
sass
嵌套写法
变量
$name: alan;
.#{$name} -> .alan@import 模块
函数 @function
@if @for @each @while
mixin
@mixin button-base($width) {
width: $width;
}
.xxx {
@include button-base(12);
}
ESM 和 CJS
- ESM
- 编译时加载、静态加载
- 值是只读的,但是对象还是可以改写
- CJS
- 运行时加载,同步加载
- 值的拷贝
滚动 Tab
mount
时获取所有item
高度保存到this
的某个变量上locationInfo
- 监听
scroll
事件,根据对比高度高亮相应的item
- 由于屏幕大小改变或其他原因导致的位置改变时,重新计算所有
item
的高度
错误捕获
window.onerror
捕获运行时错误、语法错误- 资源加载失败时,如
img
script
,error
不会向上冒泡,所以window.onerror
无法捕获,可以使用window.addEventListener('error', function(event) {})
来捕获 window.addEventListener("unhandledrejection", event => {});
来捕获Promise
的reject
错误
cookie
- 通过
Set-Cookie
设置响应头告诉浏览器下次请求携带cookie
Secure
属性和HttpOnly
属性可以限制访问cookie
interface 和 type 的不同
type 不能声明合并,interface 可以
interface Window {
title: string;
}
interface Window {
ts: TypeScriptAPI;
}interface 只能用来声明对象的结构,不能重命名原始类型
React
vdom
React.creatElement(type, props, children)
- 由于使用了 js,所以可以实现跨平台
- 内容经过了处理,可以防范 XSS 处理
diff dom
https://juejin.cn/post/6844904112027353096#heading-6
- 转化成虚拟 dom
- 通过深度优先来对比差异(同级对比)
- 每遍历一个节点,就记录一个索引值
- 如果发现差异,就把索引值对应的的变化保存起来
应用差异
- 对真实 dom 进行深度优先遍历
- 在对应的索引节点上应用差异即可
合成事件
https://juejin.cn/post/6844903988794671117
class 和 hooks
hooks
- 设计初衷
- class 逻辑难以复用,class 需要使用 HOC(而 HOC 可能会造成嵌套过多的问题),例子:渲染劫持(根据权限渲染 HOC)
- 复用组件内部的逻辑还可以通过
render props
实现 - class 逻辑分散,比如 subscribe 和 unsubscribe 分散在
componentDidMount
和componentWillUnmount
中 - 生命周期函数与业务逻辑耦合,通过 hook 可以将业务逻辑封装到自定义 hook 中,例子:将用户操作的相关逻辑封装到 hook 中
- class 中 this 问题
- 通过
eslint-plugin-react-hooks
防范错误使用 hooks - 在顶层使用 hook,不要在条件语句中使用
- 解决的问题
- 函数组件没有状态(state)的问题
- 函数更易于单元测试
- 缺点:只能在顶层使用,不能在条件语句中使用
useEffect 和 useLayoutEffect
- 共同点
- 使用方式一致
- 都用于处理副作用
- 不同点
- 使用场景
- 大多数场景使用
useEffect
- 当代码引起页面闪烁是使用
useLayoutEffect
,即有引起 DOM 样式更新的场景使用
- 大多数场景使用
- useEffect 异步调用
- useLayoutEffect DOM 更新后同步调用
- 使用场景
React Router
- 路由
- 前端控制(监听 url 变化,渲染不同组件)
- hash
- pushState
- 后端控制
- 前端控制(监听 url 变化,渲染不同组件)
- history 原理
- pushState
- replaceState
- popstate (当历史条目更改时触发,如 history.back()/history.forward 或者用户点击浏览器前进、回退按钮。pushState/replaceState 不会触发)
- hash 原理
- window.location.hash 设置和获取
- hashchange 监听
- 404 问题
nginx try_files
和webpack historyApiFallback
- 总的流程:改变路由的两种方式,通过
history.push
和直接改变 url- 直接改变 url,会触发
popState
事件,会触发 history 下的setState
方法,产生新的 location 对象,并通过 context 进行传递,匹配目标组件进行渲染。 - 通过
history.push
底层会使用pushState
来改变 url,并调用 history 下的setState
方法
- 直接改变 url,会触发
代码可维护性
- 可分析性/可读性
- 快速定位线上问题
- code review
- lint
- source-map 定位
- 可改变性/可拓展性
- 稳定性
- 避免修改代码造成 bug
- 核心业务代码覆盖率
- 减少人为因素,加强工具干预