面试问题

86 阅读18分钟

vue如何实现双向绑定

v-model通过监听用户的input事件来更新数据。
使用Object.defineProperty(),来监听数据get和set,来实现数据劫持
订阅者模式:每一个{{ name }} v-model = 'name' 都会添加一个订阅者,从而监听不同部分的变化,每一部分变化时都会循环触发相应的订阅者,更新到页面中。

Vue双向绑定和vue3.0Proxy的区别

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或修改对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor)
参数:要定义属性的对象、要定义或修改的属性名,要定义或修改的属性描述符

let obj
Object.defineProperty(obj, 'name', {
	set: function(val) {
    	if (val === 'liu') {
        	console.log('名字叫刘xx')
        } else {
        	copyObj.name = val
        }
    },
    get: function() {
    	return copyObj.name.replace()
    }
})
obj.name = 'liu' // 名字叫

堆和栈的区别

自动分配大小,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。——执行栈。 动态分配的内存,大小不会自动释放,存放引用类型,引用类型:指那些可能由多个值构成的对象(以及闭包的变量),保存在堆内存中,包含应用类型的变量,实际保存的不是变量本身,而是该对象的指针——内存堆。

堆和栈的优缺点

:存取速度快,仅次于直接位于CPU的寄存器中,数据可共享;但存在栈中的数据大小和生命周期必须是确定的,缺乏灵活性。 :堆内存中的对象不会随方法的结束而销毁,因为即使方法结束,这个对象还可能被另一哥引用变量所引用(参数传递)。创建对象是为了反复利用,这个对象将被保存到运行时数据区

堆和栈的溢出

栈:可以递归调用方法,随着栈深度的增加,JVM(内存模型)维持着一条长长的方法调用轨迹,直到内存不够分配,产生栈溢出。 堆:循环创建对象,即不断new一个对象

数组的方法

contact():链接两个及多个数组,并返回结果
forEach(): array.forEach(function(currentValue, index, arr), thisValue)
includes():数组中是否包含某元素,返回布尔
indexOf():返回数组中指定元素第一次出现的索引
isArray():判断是否是数组,返回布尔
join():将数组以指定间隔转换为字符串
lastIndexOf():从后向前查找,返回指定元素第一次出现的下标(方法:endWith)
map():遍历数组,返回一个新数组
pop():删除数组的最后一个元素并返回删除的元素
push():向数组末尾添加一个或多个元素,返回新的数组长度
reduce():给定初始值,遍历数组
reduceRight():给定初始值,遍历数组(从右往左)
reverse():翻转数组的元素顺序
shift():删除并返回数组的第一个元素
splice(): 向数组中添加或删除指定位置的元素
slice(): 选取数组的一部分,并返回一个新数组(同字符串中用法相同(参数指索引,不包含下标end的元素))
toString():数组转换为字符串
unshift():向数组开头添加一个或更多元素,返回新数组长度
valueOf():
copyWithin(): 从数组的指定位置拷贝元素到数组的另一个指定位置
entries(): 返回数组的可迭代对象
every(): 检测数组的每一个元素是否否符合条件
fill(): 使用一个固定的值填充数组(可在创建空数组时使用)
findIndex(): 返回符合传入函数条件的数组元素索引
find(): 返回符合条件的元素
form(): 通过给定对象创建一个数组
keys(): 返回包含数组键值的可迭代对象
some(): 检测数组中是否有符合指定条件的元素
sort(): 数组排序
filter(): 过滤元素

空div默认高度

默认20px左右,这个默认高度有页面文字大小决定。

<div style="color: red;background-color: red;">
	<div style="display:inline-block"></div>
</div>

可以采用font-size: 0px;来解决

<div style="color: red;background-color: red;font-size: 0px">
	<div style="display:inline-block"></div>
</div>

js 的函数作用域跟块级作用域(var和let)

全局作用域其实就是一个函数作用域
函数作用域:变量定义在函数内及嵌套的子函数内处处可见;
块级作用域:变量在离开定义的块级代码后马上被回收;


if (true) { 
     let a = 10;
 }
 console.log(a) // undefined    a is not defined
//注意:使用let,const关键字声明的变量才具有块级作用域
if (true) { 
     var a = 10;
 }
 console.log(a)//10

 function fn(){
   var i=6;
   return
 }
 console.log(i);//i is not defined

js中没有块级作用域,“块级作用域”中声明的变量将被添加到当前的执行环境中。 var声明的变量会自动添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境。
因为{}不会产生新的局部环境,而function会产生新的局部环境,因此,function中用var定义的变量在外部无法拿到;但{}中用var定义的变量可以拿到;
letconst会生成块级作用域,因此{}中用letconst定义的变量,{}外无法拿到;

在全局第一行var定义变量和let定义变量有什么区别?

var定义的变量会被绑定到window对象,而let定义的变量不会

算法:如何判断一个链表中是否有环的存在

单链表中的结点都是一个结点指向下一个结点这样链接起来的,知道尾结点的指针域没有指向,单链表就结束了。
存在环是指:尾节点的指针域不为空,而是指向此单链表的其他节点,就会形成环

js判断一个对象是否存在环

将一个js字面量对象转化为一个JSON格式的字符串

const obj = {a:1, b:2}
JSON.stringify(obj) // => '{"a":1,"b":2}'

当要转化的对象有“环”存在时(子节点属性赋值了父节点的引用),为了避免死循环,JSON.stringify 会抛出异常,

const obj = {
  foo: {
    name: 'foo',
    bar: {
      name: 'bar',
      baz: {
        name: 'baz',
        aChild: null  //待会让它指向obj.foo
      }
    }
  }
}
obj.foo.bar.baz.aChild = obj.foo // foo->bar->baz->aChild->foo 形成环
JSON.stringify(obj) // => TypeError: Converting circular structure to JSON

“环”的形成是因为给对象的子节点属性赋值了父节点的引用,所以需要记录下父节点的地址,然后再拿其子节点的属性与之前记录的父节点地址做比较,当结果一致时,就形成了“环”
先遍历对象属性,因为简单数据类型不存在引用关系,因此只需对Object;类型的属性进行处理。

function cycleDetector(obj) {
    var hasCircle = false,            //  定义一个变量,标志是否有环
        cache = [];                   //  定义一个数组,来保存对象类型的属性值
    (function(obj) {
        var keys = Object.keys(obj);    //获取当前对象的属性数组,只能拿到一层属性
        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            var value = obj[key];
            if (typeof value == 'object' && value !== null) {
                var index = cache.indexOf(value)
                if (index !== -1) {
                    hasCircle = true
                    break
                } else {
                    cache.push(value)
                    arguments.callee(value)
                    cache.pop()      //  注意:这里要推出数据,因为递归返回,后面遍历的属性不是这个数据的子属性
                }
            }
        }
    })(obj)

    return hasCircle
}

arguments.callee
在函数内部,有两个特殊的对象:arguments和this,arguments主要用于保存函数参数,arguments有一个属性callee,该属性是一个指针,指向拥有这个arguments对象的函数

function factorial(num){    
  if (num <=1) {         
     return 1;     
  } else {         
  return num * factorial(num-1)     
 } 
}  

以上递归函数中,函数的执行与函数名紧紧耦合在一起,可以使用callee解耦

function factorial(num){    
  if (num <=1) {         
     return 1;     
  } else {         
  return num * arguments.callee(num-1);
  } 
}  

nextTick的原理

用法:在下次DOM更新循环结束后执行延迟回调。在修改数据后立即使用这个方法,获取更新后的DOM。
1、DOM更新循环是指什么?
2、下次更新循环是什么时候?
3、修改数据之后使用,是加快了数据更新进度吗?
4、在什么情况下用到?

原理

VUE实现响应式并不是数据发生变化之后DOM立即变化,而是按一定的策略进行DOM的更新。 VUE异步执行DOM更新。

异步执行的运行机制
(1) 所有同步任务都在主线程上执行,形成一个执行栈
(2) 主线程之外,存在一个“任务队列”,只要异步任务有了运行结果,就在“任务队列”中放置一个事件。
(3) “一旦”执行栈中所有的同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4) 主线程不断重复第三步操作。

单线程导致主线程在遇到IO请求时,需要等待接口返回才能继续往下执行,但此时主线程是空闲的。 Event Loop实现了主线程可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等IO设备返回了结果,再回过头,把挂起的任务继续执行下去。(即,在等待异步请求接口返回时,可以先执行主线程的同步任务,再执行“任务队列”中的异步请求)

事件循环说明

Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中所有的数据变化完成后,在统一进行视图更新 1、第一步:
(1) 修改数据(同步任务)。同一事件循环的所有同步任务都在主线程上执行,形成一个执行栈,此时还未涉及DOM
(2) Vue开启一个异步队列,并缓冲在此事件循环中发生的所有数据变化。如果同一个watcher被多次触发,只会被推入到队列中一次。(nextTick为微任务)
2、第二步:(即‘下次更新循环’)
同步任务执行完毕,开始执行异步watcher队列的任务,更新DOM。Vue在内部尝试对异步队列使用原生的Promise.the和MessageChannel方法,如果执行环境不支持,会采用setTimeout(fn, 0)
3、第三步:
即之前说的“下次DOM更新循环结束之后”

用途

需要在视图更新之后,基于新的视图进行操作

created、mounted

在created和mounted阶段,如果需要操作渲染后的视图,也需要使用nextTick方法。

mounted不会承诺所有子组件也都一起被挂载。如果希望等到整个视图都渲染完毕,可以使用vm.$nextTick替换mounted

mounted: function () {
 this.$nextTick(function () {
 })
}

如何防止冒泡

e.stopPropagation()会阻止元素冒泡事件,但不会阻止默认行为
e.preventDefault()取消目标元素的默认行为,如:a链接的跳转行为,提交按钮<input type="submit">

vue的事件修饰符

  • stop:阻止事件继续传播,e.stopPropagation()
  • prevent:阻止默认行文,e.preventDefault()
  • capture:添加事件监听器使用事件捕获模式,即内部元素触发的条件先在此处理,然后才交由内部元素处理
  • self:当事件发生的对象是在当前元素自身时触发处理函数,即事件不是从内部元素触发的
  • once:点击事件只能执行一次
  • passive:立即触发默认事件(不能和.prevent一起使用)

修饰符的顺序很重要:
v-on:click.prevent.self 会阻止所有的点击
v-on:click.self.prevent 只会阻止对元素自身的点击。 按键修饰符

  • enter
  • tab
  • delete (捕获“删除”和“退格”键)
  • esc
  • space
  • up
  • down
  • left
  • right

<input v-on:keyup.13="submit">

适配不同分辨率的方法

1、根据不同分辨率加载不同css样式
2、采用媒体查询

<!-- 分辨率低于1280,采用test-01.css样式表 -->
<link rel="stylesheet" media="screen and (max-device-width:1280px)" href="test-01.css">
/*分辨率低于1280,采用下面的样式*/
    @media screen and (max-device-width:1280px){
        div{
            width: 200px;
            height: 200px;
            background-color: green;
        }
    }

3、vh/vw/百分比

keep-alive的原理

是什么

keep-alive是一个抽象组件:自身不会渲染DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive用于保存组件的渲染状态

<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
     <component :is="currentComponent"></component>
</keep-alive>

:include缓存白名单,会缓存其中的组件,:exclude缓存黑名单;:max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据

内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。

源码剖析

// src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true, // 判断当前组件虚拟dom是否渲染成真实dom的关键
  props: {
      include: patternTypes, // 缓存白名单
      exclude: patternTypes, // 缓存黑名单
      max: [String, Number] // 缓存的组件
  },
  created() {
     this.cache = Object.create(null) // 缓存虚拟dom
     this.keys = [] // 缓存的虚拟dom的键集合
  },
  destroyed() {
    for (const key in this.cache) {
       // 删除所有的缓存
       pruneCacheEntry(this.cache, key, this.keys)
    }
  },
 mounted() {
   // 实时监听黑白名单的变动
   this.$watch('include', val => {
       pruneCache(this, name => matched(val, name))
   })
   this.$watch('exclude', val => {
       pruneCache(this, name => !matches(val, name))
   })
 },
 render() {
    // 先省略...
 }
}

同定义组件的过程一样,先设置组件名为keep-alive,然后定义abstract属性为true。

  • created:初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的键集合
  • destroyed:删除this.cache中缓存的VNode实例。注:并不是单纯的将this.cache置为null,而是遍历调用pruneCacheEntry函数删除
// src/core/components/keep-alive.js
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
 const cached = cache[key]
 if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroyed() // 执行组件的destroy钩子函数
 }
 cache[key] = null
 remove(keys, key)
}
  • mounted:在mounted钩子中对include和exclude参数进行监听,然后实时更新(删除)this.cache对象数据。pruneCache函数的核心也是去调用pruneCacheEntry
function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}
  • render
render () {
  const slot = this.$slots.defalut
  const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
  const componentOptions : ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) { // 存在组件参数
    // check pattern
    const name: ?string = getComponentName(componentOptions) // 组件名
    const { include, exclude } = this
    if (// 条件匹配
      // not included
      (include && (!name || !matches(include, name)))||
      // excluded
        (exclude && name && matches(exclude, name))
    ) {
        return vnode
    }
    
    const { cache, keys } = this
    // 定义组件的缓存key
    const key: ?string = vnode.key === null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key
     if (cache[key]) { // 已经缓存过该组件
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key) // 调整key排序
     } else {
        cache[key] = vnode //缓存组件对象
        keys.push(key)
        if (this.max && keys.length > parseInt(this.max)) {
          //超过缓存数限制,将第一个删除
          pruneCacheEntry(cahce, keys[0], keys, this._vnode)
        }
     }
     
      vnode.data.keepAlive = true //渲染和执行被包裹组件的钩子函数需要用到
 
 }
 return vnode || (slot && slot[0])
}
  • 第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
  • 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
  • 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
  • 第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
  • 第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。

渲染

Vue的渲染是从render阶段开始的,但keep-alive的渲染实在patch阶段,这是构建组件树(虚拟DOM树),并将VNode转换为真正DOM节点的过程。

package.json 打包时额外参数配置

--mode        指定环境模式(默认:production)
--dest        指定输出目录(默认:dist)
--modern      面向现代浏览器带自动回退地构建应用
--target      app | lib | wc | wc-async(默认值:app)
--name        库或webComponents模式下的名字
--no-clean    在构建项目之前不清除目标目录
--report      生成report.html
--report-json 生成report.json
--watch       监听文件变化

如何避免重复刷接口?

promise

class

js实现继承的几种方法

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

1、原型链继承

将父类的实例作为子类的原型

function Cat() {
}
Cat.prototype = new Animal();
Cat.prototype.name = 'Cat'

var cat = new Cat();
function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);  // cat
console.log(cat.eat('fish'));  // cat正在吃:fish
console.log(cat.sleep());  // cat正在睡觉!
console.log(cat instanceof Animal);   // true 
console.log(cat instanceof Cat);   // true

特点

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现 缺点
  • 想要为子类新增属性和方法时,定义在new Animal之前的属性和方法会被重置为undefined,想要新增属性和方法需要在new之后执行
  • 无法实现多继承(即无法同时继承多个父元素)
  • ☆ 来自原型对象的所有属性被所有实例共享
  • ☆ 创建子类实例时,无法向父类构造函数传参(new a()时无法传参)

2、构造继承

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
	Animal.call(this);  // 将Cat中的this指给Animal
    this.name = name || 'Tom' // 覆盖父类实例的name属性
}
var cat = new Cat('cat2');
console.log(cat.name);  // Tom/cat2
console.log(cat.sleep());  // Tom/cat2正在睡觉!
console.log(cat.eat(food));  // cat.eat is not a function(报错)
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点

  • 解决了方法1中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象) 缺点
  • 实例并不是父类的实例(实例不是Animal),只是子类的实例(实例是Cat)
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法(继承了sleep但无eat方法)
  • ☆ 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3、实例继承

为父类实例添加新特性,作为子类实例返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom2'; // 覆盖父类实例的name属性
  return instance;
}
var cat = new Cat();
console.log(cat.name); // Tom2
console.log(cat.sleep()); // Tom2正在睡觉!
console.log(cat.eat('food'));  // Tom2正在吃:food
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

特点

  • 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果(即var cat = Cat()同样可以达到调用Cat子类的效果) 缺点
  • 实例是父类的实例,不是子类的实例(new的cat实例是Animal的实例,不是Cat的实例)
  • 不支持多继承(同方法1一样不支持多继承)

4、拷贝继承(不推荐)

function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  // Cat.prototype.name = name || 'Tom'; 错误的语句,修改了原型对象,会导致单个实例修改name,会影响所有实例的name值
  this.name = name || 'Tom';
}
var cat = new Cat();

特点

  • 支持多继承 缺点
  • 效率低,内存占用高(因为要拷贝父类的属性)
  • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in访问到)

5、组合继承(推荐)

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

xx函数.prototype.constructor === xx函数

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();
// 组合继承也是需要修复构造函数指向的。
Cat.prototype.constructor = Cat;
var cat = new Cat();

特点

  • 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  • 即使子类的实例,也是父类的实例
  • 不存在引用类型共享的问题
  • 可传参
  • 函数可复用 缺点
  • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

6、寄生组合继承(完美)

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造时,就不会初始化两次实例方法/属性,避免了组合继承的缺点

function Cat(name) {
	Animal.call(this);
    this.name = name || 'Tom';
}
(function() {
	// 创建一个没有实例的方法的类
    var Super = function() {};
    Super.prototype = Animal.prototype;
    // 将实例作为子类的原型
    Cat.prototype = new Super();
})();
Cat.prototype.constructor = Cat; // 需要修复构造函数
var cat = new Cat()

num++和++num的区别

num++先将值进行运算(如赋值等),后自加;++num先自加后进行运算;

vue中mixin混入和vuex状态管理相比

混入(mixin),用来分发组件中的可复用功能。一个混入对象可以包含任意组件选项。组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
可以在混入中定义任何的生命周期
全局中引入混入:

import Vue from 'vue'
import mixin from "./mixin/mixin.js"
Vue.mixin(mixin)

mixin特性

  • 组件混入对象有同名选项时,这些选项将以恰当的方式进行“合并”。比如:数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子函数在组件自身的钩子函数之前调用。
  • 值为对象的选项,如methodscomponentsdirectives(自定义指令),将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对mixin和Vuex的区别
  • vuex中的方法和变量是可以互相读取并互相更改的,mixin不会。mixin可以定义公用的变量或方法,但是mixin中的数据是不共享的,即每个组件中的mixin实例都不一样,都是单独存活的个体,不存在相互影响