# JavaScript
- common.js 和 es6 中模块引入的区别?
- 首屏和白屏时间如何计算
- Virtual Dom 的优势在哪里?
- 面向对象
- 原型链
- undefined 和 null 的区别
- 代码错误监控
- map, object 的区别
- 箭头函数和普通函数的区别
- 跨域的解决方案 并且解决前后端分离项目跨域,配置多个域名
- 简单数据类型和复杂数据类型的区别
- web 除了 cookie 和 webStorage 还有啥存储
- Promise.all()实现原理
- decodeURIComponent 解析字符串带 % 报错 Uncaught URIError: URI malformed
- 数组去重,数组里面有重复的函数对象
- class 实现继承如何使用 es5 实现
- 判断数组的多种方式
- 判断对象是否有某个 key
- 数组扁平化方法,除了用递归还有什么方法
- 实现一下「模版字符串」功能
- 手写实现一下 Promise.all (Promise 不用写);
- 观察者模式和发布订阅模式的区别
# common.js 和 es6 中模块引入的区别?
点击查看实现代码
1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 3、CommonJs 是单个值导出,ES6 Module 可以导出多个 4、CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层 5、CommonJs 的 this 是当前模块,ES6 Module 的 this 是 undefined
# 首屏和白屏时间如何计算
点击查看实现代码
- 当页面的元素数小于 x 时, 则认为是白屏. 可以获取页面的 DOM 节点数, 判断 DOM 节点数小于某个阈值 X, 则认为是白屏
- 在判断初始化页面渲染出来的地方通过 Date.now() - performance.timing.navigationStart 去获取白屏时间
# Virtual Dom 的优势在哪里?
点击查看实现代码
其次是 VDOM 和真实 DOM 的区别和优化:
- 虚拟 DOM 不会立马进行排版与重绘操作
- 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多 DOM 节点排版与重绘损耗
- 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部
# 面向对象
点击查看实现代码
封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。 多态:对象的多功能,多方法,一个方法多种表现形式。 Javascript 是一种基于对象(object-based)的语言。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有 class(类)—–es6 以前是这样的。所以 es5 只有使用函数模拟的面向对象。
# 原型链
点击查看实现代码
构造函数有自己的原型 const Cat() = function() {}
Cat.prototype
原型的 constructor = Cat
实例的proto指向构造函数的原型 const cat = new Cat() cat.proto = Cat.prototype
可以通过 Object.getPrototypeOf(cat) === Cat.prototype // true
# undefined 和 null 的区别
点击查看实现代码
undefined 表示一个无的原始值(基础类型), 转为数值为 NaN, null 表示一个无的对象, 转为数值为 0
# 代码错误监控
首先我们可以关注一下 Performance,下面就先讲一下其中的两个 API
# performance timing
点击查看实现代码
具体可以查看 w3.org/TR/navigation-timing、Navigation Timing API。
在 chrome 浏览器控制台输入 Performance.timing,会得到记录了一个浏览器访问各阶段的时间的对象。
进行错误收集的时候,可以对比这些时间,看错误发生在什么阶段
- DNS 查询耗时 :domainLookupEnd - domainLookupStart
- TCP 链接耗时 :connectEnd - connectStart
- request 请求耗时 :responseEnd - responseStart
- 解析 dom 树耗时 : domComplete - domInteractive
- 白屏时间 :responseStart - navigationStart
- domready 时间 :domContentLoadedEventEnd - navigationStart
- onload 时间 :loadEventEnd – navigationStart
# 脚本错误信息收集
点击查看实现代码
- window.onerror
window.onerror 可以捕捉运行时错误,可以拿到出错的信息,堆栈,出错的文件、行号、列号
- promise 的错误处理
promise 除了使用 catch 方法来捕获错误, 还可以使用 window 的 unhandledrejection 时间来捕获异常 所以假如说你有用 catch 去捕获错误, 那么不会触发 unhandledrejection, 只有在不用 catch 去捕获错的情况下会触发 unhandledrejection
- try catch
无法捕捉到语法错误, 只能捕捉运行时错误 对回调 setTimeout promise 无能为力
# 上报错误的方式
点击查看实现代码
- 后端提供接口, ajax 提交
- 创建一个图片, url 参数带上错误信息
function report(error) {
var reportUrl = "http://xxxx/report";
new Image().src = reportUrl + "error=" + error;
}
2
3
4
优点就是不需要解决跨域问题, 防止重复请求, 缺点就是上传大小有限制, 可携带数据有限
# map, object 的区别
- map 的 key 值可以是任何类型, object 必须是字符串
- map 是按插入的顺序排序的, object 是无序的
- map 和 object 获取值的方式不同
- map, object 对数据的操作不一样 增删改查
# 箭头函数和普通函数的区别
点击查看实现代码
- 箭头函数语法上比普通函数更加简洁(ES6 中每一种函数都可以使用形参赋默认值和剩余运算符)
- 箭头函数没有自己的 THIS,它里面的 THIS 是继承函数所处上下文中的 THIS(使用 CALL/APPY 等任何方式都无法改变 THIS 的指向) 3.箭头函数中没有 ARGUMENTS(类数组),智能基于。。。ARG 获取传递的参数集合(数组)
- 箭头函数不能被 NEW 执行(因为:箭头函数没有 THIS 也没有 prototype)
# 跨域的解决方案 并且解决前后端分离项目跨域,配置多个域名
点击查看实现代码
体量大的情况跨域的解决方案, 不要让运维通过 nginx 改 通过 java 配置
项目前后端分离以后需要配置跨域,且需要允许浏览器多个域名跨域。我们知道 Access-Control-Allow-Origin 里面是只可以写一个域名的,但是我们可以通过配置一个可被允许的 origins 数组,然后判断前端请求中的 origin 是否在这个数组中来解决这个问题~
# 简单数据类型和复杂数据类型的区别
简单数据类型存储在栈中 引用数据类型值存储在堆中, 地址存储在栈中
值类型之间传递的值 引用类型之间的传递, 传递的是地址
# web 除了 cookie 和 webStorage 还有啥存储
web SQL 和 indexedDB 两种存储方式
# Promise.all()实现原理
点击查看实现代码
- promise.all()的入参是一个数组,可以传基本类型,也可以传 promise 对象;
- 返回结果是一个 Promise 对象;
- 入参数组中每一个都返回成功,此函数才返回成功;
- 只要有一个执行失败,则返回失败;
// 封装Promise.all方法
// 判断是否为promise对象,或者使用 obj instanceof Promise方法判断
function isPromise(obj) {
return (
!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function"
);
}
Promise.all = function(values) {
return new Promise((resolve, reject) => {
let result = [];
let counter = 0;
function processData(key, value) {
result[key] = value;
// values中每个promise对象返回成功,计数器加1;
// 直到全部promise都返回成功,与values长度一致,
// 则认定都为成功,此时返回全部的promise回调结果;
if (++counter === values.length) {
resolve(result);
}
}
// 遍历values,先判断是否当前项为promise对象;
// 如果是,则执行回调函数;否,则直接返回该值;
for (let i = 0; i < values.length; i++) {
if (isPromise(values[i])) {
values[i]
.then((data) => {
processData(i, data);
})
.catch((err) => {
reject(err);
return;
}); // values[i]如果成功则返回回调数据,失败则reject
} else {
// 如果不是promise对象,则直接返回;
processData(i, values[i]);
}
}
});
};
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
# decodeURIComponent 解析字符串带 % 报错 Uncaught URIError: URI malformed
点击查看实现代码
浏览器在对 % 执行 **decodeURI、decodeURIComponent、encodeURI、encodeURIComponent **的时候会报错。因为 % 在浏览器属于不安全字符
# 解决方案
- 首先要对 param 中的不安全字符进行转译,转译完毕再进行编码和解码 ps(对于浏览器默认编码了的,如 haorooms%E5%8D%9A%E5%AE%A2%E5%A5%BD%E8%AF%84%E7%8E%8790%,不能再进行字符替换了。) // 对查询关键字中的特殊字符进行编码
function encodeSearchKey(key) {
const encodeArr = [
{
code: "%",
encode: "%25",
},
{
code: "?",
encode: "%3F",
},
{
code: "#",
encode: "%23",
},
{
code: "&",
encode: "%26",
},
{
code: "=",
encode: "%3D",
},
];
return key.replace(/[%?#&=]/g, ($, index, str) => {
for (const k of encodeArr) {
if (k.code === $) {
return k.encode;
}
}
});
}
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
- 对于已经被浏览器编译了的,可以采用如下方式,避免报错
方法 1:
function decodeURIComponentSafe(uri, mod) {
var out = new String(),
arr,
i = 0,
l,
x;
typeof mod === "undefined" ? (mod = 0) : 0;
// 分割 不保存匹配的值 也就是说不pieing %d0 %d1
arr = uri.split(/(%(?:d0|d1)%.{2})/);
for (l = arr.length; i < l; i++) {
try {
x = decodeURIComponent(arr[i]);
} catch (e) {
// 捕获不以数字结尾的 %
x = mod ? arr[i].replace(/%(?!\d+)/g, "%25") : arr[i];
}
out += x;
}
return out;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方法 2:
function decodeURIComponentSafely(uri) {
try {
return decodeURIComponent(uri);
} catch (e) {
console.log("URI Component not decodable: " + uri);
return uri;
}
}
2
3
4
5
6
7
8
浏览器 url 参数中 不建议使用不安全字符!!!
# 数组去重,数组里面有重复的函数对象
点击查看
function filterRepeat(list) {
const set = new Set();
const res = new Set();
for (let i = 0; i < list.length; i++) {
if (!set.has(list[i])) {
set.add(list[i]);
res.add(list[i]);
}
}
return Array.from(res);
}
2
3
4
5
6
7
8
9
10
11
12
# class 实现继承如何使用 es5 实现
点击查看
方法 1:
function inherit(Child, Parent) {
// 继承原型上的属性
Child.prototype = Object.create(Parent.prototype)
// 修复 constructor
Child.prototype.constructor = Child
// 存储超类
Child.super = Parent
// 静态属性继承
if (Object.setPrototypeOf) {
// setPrototypeOf es6
Object.setPrototypeOf(Child, Parent)
} else if (Child.__proto__) {
// __proto__ es6 引入,但是部分浏览器早已支持
Child.__proto__ = Parent
} else {
// 兼容 IE10 等陈旧浏览器
// 将 Parent 上的静态属性和方法拷贝一份到 Child 上,不会覆盖 Child 上的方法
for (var k in Parent) {
if (Parent.hasOwnProperty(k) && !(k in Child)) {
Child[k] = Parent[k]
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
方法 2:
function createObject(P) {
var f = function f() {};
f.prototype = P;
return new f();
}
function inherit(Child, Parent) {
Child.prototype = createObject(Parent.prototype);
Object.defineProperty(Child.prototype, "constructor", {
configurable: true,
enumerable: false,
writable: true,
value: Child,
});
// 下面这种是官方建议继承 但是也不建议使用 因为性能不好
// Object.setPrototypeOf(Child.prototype, Parent.prototype);
}
function Person(name, age, height) {
this.name = name;
this.age = age;
this.height = height;
}
Person.prototype.running = function () {
console.log("running");
};
Person.prototype.eating = function () {
console.log("eating");
};
function Student(name, age, height, sno, score) {
Person.call(this, name, age, height);
this.sno = sno;
this.score = score;
}
inherit(Student, Person);
Student.prototype.call1 = function () {
console.log(this.name);
};
var stu1 = new Student("yjx", 18, 1.88, 111, 100);
// var stu2 = new Student("wyt", 38, 1.88, 111, 100);
console.log(stu1.prototype);
console.log(stu1.running);
stu1.running();
stu1.eating();
stu1.call1();
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
# 判断数组的多种方式
点击
- instanceof 运算符 arr instanceof Array
- constructor 构造函数 arr.constructor === Array
- isArray
- Object.property.toString.call(arr) [object Array]
- Array.property.isPrototypeOf(arr)
- Object.getPrototypeOf(arr) === Array.prototype
# 判断对象是否有某个 key
点击
- in key in obj
- hasOwnProperty obj.hasOwnProperty(key)
- Reflect Reflect.has(obj, key)
- propertyIsEnumerable object.propertyIsEnumerable(key)
# 数组扁平化方法,除了用递归还有什么方法
方法 1: 如果是数字嵌套数组
function flatten(arr) {
return arr.toString().split(',').map((item) => Number(item));
},
2
3
方法 2: while 判断子集是否存在数组 循环
function flatten(arr) {
while (arr.findIndex((item) => Array.isArray(item)) > 0) {
arr = [].concat(...arr);
}
return arr;
}
2
3
4
5
6
方法 3: Array 上的 flat 方法
function flatten(arr) {
return arr.flat(Infinity);
}
2
3
方法 4: 转字符串 正则替换 [ ]
function flatten(list) {
let listStr = JSON.stringify(list).replace(/(\[|\])/g, "");
listStr = "[" + listStr + "]";
return JSON.parse(listStr);
}
2
3
4
5
6
方法 4: 使用堆栈 stack
function flatten(list) {
const stack = [...list];
const res = [];
while (stack.length) {
// 从后往前
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
res.unshift(next);
}
}
return res;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方法 5: 使用 Generator 与递归结合
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}
console.log([...flatten(arr)]);
2
3
4
5
6
7
8
9
10
11
# 实现一下「模版字符串」功能
点击
const html = "{a }开始了{ b}! 2023{ a }";
function getStr2(html, obj) {
return html.replace(/\{(.+?)\}/g, (_, key) => obj[key.trim()]);
}
console.log(getStr2(html, { a: "江浪", b: "泽丽" }));
2
3
4
5
# 手写实现一下 Promise.all (Promise 不用写);
点击查看
function PromiseAll(list) {
return new Promise((resolve, reject) => {
const res = [];
for (let i = 0; i < list.length; i++) {
list[i]
.then((r) => {
res[i] = r;
if (res.filter((v) => v).length === list.length) {
resolve(res);
}
})
.catch((err) => {
reject(err);
});
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 观察者模式和发布订阅模式的区别
点击
可以看出,发布订阅模式相比观察者模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝了订阅者和发布者的依赖关系。即订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。
# 代码实现
订阅发布模式
class PubSub {
constructor() {
this.subscribers = [];
}
subscribe(topic, callback) {
let callbacks = this.subscribers[topic];
if (!callbacks) {
this.subscribers[topic] = [callback];
} else {
callbacks.push(callback);
}
}
publish(topic, ...args) {
let callbacks = this.subscribers[topic] || [];
callbacks.forEach((callback) => callback(...args));
}
}
// 创建事件调度中心,为订阅者和发布者提供调度服务
let pubSub = new PubSub();
// A订阅了SMS事件(A只关注SMS本身,而不关心谁发布这个事件)
pubSub.subscribe("SMS", console.log);
// B订阅了SMS事件
pubSub.subscribe("SMS", console.log);
// C发布了SMS事件(C只关注SMS本身,不关心谁订阅了这个事件)
pubSub.publish("SMS", "I published `SMS` event");
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
观察者模式
class Subject {
constructor() {
this.observers = [];
}
add(observer) {
this.observers.push(observer);
}
notify(...args) {
this.observers.forEach((observer) => observer.update(...args));
}
}
class Observer {
update(...args) {
console.log(...args);
}
}
// 创建观察者ob1
let ob1 = new Observer();
// 创建观察者ob2
let ob2 = new Observer();
// 创建目标sub
let sub = new Subject();
// 目标sub添加观察者ob1 (目标和观察者建立了依赖关系)
sub.add(ob1);
// 目标sub添加观察者ob2
sub.add(ob2);
// 目标sub触发SMS事件(目标主动通知观察者)
sub.notify("I fired `SMS` event");
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
# 说说 typeof 与 instanceof 的区别
点击
# 区别
typeof 与 instanceof 都是判断数据类型的方法,区别如下:
typeof 会返回一个运算数的基本类型,instanceof 返回的是布尔值 instanceof 可以准确判断引用数据类型,但是不能正确判断原始数据类型 typeof 虽然可以判断原始数据类型(null 除外),但是无法判断引用数据类型(function 除外)
# 说说 new 操作符
点击
- 创建一个新的对象 obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的 this 绑定到新建的对象 obj 上
- 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function myNew(Func, ...args) {
// 1.创建一个新对象
const obj = {};
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype;
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args);
// 4.根据返回值判断
return result instanceof Object ? result : obj;
}
2
3
4
5
6
7
8
9
10
# GC 垃圾回收机制 Garbage Collection
现在各大浏览器通常用采用的垃圾回收有两种方法:
- 引用计数
- 标记清除
# 引用计数
点击
最早最简单的垃圾回收机制,就是给一个占用物理空间的对象附加一个引用计数器,当有其它对象引用这个对象时,这个对象的引用计数加一,反之解除时就减一,当该对象引用计数为 0 时就会被回收。
该方式很简单,但会引起内存泄漏:
// 循环引用的问题
function temp() {
var a = {};
var b = {};
a.o = b;
b.o = a;
}
2
3
4
5
6
7
这种情况下每次调用 temp 函数,a 和 b 的引用计数都是 2 ,会使这部分内存永远不会被释放,即内存泄漏。现在已经很少使用了,只有低版本的 IE 使用这种方式。
# 标记清除
V8 中主垃圾回收器就采用标记清除法进行垃圾回收。主要流程如下:
- 标记:遍历调用栈,看老生代区域堆中的对象是否被引用,被引用的对象标记为活动对象,没有被引用的对象(待清理)标记为垃圾数据。
- 垃圾清理:将所有垃圾数据清理掉