高级前端面试题总结

16 篇文章 1 订阅
10 篇文章 0 订阅

JS

V8 工作原理
数据存储
  • 栈空间:先进后出的数据结构,调用栈,存储执行上下文,以及存储原始类型的数据。
  • 堆空间:用数组实现的二叉树,存储引用类型。堆空间很大,能存放很多大的数据。存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。

原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。但JavaScript依然是值存储,对于引用类型存储的是堆空间中的首地址。

垃圾回收
  • 回收调用栈内的数据:执行上下文结束且没有被引用时,则会通过向下移动 记录当前执行状态的指针(称为 ESP) 来销毁该函数保存在栈中的执行上下文。
  • 回收堆里的数据:

V8 中会把堆分为新生代和老生代两个区域,

新生代中存放的是生存时间短的对象,

老生代中存放的生存时间久的对象。

垃圾回收重要术语:

  • 大部分对象在内存中存在的时间很短
  • 不死的对象,会活得更久
  • 代际假说
  • 分代收集
副垃圾回收器:

主要负责新生代的垃圾回收。
这个区域不大,但是垃圾回收比较频繁。
新生代的垃圾回收算法是 Scavenge 算法。
主要把新生代空间对半划分为两个区域:对象区域,空闲区域。
当对象区域快被写满时,则会进行一次垃圾清理。

流程如下:

  1. 对对象区域中的垃圾做标记
  2. 把存活的对象复制到空闲区域中
  3. 把这些对象有序地排列起来
  4. 清理完之后,对象区域会与空闲区域互换
主垃圾回收器:

主垃圾回收器主要负责老生区中的垃圾回收。
除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。
因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

流程如下:

  1. 从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,区分活动对象以及垃圾数据
  2. 标记过程和清除过程使用标记 - 清除算法
  3. 碎片过多会导致大对象无法分配到足够的连续内存时,会使用标记 - 整理算法
    一旦执行垃圾回收算法,会导致 全停顿(Stop-The-World)
    但是 V8 有 增量标记算法
    V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成。
浏览器安全
攻击方式
  • xss:将代码注入到网页
    • 持久型 :写入数据库
    • 非持久型 :修改用户代码
  • csrf:跨站请求伪造。攻击者会虚构一个后端请求地址,诱导用户通过某些途径发送请求。
  • 中间人攻击:中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。
    • DNS 欺骗:入侵 DNS 来将用户访问目标改为入侵者指定机器
    • 会话劫持:在一次正常的通信过程中,攻击者作为第三方参与到其中,或者是在数据里加入其他信息,甚至将双方的通信模式暗中改变,即从直接联系变成有攻击者参与的联系。
防御措施
  1. 预防 XSS
  • 使用转义字符过滤 html 代码
const escapeHTML = value => {
  if (!value || !value.length) {
    return value;
  }
  return value
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
};
  • 过滤 SQL 代码
const replaceSql = value => {
  if (!value || !value.length) {
    return value;
  }
  return value.replace(/select|update|delete|exec|count|'|"|=|;|>|<|%/gi, "");
};
  1. 预防 CSRF

    • 验证 HTTP Referer 字段
    • 在请求地址中添加 token 并验证
    • 在 HTTP 头中自定义属性并验证
    • Get 请求不对数据进行修改
    • 接口防跨域处理
    • 不让第三方网站访问用户 cookie
  2. 预防中间人攻击

  • 对于 DNS 欺骗:检查本机的 HOSTS 文件
  • 对于会话劫持:使用交换式网络代替共享式网络,还必须使用静态 ARP、捆绑 MAC+IP 等方法来限制欺骗,以及采用认证方式的连接等。
  1. 内容安全策略(CSP)

内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。

措施如下:

  • HTTP Header 中的 Content-Security-Policy
  • <meta http-equiv="Content-Security-Policy">
手写代码
new 操作符
var New = function(Fn) {
  var obj = {}; // 创建空对象
  var arg = Array.prototype.slice.call(arguments, 1);
  obj.__proto__ = Fn.prototype; // 将obj的原型链__proto__指向构造函数的原型prototype
  obj.__proto__.constructor = Fn; // 在原型链 __proto__上设置构造函数的构造器constructor,为了实例化Fn
  Fn.apply(obj, arg); // 执行Fn,并将构造函数Fn执行obj
  return obj; // 返回结果
};

深拷贝

const getType = data => {
  // 获取数据类型
  const baseType = Object.prototype.toString
    .call(data)
    .replace(/^\[object\s(.+)\]$/g, "$1")
    .toLowerCase();
  const type = data instanceof Element ? "element" : baseType;
  return type;
};
const isPrimitive = data => {
  // 判断是否是基本数据类型
  const primitiveType = "undefined,null,boolean,string,symbol,number,bigint,map,set,weakmap,weakset".split(
    ","
  ); // 其实还有很多类型
  return primitiveType.includes(getType(data));
};
const isObject = data => getType(data) === "object";
const isArray = data => getType(data) === "array";
const deepClone = data => {
  let cache = {}; // 缓存值,防止循环引用
  const baseClone = _data => {
    let res;
    if (isPrimitive(_data)) {
      return data;
    } else if (isObject(_data)) {
      res = { ..._data };
    } else if (isArray(_data)) {
      res = [..._data];
    }
    // 判断是否有复杂类型的数据,有就递归
    Reflect.ownKeys(res).forEach(key => {
      if (res[key] && getType(res[key]) === "object") {
        // 用cache来记录已经被复制过的引用地址。用来解决循环引用的问题
        if (cache[res[key]]) {
          res[key] = cache[res[key]];
        } else {
          cache[res[key]] = res[key];
          res[key] = baseClone(res[key]);
        }
      }
    });
    return res;
  };
  return baseClone(data);
};

手写bind

Function.prototype.bind2 = function(context) {
  if (typeof this !== "function") {
    throw new Error("...");
  }
  var that = this;
  var args1 = Array.prototype.slice.call(arguments, 1);
  var bindFn = function() {
    var args2 = Array.prototype.slice.call(arguments);
    var that2 = this instanceof bindFn ? this : context; // 如果当前函数的this指向的是构造函数中的this 则判定为new 操作。如果this是构造函数bindFn new出来的实例,那么此处的this一定是该实例本身。
    return that.apply(that2, args1.concat(args2));
  };
  var Fn = function() {}; // 连接原型链用Fn
  // 原型赋值
  Fn.prototype = this.prototype; // bindFn的prototype指向和this的prototype一样,指向同一个原型对象
  bindFn.prototype = new Fn();
  return bindFn;
};

手写函数柯里化

const curry = fn => {
  if (typeof fn !== "function") {
    throw Error("No function provided");
  }
  return function curriedFn(...args) {
    if (args.length < fn.length) {
      return function() {
        return curriedFn.apply(null, args.concat([].slice.call(arguments)));
      };
    }
    return fn.apply(null, args);
  };
};

手写 Promise

// 来源于 https://github.com/bailnl/promise/blob/master/src/promise.js
const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;

const isFunction = fn => typeof fn === "function";
const isObject = obj => obj !== null && typeof obj === "object";
const noop = () => {};

const nextTick = fn => setTimeout(fn, 0);

const resolve = (promise, x) => {
  if (promise === x) {
    reject(promise, new TypeError("You cannot resolve a promise with itself"));
  } else if (x && x.constructor === Promise) {
    if (x._stauts === PENDING) {
      const handler = statusHandler => value => statusHandler(promise, value);
      x.then(handler(resolve), handler(reject));
    } else if (x._stauts === FULFILLED) {
      fulfill(promise, x._value);
    } else if (x._stauts === REJECTED) {
      reject(promise, x._value);
    }
  } else if (isFunction(x) || isObject(x)) {
    let isCalled = false;
    try {
      const then = x.then;
      if (isFunction(then)) {
        const handler = statusHandler => value => {
          if (!isCalled) {
            statusHandler(promise, value);
          }
          isCalled = true;
        };
        then.call(x, handler(resolve), handler(reject));
      } else {
        fulfill(promise, x);
      }
    } catch (e) {
      if (!isCalled) {
        reject(promise, e);
      }
    }
  } else {
    fulfill(promise, x);
  }
};

const reject = (promise, reason) => {
  if (promise._stauts !== PENDING) {
    return;
  }
  promise._stauts = REJECTED;
  promise._value = reason;
  invokeCallback(promise);
};

const fulfill = (promise, value) => {
  if (promise._stauts !== PENDING) {
    return;
  }
  promise._stauts = FULFILLED;
  promise._value = value;
  invokeCallback(promise);
};

const invokeCallback = promise => {
  if (promise._stauts === PENDING) {
    return;
  }
  nextTick(() => {
    while (promise._callbacks.length) {
      const {
        onFulfilled = value => value,
        onRejected = reason => {
          throw reason;
        },
        thenPromise
      } = promise._callbacks.shift();
      let value;
      try {
        value = (promise._stauts === FULFILLED ? onFulfilled : onRejected)(
          promise._value
        );
      } catch (e) {
        reject(thenPromise, e);
        continue;
      }
      resolve(thenPromise, value);
    }
  });
};

class Promise {
  static resolve(value) {
    return new Promise((resolve, reject) => resolve(value));
  }
  static reject(reason) {
    return new Promise((resolve, reject) => reject(reason));
  }
  constructor(resolver) {
    if (!(this instanceof Promise)) {
      throw new TypeError(
        `Class constructor Promise cannot be invoked without 'new'`
      );
    }

    if (!isFunction(resolver)) {
      throw new TypeError(`Promise resolver ${resolver} is not a function`);
    }

    this._stauts = PENDING;
    this._value = undefined;
    this._callbacks = [];

    try {
      resolver(value => resolve(this, value), reason => reject(this, reason));
    } catch (e) {
      reject(this, e);
    }
  }

  then(onFulfilled, onRejected) {
    const thenPromise = new this.constructor(noop);
    this._callbacks = this._callbacks.concat([
      {
        onFulfilled: isFunction(onFulfilled) ? onFulfilled : void 0,
        onRejected: isFunction(onRejected) ? onRejected : void 0,
        thenPromise
      }
    ]);
    invokeCallback(this);
    return thenPromise;
  }
  catch(onRejected) {
    return this.then(void 0, onRejected);
  }
}

手写防抖函数

const debounce = (fn = {}, wait = 50, immediate) => {
  let timer;
  return function() {
    if (immediate) {
      fn.apply(this, arguments);
    }
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, wait);
  };
};

手写节流函数

var throttle = (fn = {}, wait = 0) => {
  let prev = new Date();
  return function() {
    const args = arguments;
    const now = new Date();
    if (now - prev > wait) {
      fn.apply(this, args);
      prev = new Date();
    }
  };
};

手写 instanceOf

const instanceOf = (left, right) => {
  let proto = left.__proto__;
  let prototype = right.prototype;
  while (true) {
    if (proto === null) {
      return false;
    } else if (proto === prototype) {
      return true;
    }
    proto = proto.__proto__;
  }
};

算法

递归

递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。 例如:
大雄在房里,用时光电视看着未来的情况。电视画面中的那个时候,他正在房里,用时光电视,看着未来的情况。电视画面中的电视画面的那个时候,他正在房里,用时光电视,看着未来的情况……
简单来说,就是 无限套娃

我们以斐波那契数列(Fibonacci sequence)为例,看看输入结果会为正无穷的值的情况下,各种递归的情况。
数列
首先是普通版

const fib1 = n => {
  if (typeof n !== "number") {
    throw new Error("..");
  }
  if (n < 2) {
    return n;
  }
  return fib1(n - 1) + fib1(n - 2);
};

从上面的代码分析,我们不难发现,在fib1里,JS 会不停创建执行上下文,压入栈内,而且在得出结果前不会销毁,所以数大了之后容易爆栈。
在这里插入图片描述
所以我们可以对其进行优化,就是利用 尾调用 进行优化。

尾调用是指函数的最后一步只返回一个纯函数的调用,而没有别的数据占用引用。代码如下:

const fib2 = (n, a = 0, b = 1) => {
  if (typeof n !== "number") {
    throw new Error("..");
  }
  if (n === 0) {
    return a;
  }
  return fib2(n - 1, b, a + b);
};

不过很遗憾,在 Chrome 83.0.4103.61 里还是会爆。
在这里插入图片描述

然后我们还有备忘录递归法,就是另外申请空间去存储每次递归的值,是个自顶向下的算法。
备忘录
可惜,还是挂了。

不过在一些递归问题上,我们还可以利用动态规划(Dynamic programming,简称 DP)来解决。

动态规划是算法里比较难掌握的一个概念之一,但是基本能用递归来解决的问题,都能用动态规划来解决。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。

跟备忘录递归刚好相反,是自底向上的算法。具体代码如下:

const fib3 = n => {
  if (typeof n !== "number") {
    throw new Error("..");
  }
  if (n < 2) {
    return n;
  }
  let a = 0;
  let b = 1;
  while (n--) {
    [a, b] = [b, a + b];
  }
  return a;
};

效果很好,正确输出了正无穷~

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 在前端开发中,跨域是指在浏览器发起请求时,请求的目标地址与当前页面的域名不一致,这会导致浏览器的同源策略限制,从而无法正常获取数据。为了解决跨域问题,可以采用以下几种方法:引用\[1\]中提到的方法是通过在服务器端设置响应头信息,允许跨域访问。另外,还可以使用JSONP、CORS、代理服务器等方式来实现跨域请求。引用\[2\]中提到的方法是使用vue-loader来处理vue组件中的代码,使其在解析之前不会出现花屏现象。而引用\[3\]中提到的方法是通过在Vue对象中定义全局指令或在组件内定义指令来实现对DOM元素的操作。这些方法都是解决前端开发中常见问题的有效手段。 #### 引用[.reference_title] - *1* [2023高频前端面试题(含答案)](https://blog.csdn.net/weixin_44672169/article/details/116011608)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [2023高频前端面试题总结(附答案)](https://blog.csdn.net/weixin_45102366/article/details/125525247)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值