1. 数组去重
2. 有关 let const var 区别
- 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象。
- 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升。
- 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值。
3. for…of 和 for…in 的区别
可枚举 VS 可迭代
for...in
用于可枚举(enumerable
)数据,如对象、数组、字符串,得到key
for...of
用于可迭代(next
)数据,如数组、字符串、Map、Set,得到value
注:对于数组来说,for...of
返回的就是数组中的元素值。for...in
返回的是key
,这里就是指数组中每个元素的索引值(遍历顺序有可能不是按照实际数组的内部顺序)。在实际开发中我们遍历对象一般用for...in
更合适,遍历数组一般用for...of
更合适。
4. 事件循环 - 宏任务和微任务
javascript的异步任务:宏任务和微任务
宏任务有Event Table、 Event Queue,微任务有Event Queue
- 宏任务:包括整体代码script,setTimeout,setinterval,I/O, UI 交互事件,setlmmediate(Node.js 环境)。
- 微任务:Promise,MutaionObserver,process.nextTick(Node.js 环境)
注:new Promise中的代码会立即执行,then函数分发到微任务队列,process.nextTick分发到微任务队列Event Queue。settimeout 这种异步操作的回调,只有主线程中没有执行任何同步代码的前提下,才会执行异步回调,而 settimeout(fun,0)表示立刻执行,也就是用来改变任务的执行顺序,要求浏览器尽可能快的进行回调
JS是单线程,防止代码阻塞,我们把代码(任务)分为:同步和异步(同步代码给js引擎执行,异步代码交给宿主环境)。 执行过程:任务进入执行栈->同步任务还是异步任务?->同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table 会将这个函数移入Event Queue ->主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。反复循环查看执行,这个过程就是常说的事件循环(Event Loop)。
练习测试网址:https://www.jsv9000.app
5. 跨域
同源指的是两个 URL 的协议、域名、端口一致,反之,则是跨域。
出现跨域的根本原因:浏览器的同源策略不允许非同源的 URL 之间进行资源的交互。
浏览器对跨域请求的拦截:
如何实现跨域数据请求:
现如今,实现跨域数据请求,最主要的两种解决方案,分别是 JSONP 和 CORS。
- JSONP:出现的早,兼容性好(兼容低版本E)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。缺点是只支持 GET 请求,不支持 POST 请求。
- CORS:出现的较晚,它是 W3C 标准,属于跨域 Ajax 请求的根本解决方案。支持GET 和 POST 请求。缺点是不兼容某些低版本的浏览器。
6. V8引擎
官方对V8引擎的定义:
- V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。
- 它实现 ECMAScript 和 WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行。
- V8可以独立运行,也可以嵌入到任何C++应用程序中。
V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:
Parse 模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码
- 如果函数没有被调用,那么是不会被转换成AST的;
- Parse的V8官方文档:https://v8.dev/blog/scanner
Ignition 是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
TurboFan 是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
V8执行的细节
那么我们的JavaScript源码是如何被解析(Parse过程)的呢? - Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换;
- Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens;
- 接下来tokens会被转换成AST树,经过Parser和PreParser:
- Parser就是直接将tokens转成AST树架构;
- PreParser称之为预解析,为什么需要预解析呢?
- 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率;
- 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析 是在函数被调用时才会进行;
- 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;
- 生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程;
7. 如何一次渲染10W条数据?
/**
* @description: 初级方案 使用 分页 + setTimeout
* @return {*}
*/
const renderList = async () => {
console.time('list渲染时长');
const list = new Array();
for (let i = 0; i < 10 * 10000; i++) {
list.push({ text: '测试文本' + i });
}
const total = list.length;
const page = 0;
const limit = 200;
const totalPage = Math.ceil(total / limit);
const render = (page) => {
if (page >= totalPage) return;
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i];
const div = document.createElement('div');
div.innerHTML = `<div>${item.text}</div>`;
document.getElementById('container')?.appendChild(div);
}
}, 0);
render(page + 1);
};
render(page);
console.timeEnd('list渲染时长');
};
renderList();
/**
* @description: 优化方案==>使用 分页 + requestAnimationFrame
* 请求动画帧,它是一个浏览器的宏任务
* 代替 setTimeout **多次拼接变成一次重排 以减少浏览器重排**
* @return {*}
*/
const renderList = async () => {
console.time('list渲染时长');
const list = new Array();
for (let i = 0; i < 10 * 10000; i++) {
list.push({ text: '测试文本' + i });
}
const total = list.length;
const page = 0;
const limit = 200;
const totalPage = Math.ceil(total / limit);
const render = (page) => {
if (page >= totalPage) return;
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i];
const div = document.createElement('div');
div.innerHTML = `<div>${item.text}</div>`;
document.getElementById('container')?.appendChild(div);
}
});
render(page + 1);
};
render(page);
console.timeEnd('list渲染时长');
};
renderList();
/**
* @description: 终极方案==> createDocumentFragment 文档碎片(vue底层也有使用)
* dom节点 非主dom树节点
* @return {*}
*/
const renderList = async () => {
console.time('list渲染时长');
const list = new Array();
for (let i = 0; i < 10 * 10000; i++) {
list.push({ text: '测试文本' + i });
}
const total = list.length;
const page = 0;
const limit = 200;
const totalPage = Math.ceil(total / limit);
const render = (page) => {
if (page >= totalPage) return;
// 创建一个文档碎片
const fragment = document.createDocumentFragment();
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i];
const div = document.createElement('div');
div.innerHTML = `<div>${item.text}</div>`;
fragment.appendChild(div);
}
// 一次性 appendChild
document.getElementById('container')?.appendChild(fragment);
});
render(page + 1);
};
render(page);
console.timeEnd('list渲染时长');
};
renderList();
8. JS中深拷贝和浅拷贝区别?
浅拷贝 是说只拷贝了对象的引用,这是对指针的拷贝,拷贝后两个指针指向的是同一份内存同一份数据。这意味着当原对象发生变化的时候,拷贝对象也会跟着发生变化。
深拷贝 不但对指针进行了拷贝,而且还对指针指向的内容也进行了拷贝,也就是另外申请了一块空间内存,内容和原对象一致,但是,是两份独立的数据。更改原对象,拷贝对象是不会发生变化的。
浅拷贝的五种实现方式
Object.assign
是 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
注意点:- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性。
- 扩展运算符 …
Array.prototype.concat
Array.prototype.slice
- 使用第三方库 比如lodash就提供了clone方法供用户进行浅拷贝
手写浅拷贝
const clone = (target) => {
// 如果是引用类型
if (typeof target === "object" && target !== null) {
// 判断是数据还是对象,为其初始化一个数据
const cloneTarget = Array.isArray(target) ? [] : {};
// for in 可以遍历数组 对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
// 基础类型 直接返回
return target;
}
};
JSON.parse(JSON.stringify ())
实现深拷贝时有什么缺点?
- obj 里面有
new Date()
,深拷贝后,时间会变成字符串的形式。而不是时间对象; - obj 里有
RegExp
、Error
对象,则序列化的结果会变成空对象{}
; - obj 里有
function
,undefined
,则序列化的结果会把function
或undefined
丢失; - obj 里有
NaN
、Infinity
和-Infinity
,则序列化的结果会变成null
; JSON.stringify()
只能序列化对象的可枚举的自有属性,如果 obj 中的对象是由构造函数生成的实例对象, 深拷贝后,会丢弃对象的constructor
;
手写深拷贝
/**
* @description: 手写深拷贝
* @param {*} source 拷贝数据源
* @return {*}
*/
const deepClone = (source) => {
const targetObj = source.constructor === Array ? [] : {};
for (let keys in source) {
if (source.hasOwnProperty(keys)) {
// 引用数据类型
if (source[keys] && typeof source[keys] == 'object') {
targetObj[keys] = deepClone(source[keys]); // 递归调用
} else {
// 基本数据类型 直接赋值
targetObj[keys] = source[keys];
}
}
}
};
9. 判断数据类型方法?
typeof
主要用来判断基本数据类型(7种):字符串(String
)、数字(Number
)、布尔(Boolean
)、空(Null
)、未定义(Undefined
)、BigInt
、Symbol
。instanceof
运算符返回一个布尔值,表示对象是否为某个构造函数的实例instanceof
是检测整个原型链,所以一个对象可能对多个构造函数返回true
- 但是有一种情况,左边为
null
,或者左边对象的原型链上只有null
对象时,这时,instanceof
会判断失真 instanceof
还有一种用法用于判断值的类型,但只能用于对象,不能用于基本数据类型的值(基本数据类型则返回false
)。- 特殊情况 :
instanceof
只有同一个全局window
才会返回true
,如出现嵌套页面的多个window
则不能正确判断。
var obj = Object.create(null);
typeof obj // "object"
obj instanceof Object // false
var x = [1, 2, 3];
var y = {};
console.log( x instanceof Array )// true
console.log(y instanceof Object) // true
Object.prototype.toString.call()
最准确的判断数据类型的方法,返回字符串 如[object Array]
,强烈推荐!!
扩展:
BigInt
是ES11
引入的新的基本数据类型。BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值,以任意精度表示整数。使用 BigInt解决了之前Number整数溢出的问题。Symbol
是原始值,可用于设置创建需要特殊的名字:
// Symbol(字符串) 返回一个具有唯一的标识符,标识符是原始值
const sym1 = Symbol('hello')
const sym2 = Symbol('hello')
console.log(sym1===sym2);//false
// const sym3 = new Symbol('hello') //用new调用Symbol会报错
const sym4 = Symbol()
const sym5 = Symbol()
const obj = {
x:1,
y:2,
[sym4]:'hello',
[sym5]:'你好'
}
console.log(obj[sym4]); //hello
console.log(obj[sym5]); //你好
10. 从哪些点做性能优化?
加载层面
- 减少http请求(精灵图,文件的合并)
- 减小文件大小(资源压缩,图片压缩,代码压缩)
- CDN(第三方库,大文件,大图)
- SSR服务端渲染,预渲染
- 懒加载
- 分包(小程序)
渲染层面
- 减少dom操作
- 避免回流
- 文档碎片
性能分析
- 页面加载性能(加载时间,用户体验)
- 动画与操作性能(是否流畅无卡顿)
- 内存占用(内存占用过大,浏览器崩掉等)
- 电量消耗(游戏方面,可以暂不考虑)
11. 改变this指向的三种方法?
call()
, apply()
, bind()
区别:
bind()
不调用 只改变this
指向call
,apply()
改变this
指向后 并且执行一次调用apply
主要作用于[]
call
主要作用于{}
谁最近调用 this 就指向谁