面试复习知识点参考链接:segmentfault.com/a/119000002…
执行上下文
- 类型
- 全局执行上下文:默认的上下文,创建全局的window对象,设置this指向该全局对象。一个程序中只有一个全局执行上下文。
- 函数执行上下:函数被调用时,会创建函数执行上下文。可以有多个函数执行上下文。
- Eval函数执行上下文(不常用)。
- 如何创建
- 创建阶段:this绑定;词法环境(存储函数声明和变量绑定let、const);变量环境(存储var变量绑定)。
- 执行阶段:分配完变量,执行代码。
作用域链
- 定义
- 作用域是程序代码中规定变量的区域,规定了如何查找变量,确定当前代码对变量的访问权限。
- 作用域链是由多个执行上下文的变量对象构成的链表。查找变量时先从当前变量对象查找,若没找到,再一层一层往父级中找,直到找到全局对象。
- 如何创建
- 函数创建(确定了静态作用域)、函数激活(确定动态作用域)
闭包
- 定义
- 闭包就是能访问到外部作用域的内部函数,不论外部作用域是否执行结束。
- 形成条件
- 函数嵌套
- 内部函数引用了外部函数的局部变量
- 优缺点
- 优点:避免变量污染;读取函数内部变量;在内存中维护变量;利于代码封装
- 缺点:常驻内存,增大内存使用量,使用不当容易造成内存泄露(解决方案:退出函数前将不用的局部变量全部删除)
- 使用场景:柯里化(将传入一个多入参的函数封装为嵌套的只传一个入参的函数)、模块化
- 定时器传参
- 回调等事件处理、AJAX接口请求等异步任务
- 封装变量
- 闭包何时回收
- 全局变量引用闭包时,闭包会一直保存在内存中,直到页面关闭
- 局部变量引用闭包时,内部函数执行结束后会立即销毁,下次js执行垃圾回收时判断是否使用再决定是否销毁闭包并回收内存
this的指向
- this在全局上下文中指向全局对象,若要判断函数的this指向,需找到函数被调用的位置。
- 直接调用:默认指向全局变量
- call()、apply()、bind():指向被绑定的对象
- 箭头函数:this指向外层,箭头函数没有自己的this
- 作为对象的一个方法:指向调用函数的对象
- 作为构造函数:指向正在构造的新对象
- 作为DOM事件处理函数:指向触发事件的元素,即始事件处理程序坐绑定的DOM节点
- HTML标签内联事件处理函数、jQuery的this:指向所在的DOM元素
- 如何改变this的指向
- 使用call()、apply()、bind()
- 存储this指向到变量中,如 var _this=this
- 使用箭头函数
call、apply、bind
- call:传递的参数需逐个列出来
- 使用:
Function.call(obj, param1, param2)
// 手动实现call()
Function.prototype.call = function (obj) {
obj = obj ? Object(obj) : window;
obj.fn = this;
// 利用拓展运算符直接将arguments转为数组
let args = [...arguments].slice(1);
let result = obj.fn(...args);
delete obj.fn;
return result;
};
- apply:传递的是参数数组
- 使用:
Function.apply(obj, [param1, param2])
// 手动实现apply()
Function.prototype.apply = function (obj, arr) {
obj = obj ? Object(obj) : window;
obj.fn = this;
let result;
if (!arr) {
result = obj.fn();
} else {
result = obj.fn(...arr);
}
delete obj.fn;
return result;
};
- bind:返回新函数、新函数的this无法再次更改;bind不是立即执行,被再次调用时才会执行
- 使用:
Function.bind(obj, param1)
Function.prototype.bind = function (obj) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
let args = Array.prototype.slice.call(arguments, 1);
let fn = this;
//创建中介函数
let fn_ = function () {};
let bound = function () {
let params = Array.prototype.slice.call(arguments);
//通过constructor判断调用方式,为true this指向实例,否则为obj
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
console.log(this);
};
fn_.prototype = fn.prototype;
bound.prototype = new fn_();
return bound;
};
Promise、async/await
- Promise(ES6)
- Promise是包含then方法的对象或函数,它将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。Promise的状态必须为其中一种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态)。且状态只能由pending变为resolved或rejected,变化后不可再改变。
- 使用Promise对象进行链式编程(,保证代码的执行顺序;then方法核心用途是构造下一个promise的result,用来接收处理成功时的响应数据,catch接收失败时的数据,finally用于处理正常或异步后的最后一次处理。最大的问题就是代码冗余,因为.then太多,不利于代码维护。
- 链式Promise是指在当前promise达到fullfilled状态后,会执行其回调函数,回调函数返回的结果被当作value返回给下一个Promise(即then中产生的Promise),以此类推,链式调用的效应就出来了。
- 事件循环中先执行宏任务再执行微任务。
- 类方法:
Promise.resolve()、Promise.reject()、Promise.race()、Promise.all()、Promise.allSettled() - 原型方法:
Promise.prototype.then(resolved, rejected)、Promise.prototype.catch(rejected)、Promise.prototype.finally() - 测试题:juejin.cn/post/684490…
// Promise()
function fn() {
return new Promise((resolve, reject) => {
// 成功时调用 resolve(数据)
// 失败时调用 reject(错误)
});
}
fn().then(success1, fail1).then(success2, fail2).catch(fail);
// Promise.all() - 所有的promise都调用成功后才会调用success
Promise.all([promise1, promise2]).then(success, fail)
// Promise.race() - 只要有其中一个promise调用成功就会调用success
Promise.race([promise1, promise2]).then(success, fail)
- async/await(ES7)
- async/await语法糖。async表示该函数为异步函数,不会阻塞后面函数的执行,async函数返回数据时自动封装为一个Promise对象,处理成功时用then接收,失败时用catch接收。
- 当代码执行到async函数中的await时,代码在此处等待不继续往下执行,直到await拿到Promise对象中resolve数据后,才继续往下执行,这样保证了代码的执行顺序,使异步代码看起来更像同步代码。
- await只能在使用async定义的函数中使用,不能单独使用
- await可以直接拿到Promise中resolve中的数据
- await后面可以跟任何表达式,更多的是跟一个返回Promise对象的表达式
- 如何解决回调地狱
- 使用Promise对象进行链式编程
- 使用async/await语法糖
- 如何实现一个通过Promise/A+规范的Promise
function Promise(fn) {
let state = "pending";
let value = null;
const callbacks = [];
this.then = function (onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
handle({
onFulfilled,
onRejected,
resolve,
reject,
});
});
};
this.catch = function (onError) {
this.then(null, onError);
};
this.finally = function (onDone) {
this.then(onDone, onError);
};
this.resolve = function (value) {
if (value && value instanceof Promise) {
return value;
}
if (
value &&
typeof value === "object" &&
typeof value.then === "function"
) {
const { then } = value;
return new Promise((resolve) => {
then(resolve);
});
}
if (value) {
return new Promise((resolve) => resolve(value));
}
return new Promise((resolve) => resolve());
};
this.reject = function (value) {
return new Promise((resolve, reject) => {
reject(value);
});
};
this.all = function (arr) {
const args = Array.prototype.slice.call(arr);
return new Promise((resolve, reject) => {
if (args.length === 0) return resolve([]);
let remaining = args.length;
function res(i, val) {
try {
if (val && (typeof val === "object" || typeof val === "function")) {
const { then } = val;
if (typeof then === "function") {
then.call(
val,
(val) => {
res(i, val);
},
reject
);
return;
}
}
args[i] = val;
if (remaining === 0) {
resolve(args);
}
} catch (ex) {
reject(ex);
}
}
for (let i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
};
this.race = function (values) {
return new Promise((resolve, reject) => {
for (let i = 0, len = values.length; i++; ) {
values[i].then(resolve, reject);
}
});
};
function handle(callback) {
if (state === "pending") {
callbacks.push(callback);
return;
}
const cb =
state === "fulfilled" ? callback.onFulfilled : callback.onRejected;
const next = state === "fulfilled" ? callback.resolve : callback.reject;
if (!cb) {
next(value);
return;
}
try {
const ret = cb(value);
next(ret);
} catch (e) {
callback.reject(e);
}
}
function resolve(newValue) {
const fn = () => {
if (state !== "pending") return;
if (
newValue &&
(typeof newValue === "object" || typeof newValue === "function")
) {
const { then } = newValue;
if (typeof then === "function") {
// newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
// 相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
then.call(newValue, resolve, reject);
return;
}
}
state = "fulfilled";
value = newValue;
handelCb();
};
setTimeout(fn, 0);
}
function reject(error) {
const fn = () => {
if (state !== "pending") return;
if (error && (typeof error === "object" || typeof error === "function")) {
const { then } = error;
if (typeof then === "function") {
then.call(error, resolve, reject);
return;
}
}
state = "rejected";
value = error;
handelCb();
};
setTimeout(fn, 0);
}
function handelCb() {
while (callbacks.length) {
const fn = callbacks.shift();
handle(fn);
}
}
fn(resolve, reject);
}
深浅拷贝
- 浅拷贝:A变B变
- 复制基本类型的数据或指向某对象的指针,不是复制对象本身,源对象跟目标对象共享同一内存。
- 多层数据
concat()、slice()、Object.assign()、Object.create()- 对象的解构
- 深拷贝:A变B不变
- 跟浅拷贝相反。完全是克隆一个新对象,不共享内存。实现方法就是递归调用浅拷贝。
- 一层数据
concat()、slice()、Object.assign()、Object.create()- 对象的解构
JSON.parse(JSON.stringify())(不能深拷贝循环引用)- jquery的方法
$.extend(deep,target,object1,objectN)($.extend(是否深拷贝,目标对象,即将被合并的对象))
/* deep 为 true 表示深复制,为 false 表示浅复制
* sourceObj 表示源对象
* 执行完函数,返回目标对象
*/
function clone(deep = true, sourceObj = {}) {
let targetObj = Array.isArray(sourceObj) ? [] : {};
let copy;
for (var key in sourceObj) {
copy = sourceObj[key];
if (deep && typeof copy === "object") {
if (copy instanceof Object) {
targetObj[key] = clone(deep, copy);
} else {
targetObj[key] = copy;
}
} else if (deep && typeof copy === "function") {
targetObj[key] = eval(copy.toString());
} else {
targetObj[key] = copy;
}
}
return targetObj;
}
- 如何解决循环引用的问题
- 父级引用:判断一个对象的字段是否引用了这个对象或这个对象的任意父级。
- 同级引用
- 设置一个数组或哈希表用来存储已拷贝过的对象,当检测到对象已存在于哈希表中时,取出值并返回。
- 使用时有哪些注意事项
- 使用
JSON.parse(JSON.stringify())实现深拷贝时,构造函数、function、Undefined等丢失,正则表达式变{},Data变String,Set类型、Map类型变Array。
js运行机制、Event Loop、宏任务、微任务
- js运行机制
- 每次主线程执行栈(同步任务)为空的时候,引擎再执行异步任务,异步任务分为宏任务和微任务,在事件轮询中会优先处理微任务,队列处理完微任务队列里的所有任务,再去处理下一宏任务。
- 事件循环
- Event Loop是让js既是单线程、又不会阻塞,用来协调各种事件、用户交互、脚本执行、UI渲染、网络请求等的一种异步机制。可以有多个task(macrotask)、一个microtask队列。
- Event Loop的运行机制
- 执行栈选择最先进入队的宏任务(一般是script),执行其同步代码直至结束
- 检查是否存在微任务,有的话则执行至微任务队列为空
- 若宿主为浏览器,可能会渲染页面
- 开始下一轮事件轮询,执行宏任务中的异步代码
- Event loop相关结论
- 在一轮Event loop中多次修改统一dom,只有最后一次回进行绘制。
- UI的渲染更新会在Event loop中的tasks和microtasks完成后进行,并非每轮Event loop都会更新渲染,这取决于浏览器觉得是否有必要实时将新状态呈现给用户;若一帧时间内修改了多处dom,浏览器可能将变动攒起来最后只进行一次绘制。
- 若希望每轮Event loop都及时呈现变动,可使用
requestAnimationFrame。
- 常见的宏任务微任务
- 宏任务:由宿主(node、浏览器)发起的,DOM渲染后触发
setTimeout、setInterval、setImmediate(node.js)、requestAnimationFrame、postMessage、script、UI render
- 微任务:由js引擎发起的,DOM渲染前触发
process.nextTick(node.js)、promise.then、async/await、Object.observe、MutationObserve
- 例题
// 思路:第一轮宏任务 -> 第一轮微任务 -> 第二轮宏任务 -> ···
console.log("script start"); // 1
async function async2() {
console.log("async2 end"); // 2
}
async function async1() {
await async2();
console.log("async1 end"); // 5
}
async1();
setTimeout(function() {
console.log("setTimeout"); // 8
}, 0);
// Promise本身是同步任务,.then执行微任务
new Promise(resolve => {
console.log("Promise"); // 3
resolve();
}).then(function() {
console.log("promise1"); // 6
}).then(function() {
console.log("promise2"); // 7
});
console.log("script end"); // 4
- 实现一个事件订阅-发布
// 定义事件中心类
class MyEvent {
handlers = {}; // 存放事件 map,发布者,存放订阅者
$on(type, fn) {
if (!Reflect.has(this.handlers, type)) {
// 如果没有定义过该事件,初始化该订阅者列表
this.handlers[type] = [];
}
this.handlers[type].push(fn); // 存放订阅的消息
}
$emit(type, ...params) {
if (!Reflect.has(this.handlers, type)) {
// 如果没有该事件,抛出错误
throw new Error(`未注册该事件${type}`);
}
this.handlers[type].forEach(fn => {
// 循环事件列表,执行每一个事件,相当于向订阅者发送消息
fn(...params);
});
}
$remove(type, fn) {
if (!Reflect.has(this.handlers, type)) {
throw new Error(`无效事件${type}`);
}
if (!fn) {
// 如果没有传入方法,表示需要将该类型的所有消息取消订阅
return Reflect.deleteProperty(this.handlers, type);
}
const inx = this.handlers[type].findIndex(handler => handler === fn);
if (inx === -1) {
// 如果该事件不在事件列表中,则抛出错误
throw new Error("无效事件");
}
this.handlers[type].splice(inx, 1); // 从事件列表中删除该事件
if (!this.handlers[type].length) {
// 如果该类事件列表中没有事件了,则删除该类事件
return Reflect.deleteProperty(this.handlers, type);
}
}
}
原型、原型链、原型实现继承
可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性;使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true。
浏览器
- js是单线程,但浏览器内核是多线程,故可以将UI渲染、定时器触发、HTTP请求等工作交给专门的线程来处理
- 内核组成:人机交互部分、网络请求部分、js引擎部分(解析并执行js)、渲染引擎部分(渲染html、css)、数据存储部分
- 渲染流程:解析html、css,生成布局树,布局计算,生成图层树,绘制,光栅化、生成位图,合成,显示界面
- 页面优化
- 加载阶段原理
- 减少关键资源个数,减少请求次数
- 减小关键资源大小,提到加载速度
- 传输资源需要的往返时间RTT(Round Trip Time)
- 加载阶段具体优化方法
- 压缩HTML文件,移除不必要的注释
- 合并并压缩CSS、JS等文件,script标签加上async或defer属性
- 使用缓存(第二次请求中若数据有数据则直接读取缓存)
- 避免使用table布局
- 更新阶段原理
- 减少页面渲染过程的重排、重绘
- 更新阶段具体优化方法
- 减少DOM操作,将多次操作合并为一次
- 减少逐项更改样式,最好一次性更改style,或者定义为class一次性更新
- 避免多次读取offset等属性,使用变量做缓存
- 防抖、节流
- 做动画效果时,使用will-change和transform做优化
- 前端框架Vue、React(虚拟DOM和Diff算法等)
- 加载阶段原理
- 浏览器安全
- XSS攻击:跨站脚本攻击
- 攻击方式
- 通过document.cookie获取用户的Cookie信息,发送到恶意服务器,模拟用户登录、转账等操作
- 通过addEventListener来监听用户行为,获取用户信息,发送至恶意服务器
- 生成广告影响用户体验
- 解决方法
- 对输入的脚本进行过滤或转码
- 响应头Set-Cookie加使用权限
- 攻击方式
- CSRF攻击:跨站请求伪造
- 攻击方式
- 通过img的src自动跳转到恶意网站
- 诱导用户点击隐藏链接,指向恶意网站
- 解决方法
- 使用Tokenz验证
- 验证请求来源站点
- SameSite=Strict,完全禁止此Cookie,不能随着跳转链接跨站发送
- 攻击方式
- XSS攻击:跨站脚本攻击
常用方法:数组方法、ES6后的
-
加粗的方法是能够修改原数组的。 | 方法 | 使用 | 说明 | | :------ | ------ | ------ | | concat | arr.concat(arr1, arr2) | 连接多个数组 | | join | arr.join(",") | 数组转字符串时指定分隔符 | | pop | arr.pop() | 删除数组最后一个元素 | | push | arr.push("str") | 添加 | | reverse | arr.reverse() | 颠倒数组的元素 | | shift | arr.shift() | 删除数组的第一个元素 | | unshift | arr.unshift("str") | 在数组开头添加一个或多个元素 | | slice | arr.slice(startIndex, endIndex) | 从数组中选定从开始索引(包括)到结束索引(不包括)的元素。索引的正负值决定了从数组头部还是尾部算 | | sort | arr.sort((a,b) => {return a-b}) | 数组排序,默认升序 | | splice | arr.splice(index, num, any参数) | 从指定索引index删除num个元素;也可插入新元素 | | reduce | arr.reduce((a,b) => a+b) | 数组累加器,求和 | | includes | arr.includes("str", index) | 数组是否包含指定值,index默认为0 | | indexOf | arr.indexOf("str") | 返回某个指定元素的索引 |
-
数组的其它方法
- 遍历:every()、some()、filter()、map()、find()、forEach()、findIndex()
- 判断是否是数组:Array.isArray()
- 浅拷贝新建一个数组:Array.from()
- 创建数组且具有可变数量的参数:Array.of()
- 数组去重:
var arr2 = [...new Set(arr)]
- ES6后的方法
- sort、Array.isArray、Array.from、Array.of、扩展运算符、includes、flat、flatMap、entries、keys、values、fill、find、findIndex、copyWithin
service worker
web worker
require和import的区别
- require/exports - CommonJS
- 原生浏览器不支持
- 运行时动态加载,加载的是一个对象,输出得是一个值得拷贝,所以文件引用的模块之改变,require引入的模块值不会改变
- 先引用后使用
- 在模块顶层、函数、判断语句等代码块中均可引用
- require.content(目录,是否还搜索子目录,匹配的正则表达式)
- import/export - ES6
- 在浏览器中无法直接使用,浏览器引入模块的
<script>元素要添加type="module属性 - 静态编译,输出得是值的引用,所以文件引用的模块值改变,import引入的模块值会改变。可使用import()函数实现动态加载
- 也可在引用前使用模块
- 只能在模块顶层使用,不能在函数、判断语句等代码块中引用,因为import在代码静态解析阶段就会生成,不会去分析代码块里面的import