【前端面试题】我靠它拿到了大厂Offer 三

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 【前端面试题】我靠它拿到了大厂Offer

第50题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组:

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 +

遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

而要取代它的Proxy有以下如下优点;:

  • 可以劫持整个对象,并返回一个新对象
  • 有13种劫持操作
  • Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

参考

第49题:实现 (5).add(3).minus(2) 功能,例: 5 + 3 - 2,结果为 6

Number.MAX_SAFE_DIGITS = Number.MAX_SAFE_INTEGER.toString().length-2
Number.prototype.digits = function(){
  let result = (this.valueOf().toString().split('.')[1] || '').length
  return result > Number.MAX_SAFE_DIGITS ? Number.MAX_SAFE_DIGITS : result
}
Number.prototype.add = function(i=0){
  if (typeof i !== 'number') {
          throw new Error('请输入正确的数字');
      }
  const v = this.valueOf();
  const thisDigits = this.digits();
  const iDigits = i.digits();
  const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
  const result = (v * baseNum + i * baseNum) / baseNum;
  if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
  else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
Number.prototype.minus = function(i=0){
  if (typeof i !== 'number') {
          throw new Error('请输入正确的数字');
      }
  const v = this.valueOf();
  const thisDigits = this.digits();
  const iDigits = i.digits();
  const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
  const result = (v * baseNum - i * baseNum) / baseNum;
  if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
  else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
  • 大数加减:直接通过 Number 原生的安全极值来进行判断,超出则直接取安全极值
  • 超级多位数的小数加减:取JS安全极值位数-2作为最高兼容小数位数

第 48 题:为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片

  1. 避免跨域(img 天然支持跨域)
  2. 利用空白gif或1x1 px的img是互联网广告或网站监测方面常用的手段,简单、安全、相比PNG/JPG体积小,1px 透明图,对网页内容的影响几乎没有影响,这种请求用在很多地方,比如浏览、点击、热点、心跳、ID颁发等等,
  3. 图片请求不占用 Ajax 请求限额
  4. 不会阻塞页面加载,影响用户的体验,只要new Image对象就好了,一般情况下也不需要append到DOM中,通过它的onerror和onload事件来检测发送状态。

示例:

<script type="text/javascript">
 var thisPage = location.href;
 var referringPage = (document.referrer) ? document.referrer : "none";
 var beacon = new Image();
 beacon.src = "http://www.example.com/logger/beacon.gif?page=" + encodeURI(thisPage)
 + "&ref=" + encodeURI(referringPage);
</script>

参考

第 47 题:call 和 apply 的区别是什么,哪个性能更好一些

  1. Function.prototype.apply和Function.prototype.call 的作用是一样的,区别在于传入参数的不同;
  2. 第一个参数都是,指定函数体内this的指向
  3. 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
    例如:
fun.apply(thisArg, [argsArray])
fun.call(thisArg, arg1, arg2, ...)
  1. call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式

参考


第 46 题:双向绑定和 vuex 是否冲突

官方文档解释:https://vuex.vuejs.org/zh/guide/forms.html


第 45 题:输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
  • 解释:我们现在控制台输入并输出,对照来看:

    很显然,从结果上对照,obj.push(1) 访问的是对象obj的push属性,该push属性具有数组的push方法,因此,obj.push(1)和obj.push(2)会向Array中追加并返回修改后的数组。但是,问题来了!怎么追加?往哪个位置追加?
    我们看到打印输出时,obj是一个数组形式输出,但’obj instanceof Array’又不是一个真实的数组,所以这是一个类数组形式,我们给这个类数组起一个名称S_Arr
    细心的你,一定注意到empty x 2,即输出了2个空。所以,我们对此可知,在进行obj.push(1)和obj.push(2)操作时,S_Arr[2]=1,S_Arr[3]=2,可见S_Arr[0]和S_Arr[1]为empty,但这两个下标仍旧占位。现在,可以解释为什么length为4了吧。

第 44 题:HTTPS 握手过程中,客户端如何验证证书的合法性

浏览器和系统会内置默认信任的证书。

如果只劫持了站点返回自己签发的别的证书,证书因为与域名不符会验证失败。证书是没法伪造的,因为你没有证书的私钥。除非用给原证书签名的根证书重新给你签发一个同域名的证书。

rsa 加密唯一的缺点就是不能防中间人攻击。所以系统和浏览器内置了信任证书。像 ssh 连接第一次会提示指纹一样。你得先信任指纹才能继续操作,指纹发生改变后就会提示你指纹错误。

除此之外,证书颁发机构会验证域名所有权,你得证明域名的所有权在你手里:

  • 你往网站根目录放置一个机构提供的特定的文件,然后机构会定时抓取这个文件,如果能抓取到说明你确实有这个网站的管理权限。注意,只支持 80 和 443 端口,8080 登端口不认。
  • 机构往域名信息里的管理员邮箱发一封验证邮件,邮件里有验证链接,域名管理员要点开链接输入验证码确认。
  • 要求你在域名 DNS 控制面板里添加一条特定的域名记录。

以上操作都只有域名管理员或者网站管理员才能做到,避免了他人伪造证书。

还有一点很重要的:

如果发现申请的域名包含知名品牌、知名网站域名,证书机构会人工审核,有可能会要求你提供相关证明文件。比如我想申请 www.nikeshop.com 的证书,因为包含 Nike 字样,极有可能会被拒绝。

参考

第 43题:介绍 HTTPS 握手过程

建议去看看计算机网络讲解HTTP,然后再看HTTPS。

HTTPS三次握手

握手要解决的问题

  • 客户端和服务器身份的互相确认
  • 协商之后通信中对称加密的秘钥

握手流程

  • 步骤1: 客户端发出请求(ClientHello)
    首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。
    在这一步,客户端主要向服务器提供以下信息。
  1. 客户端支持的SSL的指定版本
  2. 客户端产生的随机数(Client Random, 稍后用于生成"对话密钥"
  3. 客户端支持的加密算法
  • 步骤2:服务器回应(SeverHello)
    服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。
  1. 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
  2. 一个服务器生成的随机数(Server Random),稍后用于生成"对话密钥"。
  3. 确认使用的加密方法,比如RSA公钥加密。
  4. 服务器证书

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

第一次握手结束


  • 步骤3:客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。

  1. 一个随机数(pre-master key), 稍后用于生成"对话密钥"。
  2. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
  3. 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。

第二次握手结束


  • 步骤4:服务器的最后回应
    服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。
  1. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
  2. 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容,也就是对称加密。

第三次握手结束

参考

第 42 题:使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

let arr = [3, 15, 8, 29, 102, 22];
arr.sort((a,b)=>{a-b}); // 升序
arr.sort((a,b)=>{b-a}); //倒序
  • sort()函数解释:

如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。

如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:

  • 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
  • 若 a 等于 b,则返回 0。
  • 若 a 大于b,则返回一个大于 0 的值。

第 41 题:实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现

分别给出4中方式:

  //Promise
  const sleep = time => {
    return new Promise(resolve=>setTimeout(resolve,time))
  }
  sleep(1000).then(()=>{
    console.log(1)
  })
  //Generator
  function* sleepGenerator(time) {
    yield new Promise(function(resolve,reject){
      setTimeout(resolve,time);
    })
  }
  sleepGenerator(1000).next().value.then(()=>{console.log(1)})
  //async
  function sleep(time) {
    return new Promise(resolve=>setTimeout(resolve,time))
  }
  async function output() {
    let out = await sleep(1000);
    console.log(1);
    return out;
  }
  output();
  //ES5
  function sleep(callback,time) {
    if(typeof callback === 'function')
      setTimeout(callback,time)
  }
  function output(){
    console.log(1);
  }
  sleep(output,1000);

参考


第 40 题:下面代码将打印什么?

  var a = 10;
  (function () {
      console.log(a);    //undefined
      a = 5 
      console.log(window.a) // 10
      var a = 20;
      console.log(a) //20
  })()

原因:在内部声名var a = 20;相当于先声明var a;然后再执行赋值操作,这是在IIFE内形成的独立作用域

B 情况:

  var a = 10;
  (function () {
      console.log(a);    //10
      a = 5 
      console.log(window.a) // 5
      //var a = 20;
      console.log(a) //5
  })()

C情况:

 
         

var a = 10;

(function () {

console.log(a); //10

//a = 5

console.log(window.a) // 10

//var a = 20;

console.log(a) //10

})()

 
         

第 39 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop,如果修改了,Vue 是如何监控到属性的修改并给出警告的

  1. 子组件为何不可以修改父组件传递的 Prop:
    单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。同时,因为每当父组件属性值修改时,该值都将被覆盖;如果要有不同的改变,可以用基于prop的data或者computed
  2. Vue 是如何监控到属性的修改并给出警告的
    initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warning提示。
if (process.env.NODE_ENV !== 'production') {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }

需要特别注意的是,由于值传递与地址传递的原因当你从子组件修改的prop,属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此key时也会触发提示(但实际上不会影响父组件的数据源), 当你修改object的属性时不会触发提示,并且会修改父组件数据源的数据。(参考:https://blog.csdn.net/ImagineCode/article/details/54409272


第 38 题:介绍下 BFC 及其应用

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:

  • html 根元素
  • float 浮动
  • 绝对定位
  • overflow 不为 visiable
  • display 为表格布局或者弹性布局

BFC 主要的作用是:

  • 清除浮动
  • 防止同一 BFC 容器中的相邻元素间的外边距重叠问题

参考

第37题:下面代码中 a 在什么情况下会打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
  console.log(1);
}

解析

因为==会进行隐式类型转换 。

  • 利用toString方式
let a = {
  i: 1,
  toString () {
    return a.i++
  }
}
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
  • 利用valueOf
let a = {
  i: 1,
  valueOf () {
    return a.i++
  }
}
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
  • 数组方式
var a = [1,2,3];
a.join = a.shift;
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
  • ES6的symbol
let a = {[Symbol.toPrimitive]: ((i) => () => ++i) (0)};
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}

参考:

第36题:为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?

参考:

第35题:使用迭代的方式实现 flatten 函数

flatten函数,即扁平化函数,是使一个嵌套数组变成一维数组的方式。

例如:

  function flatten(arr=[1,[2],[[3]],[[[4]]]]) {
    return arr.toString().split(',')
  }
  alert(flatten());

那么怎么使用迭代的方式实现flatten函数呢?往下看。

  function flatten(arr,result=[]) {
    for(let item of arr){
      if(Array.isArray(item))
        flatten(item,result)
      else
        result.push(item)
    }
    return result
  }
  var array = [[1,2,3],4,5,6,[[7]],[]];
  var result = flatten(array);
  console.log(result);

上面我们使用迭代递归的方式,使用 result 变量存储结果,然后迭代当前数组,如果值也是数组则继续扁平化,否则将值放入 result 里

参考:

第34题:浏览器缓存可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?

参考

第33题:下面的代码打印什么内容,为什么?

var b = 10;
(function b() {
  b = 20;
  console.log(b)
})()
  • 解析:
    这上面的代码中,我们知道这里面涉及到作用域和IIFE的知识。
    首先我们回顾下IIFE(立即调用函数表达式),参考MDN

IIFE

IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

下面这句话很重要:

第一部分是包围在 圆括号运算符() 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问

(function () { 
    var name = "Barry";
})();
// 外部不能访问变量 name
name // undefined

将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果:

var result = (function () { 
    var name = "Barry"; 
    return name; 
})(); 
// IIFE 执行后返回的结果:
result; // "Barry"

回头看

OK,了解完了IIFE,我们回过头来看这道题:

var b = 10;
    (function b() {
      b = 20;
      console.log(b)
})()
  • b()是个IIFE具名函数,而非匿名函数。它将被立即执行。
  • 在内部作用域中,IIFE函数无法对进行赋值,有些类似于const的意思。所以b=20无效
  • console.log(b) 中,访问变量b,首先在IIFE内部中查找已声明的变量b,结果查找到b(),于是,输出b()这个具名函数。而b=20并没有进行声明,所以,无效。

现在我们对代码进行改造,再来看看其他种情况:

var b = 10;
(function b() {
    window.b = 20; 
    console.log(b); // [Function b]
    console.log(window.b); // 20是必然的
})();

分别打印20与10:

var b = 10;
(function b() {
    var b = 20; // IIFE内部变量
    console.log(b); // 20
   console.log(window.b); // 10 
})();

第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?

参考

第 31 题:改造下面的代码,使之输出0 - 9,写出你能想到的所有解法

for (var i = 0; i< 10; i++){
    setTimeout(() => {
  console.log(i);
    }, 1000)
}

改造

  • 改造1
  function fn() {
    for(var i=0;i<10;i++){//可以将for循环用while循环替换
      console.log(i);
      clearTimeout(timer);
    }
  }
  var timer = setTimeout(fn,1000);
或者:
for (let i = 0; i< 10; i++){
  setTimeout(() => {
    console.log(i);
  }, 1000)
}
或者 简写:
for (var i = 0; i< 10; i++){
    setTimeout(console.log, 1000, i)
}
  • 改造2
(function fn(i){
    if(i<10){
        console.log(i);
        i++;
        setTimeout(fn, 1000,i);
   }
})(0)

-改造3 - 利用闭包特性

for(var i=0;i<10;i++){
    (function(i){
      setTimeout(()=>{
        console.log(i);
      },1000);
    })(i);
  }
或者:
for(var i=0;i<10;i++){
    (function(){
    var j = i; 
      setTimeout(()=>{
        console.log(i);
      },1000);
    })();
  }

第30题:数组合并

题目描述:请把俩个数组 [‘A1’, ‘A2’, ‘B1’, ‘B2’, ‘C1’, ‘C2’, ‘D1’, ‘D2’, ‘E1’, ‘E2’] 和 [‘A’, ‘B’, ‘C’, ‘D’,‘E’],合并为 [“A1”, “A2”, “A”, “B1”, “B2”, “B”, “C1”, “C2”, “C”, “D1”, “D2”, “D”, “E1”, “E2”, “E”]

解析: 观察题目,数组是有规律的!

var arr1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2', 'E1', 'E2'];
var arr2 = ['A', 'B', 'C', 'D','E'];
var arr3 = [];
  arr1.forEach(function(item,index){
    if((index+1)%2===0) {
      arr3.push(arr1[index]);
      arr3.push(arr2[(index+1)/2-1]);
    }else {
      arr3.push(arr1[index]);
    }
  });
  console.log(arr3);

优化:

  arr1.map(function(item,index){
    (index+1)%2===0 ? arr3.push(arr1[index]) && arr3.push(arr2[(index+1)/2-1]) : arr3.push(arr1[index])
  });

相关文章
|
14天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
48 1
|
2月前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
3月前
|
存储 XML 移动开发
前端大厂面试真题
前端大厂面试真题
|
24天前
|
Web App开发 JavaScript 前端开发
前端Node.js面试题
前端Node.js面试题
|
2月前
|
存储 移动开发 前端开发
「offer来了」面试中必考的15个html知识点
该文章汇总了前端面试中常见的15个HTML知识点,涵盖了从HTML文档的规范书写、doctype声明的作用到新兴的HTML5标签应用及移动端viewport设置等内容,旨在帮助求职者更好地准备相关技术面试。
「offer来了」面试中必考的15个html知识点
|
2月前
|
Web App开发 前端开发 JavaScript
「offer来了」1张思维导图,6大知识板块,带你梳理面试中CSS的知识点!
该文章通过一张思维导图和六大知识板块系统梳理了前端面试中涉及的CSS核心知识点,包括CSS框架、基础样式问题、布局技巧、动画处理、浏览器兼容性及性能优化等方面的内容。
|
3月前
|
存储 前端开发 JavaScript
44 个 React 前端面试问题
【8月更文挑战第18天】
49 2
|
3月前
|
存储 JavaScript 前端开发
2022年前端js面试题
2022年前端js面试题
37 0
|
3月前
|
存储 前端开发 JavaScript
44 个 React 前端面试问题
44 个 React 前端面试问题
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
下一篇
无影云桌面