资讯专栏INFORMATION COLUMN

刷《一年半经验,百度、有赞、阿里面试总结》·手记

NusterCache / 989人阅读

摘要:在掘金上看到了一位大佬发了一篇很详细的面试记录文章一年半经验,百度有赞阿里面试总结,为了查漏补缺,抽空就详细做了下。

在掘金上看到了一位大佬发了一篇很详细的面试记录文章-《一年半经验,百度、有赞、阿里面试总结》,为了查漏补缺,抽空就详细做了下。(估计只有我这么无聊了哈哈哈

有给出的或者有些不完善的答案,也尽力给出/完善了(可能有错,大家自行辨别)。有些很困难的题目(例如实现Promise),附带相关链接(懒癌患者福利)。

总的来说,将这些题目分成了“Javascript”、“CSS”、“浏览器/协议”、“算法”和“Web工程化”5个部分进行回答和代码实现。

最后,欢迎来我的博客和我扯犊子:godbmw.com。直接戳本篇原文的地址:刷《一年半经验,百度、有赞、阿里面试总结》·手记

1. Javascript相关 1.1 回文字符串
题目:实现一个函数,判断是不是回文字符串

原文的思路是将字符串转化成数组=>反转数组=>拼接成字符串。这种做法充分利用了js的BIF,但性能有所损耗

function run(input) {
  if (typeof input !== "string") return false;
  return input.split("").reverse().join("") === input;
}

其实正常思路也很简单就能实现,性能更高,但是没有利用js的特性

// 回文字符串
const palindrome = (str) => {
  // 类型判断
  if(typeof str !== "string") {
    return false;
  }

  let len = str.length;
  for(let i = 0; i < len / 2; ++i){
    if(str[i] !== str[len - i - 1]){
      return false;
    }
  }
  return true;
}
1.2 实现Storage
题目:实现Storage,使得该对象为单例,并对localStorage进行封装设置值setItem(key,value)和getItem(key)

题目重点是单例模式,需要注意的是借助localStorage,不是让自己手动实现!

感谢@whiteyork_和@chenzesam的提醒:箭头函数没有prototype

function Storage(){}

Storage.getInstance = (function(){
  var instance = null
  return function(){
    if(!instance){
      instance = new Storage()
    }
    return instance
  }
})()

Storage.prototype.setItem = function(key, value) {
  return localStorage.setItem(key, value)
}

Storage.prototype.getItem = function(key){
  return localStorage.getItem(key)
}

// 测试代码:Chrome环境
let a = Storage.getInstance()
let b = Storage.getInstance()
console.log(a === b)

a.setItem("key", 1)
console.log(b.getItem("key"))
1.3 JS事件流
题目:说说事件流吧

事件流分为冒泡和捕获。

事件冒泡:子元素的触发事件会一直向父节点传递,一直到根结点停止。此过程中,可以在每个节点捕捉到相关事件。可以通过stopPropagation方法终止冒泡。

事件捕获:和“事件冒泡”相反,从根节点开始执行,一直向子节点传递,直到目标节点。印象中只有少数浏览器的老旧版本才是这种事件流,可以忽略。这里说的确实有问题,更正下:addEventLister给出了第三个参数同时支持冒泡与捕获。

感谢@junior-yang的提醒

1.4 实现函数继承
题目:现在有一个函数A和函数B,请你实现B继承A。并且说明他们优缺点。

方法一:绑定构造函数

优点:可以实现多继承

缺点:不能继承父类原型方法/属性

function Animal(){
  this.species = "动物";
}

function Cat(){
  Animal.apply(this, arguments); // 父对象的构造函数绑定到子节点上
}

var cat = new Cat()
console.log(cat.species) // 输出:动物

方法二:原型链继承

优点:能够继承父类原型和实例方法/属性,并且可以捕获父类的原型链改动

缺点:无法实现多继承,会浪费一些内存(Cat.prototype.constructor = Cat)。除此之外,需要注意应该将Cat.prototype.constructor重新指向本身。

js中交换原型链,均需要修复prototype.constructor指向问题。

function Animal(){
  this.species = "动物";
}
Animal.prototype.func = function(){
  console.log("heel")
}

function Cat(){}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

var cat = new Cat()
console.log(cat.func, cat.species)

方法3:结合上面2种方法

function Animal(){
  this.species = "动物";
}
Animal.prototype.func = function(){
  console.log("heel")
}

function Cat(){
  Animal.apply(this, arguments)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat;

var cat = new Cat()
console.log(cat.func, cat.species)
1.5 ES5对象 vs ES6对象
题目:es6 class 的new实例和es5的new实例有什么区别?

ES6中(和ES5相比),classnew实例有以下特点:

class的构造参数必须是new来调用,不可以将其作为普通函数执行

es6class不存在变量提升

最重要的是:es6内部方法不可以枚举。es5的prototype上的方法可以枚举。

为此我做了以下测试代码进行验证:

console.log(ES5Class()) // es5:可以直接作为函数运行
// console.log(new ES6Class()) // 会报错:不存在变量提升

function ES5Class(){
  console.log("hello")
}

ES5Class.prototype.func = function(){ console.log("Hello world") }

class ES6Class{
  constructor(){}
  func(){
    console.log("Hello world")
  }
}

let es5 = new ES5Class()
let es6 = new ES6Class()

console.log("ES5 :")
for(let _ in es5){
  console.log(_)
}

// es6:不可枚举
console.log("ES6 :")
for(let _ in es6){
  console.log(_)
}

这篇《JavaScript创建对象—从es5到es6》对这个问题的深入解释很好,推荐观看!

1.6 实现MVVM
题目:请简单实现双向数据绑定mvvm

vuejs是利用Object.defineProperty来实现的MVVM,采用的是订阅发布模式。每个data中都有set和get属性,这种点对点的效率,比Angular实现MVVM的方式的效率更高。


  
  
1.7 实现Promise

这是一位大佬实现的Promise版本:过了Promie/A+标准的测试!!!网上能搜到的基本都是从这篇文章变形而来或者直接照搬!!!原文地址,直接戳:剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

下面附上一种近乎完美的实现:可能无法和其他Promise库的实现无缝对接。但是,上面的原文实现了全部的,欢迎Mark!

function MyPromise(executor){
  var that = this
  this.status = "pending" // 当前状态
  this.data = undefined
  this.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
  this.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面

  // 更改状态 => 绑定数据 => 执行回调函数集
  function resolve(value){
    if(that.status === "pending"){
      that.status = "resolved"
      that.data = value
      for(var i = 0; i < that.onResolvedCallback.length; ++i){
        that.onResolvedCallback[i](value)
      }
    }
  }

  function reject(reason){
    if(that.status === "pending"){
      that.status = "rejected"
      that.data = reason
      for(var i = 0; i < that.onResolvedCallback.length; ++i){
        that.onRejectedCallback[i](reason)
      }
    }
  }

  try{ 
    executor(resolve, reject) // resolve, reject两个函数可以在外部传入的函数(executor)中调用
  } catch(e) { // 考虑到执行过程可能有错
    reject(e)
  }
}

// 标准是没有catch方法的,实现了then,就实现了catch
// then/catch 均要返回一个新的Promise实例

MyPromise.prototype.then = function(onResolved, onRejected){
  var that = this
  var promise2

  // 值穿透
  onResolved = typeof onResolved === "function" ? onResolved : function(v){ return v }
  onRejected = typeof onRejected === "function" ? onRejected : function(r){ return r }

  if(that.status === "resolved"){
    return promise2 = new MyPromise(function(resolve, reject){
      try{
        var x = onResolved(that.data)
        if(x instanceof MyPromise){ // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
          x.then(resolve, reject)
        }
        resolve(x) // 否则,以它的返回值做为promise2的结果 
      } catch(e) {
        reject(e) // 如果出错,以捕获到的错误做为promise2的结果
      }
    })
  }

  if(that.status === "rejected"){
    return promise2 = new MyPromise(function(resolve, reject){
      try{
        var x = onRejected(that.data)
        if(x instanceof MyPromise){
          x.then(resolve, reject)
        }
      } catch(e) {
        reject(e)
      }
    })
  }

  if(that.status === "pending"){
    return promise2 = new MyPromise(function(resolve, reject){
      self.onResolvedCallback.push(function(reason){
        try{
          var x = onResolved(that.data)
          if(x instanceof MyPromise){
            x.then(resolve, reject)
          }
        } catch(e) {
          reject(e)
        }
      })

      self.onRejectedCallback.push(function(value){
        try{
          var x = onRejected(that.data)
          if(x instanceof MyPromise){
            x.then(resolve, reject)
          }
        } catch(e) {
          reject(e)
        }
      })
    })
  }
}

MyPromise.prototype.catch = function(onRejected){
  return this.then(null, onRejected)
}

// 以下是简单的测试样例:
new MyPromise(resolve => resolve(8)).then(value => {
  console.log(value)
})
1.8 Event Loop
题目:说一下JS的EventLoop

其实阮一峰老师这篇《JavaScript 运行机制详解:再谈Event Loop》已经讲的很清晰了(手动赞)!

这里简单总结下:

JS是单线程的,其上面的所有任务都是在两个地方执行:执行栈和任务队列。前者是存放同步任务;后者是异步任务有结果后,就在其中放入一个事件。

当执行栈的任务都执行完了(栈空),js会读取任务队列,并将可以执行的任务从任务队列丢到执行栈中执行。

这个过程是循环进行,所以称作Loop

2. CSS相关 2.1 水平垂直居中
题目: 两种以上方式实现已知或者未知宽度的垂直水平居中

第一种方法就是利用CSS3translate进行偏移定位,注意:两个参数的百分比都是针对元素本身计算的。

.wrap {
  position: relative;
  width: 100vw;
  height: 100vh;
}

.box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

第二种方法是利用CSS3flex布局,父元素diplay属性设置为flex,并且定义元素在两条轴线的布局方式均为center

.wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100vw;
  height: 100vh;
}

.wrap .box {
  width: 100px;
  height: 100px;
}

第三种方法是利用margin负值来进行元素偏移,优点是浏览器兼容好,缺点是不够灵活(要自行计算margin的值):

.wrap {
  position: relative;
  width: 100vw;
  height: 100vh;
}

.box {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100px;
  height: 100px;
  margin: -50px 0 0 -50px;
}
2.2 “点击”改变样式
题目:实现效果,点击容器内的图标,图标边框变成border 1px solid red,点击空白处重置。

利用event.target可以判断是否是指定元素本身(判断“空白处”),除此之外,注意禁止冒泡(题目指明了“容器内”)。




  
  
  
  Document
  


  
123456
3. 浏览器/协议相关 3.1 缓存机制
题目:说一下浏览器的缓存机制。

浏览器缓存分为强缓存和协商缓存。缓存的作用是提高客户端速度、节省网络流量、降低服务器压力

强缓存:浏览器请求资源,如果header中的Cache-ControlExpires没有过期,直接从缓存(本地)读取资源,不需要再向服务器请求资源。

协商缓存:浏览器请求的资源如果是过期的,那么会向服务器发送请求,header中带有Etag字段。服务器再进行判断,如果ETag匹配,则返回给客户端300系列状态码,客户端继续使用本地缓存;否则,客户端会重新获取数据资源。

关于过程中详细的字段,可以参考这篇《http协商缓存VS强缓存》

3.2 从URL到页面生成
题目:输入URL到看到页面发生的全过程,越详细越好

DNS解析

建立TCP连接(3次握手)

发送HTTP请求,从服务器下载相关内容

浏览器构建DOM树和CSS树,然后生成渲染树。这个一个渐进式过程,引擎会力求最快将内容呈现给用户。

在第四步的过程中,

阅读需要支付1元查看
<