面试题总结

191 阅读14分钟

1.原型和原型链

QQ截图20230203115654.png

每个对象都有一个属性proto 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去proto中查找 proto指向构造函数原型 原型对象又会有自己的proto 一直这样查找下去,直到直到原型链的末端还没有找到,则返回 undefined

prototype

每个函数都有一个prototype属性,该属性是一个指针,指向一个对象(构造函数的原型对象) ,这个对象包含所 有实例共享的属性和方法。原型对象都有一个 constructor 属性,这个属性指向所关联的构造函数。使用这个对象 的好处就是可以让所有实例对象共享它所拥有的属性和方法。这个属性只用js中的类(或者说能够作为构造函数的对 象)才会有。

proto

每个实例对象都有一个proto属性,用于指向构造函数的原型对象( prototype )。 __proto__属性是在调用构造函 数创建实例对象时产生的。该属性存在于实例和构造函数的原型对象之间,而不是存在于实例与构造函数之间。

  • proto :是 对象 就会有这个属性(强调是对象); 函数 也是对象,那么函数也有这个属性咯,它指向 构造函数 的 原型对象;

  • prototype :是 函数 都会有这个属性(强调是函数), 普通对象 是没有这个属性的(JS 里面,一切皆为对象,所 以这里的 普通对象 不包括 函数对象 ).它是构造函数的原型对象;

  • constructor :这是 原型对象 上的一个指向 构造函数 的属性。

二 ES5/ES6的继承的区别

6.寄生组合式继承

function Parent(name) {
  this.name = name
  this.likeFood = ["水果", "鸡", "烤肉"]
}

function Child(name, age) {
  Parent.call(this, name) // 第二次调用
  this.age = age
}

Parent.prototype.getName = function() {
  return this.name
}

Child.prototype = new Parent() // 第一次调用
Child.prototype.constructor = Child

Child.prototype.getAge = function() {
  return this.age
}
复制代码

这个两次调用的问题之前有提及过。过程大致:

  • 第一次调用,Child 的原型被赋值了 name 和 likeFood 属性
  • 第二次调用,注入this,会在Child 的实例对象上注入 name 和 likeFood 属性,这样就屏蔽了原型上的特性。 只要了问题,我们就来解决这个问题~
function Parent(name) {
  this.name = name
  this.likeFood = ["水果", "鸡", "烤肉"]
}

function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}

Parent.prototype.getName = function() {
  return this.name
}

// Child.prototype = new Parent() 使用新方法解决
// Child.prototype.constructor = Child

inheritPrototype(Child, Parent)

function inheritPrototype(childFunc, parentFunc) {
  var prototype = realizeInheritance(parentFunc.prototype) //创建对象,我们继续是用原型式继承的创建
  prototype.constructor = childFunc //增强对象
  childFunc.prototype = prototype //指定对象
}

function realizeInheritance(parent) {
// 临时函数
  function tempFunc() {}
  tempFunc.prototype = parent
  return new tempFunc()
}

Child.prototype.getAge = function() {
  return this.age
}

var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')
console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"
console.log(chongqingChild.getName()) // "重庆孩子"
console.log(chongqingChild.getAge()) // 18
复制代码

这种方法的核心思想:

  • 首先,用一个空对象建立和父类关系。
  • 然后,再用这个空对象作为子类的原型对象。

这样,中间的对象就不存在new 构造函数的情况(这个对象本来就没有自定义的函数),这样就避免了执行构造函 数,这就是高效率的体现。并且,在中间对象继承过程中,父类构造器也没有执行。所以,没有在子类原型上绑定 属性。 这种继承方式也被开发人员普遍认为是引用类型最理想的继承范式。

Class extends 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point { } 
class ColorPoint extends Point { }
复制代码

上面代码定义了一个 ColorPoint 类,该类通过 extends 关键字,继承了 Point 类的所有属性和方法。但是由于没有 部署任何代码,所以这两个类完全一样,等于复制了一个 Point 类。下面,我们在 ColorPoint 内部加上代码。

class ColorPoint extends Point { 
  constructor(x, y, color) { 
    super(x, y); // 调用父类的constructor(x, y)   
    this.color = color; 
  } 
  toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() 
  } 
}
复制代码

上面代码中, constructor 方法和 toString 方法之中,都出现了 super 关键字,它在这里表示父类的构造函数,用来 新建父类的 this 对象。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通 过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性 和方法。如果不调用 super 方法,子类就得不到 this 对象。

对闭包的理解

闭包有两个常用的用途;

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

1.函数嵌套

2.内部函数引用了外部函数的局部变量

PS:正常情况下,当一个函数调用完之后,内存中关于函数的东西会全部释放掉,局部变量也会消失,而闭包是一种特殊的存在。由于外部函数返回的是内部函数的引用,相当于你返回了一个函数,这个函数还未被真正调用。但是内函数又使用了外函数的变量,导致内存不能释放它们,需要等到内函数使用完成之后才能释放它们----由此形成了闭包

3.闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量

使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

hash和history的区别

1. hash模式

简介: hash模式是开发中默认的模式,它的URL带着一个#,例如:www.abc.com/#/vue,它的hash值就是#/vue

特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。

hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。 此时url变化时,浏览器是不会重新加载的.Hash(即#)是url的锚点,代表的是网页中的一个位置,仅仅改变#后面部分,浏览器只会滚动对应的位置,而不会重新加载页面.#仅仅只是对浏览器进行指导,而对服务端是完全没有作用的!它不会被包括在http请求中,故也不会重新加载页面.同时hash发生变化时,url都会被浏览器记录下来,这样你就可以使用浏览器的后退了.

window.onhashchange = function(event){
	console.log(event.oldURL, event.newURL);
	let hash = location.hash.slice(1);
}
复制代码

使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。

2. history模式

简介: history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。 特点: 当使用history模式时,URL就像这样:abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。 API: history api可以分为两大部分,切换历史状态和修改历史状态:

  • 修改历史状态:包括了 HTML5 History Interface 中新增的 pushState()replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。
  • 切换历史状态: 包括forward()back()go()三个方法,对应浏览器的前进,后退,跳转操作。

虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。

如果想要切换到history模式,就要进行以下配置(后端也要进行配置):

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})
复制代码

3. 两种模式对比

调用 history.pushState() 相比于直接修改 hash,存在以下优势:

  • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
  • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
  • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
  • pushState() 可额外设置 title 属性供后续使用。
  • hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。

hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。

简单实现节流

节流

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。

对于节流,有多种方式可以实现 时间戳 定时器 束流等。

ps : 技能CD :

策略

固定周期内,只执行一次动作,若没有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。

特点:

连续高频触发事件时,动作会被定期执行,响应平滑

节流案例

<button id="debounce">点我防抖!</button>
    <script>

      // 1、获取这个按钮,并绑定事件
      var myDebounce = document.getElementById("debounce");
      // myDebounce.addEventListener("click", debounce(sayDebounce));
      myDebounce.onclick = throttle(sayDebounce);


      function throttle(fn, interval) {
        let timer = null; // 定时器
        let firstTime = true; // 判断是否是第一次执行
        
        // 利用闭包
        return function () {

          // 拿到函数的参数数组

          let args = Array.prototype.slice.call(arguments, 0);

          // 拿到当前的函数作用域

          let _this = this;

          // 如果是第一次执行的话,需要立即执行该函数

          if (firstTime) {

            // 通过apply,绑定当前函数的作用域以及传递参数

            fn.apply(_this, args);

            // 修改标识为null,释放内存

            firstTime = null;

          }

          // 如果当前有正在等待执行的函数则直接返回

          if (timer) return;

          // 开启一个倒计时定时器

          timer = setTimeout(function () {

            // 通过apply,绑定当前函数的作用域以及传递参数

            fn.apply(_this, args);

            // 清除之前的定时器

            timer = null;

            // 默认300ms执行一次

          }, interval || 3000);

        };

      }


      // 3、需要进行防抖的事件处理

      function sayDebounce(a) {

        // ... 有些需要防抖的工作,在这里执行

        console.log(111);

        console.log(a.target);

      }
复制代码

function throttle(fn,wait) {
    let timer;
    return function() {
        let args = arguments;
        if(!timer) {
            timer = setTimeout(()=>{
                timer = null;
                fn.apply(this,args);
            },wait)
        }
    }
}

介绍下 Promise.all 使用、原理实现及错误处理

通常处理多个请求的时候我们会用Promise.all()方法。该方法指当所有在可迭代参数中的 promises 已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。但是当其中任何一个被拒绝的话。主Promise.all([..])就会立即被拒绝,并丢弃来自其他所有promis的全部结果。

//为防止报错

所以每一个promise都加了catch后就可以继续执行,原因就在于promise的链式编程特点,同理在promise.race里 加上catch后,亦可继续执行then中代码模块

ref获取dom节点

$refs对象介绍及基本用法

refs对象的介绍:在每个vue的组件实例上,都包含一个 r e f 对象 = = 存储着对应的 = = D O M 元素 = = 或 = = 组件 = = 的引用, = = 默认情况下,组件的 ref对象==存储着对应的==DOM元素==或==组件==的引用,==默认情况下,组件的 ref对象==存储着对应的==DOM元素==或==组件==的引用,==默认情况下,组件的refs指向一个空对象,凡是以$开头的,都是vue的内置对象

    <h1 ref="myh1">App 根组件</h1>
    this.$refs.myh1.style.color = "red";

总结一下:vue中有一个refs对象,默认为空。当我们在DOM元素中为其设置ref值后,相当于在vue的内置对象refs对象,默认为空。当我们在DOM元素中为其设置ref值后,相当于在vue的内置对象 refs中添加了新的成员,我们如果需要操作DOM对象直接通过 $refs获取即可。

vue组合式api中ref

ref(0); // 通过监听对象(类)的value属性实现响应式 
ref({a: 6}); 
// 调用reactive方法对对象进行深度监听,.value时获取的则是这个响应式对象,
//访问value时是ref实现响应式,访问.value.a 时则是 reactive实现的响应式。
  • reactive 将引用类型值变为响应式,使用Proxy实现

  • ref 可将基本类型和引用类型都变成响应式,通过监听类的value属性的getset实现,但是当传入的值为引用类型时实际上内部还是使用reactive方法进行的处理

  • ref经修改实现方式后性能更高,推荐使用ref一把梭

vue响应式数据和v-model数据双向绑定

juejin.cn/post/711742…

vue自定义指令

juejin.cn/post/720252…

vuetoken

# 用vuex对token进行管理以及处理token过期问题 # vue 登录权限--token刷新

# Vue 项目中用户登录及 token 验证的思路

# vue 登录权限--token刷新

# 前端无痛刷新Token

。。。。。。 # 前端根据登录用户做不同权限控制

# 前端权限判断

fiber和diff

fiber和diff

动态路由权限

blog.csdn.net/liusixsixsi… juejin.cn/post/684490…

动态路由 + token 刷新等

动态路由

# Vue中后台鉴权的另一种方案 - 动态路由

# vue admin 动态路由权限管理

# 前端权限控制(一):前端权限管理及动态路由配置方案

# 手摸手,带你用vue撸后台 系列二(登录权限篇)

# 前端权限控制(三):根据后台接口数据传递页面按钮权限-自定义指令-实现按钮级权限控制

token基础验证 和 刷新token

基础验证登录 # vue项目中使用token的身份验证的简单实践

# 06-用vuex对token进行管理以及处理token过期问题

# vue 登录权限--token刷新

# 前端无痛刷新Token

# 前端无痛刷新Token

什么是vue虚拟dom

⽤ JavaScript 对象表示 DOM 信息和结构,更新后使之与真实dom保持同步,同步过程就是协调,核心是diff算法

DOM操作很慢,轻微的操作都可能导致⻚面重新排 版,⾮常耗性能。相对于DOM对象,js对象处理起来更快, 而且更简单。通过diff算法对比新旧vdom之间的差异,可以 批量的、最⼩化的执行dom操作,从而提高性能

在使用Vue开发中如果数据发生变化而视图没有更新的原因是什么怎么解决

Vue.$forceUpdate(手动强制更新视图)

Vue的指令有哪些尽量写全

# Vue内置指令大全

ref的用法

在普通元素上,通过this.refs获取dom元素加载子组件上,this.refs 获取dom元素 加载子组件上,this.refs获取到组件实例,可以使用组件的的所有方法

MVVM

  • Model代表数据模型,数据和业务逻辑都在Model层中定义;
  • View代表UI视图,负责数据的展示;
  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM

diff算法和fiber

# 为什么 React 的 Diff 算法不采用 Vue 的双端对比算法