浏览器 XSS、CSRF 攻击以及如何规避
- XSS(跨站脚本攻击):将恶意代码注入网页当中执行。
- cookie 中设置 HTTP Only,JS 就无法获取到 cookie 信息。
- 输入验证和过滤字符,对特殊的字符进行转义,避免在html 当作 js 来执行。
- CSRF(跨站网站伪造):诱导进入第三方网站向被攻击的网站发送跨站请求。
- 阻止不明外域的访问,同源策略检测(Referer)
- 双重 Cookie 验证
- CSRF Token
- Samesite Cookie
总结:XSS 是代码注入问题,在输入的是时候内容没过滤导致浏览器将恶意代码执行,而 CSRF 是HTTP 发请求的时候进行。
bind、call、apply 三者之间区别
三个方法都是来更改this的指向
- bind:会创建一个新函数,改变上下文并填充参数;绑定函数并不会立即执行,而是要等待被调用的时候才执行。
- call:会立即执行函数,第二个参数是以参数列表进行传递给函数。
- apply:会立即执行函数,第二个参数是以数组形式的参数集合。
总结: 1、bind 会创建一个新函数并改变其上下文,但是并不立即执行;而call 和 apply 调用时就立即执行了。 2、bind 和 call 是以参数列表来传递;而 apply 是以数组的形式
防抖和节流
- debounce:是指事件触发后,等待一定时间间隔在执行,如果在这个时间段再次触发该函数则重新进行计算。
- throttle:是指在一定的时间间隔内执行一次特定的操作,只能触发一次函数执行。 总结:两个都是优化函数的调用频率,区别只在于函数调用的时机不同;防抖(debounce)适用于需要等待用户操作后才执行的场景,如输入框联想搜索等,而节流(throttle)适用于控制函数执行频率,如页面滚动、resize事件等。
原型/原型链的理解
- 原型(Prototype):每个函数在创建的时候都有一个内置特殊属性 .prototype,这个属性指向一个对象,称之为原型对象。
- 原型链(Prototype Chain):当访问一个对象或方法时,首先是在该对象的自身上去找,如果没有找到他会继续在沿着原型对象一层一层的往上找,直至到达原型链的顶端(Object.prototype) ,这一过程称之为原型链。
JS继承有哪些以及对应的作用
- 原型继承:本质是通过子类的原型指向父类的实例,这样子类就可以通过__proto__来访问到父类的一些方法及属性。
function PrototypeParent() {
this.name = '';
this.age = '';
this.setName = function (name) {
this.name = name;
};
}
PrototypeParent.prototype.setAge = function (age) {
this.age = age;
};
function PrototypeChildren(sex) {
this.sex = sex;
}
PrototypeChildren.prototype = new PrototypeParent();
var prototypeChild1 = new PrototypeChildren('男');
var prototypeChild2 = new PrototypeChildren('女');
prototypeChild1.__proto__.setName('ly');
- 构造继承:子类构造函数中通过call 来调用父类构造函数,但这种只能实现部分继承,如果父类的原型还有方法或属性,子类是拿不到这些。
function ConstructorParent(name, age) {
this.name = name;
this.age = age;
}
ConstructorParent.prototype = function setName(name) {
this.name = name;
};
function ConstructorChildren(sex) {
this.sex = sex;
ConstructorParent.call(this, 'ly', 26);
}
var constructorChild = new ConstructorChildren('男');
console.log(constructorChild);
constructorChild.setName('哈哈');
- 组合继承(原型+构造):通过调用父类的构造,继承子类的属性保留传参的特点,然后将父类的实例指向子类的原型,实现函数复用。
function Parent(name, age) {
this.name = '';
this.age = age;
}
Parent.prototype.setName = function (name) {
this.name = name;
};
function Children(sex) {
this.sex = sex;
Parent.call(this, '', 25);
}
Children.prototype = new Parent();
Children.prototype.constructor = Children; // 修复构造函数的指向
Children.prototype.sayFn = function () {
console.log('say...');
};
var child1 = new Children('男');
child1.__proto__.setName('ly');
console.log(child1.constructor);
- 寄生继承:
- 组合寄生继承(组合继承+寄生继承):
- 类继承:以 ES6 特性所提供的关键字(extends)来实现。
浅拷贝和深拷贝的区别
- 浅拷贝(shallowCopy):只复制对象的第一层的属性,对数组、对象只复制其引用地址而非实际值。可以用 Object.assign、解构、循环遍历等实现浅拷贝。
- 深拷贝(deepCopy):会创建一个完全独立的新对象,递归复制该对象所有层级的属性,包括数组、嵌套对象等,而不仅仅是一个引用地址。
总结:浅拷贝只是复制一层的属性,深层属性共享同一份引用;深拷贝负责所有的层级的属性,确保新旧对象完全分离。
箭头函数和普通函数的区别
- 区别1:箭头函数不存在独立的作用域,所以没有this指向和arguments
- 区别2: 箭头函数不能用于构造函数,没法实例化,也没有自己的 prototype 属性。
- 区别3:箭头函数写法上更简洁。
总结:箭头函数更适合用于简单的表达式和继承上下文的情况,而普通函数拥有自己独立的作用域,操作性更强,适用于编写更复杂逻辑场景。
import 和 require 的区别
- 区别1:import 只能在编译时调用,所以只能在头部声明,而 require 是在运行时调用,可以用于代码当中直接使用。
- 区别2:import ES 模块规范定义加载方式,require CommonJS 规范定义模块加载方式。
- 区别3:require 一般为同步加载,可阻塞执行流,import 支持异步加载
- 区别4:import 可以默认导出、命名导出或整个对象,语法更加灵活,而 require 返回的是该模块的对象或是默认导出。
描述下 Symbol 和 Bigint 的作用
两个都是ES6新增的基本数据类型
- Symbol:是独一无二且不可变的值,创建唯一标识符,用于解决会出现命名冲突的问题。
- Bigint:可定义一个大范围的数值,是为了弥补 JavaScript Number 在处理大整数上的局限性。
Event Loop(事件循环)
- 是指 JavaScript 运行环境中实现异步编程模型的核心机制,确保了 JS 单线程执行环境既能处理同步代码,又能有效的执行异步任务。
- 宏任务:当你执行一个任务,该任务会被推到任务队列中,宏任务的执行一般是同步的,在当前执行栈中所有的同步代码执行完毕后,事件循环会检查宏任务队列并逐个进行执行。
- 微任务:通常是由异步操作完成后的回调函数组成,比如:Promise.then 方法或是MutationObsever 回调,他们会在当前宏任务执行完毕立即执行,在下一个宏任务开始前会被清空。
- 事件循环过程:在每一次事件循环中,宏任务先执行,而微任务会在当前宏任务执行完成后立即执行,确保微任务的优先级高于宏任务。
总结:通过这种事件驱动机制,JS事件循环使得单线程环境下也能高效的处理异步操作,即是在等待的异步响应过程中也能处理其他任务,避免阻塞主线程,从而提升了应用的响应性和执行效率。
说一些常用的 Promise 方法以及作用
- Promise.all():对传入的多个 Promise 同时执行,当所有的 Promise 都 resolve(成功时),返回一个包含所有结果的数组;如果其中有任何一个 reject(失败时)将立即 rejected 的 Promise 并忽略其余的结果。
- Promise.allSettled():与 all 类似,但 allSettled 会等到所有都执行完才返回,包含成功的或失败的结果。
- Promise.race():并发执行,以最快的 promise 返回的结果作为返回值;有 reject 的话也会被立即终止。
- Promise.resolve():成功返回结果
- Promise.reject():失败返回结果
闭包
- 是指一个函数内嵌套另一个函数,内层函数有权对外层函数作用域访问。
- 优点:主要是用于对变量私有化和延长变量生命周期。
- 缺点:使用不当会造成内存泄漏
总结:闭包就是能很方便的对数据进行操作,一些场景有模块化/防抖/节流/柯里化等。
对于JS内存泄漏应当如何来解决
- 通过分析工具排查确认问题所在,然后再结合代码调试。
- 代码层面:需要注意闭包引用、事件绑定清空等
作用域/作用域链
是用来描述变量和函数在代码中的可访问性和查找规则。
- 作用域(Scope):是指变量和函数的可访问范围,其中又细分为全局作用域、局部作用域以及块级作用域,他们都有各自的范围。
- 作用域链:是指由多个作用域组织的链式结构,函数定义时会拥有自己的作用域,这种作用域形成了嵌套结构称之为作用域链。
总结:简单来说作用域描述了变量和函数的可访问性范围,而作用域链描述了在嵌套的作用域结构中变量查找的顺序。
事件委托/冒泡/捕获
DOM事件流存的这三个阶段
- 冒泡:从当前元素的事件源由内到外的传递,直到根节点;可以通过 event.stopPropagation() 来阻止冒泡。
- 捕获:与冒泡相反,事件从根节点到事件源由外到内的传递。
- 委托:利用事件冒泡机制,把子节点元素的事件都绑到父元素上面执行;用这种方式可以代替循环绑定事件的操作,减少内存的消耗,提供性能。
DOM 与 BOM 的区别
- DOM:文档对象模型,是指HTML网页内容;
- BOM:浏览器对象模型,是指浏览器提供操作的一些对象和方法;
JS垃圾回收机制
- 计数引用
- 标记引用
- V8
什么是 Virtual DOM
- Virtual DOM 是一种浏览器中的概念,是使用 JavaScript 对象来表示 DOM 对象的一种技术,它被设计用于解决前端开发中频繁更新 DOM 所带来的性能问题。
有哪些操作数据的方法
- 遍历:for、for in、for of、forEach、map、filter
- 查询:find、findIndex、indexOf、includes、slice
- 新增:splice、unshift
- 删除:pop、shift
- 其他:sort、reverse、reduce、concat、join、some、every
CommonJS 和 ES Module 有什么区别
- 都是处理模块化规范的方式
- Common JS:它是Node采用的规范,主要的特点是同步加载模块,模块输出的是一个值拷贝而不是引用,都可以在服务端和客户端使用。
- ES Module:它是ES6制定的标准规范,可以支持异步加载模块, 模块输出的值是引用而不是拷贝;在加载模块时可以静态分析和Tree Shaking优化等,在浏览当中使用需要进行转换,目前还不能支持所有的浏览器。
白屏时间和首屏时间的区别
- 白屏时间(FCP):指浏览器渲染页面出现第一个字符内容的时间。
- 首屏时间(LCP):指浏览器渲染出页面主要内容的一个时间。
如何计算出首屏时间?
1、通过 performance API 来获取, new Date.now() - performance.timing.navigationStart 来执行,但这种不准确 2、通过 Mutation Observer 来监听 DOM 的变化,把每次变化都记录下来进行统计,幅度最大的一次那就是首屏渲染的时间。
什么是柯里化
- 概念:所谓柯里化就是把一个接收多参数的函数转变成接受一个单一参数,且返回接受剩余参数并能返回结果的新函数的技术。
// 以闭包的形式实现 add(1)(2)(3)
function add(a) {
return function(b) {
return function (c) {
return a + b + c
}
}
}
如何实现图片懒加载
让其在可视范围内才让进行加载图片,是一种优化性能的手段。
- 根据当前元素来计算是否存在适口内,是否满足当前元素距离顶部距离 < 可见高 + 滚动条滚动的距离,然后在进行动态给 src 赋值。
imgs[i].offsetTop < clientHeight + scrollTop
- IntersectionObserver API 可以支持来对当前元素进行监听是否在适口中。
- 通过 getBoundingClientRect 拿到元素几何信息,判断 当前元素top < window.innerHeight
// 可视觉区域的高度
const viewHeight = window.innerHeight;
function lazyLoad(){
// 拿到所有的img元素 img[data-original]只要具有data-original属性的img元素
let imgs = document.querySelectorAll('img[data-original]');
imgs.forEach(el=>{
const rect = el.getBoundingClientRect()
if(rect.top < viewHeight){
//进行赋值操作
}
})
}
Ajax 原理是什么,如何实现
- 是基于 XMLHttpRequest 对象进行来向服务端发送异步请求
// 基于XHR 封装 ajax 请求
function ajax(options = {}) {
const xhr = new XMLHttpRequest();
const { type = 'GET', dataType, data: params = {} } = options;
options.type = type.toUpperCase();
options.dataType = dataType || 'json';
if (options.type === 'GET') {
xhr.open('GET', `${options.url}?${params}}`, true);
xhr.send(null);
}
if (options.type === 'POST') {
xhr.open('POST', options.url, true);
xhr.send(JSON.stringify(params));
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const response =
options.dataType === 'json'
? JSON.stringify(xhr.responseText)
: xhr.responseText;
if (xhr.status >= 200 && xhr.status < 300) {
options.success && options.success(response);
} else {
options.fail && options.fail(xhr.status);
}
}
};
}
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(response) {
console.log(response)
},
fail: function(status) {
console.log(status)
}
})
Promise 是什么,说说实现原理思路
- 是异步编程解决方案,支持对传统(嵌套回调)以链式操作降低编码难度, 提高了代码可阅读性。
- 自身仅有三种状态(pending、fulfilled、rejectd),一旦执行状态就不能更改。
- promise 的实现要遵从 Promise A+ 规范
/**
* 实现步骤如下:
* 1、Promise 类中实现一个构造函数并初始化值,然后指定一个 executor 函数,并在构造函数中立即执行该函数。
* 2、定义 resolve 和 reject 函数,分别是对来对状态发生改变并执行相应的回调函数。
* 3、executor 函数接收两个参数,分别是 resolve 和 reject,这两个参数是函数类型。
* 4、在 executor 函数中,使用 try-catch 语句来捕获可能出现的异常,并使用 resolve 和 reject 函数来改变状态。
* 5、实现 then 方法,该方法接收两个参数,分别是 onFulfilled 和 onRejected,这两个参数是函数类型。
* 6、在 then 方法中,根据当前状态来执行相应的回调函数;如果状态未发生改变就使用数组来存储成功和失败的回调函数,并在状态发生改变时依次执行这些回调函数。
*/
class MyPromise {
constructor(executor) {
this.status = 'pending'; // 初始状态,默认为 pending
this.value = undefined; // 成功状态的值,默认为 undefined
this.reason = undefined; // 失败状态的原因,默认为 undefined
this.onFulfilledCallbacks = []; // 成功回调函数集
this.onRejectedCallbacks = []; // 失败回调函数集
// 成功时执行的函数
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach((callback) => callback(this.value));
}
};
// 失败时执行的函数
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach((callback) => callback(this.reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this.status === 'fulfilled') {
onFulfilled(this.value);
} else if (this.status === 'rejected') {
onRejected(this.reason);
} else if (this.status === 'pending') {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
}
catch(onRejected) {
this.then(null, onRejected);
}
}
new 具体干了什么
- 1)创建一个对象与传入的函数副本原型绑定。
- 2)对传入的函数副本 this 为其指向新对象并立即执行。
- 3)判断执行的结果是否为对象类型,如果是则返回执行结果,否则返回新对象。
function myNew(fn, ...args) {
const obj = Object.create(fn.prototype);
const result = fn.apply(obj, args);
return result instanceof Object && result !== null ? result : obj;
}
函数式编程理解
- 是一种编程范式,用于编写程序的方法论;其特性有纯函数、高阶函数、函数组合等一些方式来组织编码
- 函数式编程旨在提高代码无状态和不变性、可维护和可读性、代码复用
JS 数字精度丢失
回顾一个经典问题:0.1+0.2 === 0.3 // false
- 在 JS 中会把 Number 类型的数据转换为二进制后再进行运算。
- Number 采用的是 IEE754 规范中 64 双精度浮点数编码,对于一个整数可以很轻易转成二进制,但对于一个浮点数来说小数点位置不是固定的,所以二进制计算完再转换十进制中会有结果误差的问题。
- 解决方法:
// 方法一:通过 toPrecision(12) 凑整,然后以 parseFloat 转小数
function strip(num, precision = 12) {
return +parseFloat(num.toPrecision(precision));
}
// 方法二:把小数转整数再计算
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}