写点什么

来自 2 年前端的面经

作者:loveX001
  • 2022-11-11
    浙江
  • 本文字数:8209 字

    阅读完需:约 27 分钟

实现 JSONP 跨域

JSONP 核心原理script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;


实现


const jsonp = (url, params, callbackName) => {    const generateUrl = () => {        let dataSrc = "";        for(let key in params) {            if(params.hasOwnProperty(key)) {                dataSrc += `${key}=${params[key]}&`            }        }        dataSrc += `callback=${callbackName}`;        return `${url}?${dataSrc}`;    }    return new Promise((resolve, reject) => {        const scriptEle = document.createElement('script');        scriptEle.src = generateUrl();        document.body.appendChild(scriptEle);        window[callbackName] = data => {            resolve(data);            document.removeChild(scriptEle);        }    });}
复制代码

计算属性和 watch 有什么区别?以及它们的运用场景?

// 区别  computed 计算属性:依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算computed的值。  watch 侦听器:更多的是观察的作用,无缓存性,类似与某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作//运用场景  当需要进行数值计算,并且依赖与其它数据时,应该使用computed,因为可以利用computed的缓存属性,避免每次获取值时都要重新计算。  当需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch选项允许执行异步操作(访问一个API),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
复制代码

代码输出结果

function SuperType(){    this.property = true;}
SuperType.prototype.getSuperValue = function(){ return this.property;};
function SubType(){ this.subproperty = false;}
SubType.prototype = new SuperType();SubType.prototype.getSubValue = function (){ return this.subproperty;};
var instance = new SubType();console.log(instance.getSuperValue());
复制代码


输出结果:true


实际上,这段代码就是在实现原型链继承,SubType 继承了 SuperType,本质是重写了 SubType 的原型对象,代之以一个新类型的实例。SubType 的原型被重写了,所以 instance.constructor 指向的是 SuperType。具体如下:

instanceof

作用:判断对象的具体类型。可以区别 arrayobjectnullobject 等。


语法A instanceof B


如何判断的?: 如果 B 函数的显式原型对象在 A 对象的原型链上,返回true,否则返回false


注意:如果检测原始值,则始终返回 false


实现:


function myinstanceof(left, right) {    // 基本数据类型都返回 false,注意 typeof 函数 返回"function"    if((typeof left !== "object" && typeof left !== "function") || left === null) return false;    let leftPro = left.__proto__;  // 取左边的(隐式)原型 __proto__    // left.__proto__ 等价于 Object.getPrototypeOf(left)    while(true) {        // 判断是否到原型链顶端        if(leftPro === null) return false;        // 判断右边的显式原型 prototype 对象是否在左边的原型链上        if(leftPro === right.prototype) return true;        // 原型链查找        leftPro = leftPro.__proto__;    }}
复制代码


参考:前端进阶面试题详细解答

动态规划求解硬币找零问题

题目描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1


示例1:输入: coins = [1, 2, 5], amount = 11输出: 3解释: 11 = 5 + 5 + 1
示例2:输入: coins = [2], amount = 3输出: -1
复制代码


实现代码如下:


const coinChange = function (coins, amount) {  // 用于保存每个目标总额对应的最小硬币个数  const f = [];  // 提前定义已知情况  f[0] = 0;  // 遍历 [1, amount] 这个区间的硬币总额  for (let i = 1; i <= amount; i++) {    // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新    f[i] = Infinity;    // 循环遍历每个可用硬币的面额    for (let j = 0; j < coins.length; j++) {      // 若硬币面额小于目标总额,则问题成立      if (i - coins[j] >= 0) {        // 状态转移方程        f[i] = Math.min(f[i], f[i - coins[j]] + 1);      }    }  }  // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1  if (f[amount] === Infinity) {    return -1;  }  // 若有解,直接返回解的内容  return f[amount];};
复制代码

代码输出结果

var myObject = {    foo: "bar",    func: function() {        var self = this;        console.log(this.foo);          console.log(self.foo);          (function() {            console.log(this.foo);              console.log(self.foo);          }());    }};myObject.func();
复制代码


输出结果:bar bar undefined bar


解析:


  1. 首先 func 是由 myObject 调用的,this 指向 myObject。又因为 var self = this;所以 self 指向 myObject。

  2. 这个立即执行匿名函数表达式是由 window 调用的,this 指向 window 。立即执行匿名函数的作用域处于 myObject.func 的作用域中,在这个作用域找不到 self 变量,沿着作用域链向上查找 self 变量,找到了指向 myObject 对象的 self。

说一下 HTTP 和 HTTPS 协议的区别?

1、HTTPS协议需要CA证书,费用较高;而HTTP协议不需要2、HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;3、使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;4、HTTP协议连接很简单,是无状态的;HTTPS协议是具有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全
复制代码

代码输出结果

function foo(something){    this.a = something}
var obj1 = { foo: foo}
var obj2 = {}
obj1.foo(2); console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);console.log(obj2.a); // 3
var bar = new obj1.foo(4)console.log(obj1.a); // 2console.log(bar.a); // 4
复制代码


输出结果: 2 3 2 4


解析:


  1. 首先执行 obj1.foo(2); 会在 obj 中添加 a 属性,其值为 2。之后执行 obj1.a,a 是右 obj1 调用的,所以 this 指向 obj,打印出 2;

  2. 执行 obj1.foo.call(obj2, 3) 时,会将 foo 的 this 指向 obj2,后面就和上面一样了,所以会打印出 3;

  3. obj1.a 会打印出 2;

  4. 最后就是考察 this 绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出 4。

setTimeout 模拟 setInterval

描述:使用setTimeout模拟实现setInterval的功能。


实现


const mySetInterval(fn, time) {    let timer = null;    const interval = () => {        timer = setTimeout(() => {            fn();  // time 时间之后会执行真正的函数fn            interval();  // 同时再次调用interval本身        }, time)    }    interval();  // 开始执行    // 返回用于关闭定时器的函数    return () => clearTimeout(timer);}
// 测试const cancel = mySetInterval(() => console.log(1), 400);setTimeout(() => { cancel();}, 1000); // 打印两次1
复制代码

代码输出结果

function a(xx){  this.x = xx;  return this};var x = a(5);var y = a(6);
console.log(x.x) // undefinedconsole.log(y.x) // 6
复制代码


输出结果: undefined 6


解析:


  1. 最关键的就是 var x = a(5),函数 a 是在全局作用域调用,所以函数内部的 this 指向 window 对象。所以 this.x = 5 就相当于:window.x = 5。之后 return this,也就是说 var x = a(5) 中的 x 变量的值是 window,这里的 x 将函数内部的 x 的值覆盖了。然后执行 console.log(x.x), 也就是 console.log(window.x),而 window 对象中没有 x 属性,所以会输出 undefined。

  2. 当指向 y.x 时,会给全局变量中的 x 赋值为 6,所以会打印出 6。

代码输出结果

const promise1 = new Promise((resolve, reject) => {  console.log('promise1')  resolve('resolve1')})const promise2 = promise1.then(res => {  console.log(res)})console.log('1', promise1);console.log('2', promise2);
复制代码


输出结果如下:


promise11 Promise{<resolved>: resolve1}2 Promise{<pending>}resolve1
复制代码


需要注意的是,直接打印 promise1,会打印出它的状态值和参数。


代码执行过程如下:


  1. script 是一个宏任务,按照顺序执行这些代码;

  2. 首先进入 Promise,执行该构造函数中的代码,打印promise1

  3. 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来;

  4. 碰到promise1.then这个微任务,将它放入微任务队列;

  5. promise2是一个新的状态为pendingPromise

  6. 执行同步代码 1, 同时打印出promise1的状态是resolved

  7. 执行同步代码 2,同时打印出promise2的状态是pending

  8. 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。

代码输出结果

setTimeout(function () {  console.log(1);}, 100);
new Promise(function (resolve) { console.log(2); resolve(); console.log(3);}).then(function () { console.log(4); new Promise((resove, reject) => { console.log(5); setTimeout(() => { console.log(6); }, 10); })});console.log(7);console.log(8);
复制代码


输出结果为:


23784561
复制代码


代码执行过程如下:


  1. 首先遇到定时器,将其加入到宏任务队列;

  2. 遇到 Promise,首先执行里面的同步代码,打印出 2,遇到 resolve,将其加入到微任务队列,执行后面同步代码,打印出 3;

  3. 继续执行 script 中的代码,打印出 7 和 8,至此第一轮代码执行完成;

  4. 执行微任务队列中的代码,首先打印出 4,如遇到 Promise,执行其中的同步代码,打印出 5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;

  5. 执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为 100ms,第二个定时器的时间为 10ms,所以先执行第二个定时器,打印出 6;

  6. 此时微任务队列为空,继续执行宏任务队列,打印出 1。


做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为 0 哦。

你在工作终于到那些问题,解决方法是什么

经常遇到的问题就是Cannot read property ‘prototype’ of undefined解决办法通过浏览器报错提示代码定位问题,解决问题
Vue项目中遇到视图不更新,方法不执行,埋点不触发等问题一般解决方案查看浏览器报错,查看代码运行到那个阶段未之行结束,阅读源码以及相关文档等然后举出来最近开发的项目中遇到的算是两个比较大的问题。
复制代码

说一下怎么把类数组转换为数组?

//通过call调用数组的slice方法来实现转换Array.prototype.slice.call(arrayLike)
//通过call调用数组的splice方法来实现转换Array.prototype.splice.call(arrayLike,0)
//通过apply调用数组的concat方法来实现转换Array.prototype.concat.apply([],arrayLike)
//通过Array.from方法来实现转换Array.from(arrayLike)
复制代码

节流与防抖

  • 函数防抖 是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

  • 函数节流 是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。


// 函数防抖的实现function debounce(fn, wait) {  var timer = null;
return function() { var context = this, args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; }
// 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); };}
// 函数节流的实现;function throttle(fn, delay) { var preTime = Date.now();
return function() { var context = this, args = arguments, nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } };}
复制代码

对 this 对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。


  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。

  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。

  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。

  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。


这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

Object.is()

描述Object.is 不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别。


  1. NaN=== 中是不相等的,而在 Object.is 中是相等的

  2. +0-0=== 中是相等的,而在 Object.is 中是不相等的


实现:利用 ===


Object.is = function(x, y) {    if(x === y) {        // 当前情况下,只有一种情况是特殊的,即 +0 -0        // 如果 x !== 0,则返回true        // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断        return x !== 0 || 1 / x === 1 / y;    }    // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样    // x和y同时为NaN时,返回true    return x !== x && y !== y;}
复制代码

渐进增强和优雅降级之间的区别

(1)渐进增强(progressive enhancement):主要是针对低版本的浏览器进行页面重构,保证基本的功能情况下,再针对高级浏览器进行效果、交互等方面的改进和追加功能,以达到更好的用户体验。 (2)优雅降级 graceful degradation: 一开始就构建完整的功能,然后再针对低版本的浏览器进行兼容。


两者区别:


  • 优雅降级是从复杂的现状开始的,并试图减少用户体验的供给;而渐进增强是从一个非常基础的,能够起作用的版本开始的,并在此基础上不断扩充,以适应未来环境的需要;

  • 降级(功能衰竭)意味着往回看,而渐进增强则意味着往前看,同时保证其根基处于安全地带。


“优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。 在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。


“渐进增强”观点则认为应关注于内容本身。内容是建立网站的诱因,有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得“渐进增强”成为一种更为合理的设计范例。这也是它立即被 Yahoo 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。

TLS/SSL 的工作原理

TLS/SSL 全称安全传输层协议(Transport Layer Security), 是介于 TCP 和 HTTP 之间的一层安全协议,不影响原有的 TCP 协议和 HTTP 协议,所以使用 HTTPS 基本上不需要对 HTTP 页面进行太多的改造。


TLS/SSL 的功能实现主要依赖三类基本算法:散列函数 hash对称加密非对称加密。这三类算法的作用如下:


  • 基于散列函数验证信息的完整性

  • 对称加密算法采用协商的秘钥对数据加密

  • 非对称加密实现身份认证和秘钥协商

(1)散列函数 hash

常见的散列函数有 MD5、SHA1、SHA256。该函数的特点是单向不可逆,对输入数据非常敏感,输出的长度固定,任何数据的修改都会改变散列函数的结果,可以用于防止信息篡改并验证数据的完整性。


特点: 在信息传输过程中,散列函数不能三都实现信息防篡改,由于传输是明文传输,中间人可以修改信息后重新计算信息的摘要,所以需要对传输的信息和信息摘要进行加密。

(2)对称加密

对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。 这就要用到非对称加密的方法。


常见的对称加密算法有 AES-CBC、DES、3DES、AES-GCM 等。相同的秘钥可以用于信息的加密和解密。掌握秘钥才能获取信息,防止信息窃听,其通讯方式是一对一。


特点: 对称加密的优势就是信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和 N 个客户端通信,需要维持 N 个密码记录且不能修改密码。

(3)非对称加密

非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户, 都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。


常见的非对称加密算法有 RSA、ECC、DH 等。秘钥成对出现,一般称为公钥(公开)和私钥(保密)。公钥加密的信息只有私钥可以解开,私钥加密的信息只能公钥解开,因此掌握公钥的不同客户端之间不能相互解密信息,只能和服务器进行加密通信,服务器可以实现一对多的的通信,客户端也可以用来验证掌握私钥的服务器的身份。


特点: 非对称加密的特点就是信息一对多,服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。


综合上述算法特点,TLS/SSL 的工作方式就是客户端使用非对称加密与服务器进行通信,实现身份的验证并协商对称加密使用的秘钥。对称加密算法采用协商秘钥对信息以及信息摘要进行加密通信,不同节点之间采用的对称秘钥不同,从而保证信息只能通信双方获取。这样就解决了两个方法各自存在的问题。

说说浏览器缓存

缓存可以减少网络 IO 消耗,提高访问速度。浏览器缓存是一种操作简单、效果显著的前端性能优化手段很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”。但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
Memory CacheService Worker CacheHTTP CachePush Cache
缓存它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存 实现强缓存,过去我们一直用 expires。 当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段,现在一般使用Cache-Control 两者同时出现使用Cache-Control 协商缓存,Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:每次请求去判断这个时间戳是否发生变化。 从而去决定你是304读取缓存还是给你返回最新的数据
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
来自2年前端的面经_JavaScript_loveX001_InfoQ写作社区