新鲜出炉!2023年滴滴面经

lxf2023-03-08 19:39:01

前言

今年过完年就听说有些公司已经开始招实习了,然后就火急火燎的赶回了南昌(本人现在大三),希望能吃一波春招的福利,于是就写好了简历,由师长帮我找内推投简历,但是此刻的我还在写手头上的一个项目的结尾,然后我的师长前两天突然跟我说,滴滴有内推,昨天帮我投进了去了。我就记了一下,准备今天把手头上的项目结一下,然后开始看面试题

可人算不如天算啊!!!昨天投的简历,今天上午十一点滴滴就打来了电话,问我下午有时间面试嘛,我当时就懵了,完全没想到会那么快!(这里确实感叹一下滴滴的效率确实是高!昨天投的简历,今天就来了电话约面试),就这么毫无防备的剩下了几个小时的面试时间。

前言到此结束,说多了都是泪啊!现在就开始讲我这滴滴一面的面试经过了。

面经正文

我的面试经历主要分为那么三个步骤,自我介绍 -> 面试官根据你的简历问问题 -> 结尾面试官问你有没有什么想要知道的东西。

自我介绍

作为第一次参与面试,我可谓是相当的毫无防备,在自我介绍的就可谓是紧张的一批,简单的讲了一下

XX老师,您好,我是来自于XXXX大学的几几级学生,目前在校大几,掌握的技术栈有XXXXXX,存在项目合作经验,有独自开发一个小型全栈项目的经验,也做过前后端完全分离的项目......

这已经算是相当的简略啦,然后那个面试官老师人挺好的,当时可能看出来我很紧张,然后告诉我,别紧张,放轻松,后面也是多次告诉我别紧张。

面试题

第一题:vue 的生命周期 created 跟 mounted 的区别

在created阶段: vue实例的数据对象data有了,el为undefined,还未初始化。

在mounted阶段: vue实例挂载完成,data.message成功渲染。

第二题:在生命周期 created 函数加 async 前缀会不会阻塞 mounted 函数的执行,那如果延时 created 函数5秒执行会不会阻塞 mounted 函数的执行呢

第一个在 created 函数执行前加 async 前缀会不会阻塞 mounted 函数的执行的问题,我的回答是不会;

因为就像 promise 跟 .then 的关系一样,生命周期是也是有自己的执行顺序的,就像一个 promise 和后面接了多个 .then 一样,你在第一个 .then 加上一个 async 是不会影响后面的 mounted 生命周期执行的。

第二个问题我可能有点理解问题,我的理解是如果 created 函数延时执行会不会影响到 mounted 函数的执行,可能当时面试官老师是说,如果给 created 函数加上定时器,定时五秒,会不会影响到 mounted 的执行。

在我当时理解的问题里,我是觉得会阻塞后面的执行,因为 created 是在vue实例创建后,如果都没创建(就是创建延时),何来执行挂载?更别说 mounted 挂载后。

当然,如果面试官老师是说给 created 函数加上定时器,定时五秒,会不会影响到 mounted 的执行。那答案应该是不会影响到后面 mounted 函数的执行。因为当钩子函数中有异步请求(即使阻塞),不会妨碍下一阶段钩子函数的执行。

第三题:vue 的响应式原理

这里我当时的回答是,vue2 的响应式原理是利用 Object 的 defineProperty 数据劫持方法对 vue2 中数据源 data 的数据进行数据劫持,用里面的 get 方法进行取值,set 方法进行新老值的对比更新出新值。

vue2响应式代码实现:

// vue2.0 数据变了  可以更新视图
function type(data) {
  return Object.prototype.toString.call(data).slice(8, -1) // 'Number'
}


let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype) // 继承

Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
  proto[method] = function () { // pushxxxx  函数劫持, 把函数重写 
    oldArrayPrototype[method].call(this, ...arguments)
    updateView()
  }
})


function observer(target) { // 专门用于劫持数据的
  if (typeof target !== 'object' || typeof target == null) { // 崩
    return target
  }

  if (Array.isArray(target)) {
    // target.__proto__ = proto
    Object.setPrototypeOf(target, proto)
  }

  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}
// 响应式
function defineReactive(target, key, value) {
  observer(value)
  Object.defineProperty(target, key, {  // Object.defineProperty只能劫持对象
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        value = newVal
        updateView()
      }
    }
  })
}

function updateView() {
  console.log('更新视图');
}

// Object.defineProperty  可以重新定义属性 给属性安插 getter setter 方法
let data = {
  name: '潘公子',
  age: [1, 2, 3]
}

observer(data)

// Array.prototype.push = function() {}
data.age.push(4)

而 vue3 则是用 proxy 对象进行数据响应式,Reflect.get 进行值的获取 ,Reflect.set 进行新值的更新,Reflect.deleteProperty 进行数据的删除。

vue3 响应代码实现:

//1. vue2 对象中不存在的属性不能被拦截
//2. 数组改变length属性是无效的
//3. vue2中的对象是默认执行的,vue3用到才会执行

// vue3 响应式

//判断对象
function isObject(val){
    return typeof val === 'object' && val != null
}

//核心方法
function reactive(target){
    //创建响应式对象
    return createReactiveObject(target)
}

//创建响应式对象
function createReactiveObject(target){
    if(!isObject(target)){         //是原始类型,直接返回
        return target
    }

    let baseHandle = {
        get(target,key,receiver){
            console.log('获取')
            let result = Reflect.get(target,key,receiver)        //用result做一个反射
            return isObject(result)?reactive(result):result
        },
        set(target,key,value,receiver){
            let res = Reflect.set(target,key,value,receiver)
            console.log('设置')
            return res
            
        },
        deleteProperty(target,key){
            let res = Reflect.deleteProperty(target,key)
            console.log('删除')
            return res
        }
        
    }
    let observed = new Proxy(target,baseHandle)
    return observed
}

let proxy = reactive({name:'华宝',age:{n:18}})    //多层代理,利用get 读取时才代理
// proxy.name = '阿轩'
// delete proxy.name
proxy.age.n = 19
console.log(proxy.age.n)

第四题:vue3 的 watch 和 watchEffect 的区别

这一题说实话,当时我有点懵,我写了挺久的 vue3 了,文档也看了不少了,可是我偏偏就没注意到 watchEffect ,呜呜呜...我还唯唯诺诺的问了一句 watchEffect 是不是 vue3 语法糖里的那个方法,面试官老师回了一句不是,最后这一题,我只能含泪说这一题我不太会...面试官老师就跟我说还要去仔细看一下 vue3 的官方文档,watchEffect 就在 watch 旁边。

后面我也就跑到 vue.js 的官方文档,查了一查,确实,它们之间就隔了这么两个 API 。

新鲜出炉!2023年滴滴面经

于是乎,仔细的看了一下,也是看明白了一些。

watchEffect 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

它接收两个参数。

第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。

第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。

watch 我倒是一直就知道,它的作用就是侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

第五题:vue-router你使用过吗?使用过 history 方式吗?使用过 hash 方式吗?使用过abstract方式吗?能不能说一下它路由的原理。

上面第三个说使用过 abstract 方式吗不是我虚构的,是面试官老师真实的问了我有没有用过那第三种路由方式,我记得是一个英文单词,但是我没记住,后来也百度了一下,看了很多文章,都是告诉我两种路由模式,最后找到我的师长问了一下,才找到这种路由模式,它叫 abstract 。说到底,还是自己在这方面学习的少,仍需要继续深入学习。

面试时我的回答是我是用过 history 模式,以及 hash 方式。

至于它们路由的原理是分别是(有很多当时面试没有回答那么全面,后期补上的):

hash: 早期前端路由的实现就是基于location.hash来实现的,其实实现原理很简单,location.hash的值就是URL中#后面的内容。

它的实现原理是通过监听 window 下的 hashchange 事件,判断改变的 pathname 属于已记录的哪几个 router 地址,对应的 router-view 容器就渲染相应的路由代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>
            <a href="#/home">首页</a>
        </li>
        <li>
            <a href="#/about">关于</a>
        </li>
    </ul>

    <!-- 渲染对应的UI -->
    <div id="routerView"></div>

    <script>
        let routerView = document.getElementById('routerView')
        window.addEventListener('hashchange',onHashChange)

        //控制渲染对应的ui
        function onHashChange(){
            switch(location.hash){
                case '#/home':routerView.innerHTML='首页'
                braek;
                case '#/about':routerView.innerHTML='关于'
                break;
                default:return
            }
        }
    </script>
</body>
</html>

history: HTML5提供了History API来实现URL的变化,其中最主要的两个API有以下两个 history.pushState()和history.replaceState()。这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录

而 history 对比 hash 不同的是通过阻塞浏览器 url 地址栏更改之后的跳转,然后将其 pathname 拼接上去,实现路由,具体代码实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>
            <a href="/home">首页</a>
        </li>
        <li>
            <a href="/about">关于</a>
        </li>
    </ul>

    <!-- 渲染对应的UI -->
    <div id="routerView"></div>
    <script>
        let routerView = document.getElementById('routerView')

        // window.addEventListener('popstate',()=>{
        //     console.log(13)
        // })

        // window.onpopstate = function(){
        //     console.log(13)
        // }

        window.addEventListener('DOMContentLoaded',onLoad)
        // window.addEventListener('popstate',PoPState)          //浏览器的前进后退能匹配
        function onLoad(){
            PoPState()
            let links = document.querySelectorAll('li a[href]')
            links.forEach(a=>{
                a.addEventListener('click',(e)=>{
                    e.preventDefault();      //阻止a标签的href标签
                    
                    history.pushState(null,'',a.getAttribute('href'))
                    PoPState()
                })
            })
        }
        onLoad()
        function PoPState(){
            console.log(location.pathname)
            switch(location.pathname){
                case '/home':
                    routerView.innerHTML = '<h2>home page</h2>'
                    return
                case '/about':
                    routerView.innerHTML = '<h2>about page</h2>'
                    return
                default:
                    return
            }
        }
    </script>
</body>
</html>

因为当时我说是用的 window 下的一些方法堵塞 url 跳转之类的,所有后来面试官有问我,记不记得具体是 window 下的哪些方法,我当时是确实不记得了哈哈哈,毕竟这个是已经是去年暑假看的了,确实挺久没拿出来复习了。

第六题:ES6 新增的 symbol 数据类型有什么作用?(从这开始就都是原生JS的问题了)

答案: ES6 新增的 symbol 数据类型是用来声明一个唯一值的数据声明,用它声明过的数据都是独一无二的,不管拿什么相等的数据来与它做对比,返回都是 false ,不相等。

第七题:你知道有哪些方法可以判断数据类型

当时我一时紧张,只记起来了两种方式,一种 typeof ,另外一种则是 instanceof ,第三种的是模模糊糊的回答上 Object.prototype.toString 。

typeof 特点:可以准确的判断出原始类型的种类,但是会对 null 判断成 Object ,算是 JS 历史残留问题,引用类型除了 function 能够准确判断出来,其它类型一律会被判断成 Object 类型。

instanceof 特点:可以准确判断出任意数据类型,但不能判断数据类型 null ,且由于原型链可能被修改,就会导致检测结果不准确。

Object.prototype.toString 特点:看名字它是用来将一个值转为字符串的,但其实并不是,它是一个专门检测数据类型的方法。它返回的值是一个形如 [object Object] 的字符串,且什么类型都可以准确判断。

当然,肯定还有其他方法,不过当时我确实是不记得了,后面回想起来一些,也再查了一下,其他方法如下:constructor、Symbol.toStringTag、Object.prototype.isPrototypeOf、Array.isArray、Number.isNaN等等......

第八题:隐式类型判断(双等号如何判断是否相等),双等号跟三等号区别

双等号与三等号的区别:== 代表相同, ===代表严格相同,当进行双等号比较时候: 先检查两个操作数数据类型,如果相同, 则进行三等比较, 如果不同, 则愿意为你进行一次类型转换, 转换成相同类型后再进行比较, 而三等比较时, 如果类型不同,直接就是false.

双等号:

  1. 如果两个值类型相同,再进行三个等号(===)的比较

  2. 如果两个值类型不同,也有可能相等,需根据以下规则进行类型转换在比较:

  3. 如果一个是null,一个是undefined,那么相等

  4. 如果一个是字符串,一个是数值,把字符串转换成数值之后再进行比较

三等号:

  1. 如果类型不同,就一定不相等

  2. 如果两个都是数值,并且是同一个值,那么相等;如果其中至少一个是NaN,那么不相等。(判断一个值是否是NaN,只能使用isNaN( ) 来判断)

  3. 如果两个都是字符串,每个位置的字符都一样,那么相等,否则不相等。

  4. 如果两个值都是true,或是false,那么相等

  5. 如果两个值都引用同一个对象或是函数,那么相等,否则不相等

  6. 如果两个值都是null,或是undefined,那么相等

这里我拿代码说明

# 对象转化为原始类型

ToPrimitive(obj,Number)
1. 如果obj是基本类型,直接返回
2. 否则,调用valueof方法,如果得到一个原始类型,则返回
3. 否则,调用toString方法,如果得到一个原始类型,则返回
4. 否则报错

ToPrimitive(obj,toString)
1. 如果obj是基本类型,直接返回
2. 否则,调用toString方法,如果得到一个原始类型,则返回
3. 否则,调用valueof方法,如果得到一个原始类型,则返回
4. 否则报错

# 一元操作符 +
当加号运算作为一元操作符时会调用toNumber()处理该值

# 二元操作符 +
v1+v2
1.lprim = ToPrimitive(v1)
2.rprim = ToPrimitive(v2)
3.如果lprim或者rprim是字符串,那么返回ToString(lprim)和ToString(rprim)的拼接
4.返回ToNumber(lprim)和ToNumber(rprim)的相加

# ==
x==y
1. 如果x,y是同一类类型
    (1).x是undefined,返回true
    (2).x是null,返回true
    (3).x是数字,返回
        x是NaN,返回false
    (4).x和y指向同一个对象,返回true,否则返回false
2.x,y不是同一种类型:
    (1).null==undefined返回为true
    (2).数字和字符串如果值它会往数字转,1=='1'  和ToNumber('1')
    (3).fasle == '1'     //ToNumber(fasle)  和ToNumber('1')

第九题:怎么判断一个数组是数组

Array.isArray、Object.prototype.toString.call()、instanceof;

详细看上面数据类型判断。

第十题:instanceof 方法的局限性

instanceof 很好用,但是术业有专攻,所以它也有缺点:

  • 不能检测基本数据类型
  • 原型链可能被修改,导致检测结果不准确
  • 只要能在原型链上找到构造函数,就返回 true,所以类型可能不准确

第十一题:怎么让对象上的属性成为不可更改的属性

当时我是没回答上,然后面试官老师提醒了我一下,之前 vue2 响应式的数据劫持里有一个属性设置为 false 就可以使对象上的某个属性变成不可修改属性。

  1. Object.defineProperty 上有一个 writable 属性设置为 false 可以使其对象上的属性不可修改。
  2. 对象冻结:Object.freeze(obj),使被冻结的对象不可被修改,不可添加新属性,不可修改值,不可删除
  3. 对象密封:Object.seal(obj),使被密封的对象不可添加,不可以改动键值,但能改value
  4. 阻止扩展:Object.preventExtensions(obj),阻止对象增加属性,但不会阻止其它操作

第十二题:leetcode 算法 21题

合并两个有序链表

第十三题:leetcode 算法 104题

二叉树的最大深度

结尾

面试官老师在问完我所有问题后,也有问我有没有什么想问他的,我思索了一会儿就问了大厂对框架使用的要求。

挺感谢那位面试官老师很耐心,当时真的挺紧张的,他很耐心的让我别紧张。这个第一次面试的经历可能也是以后记忆中不忘的一段的经历吧。