前端八股文之JS

286 阅读13分钟

浏览器 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;
}